(() => {
  'use strict';

  const I18n = globalThis.PyroI18n;
  const Runtime = globalThis.PyroRuntime;
  const Security = globalThis.PyroSecurityPolicy;
  const Tabler = globalThis.PyroTablerIcons;
  const Catalog = globalThis.PyroToolCatalog;
  const AutoShapes = globalThis.PyroAutoShapes;
  const BitmapSelection = globalThis.PyroBitmapSelection;
  const BitmapModel = globalThis.PyroBitmapModel;
  const BitmapRetouch = globalThis.PyroBitmapRetouch;
  const BitmapWarp = globalThis.PyroBitmapWarp;
  const FillPaint = globalThis.PyroFillPaint;
  const StrokeStyle = globalThis.PyroStrokeStyle;
  const PathOperations = globalThis.PyroPathOperations;
  const PathWorkbench = globalThis.PyroPathWorkbench;
  const FreeTransform = globalThis.PyroFreeTransform;
  const PathMetrics = globalThis.PyroPathMetrics;
  const Snap = globalThis.PyroSnap;
  const Effects = globalThis.PyroEffects;
  const DitherPatterns = globalThis.PyroDitherPatterns;
  const SceneController = globalThis.PyroSceneController;
  const WorkerClient = globalThis.PyroWorkerClient;
  const HistoryAssets = globalThis.PyroHistoryAssets;
  const DocumentStats = globalThis.PyroDocumentStats;
  const ReusableAssets = globalThis.PyroReusableAssets;
  const Png = globalThis.PyroPng;
  const ProjectContainer = globalThis.PyroProjectContainer;
  const SvgImport = globalThis.PyroSvgImport;
  if (!I18n || !Runtime || !Security || !Tabler || !Catalog || !AutoShapes || !BitmapSelection || !BitmapModel || !BitmapRetouch || !BitmapWarp || !FillPaint || !StrokeStyle || !PathOperations || !PathWorkbench || !FreeTransform || !PathMetrics || !Snap || !Effects || !DitherPatterns || !SceneController || !WorkerClient || !HistoryAssets || !DocumentStats || !ReusableAssets || !Png || !ProjectContainer || !SvgImport) throw new Error('PYRO-Module konnten nicht geladen werden');
  I18n.start();
  const { svgNS, byId: $, createSvg: E, clamp, uid, deepClone, num, round, normalizeDocumentTitle } = Runtime;
  const importErrorMessage = (error, fallback) => error instanceof Security.SecurityPolicyError ? error.message : fallback;
  const filename = (extension) => {
    const title = normalizeDocumentTitle(activeDocument()?.title, 'Unbenannt')
      .normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
      .replace(/[<>:"/\\|?*\x00-\x1f]/g, ' ').replace(/\s+/g, '-').replace(/^-+|-+$/g, '');
    return `${title || 'Unbenannt'}.${extension}`;
  };

  const refs = {
    artboard: $('artboard'), workspace: $('workspace'), shell: $('canvasShell'), emptyHint: $('emptyHint'), workColumn: document.querySelector('.work-column'),
    toolRail: $('toolRail'), toolLabelsToggle: $('toolLabelsToggle'), propertyPanelsModeToggle: $('propertyPanelsModeToggle'), status: $('appStatus'), count: $('objectCount'), cursor: $('cursorPosition'),
    undoStatus: $('undoStatus'), undoStatusText: $('undoStatusText'),
    summary: $('selectionSummary'), inspectorEmpty: $('inspectorEmpty'), inspector: $('inspectorForm'), tabs: $('documentTabs'),
    tabsStrip: $('documentTabsStrip'), tabsScrollLeft: $('documentTabsScrollLeft'), tabsScrollRight: $('documentTabsScrollRight'),
    tabContextMenu: $('documentTabContextMenu'),
    layerList: $('layerList'), toast: $('toastRegion'), inlineText: $('inlineTextEditor'),
    layout: $('studioLayout'), toolSidebar: document.querySelector('.tool-sidebar'), rightSidebar: $('rightSidebar'), rightSidebarResize: $('rightSidebarResize'), rightSidebarToggle: $('rightSidebarToggle'),
    propertyDock: $('propertyDock'), propertyDockEyebrow: $('propertyDockEyebrow'), propertyDockHeading: $('propertyDockHeading'), propertyDockSummary: $('propertyDockSummary'), propertyDockSizeBtn: $('propertyDockSizeBtn'), propertyDockSizeLabel: $('propertyDockSizeLabel'),
    propertyDockTools: $('propertyDockTools'), propertyDockInspector: $('propertyDockInspector'), propertyDockReadOnly: $('propertyDockReadOnly'), modifierPanelContent: $('modifierPanelContent'),
    horizontalRuler: $('horizontalRuler'), verticalRuler: $('verticalRuler'),
    horizontalRulerCanvas: $('horizontalRulerCanvas'), verticalRulerCanvas: $('verticalRulerCanvas'), guideLock: $('guideLockBtn')
  };

  if (!refs.artboard) return;

  function arrangeWorkspacePanels() {
    const options = $('propertyToolOptions');
    const inspectorPanel = document.querySelector('.inspector-panel');
    const alignSection = document.querySelector('.align-section');
    const transformControls = $('transformControls');
    const freeTransformControls = $('freeTransformControls');
    const arrangeActions = document.querySelector('.arrange-actions');
    const groupActions = document.querySelector('.group-actions');
    const pathOperations = $('pathOperations');
    const duplicateButton = $('duplicateBtn');
    const objectTransformSection = $('objectTransformProperties');
    const textProperties = $('textProperties');
    if (objectTransformSection && textProperties) objectTransformSection.after(textProperties);
    if (refs.propertyDockTools && options) refs.propertyDockTools.append(options);
    if (refs.propertyDockInspector && inspectorPanel) refs.propertyDockInspector.append(inspectorPanel);
    if (refs.modifierPanelContent) {
      if (alignSection) refs.modifierPanelContent.append(alignSection);
      if (transformControls) refs.modifierPanelContent.append(transformControls);
      if (freeTransformControls) refs.modifierPanelContent.append(freeTransformControls);
      if (arrangeActions) refs.modifierPanelContent.append(arrangeActions);
      if (groupActions) refs.modifierPanelContent.append(groupActions);
      if (pathOperations) refs.modifierPanelContent.append(pathOperations);
      if (duplicateButton) refs.modifierPanelContent.append(duplicateButton);
    }
  }

  arrangeWorkspacePanels();
  const Core = globalThis.PyroCore;
  if (!Core) throw new Error('PYRO-Kern konnte nicht geladen werden');

  const {
    names, eraserToolInfo, retouchToolInfo, warpToolInfo, marqueeToolInfo, lineToolInfo, pathModifierToolInfo, shapeToolInfo,
    openVectorShapeTypes, cuttableVectorTypes, fillableVectorShapeTypes, closedKnifeTypes, polygonConvertibleTypes, colorableVectorTypes,
    DEFAULT_STROKE_TYPE, normalizeStrokeType, strokeTypeDefinition,
    blendModes, normalizeBlendMode, uiThemes
  } = Catalog;
  const containerObjectTypes = new Set(['group', 'mask-group', 'clip-group']);
  const specialCharacters = Object.freeze([
    ['©', 'Copyright'], ['®', 'Eingetragene Marke'], ['™', 'Markenzeichen'], ['§', 'Paragraf'], ['¶', 'Absatzmarke'], ['•', 'Aufzählungspunkt'], ['·', 'Mittelpunkt'], ['…', 'Auslassungspunkte'],
    ['–', 'Halbgeviertstrich'], ['—', 'Geviertstrich'], ['‹', 'Einfaches Anführungszeichen links'], ['›', 'Einfaches Anführungszeichen rechts'], ['«', 'Guillemet links'], ['»', 'Guillemet rechts'],
    ['„', 'Deutsches Anführungszeichen unten'], ['“', 'Anführungszeichen oben'], ['‘', 'Einfaches Anführungszeichen links oben'], ['’', 'Einfaches Anführungszeichen rechts oben'],
    ['€', 'Euro'], ['£', 'Pfund'], ['¥', 'Yen'], ['¢', 'Cent'], ['±', 'Plusminus'], ['×', 'Multiplikation'], ['÷', 'Division'], ['≠', 'Ungleich'], ['≤', 'Kleiner oder gleich'], ['≥', 'Größer oder gleich'], ['≈', 'Ungefähr gleich'],
    ['°', 'Grad'], ['‰', 'Promille'], ['µ', 'Mikro'], ['Ω', 'Omega'], ['∞', 'Unendlich'], ['√', 'Wurzel'], ['¿', 'Umgekehrtes Fragezeichen'], ['¡', 'Umgekehrtes Ausrufezeichen'],
    ['Æ', 'AE-Ligatur groß'], ['æ', 'AE-Ligatur klein'], ['Œ', 'OE-Ligatur groß'], ['œ', 'OE-Ligatur klein'], ['Ø', 'O mit Schrägstrich groß'], ['ø', 'O mit Schrägstrich klein'], ['Å', 'A mit Ring groß'], ['å', 'A mit Ring klein'],
    ['Ä', 'A Umlaut groß'], ['ä', 'A Umlaut klein'], ['Ö', 'O Umlaut groß'], ['ö', 'O Umlaut klein'], ['Ü', 'U Umlaut groß'], ['ü', 'U Umlaut klein'], ['ß', 'Eszett'], ['Ç', 'C mit Cedille groß'], ['ç', 'C mit Cedille klein'], ['Ñ', 'N mit Tilde groß'], ['ñ', 'N mit Tilde klein'],
    ['À', 'A mit Gravis groß'], ['à', 'A mit Gravis klein'], ['Á', 'A mit Akut groß'], ['á', 'A mit Akut klein'], ['Â', 'A mit Zirkumflex groß'], ['â', 'A mit Zirkumflex klein'], ['Ã', 'A mit Tilde groß'], ['ã', 'A mit Tilde klein'],
    ['É', 'E mit Akut groß'], ['é', 'E mit Akut klein'], ['È', 'E mit Gravis groß'], ['è', 'E mit Gravis klein'], ['Ê', 'E mit Zirkumflex groß'], ['ê', 'E mit Zirkumflex klein'], ['Ë', 'E mit Trema groß'], ['ë', 'E mit Trema klein'],
    ['Í', 'I mit Akut groß'], ['í', 'I mit Akut klein'], ['Î', 'I mit Zirkumflex groß'], ['î', 'I mit Zirkumflex klein'], ['Ï', 'I mit Trema groß'], ['ï', 'I mit Trema klein'],
    ['Ó', 'O mit Akut groß'], ['ó', 'O mit Akut klein'], ['Ô', 'O mit Zirkumflex groß'], ['ô', 'O mit Zirkumflex klein'], ['Õ', 'O mit Tilde groß'], ['õ', 'O mit Tilde klein'],
    ['Ú', 'U mit Akut groß'], ['ú', 'U mit Akut klein'], ['Ù', 'U mit Gravis groß'], ['ù', 'U mit Gravis klein'], ['Û', 'U mit Zirkumflex groß'], ['û', 'U mit Zirkumflex klein'], ['Ý', 'Y mit Akut groß'], ['ý', 'Y mit Akut klein'], ['Ÿ', 'Y mit Trema groß'], ['ÿ', 'Y mit Trema klein']
  ]);
  const specialCharacterPageSize = 30;
  let specialCharacterPage = 0;
  const emojiCategories = Object.freeze([
    { id: 'recent', icon: '🕘', label: 'Zuletzt verwendet', emojis: Object.freeze([]) },
    { id: 'smileys', icon: '😀', label: 'Smileys und Gefühle', emojis: Object.freeze(['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '🙂', '🙃', '😉', '😍', '🥰', '😘', '😋', '😎', '🤩', '🥳', '🤔', '🤗', '🤭', '😴', '😭', '😡', '🤯', '🥶', '😱', '🤓', '🫠', '❤️', '💔']) },
    { id: 'people', icon: '👋', label: 'Menschen und Gesten', emojis: Object.freeze(['👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🫶', '🤟', '🤘', '👍', '👎', '👏', '🙌', '👐', '🤝', '🙏', '💪', '👀', '🧠', '👶', '🧑', '👩', '👨', '🧓', '👩‍💻', '👨‍🎨', '🧑‍🚀', '🦸']) },
    { id: 'animals', icon: '🐻', label: 'Tiere und Natur', emojis: Object.freeze(['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🐔', '🐧', '🐦', '🦄', '🐝', '🦋', '🐞', '🐢', '🐙', '🐠', '🌸', '🌻', '🌲', '🌵', '🍀', '🔥', '🌈']) },
    { id: 'food', icon: '🍕', label: 'Essen und Trinken', emojis: Object.freeze(['🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍒', '🥝', '🍅', '🥑', '🥕', '🌽', '🥐', '🍞', '🧀', '🍔', '🍟', '🍕', '🌮', '🍜', '🍣', '🍰', '🍪', '🍫', '☕', '🍵', '🥤', '🍺', '🍷', '🥂']) },
    { id: 'activities', icon: '⚽', label: 'Aktivitäten', emojis: Object.freeze(['⚽', '🏀', '🏈', '⚾', '🎾', '🏐', '🏓', '🏸', '🥊', '🎯', '🎮', '🕹️', '🎲', '🧩', '🎨', '🎭', '🎤', '🎧', '🎸', '🎹', '🎺', '🥁', '🏆', '🥇', '🚴', '🏊', '⛷️', '🏕️', '🎬', '📷', '🎉', '🎁']) },
    { id: 'travel', icon: '🚗', label: 'Reisen und Orte', emojis: Object.freeze(['🚗', '🚕', '🚌', '🚓', '🚑', '🚒', '🚲', '🏍️', '🚂', '✈️', '🚀', '🛸', '🚁', '⛵', '🚢', '🗺️', '🧭', '🏠', '🏢', '🏰', '🗼', '🗽', '⛲', '⛰️', '🏖️', '🏝️', '🌋', '🌍', '🌙', '⭐', '☀️', '☁️']) },
    { id: 'objects', icon: '💡', label: 'Objekte', emojis: Object.freeze(['⌚', '📱', '💻', '⌨️', '🖨️', '📷', '📺', '📻', '💡', '🔦', '📕', '📚', '📝', '📌', '📎', '✂️', '🔒', '🔑', '🔨', '🪛', '⚙️', '🧲', '🔬', '🔭', '💊', '🧸', '🪴', '🛒', '📦', '✉️', '🔔', '⏰']) },
    { id: 'symbols', icon: '❤️', label: 'Symbole', emojis: Object.freeze(['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💯', '✅', '❌', '⚠️', '❓', '❗', '⭕', '🚫', '♻️', '▶️', '⏸️', '⏹️', '🔀', '🔁', '➕', '➖', '✖️', '➗', '©️', '™️']) }
  ]);
  const emojiHistoryLimit = 24;
  const knownEmojis = new Set(emojiCategories.flatMap((category) => category.emojis));
  const notoEmojiPageSize = 96;
  const notoEmojiFiles = Object.freeze((Array.isArray(globalThis.PYRO_NOTO_EMOJI_FILES) ? globalThis.PYRO_NOTO_EMOJI_FILES : [])
    .filter((name, index, files) => /^emoji_u[0-9a-f_]+\.svg$/.test(name) && files.indexOf(name) === index));
  const notoEmojiFileSet = new Set(notoEmojiFiles);
  const notoEmojiFileIndex = new Map(notoEmojiFiles.map((name, index) => [name, index]));
  const notoEmojiSearch = globalThis.PYRO_NOTO_EMOJI_SEARCH?.entries || {};
  const notoEmojiSearchLocales = Array.isArray(globalThis.PYRO_NOTO_EMOJI_SEARCH?.locales)
    ? globalThis.PYRO_NOTO_EMOJI_SEARCH.locales : ['de', 'en', 'fr', 'ru', 'es', 'zh'];
  const notoEmojiCategories = Object.freeze([
    ...emojiCategories,
    { id: 'all', icon: '✨', label: 'Alle Noto SVG Emojis', emojis: Object.freeze([]) }
  ]);
  const vectorFlagPageSize = 60;
  const vectorFlagLocales = Array.isArray(globalThis.PYRO_FLAG_ICONS_FLAGS?.locales)
    ? globalThis.PYRO_FLAG_ICONS_FLAGS.locales : ['de', 'en', 'fr', 'ru', 'es', 'zh'];
  const vectorFlags = Object.freeze((Array.isArray(globalThis.PYRO_FLAG_ICONS_FLAGS?.entries)
    ? globalThis.PYRO_FLAG_ICONS_FLAGS.entries : []).filter((entry, index, entries) => (
    entry && /^[A-Z]{2,5}(?:-[A-Z]{2,3})?$/.test(entry.code) && /^[A-Z0-9-]+\.svg$/.test(entry.file)
    && entries.findIndex((candidate) => candidate?.code === entry.code) === index
  )));
  const vectorFlagByCode = new Map(vectorFlags.map((entry) => [entry.code, entry]));
  const vectorFlagFileIndex = new Map(vectorFlags.map((entry, index) => [entry.file, index]));
  let activeEmojiCategory = 'smileys';
  let recentEmojis = [];
  let activeNotoEmojiCategory = 'smileys';
  let selectedNotoEmojiFile = 'emoji_u1f600.svg';
  let recentNotoEmojiFiles = [];
  let notoEmojiPage = 0;
  let notoEmojiSearchQuery = '';
  let notoEmojiInsertionPending = false;
  const notoEmojiMarkupCache = new Map();
  const notoEmojiSourceCache = new Map();
  const notoEmojiSourceChunkPromises = new Map();
  let selectedVectorFlagCode = 'DE';
  let vectorFlagPage = 0;
  let vectorFlagSearchQuery = '';
  let vectorFlagInsertionPending = false;
  const vectorFlagMarkupCache = new Map();
  const vectorFlagSourceCache = new Map();
  const vectorFlagSourceChunkPromises = new Map();
  globalThis.PYRO_NOTO_EMOJI_SOURCE_BRIDGE = Object.freeze({
    accept(entries) {
      if (!entries || typeof entries !== 'object') return;
      Object.entries(entries).forEach(([filename, source]) => {
        if (notoEmojiFileSet.has(filename) && typeof source === 'string') notoEmojiSourceCache.set(filename, source);
      });
    }
  });
  globalThis.PYRO_FLAG_ICONS_SOURCE_BRIDGE = Object.freeze({
    accept(entries) {
      if (!entries || typeof entries !== 'object') return;
      Object.entries(entries).forEach(([filename, source]) => {
        if (vectorFlagFileIndex.has(filename) && typeof source === 'string') vectorFlagSourceCache.set(filename, source);
      });
    }
  });
  const supportsStrokeType = (object) => Boolean(object && (object.type === 'text' || object.type === 'text-outline' || colorableVectorTypes.has(object.type)));
  const supportsObjectFill = (object) => Boolean(object && (fillableVectorShapeTypes.has(object.type) || ['text', 'text-outline'].includes(object.type) || (object.type === 'polyline' && object.closed === true) || (AutoShapes.isAutoShape(object) && AutoShapes.contours(object).length)));
  const supportsAppearanceFill = (object) => Boolean(object && (supportsObjectFill(object) || object.type === 'image'));
  const bitmapModeFor = (object) => object?.type === 'image' ? BitmapModel.modeFor(object) : null;
  const bitmapInterpolationFor = (object) => BitmapModel.interpolationFor(object);
  const supportsAdvancedFill = (object) => Boolean(supportsObjectFill(object)
    && !['image', ...containerObjectTypes].includes(object.type)
    && !(object.type === 'pen' && object.closed !== true)
    && !(object.type === 'polyline' && object.closed !== true));
  const strokeStackFor = (object) => supportsStrokeType(object) ? StrokeStyle.stackFor(object) : [];
  const primaryStrokeStyle = (object) => strokeStackFor(object)[0] || StrokeStyle.legacyStyle(object);
  const objectStrokeType = (object) => {
    if (!supportsStrokeType(object)) return 'none';
    const style = primaryStrokeStyle(object);
    return StrokeStyle.isVisible(style) ? normalizeStrokeType(style.strokeType) : 'none';
  };
  function strokeStyleAttributes(object, rawStyle = primaryStrokeStyle(object)) {
    const style = StrokeStyle.normalizeStyle(rawStyle, { fallback: StrokeStyle.legacyStyle(object) });
    const strokeType = StrokeStyle.isVisible(style) ? normalizeStrokeType(style.strokeType) : 'none';
    const dashes = StrokeStyle.renderedDashArray(style);
    const primaryId = primaryStrokeStyle(object).id;
    const gradientStroke = style.id === primaryId ? normalizedObjectStrokePaint(object) : null;
    return {
      style,
      strokeType,
      attributes: {
        stroke: strokeType === 'none' ? 'none' : (object?.type === 'eraser' ? state.doc.bg : gradientStroke ? `url(#${strokePaintId(object)})` : style.color),
        'stroke-width': style.width,
        'stroke-linecap': style.lineCap,
        'stroke-linejoin': style.lineJoin,
        'stroke-miterlimit': style.miterLimit,
        'stroke-dasharray': dashes.length ? dashes.join(' ') : undefined,
        'stroke-dashoffset': dashes.length ? style.dashOffset : undefined,
        'stroke-opacity': object?.svgStrokeOpacity == null ? undefined : clamp(num(object.svgStrokeOpacity, 1), 0, 1),
        'data-stroke-type': strokeType,
        'data-stroke-pattern': style.pattern,
        'data-stroke-alignment': style.alignment
      }
    };
  }
  const strokeTypeLabel = (value) => value === 'Gemischt' ? value : strokeTypeDefinition(value).label;
  let currentEraserTool = 'eraser';
  let eraserPress = null;
  let eraserPressTimer = null;
  let suppressEraserClick = false;
  let currentRetouchTool = 'clone';
  let retouchPress = null;
  let retouchPressTimer = null;
  let suppressRetouchClick = false;
  let currentWarpTool = 'liquify';
  let warpPress = null;
  let warpPressTimer = null;
  let suppressWarpClick = false;
  const warpGridDensities = { 'mesh-warp': 4, 'shape-grid': 7 };
  let currentMarqueeTool = 'marquee';
  let marqueePress = null;
  let marqueePressTimer = null;
  let suppressMarqueeClick = false;
  let currentLineTool = 'line';
  let linePress = null;
  let linePressTimer = null;
  let suppressLineClick = false;
  let currentPathModifierTool = 'freeform';
  let pathModifierPress = null;
  let pathModifierPressTimer = null;
  let suppressPathModifierClick = false;
  let currentShapeTool = 'rectangle';
  let shapePress = null;
  let shapePressTimer = null;
  let suppressShapeClick = false;
  let fillStopSelection = { objectId: null, stopId: null };
  let strokeSelection = { objectId: null, strokeId: null };
  let bitmapImportChoiceResolver = null;
  let bitmapRasterizeResolver = null;
  let bitmapPropertyResizeOrigin = null;
  let layerObjectDrag = null;
  let pixelDialogReturnFocus = null;
  let cropDialogReturnFocus = null;

  const makeLayer = (name = `${I18n.t('document.layer')} 1`) => ({ id: uid('layer'), name, visible: true, locked: false, blendMode: 'normal', objects: [] });
  const initialLayer = makeLayer();
  const inspectorSectionsStorageKey = 'pyro-property-dock-sections-v2';
  const inspectorAccordionSectionsStorageKey = 'pyro-property-dock-accordion-sections-v1';
  const rightSidebarSectionsStorageKey = 'pyro-right-sidebar-sections-v1';
  const propertyDockCompactStorageKey = 'pyro-property-dock-compact-v1';
  const propertyDockAccordionStorageKey = 'pyro-property-dock-accordion-v1';
  const toolColorsStorageKey = 'pyro-tool-colors-v1';
  const eraserSettingsStorageKey = 'pyro-eraser-settings-v1';
  const tabSessionEnabledStorageKey = 'pyro-save-tabs-on-exit-v1';
  const tabSessionStorageKey = 'pyro-tab-session-v1';
  const tabSessionCommitStorageKey = 'pyro-tab-session-commit-v2';
  const tabSessionSlotStorageKeys = Object.freeze({ a: 'pyro-tab-session-slot-a-v3', b: 'pyro-tab-session-slot-b-v3' });
  const snapSettingsStorageKey = 'pyro-snap-settings-v1';
  const emojiRecentStorageKey = 'pyro-recent-emojis-v1';
  const vectorEmojiSettingsStorageKey = 'pyro-vector-emoji-settings-v1';
  const vectorFlagSettingsStorageKey = 'pyro-vector-flag-settings-v1';
  const performanceProfilerStorageKey = 'pyro-performance-profiler-enabled-v1';
  const tabSessionMaxCharacters = 3_000_000;
  let tabSessionStartupWarning = '';
  let tabSessionSavedForNavigation = false;
  const validToolColor = (value, fallback) => /^#[0-9a-f]{6}$/i.test(String(value || '')) ? String(value).toLowerCase() : fallback;
  const persistToolColors = () => {
    try {
      localStorage.setItem(toolColorsStorageKey, JSON.stringify({
        stroke: validToolColor($('strokeColor')?.value, '#172033'),
        fill: validToolColor($('fillColor')?.value, '#4f6df5'),
        noFill: Boolean($('noFill')?.checked)
      }));
    } catch { /* Lokale Einstellung ist optional. */ }
  };
  const restoreToolColors = () => {
    try {
      const saved = JSON.parse(localStorage.getItem(toolColorsStorageKey) || '{}');
      if (!saved || typeof saved !== 'object' || Array.isArray(saved)) return;
      $('strokeColor').value = validToolColor(saved.stroke, '#172033');
      $('fillColor').value = validToolColor(saved.fill, '#4f6df5');
      $('noFill').checked = Boolean(saved.noFill);
    } catch { /* Defekte oder nicht verfügbare Einstellungen fallen auf die Standardfarben zurück. */ }
  };
  const persistEraserToolSettings = () => {
    try {
      localStorage.setItem(eraserSettingsStorageKey, JSON.stringify({
        tool: eraserToolInfo[currentEraserTool] ? currentEraserTool : 'eraser',
        size: clamp(Math.round(num($('eraserSize')?.value, 1)), 1, 128),
        shape: $('eraserShape')?.value === 'circle' ? 'circle' : 'square',
        softness: clamp(Math.round(num($('eraserSoftness')?.value, 0)), 0, 100),
        tolerance: clamp(Math.round(num($('colorEraserTolerance')?.value, 18)), 0, 100)
      }));
    } catch { /* Lokale Werkzeugvorgaben sind optional. */ }
  };
  const restoreEraserToolSettings = () => {
    try {
      const saved = JSON.parse(localStorage.getItem(eraserSettingsStorageKey) || '{}');
      if (!saved || typeof saved !== 'object' || Array.isArray(saved)) return;
      currentEraserTool = eraserToolInfo[saved.tool] ? saved.tool : 'eraser';
      $('eraserSize').value = String(clamp(Math.round(num(saved.size, 1)), 1, 128));
      $('eraserShape').value = saved.shape === 'circle' ? 'circle' : 'square';
      $('eraserSoftness').value = String(clamp(Math.round(num(saved.softness, 0)), 0, 100));
      $('colorEraserTolerance').value = String(clamp(Math.round(num(saved.tolerance, 18)), 0, 100));
    } catch { /* Defekte oder nicht verfügbare Einstellungen fallen auf die Standardwerte zurück. */ }
  };
  const loadInspectorSectionStates = () => {
    try {
      const stored = JSON.parse(localStorage.getItem(inspectorSectionsStorageKey) || '{}');
      if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return {};
      const clean = Object.create(null);
      Object.entries(stored).slice(0, 256).forEach(([context, sections]) => {
        if (Security.isBlockedObjectKey(context) || context.length > 160 || !sections || typeof sections !== 'object' || Array.isArray(sections)) return;
        clean[context] = Object.fromEntries(Object.entries(sections).slice(0, 64)
          .filter(([section, expanded]) => !Security.isBlockedObjectKey(section) && section.length <= 128 && typeof expanded === 'boolean'));
      });
      return clean;
    } catch { return {}; }
  };
  const loadPropertyDockCompact = () => {
    try { return localStorage.getItem(propertyDockCompactStorageKey) === 'true'; }
    catch { return false; }
  };
  const loadPropertyDockAccordion = () => {
    try { return localStorage.getItem(propertyDockAccordionStorageKey) === 'true'; }
    catch { return false; }
  };
  const loadInspectorAccordionSections = () => {
    try {
      const stored = JSON.parse(localStorage.getItem(inspectorAccordionSectionsStorageKey) || '{}');
      if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return {};
      return Object.fromEntries(Object.entries(stored).slice(0, 256)
        .filter(([context, section]) => !Security.isBlockedObjectKey(context) && context.length <= 160 && typeof section === 'string' && section.length <= 128));
    } catch { return {}; }
  };
  const loadTabSessionEnabled = () => {
    try { return localStorage.getItem(tabSessionEnabledStorageKey) === 'true'; }
    catch { return false; }
  };
  const loadPerformanceProfilerEnabled = () => {
    try { return localStorage.getItem(performanceProfilerStorageKey) !== 'false'; }
    catch { return true; }
  };
  const loadSnapSettings = () => {
    try { return Snap.normalizeSettings(JSON.parse(localStorage.getItem(snapSettingsStorageKey) || '{}')); }
    catch { return Snap.normalizeSettings(); }
  };
  const state = {
    doc: { width: 960, height: 640, dpi: 72, bg: '#ffffff', layers: [initialLayer], activeLayerId: initialLayer.id, guides: { horizontal: [], vertical: [], locked: false }, grid: { visible: false, snap: false, size: 10 } },
    tool: 'select', selected: [], zoom: 1, pixelGrid: true, snap: true, canvasAspectLocked: false, canvasAspectRatio: 960 / 640,
    history: [], historyIndex: -1, historyBytes: 0, historyAssets: makeHistoryAssetStore(), savedSnapshot: null, savedTitle: 'Unbenannt', clipboard: [], sharedClipboard: [], sharedClipboardResources: null, resourceClipboard: null, effectClipboard: [], gesture: null, spaceDown: false,
    pendingTextPoint: null, editingTextId: null, inlineTextSession: null, textClick: null, dirty: false, snapGuides: [], guidePreview: null, pixelSession: null, bitmapEditSession: null, bitmapMarquee: null, wandSeed: null, wandRequest: null, retouchSource: null, cropSession: null, eraserCursorPoint: null, pathModifierCursorPoint: null, warpCursorPoint: null, warpSession: null, warpPreparation: null, liquifyBase: null, vectorPoint: null, vectorPoints: [], vectorSegments: [], vectorClick: null, localFontFamilies: [], bundledFontFamilies: [], bundledFontCategories: {}, installedGoogleFontFamilies: [], inspectorSectionStates: loadInspectorSectionStates(), inspectorAccordionSections: loadInspectorAccordionSections(),
    expandedLayerIds: new Set([initialLayer.id]), documents: [], activeDocumentId: null, hasSavedVersion: false, propertyDockCompact: loadPropertyDockCompact(), propertyDockAccordion: loadPropertyDockAccordion(), saveTabsOnExit: loadTabSessionEnabled(), canvasResizeMode: false,
    freeTransformMode: null, snapSettings: loadSnapSettings()
  };
  let documentPixelPreviewRevision = 0;
  let documentPixelPreviewRenderedRevision = -1;
  let documentPixelPreviewToken = 0;
  let documentPixelPreviewTimer = null;
  let documentPixelPreviewUrl = '';
  const debugEntries = [];
  const performanceEntries = [];
  const performanceLongTasks = [];
  const performancePhaseTotals = Object.create(null);
  const performancePhaseCounts = Object.create(null);
  const performanceStaticUiLabels = new Set([...I18n.availableLanguages]
    .flatMap((language) => Object.values(I18n.catalog(language)))
    .filter((value) => typeof value === 'string' && value && !value.includes('{')));
  let consoleEntrySequence = 0;
  let performanceProfilerEnabled = loadPerformanceProfilerEnabled();
  let performanceProfilerBound = false;
  let performanceLongTaskObserver = null;
  let activePerformanceGestureAction = null;
  let lastSvgImportReport = null;

  function recordPerformancePhase(name, durationMs) {
    if (!performanceProfilerEnabled || !name) return;
    performancePhaseTotals[name] = num(performancePhaseTotals[name]) + Math.max(0, num(durationMs));
    performancePhaseCounts[name] = num(performancePhaseCounts[name]) + 1;
  }

  function measurePerformancePhase(name, callback) {
    if (!performanceProfilerEnabled) return callback();
    const startedAt = performance.now();
    try { return callback(); }
    finally { recordPerformancePhase(name, performance.now() - startedAt); }
  }

  function performancePhaseSnapshot() {
    return {
      totals: { ...performancePhaseTotals },
      counts: { ...performancePhaseCounts }
    };
  }

  const initialDocument = { id: uid('document'), title: 'Unbenannt', automaticTitle: true, doc: state.doc, selected: [], history: [], historyIndex: -1, historyBytes: 0, historyAssets: state.historyAssets, savedSnapshot: null, savedTitle: 'Unbenannt', clipboard: [], dirty: false, hasSavedVersion: false, autoSaveEnabled: false, expandedLayerIds: new Set([initialLayer.id]), zoom: 1, scrollLeft: 0, scrollTop: 0, fileHandle: null };
  state.documents.push(initialDocument); state.activeDocumentId = initialDocument.id;

  const activeLayer = () => state.doc.layers.find((layer) => layer.id === state.doc.activeLayerId) || state.doc.layers.at(-1);
  const allObjects = () => state.doc.layers.flatMap((layer) => layer.objects);
  const objectTree = (object) => [object, ...(Array.isArray(object.children) ? object.children.flatMap(objectTree) : [])];
  const renderableObjects = () => allObjects().flatMap(objectTree);
  const objectEntryIndexes = new WeakMap();
  const reidentifyObjectTree = (object) => {
    const copy = deepClone(object); copy.id = uid('object');
    if (Array.isArray(copy.children)) copy.children = copy.children.map(reidentifyObjectTree);
    return copy;
  };
  const cleanObject = (object) => Core.sanitizeObject(object, { createId: uid }).object;
  const buildObjectEntryIndex = (document) => {
    const entries = new Map();
    for (const layer of document.layers) {
      const pending = layer.objects.map((object, index) => ({ object, layer, index, parent: null, container: layer.objects }));
      while (pending.length) {
        const entry = pending.pop();
        if (!entry?.object || entries.has(entry.object.id)) continue;
        entries.set(entry.object.id, entry);
        if (!Array.isArray(entry.object.children)) continue;
        entry.object.children.forEach((object, index) => {
          pending.push({ object, layer, index, parent: entry.object, container: entry.object.children });
        });
      }
    }
    objectEntryIndexes.set(document, entries);
    return entries;
  };
  const cachedObjectEntryIsCurrent = (entry, entries, seen = new Set()) => {
    if (!entry?.object || !entry.container || entry.container[entry.index] !== entry.object || seen.has(entry.object)) return false;
    seen.add(entry.object);
    if (!entry.parent) return entry.layer.objects === entry.container && state.doc.layers.includes(entry.layer);
    if (entry.parent.children !== entry.container) return false;
    return cachedObjectEntryIsCurrent(entries.get(entry.parent.id), entries, seen);
  };
  const objectEntry = (id) => {
    if (!id) return null;
    let entries = objectEntryIndexes.get(state.doc);
    let entry = entries?.get(id);
    if (entry && cachedObjectEntryIsCurrent(entry, entries)) return entry;
    entries = buildObjectEntryIndex(state.doc);
    entry = entries.get(id);
    return entry && cachedObjectEntryIsCurrent(entry, entries) ? entry : null;
  };
  const selectedObjects = () => state.selected.map((id) => {
    const entry = objectEntry(id);
    return entry && entry.layer.visible && !entry.layer.locked && entry.object.visible !== false && !entry.object.locked ? entry.object : null;
  }).filter(Boolean);
  const primaryObject = () => objectEntry(state.selected.at(-1))?.object || null;
  const alignmentKeyObjectId = (objects = selectedObjects()) => (
    $('alignTarget')?.value === 'key' && objects.length > 1 ? objects.at(-1)?.id || null : null
  );
  const layerForObject = (id) => objectEntry(id)?.layer || null;

  const reusableLibrary = () => ReusableAssets.ensureLibrary(state.doc);
  const symbolDefinition = (id) => reusableLibrary().symbols.find((entry) => entry.id === id) || null;

  function symbolRenderObject(instance) {
    if (instance?.type !== 'symbol-instance') return instance;
    const definition = symbolDefinition(instance.symbolId); if (!definition?.object) return null;
    const source = deepClone(definition.object);
    source.id = instance.id; source.x = 0; source.y = 0; source.rotation = 0; source.flipX = false; source.flipY = false; source.visible = true; source.locked = false;
    delete source.symbolMasterId; return source;
  }

  function symbolDefinitionObject(master, objectId = uid('symbol-object')) {
    const scaleX = Math.max(.0001, num(master.symbolMasterScaleX, 1)); const scaleY = Math.max(.0001, num(master.symbolMasterScaleY, 1));
    const source = Math.abs(scaleX - 1) > .0001 || Math.abs(scaleY - 1) > .0001 ? scaleObjectTree(master, 1 / scaleX, 1 / scaleY) : deepClone(master); source.id = objectId;
    source.x = 0; source.y = 0; source.rotation = 0; source.flipX = false; source.flipY = false; source.visible = true; source.locked = false;
    delete source.symbolMasterId; delete source.symbolMasterScaleX; delete source.symbolMasterScaleY; delete source.transformPivot;
    if (source.type === 'text' && source.pathBinding) delete source.pathBinding;
    return source;
  }

  function syncSymbolMasterObject(master) {
    if (!master?.symbolMasterId) return false;
    const definition = symbolDefinition(master.symbolMasterId); if (!definition) { delete master.symbolMasterId; return false; }
    const previousWidth = Math.max(.0001, num(definition.object?.width, master.width)); const previousHeight = Math.max(.0001, num(definition.object?.height, master.height));
    const next = symbolDefinitionObject(master, definition.object?.id || uid('symbol-object'));
    if (JSON.stringify(next) === JSON.stringify(definition.object)) return false;
    definition.object = next; definition.name = master.name || definition.name;
    renderableObjects().filter((object) => object.type === 'symbol-instance' && object.symbolId === definition.id).forEach((instance) => {
      const scaleX = num(instance.width, previousWidth) / previousWidth; const scaleY = num(instance.height, previousHeight) / previousHeight;
      instance.width = Math.max(.0001, next.width * scaleX); instance.height = Math.max(.0001, next.height * scaleY);
    });
    return true;
  }

  function syncSymbolMasters(objects = renderableObjects()) {
    let changed = false; objects.filter((object) => object?.symbolMasterId).forEach((object) => { changed = syncSymbolMasterObject(object) || changed; }); return changed;
  }

  function cleanDocument(raw) {
    return Core.sanitizeDocument(raw, { createId: uid }).document;
  }

  const historyMaxEntries = 100;
  const historyMaxBytes = 128 * 1024 * 1024;

  function makeHistoryAssetStore() {
    return HistoryAssets.createStore();
  }

  function compactHistoryDocument(document, store = state.historyAssets) {
    return HistoryAssets.compactDocument(document, store, { createId: uid });
  }

  function hydrateHistoryDocument(value, store = state.historyAssets) {
    return HistoryAssets.hydrateDocument(value, store);
  }

  function snapshot(document = state.doc, store = state.historyAssets, { canonicalize = true } = {}) {
    return measurePerformancePhase('history.snapshot.total', () => {
      // History, gespeicherter Checkpoint und Restore vergleichen dieselbe
      // schema-kanonische Darstellung. Andernfalls fuegt sanitizeDocument beim
      // Undo harmlose Standardfelder hinzu (z. B. flipX: false), wodurch ein
      // unveraenderter gespeicherter Stand faelschlich als dirty erschien.
      if (document === state.doc) measurePerformancePhase('history.snapshot.symbols', syncSymbolMasters);
      const canonical = canonicalize
        ? measurePerformancePhase('history.snapshot.canonicalize', () => Core.sanitizeDocument(document, { createId: uid }).document)
        : document;
      const compact = measurePerformancePhase('history.snapshot.compact', () => compactHistoryDocument(canonical, store));
      return measurePerformancePhase('history.snapshot.stringify', () => JSON.stringify(compact));
    });
  }

  function historyAssetBytes(store = state.historyAssets) {
    return HistoryAssets.storeBytes(store);
  }

  function pruneHistoryAssets() {
    HistoryAssets.prune(state.historyAssets, [...state.history.map((entry) => entry.value), state.savedSnapshot]);
  }

  function updateHistoryBytes() {
    state.historyBytes = state.history.reduce((sum, entry) => sum + (entry.bytes || entry.value.length * 2), 0) + historyAssetBytes();
  }

  function trimHistory({ pruneAssets = false } = {}) {
    let removedEntries = false;
    updateHistoryBytes();
    while (state.history.length > 2 && (state.history.length > historyMaxEntries || state.historyBytes > historyMaxBytes)) {
      const removed = state.history.shift(); state.historyIndex -= 1; state.historyBytes -= removed.bytes || removed.value.length * 2;
      removedEntries = true;
    }
    if (pruneAssets || removedEntries) pruneHistoryAssets();
    updateHistoryBytes();
  }

  function refreshDirtyState(currentSnapshot = null) {
    const document = activeDocument();
    const title = document?.title || 'Unbenannt';
    state.dirty = state.savedSnapshot == null || (currentSnapshot ?? snapshot()) !== state.savedSnapshot || title !== state.savedTitle;
    if (document) document.dirty = state.dirty;
    updateCommandState();
    return state.dirty;
  }

  function initializeHistory(label = 'Start', { clean = true } = {}) {
    const value = snapshot();
    state.history = [{ id: uid('history'), value, label, bytes: value.length * 2, createdAt: Date.now() }];
    state.historyIndex = 0;
    if (clean) {
      state.savedSnapshot = value;
      state.savedTitle = activeDocument()?.title || 'Unbenannt';
    }
    trimHistory({ pruneAssets: true }); refreshDirtyState(value);
  }

  function commitHistory(label = 'Änderung', { canonicalize = true } = {}) {
    return measurePerformancePhase('history.commit.total', () => {
      const value = snapshot(state.doc, state.historyAssets, { canonicalize });
      if (state.history[state.historyIndex]?.value === value) { refreshDirtyState(value); return false; }
      const removedRedoEntries = state.historyIndex + 1 < state.history.length;
      state.history.splice(state.historyIndex + 1);
      state.history.push({ id: uid('history'), value, label, bytes: value.length * 2, createdAt: Date.now() });
      state.historyIndex = state.history.length - 1;
      measurePerformancePhase('history.trim', () => trimHistory({ pruneAssets: removedRedoEntries }));
      refreshDirtyState(value);
      return true;
    });
  }

  function restoreHistory(index) {
    const entry = state.history[index];
    if (!entry) return;
    state.doc = cleanDocument(hydrateHistoryDocument(entry.value));
    state.expandedLayerIds = new Set(state.doc.layers.filter((layer) => state.expandedLayerIds.has(layer.id) || layer.id === state.doc.activeLayerId).map((layer) => layer.id));
    state.historyIndex = index;
    state.selected = []; state.bitmapEditSession = null; state.liquifyBase = null; state.freeTransformMode = null;
    clearVectorSubselection();
    refreshDirtyState();
    renderAll();
    toast(entry.label);
  }

  function undo() {
    if (state.warpSession) { if (state.warpSession.dirty) resetWarpSession(); else cancelWarpSession(); return; }
    if (state.historyIndex > 0) restoreHistory(state.historyIndex - 1);
  }
  function redo() {
    if (state.warpSession || state.warpPreparation) { cancelWarpSession(); return; }
    if (state.historyIndex < state.history.length - 1) restoreHistory(state.historyIndex + 1);
  }

  function beginDocumentTransaction(label = 'Änderung', { reuseCurrentHistory = false } = {}) {
    const currentHistory = state.history[state.historyIndex]?.value;
    return {
      id: uid('transaction'),
      label,
      before: reuseCurrentHistory && currentHistory ? currentHistory : snapshot(),
      reusedHistory: Boolean(reuseCurrentHistory && currentHistory),
      selected: [...state.selected],
      activeLayerId: state.doc.activeLayerId,
      startedAt: performance.now()
    };
  }

  function rollbackDocumentTransaction(transaction) {
    if (!transaction?.before) return false;
    state.doc = cleanDocument(hydrateHistoryDocument(transaction.before));
    state.doc.activeLayerId = state.doc.layers.some((layer) => layer.id === transaction.activeLayerId)
      ? transaction.activeLayerId
      : state.doc.activeLayerId;
    state.selected = transaction.selected.filter((id) => objectEntry(id));
    clearVectorSubselection();
    return true;
  }

  function cancelActiveGesture({ render = true } = {}) {
    const gesture = state.gesture; if (!gesture) return false;
    gesture.cancelled = true;
    if (gesture.kind === 'warp-control') {
      gesture.session.destinationPoints = gesture.originPoints.map((point) => ({ ...point }));
      gesture.session.dirty = gesture.session.destinationPoints.some((point, index) => Math.abs(point.x - gesture.session.sourcePoints[index].x) > .01 || Math.abs(point.y - gesture.session.sourcePoints[index].y) > .01);
      state.gesture = null; scheduleWarpPreview(gesture.session); updateWarpActionState();
      if (render) renderEditorOverlays(); return true;
    }
    if (gesture.kind === 'puppet-pin') {
      if (gesture.added) gesture.session.pins = gesture.session.pins.filter((pin) => pin.id !== gesture.pinId);
      else {
        const pin = gesture.session.pins.find((entry) => entry.id === gesture.pinId); if (pin) pin.target = { ...gesture.originTarget };
      }
      gesture.session.dirty = gesture.session.pins.some((pin) => !pin.locked && (Math.abs(pin.target.x - pin.source.x) > .01 || Math.abs(pin.target.y - pin.source.y) > .01));
      state.gesture = null; scheduleWarpPreview(gesture.session); updateWarpActionState();
      if (render) renderEditorOverlays(); return true;
    }
    if (gesture.kind === 'stretch-select' || gesture.kind === 'stretch-handle') {
      const session = gesture.session;
      session.selection = gesture.originSelection ? { ...gesture.originSelection } : null;
      session.dirty = Boolean(gesture.originDirty);
      state.gesture = null;
      if (session.selection && session.dirty) scheduleWarpPreview(session);
      else { session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0); }
      updateWarpActionState(); if (render) renderEditorOverlays(); return true;
    }
    if (gesture.kind === 'bitmap') {
      const session = gesture.session;
      if (session?.previewFrame) cancelAnimationFrame(session.previewFrame);
      detachDirectBitmapPreview(session);
      if (state.bitmapEditSession === session) state.bitmapEditSession = null;
    }
    if (gesture.kind === 'canvas-resize') {
      refs.shell.style.transform = gesture.shellTransform || '';
      refs.workspace.style.cursor = '';
    }
    rollbackDocumentTransaction(gesture.transaction);
    state.gesture = null; state.snapGuides = []; state.guidePreview = null;
    refs.artboard.style.cursor = cursorForTool();
    syncPixelToolOptions();
    if (render) renderAll();
    return true;
  }

  const activeDocument = () => state.documents.find((document) => document.id === state.activeDocumentId) || state.documents[0];

  function persistActiveDocument() {
    const document = activeDocument(); if (!document) return;
    document.doc = state.doc; document.selected = [...state.selected]; document.history = state.history; document.historyIndex = state.historyIndex;
    document.historyBytes = state.historyBytes; document.historyAssets = state.historyAssets; document.savedSnapshot = state.savedSnapshot; document.savedTitle = state.savedTitle;
    document.clipboard = state.clipboard; document.dirty = state.dirty; document.hasSavedVersion = state.hasSavedVersion; document.expandedLayerIds = new Set(state.expandedLayerIds);
    document.zoom = state.zoom; document.scrollLeft = refs.workspace.scrollLeft; document.scrollTop = refs.workspace.scrollTop;
  }

  function loadDocument(document) {
    state.doc = document.doc; state.selected = [...document.selected]; state.history = document.history; state.historyIndex = document.historyIndex;
    state.historyBytes = document.historyBytes || 0; state.historyAssets = document.historyAssets || makeHistoryAssetStore(); state.savedSnapshot = document.savedSnapshot ?? null; state.savedTitle = document.savedTitle || document.title;
    state.clipboard = document.clipboard; state.dirty = document.dirty; state.hasSavedVersion = Boolean(document.hasSavedVersion); state.expandedLayerIds = new Set(document.expandedLayerIds);
    state.zoom = document.zoom || 1; state.gesture = null; state.pixelSession = null; state.bitmapEditSession = null; state.bitmapMarquee = null; state.wandSeed = null; state.wandRequest = null; state.retouchSource = null; state.cropSession = null; state.eraserCursorPoint = null; state.pathModifierCursorPoint = null; state.warpCursorPoint = null; state.warpSession = null; state.warpPreparation = null; state.liquifyBase = null; state.freeTransformMode = null; clearVectorSubselection(); state.canvasResizeMode = false; refs.shell.style.transform = '';
    if (state.canvasAspectLocked) state.canvasAspectRatio = state.doc.width / state.doc.height;
  }

  function documentObjectIds(document) {
    const ids = new Set();
    const collect = (object) => {
      if (object?.id) ids.add(object.id);
      if (Array.isArray(object?.children)) object.children.forEach(collect);
    };
    document.layers.forEach((layer) => layer.objects.forEach(collect));
    return ids;
  }

  function tabSessionPayload() {
    persistActiveDocument();
    return {
      version: 1,
      savedAt: new Date().toISOString(),
      activeDocumentId: state.activeDocumentId,
      documents: state.documents.map((document) => ({
        id: document.id,
        title: document.title,
        automaticTitle: Boolean(document.automaticTitle),
        exampleDocument: Boolean(document.exampleDocument),
        document: cleanDocument(document.doc),
        selected: [...document.selected],
        dirty: Boolean(document.dirty),
        hasSavedVersion: Boolean(document.hasSavedVersion),
        savedTitle: document.savedTitle || document.title,
        expandedLayerIds: [...document.expandedLayerIds],
        zoom: clamp(num(document.zoom, 1), .06, 64),
        scrollLeft: Math.max(0, num(document.scrollLeft)),
        scrollTop: Math.max(0, num(document.scrollTop))
      }))
    };
  }

  function clearStoredTabSession({ disable = false, warning = '' } = {}) {
    try {
      localStorage.removeItem(tabSessionStorageKey);
      localStorage.removeItem(tabSessionCommitStorageKey);
      Object.values(tabSessionSlotStorageKeys).forEach((key) => localStorage.removeItem(key));
      if (disable) localStorage.setItem(tabSessionEnabledStorageKey, 'false');
    } catch { /* Ein nicht verfügbarer Browserspeicher soll den Programmstart nicht blockieren. */ }
    if (disable) state.saveTabsOnExit = false;
    if (warning) tabSessionStartupWarning = warning;
  }

  function saveTabSession({ navigation = false } = {}) {
    if (!state.saveTabsOnExit) return false;
    if (navigation && tabSessionSavedForNavigation) return true;
    try {
      const payload = tabSessionPayload();
      const serialized = JSON.stringify(payload);
      if (serialized.length > tabSessionMaxCharacters) throw new Error('Tab-Sitzung ist zu groß');
      const previousCommit = JSON.parse(localStorage.getItem(tabSessionCommitStorageKey) || 'null');
      const slot = previousCommit?.version === 3 && previousCommit.slot === 'a' ? 'b' : 'a';
      // Erst in den inaktiven Slot schreiben und erst danach den kleinen
      // Commit-Pointer umschalten. Scheitert einer der Writes, bleibt der
      // vorherige, vollständig bestätigte Restore-Stand unangetastet.
      localStorage.setItem(tabSessionSlotStorageKeys[slot], serialized);
      localStorage.setItem(tabSessionCommitStorageKey, JSON.stringify({
        version: 3,
        slot,
        length: serialized.length,
        savedAt: payload.savedAt
      }));
      localStorage.removeItem(tabSessionStorageKey);
      if (navigation) tabSessionSavedForNavigation = true;
      return true;
    } catch {
      tabSessionStartupWarning = 'Die Tab-Sitzung konnte nicht aktualisiert werden. Der letzte vollständig gespeicherte Stand bleibt erhalten.';
      return false;
    }
  }

  function restoredTabDocument(source, usedIds) {
    if (!source || typeof source !== 'object' || !source.document) return null;
    const doc = cleanDocument(source.document); const objectIds = documentObjectIds(doc);
    let id = String(source.id || '').slice(0, 128);
    if (!id || usedIds.has(id)) id = uid('document');
    usedIds.add(id);
    const title = normalizeDocumentTitle(source.title, 'Unbenannt');
    const historyAssets = makeHistoryAssetStore();
    const value = JSON.stringify(compactHistoryDocument(doc, historyAssets));
    const dirty = Boolean(source.dirty);
    return {
      id, title, automaticTitle: Boolean(source.automaticTitle), exampleDocument: Boolean(source.exampleDocument), doc,
      selected: Array.isArray(source.selected) ? source.selected.filter((objectId) => objectIds.has(objectId)) : [],
      history: [{ id: uid('history'), value, label: 'Sitzung wiederhergestellt', bytes: value.length * 2, createdAt: Date.now() }],
      historyIndex: 0,
      historyBytes: value.length * 2 + historyAssetBytes(historyAssets),
      historyAssets,
      savedSnapshot: dirty ? null : value,
      savedTitle: dirty ? String(source.savedTitle || title) : title,
      clipboard: [], dirty,
      hasSavedVersion: Boolean(source.hasSavedVersion),
      autoSaveEnabled: false,
      expandedLayerIds: new Set(Array.isArray(source.expandedLayerIds)
        ? source.expandedLayerIds.filter((layerId) => doc.layers.some((layer) => layer.id === layerId))
        : doc.layers.map((layer) => layer.id)),
      zoom: clamp(num(source.zoom, 1), .06, 64),
      scrollLeft: Math.max(0, num(source.scrollLeft)),
      scrollTop: Math.max(0, num(source.scrollTop)),
      fileHandle: null
    };
  }

  function restoreTabSession() {
    if (!state.saveTabsOnExit) return false;
    try {
      const commit = JSON.parse(localStorage.getItem(tabSessionCommitStorageKey) || 'null');
      const slotKey = commit?.version === 3 && ['a', 'b'].includes(commit.slot) ? tabSessionSlotStorageKeys[commit.slot] : null;
      const serialized = slotKey ? (localStorage.getItem(slotKey) || '') : (localStorage.getItem(tabSessionStorageKey) || '');
      if (!serialized) return false;
      if (serialized.length > tabSessionMaxCharacters || ![2, 3].includes(commit?.version) || commit.length !== serialized.length) {
        clearStoredTabSession({ disable: true, warning: 'Ein alter oder unvollständiger Tab-Zustand wurde beim Start entfernt.' });
        return false;
      }
      const saved = JSON.parse(serialized);
      if (saved?.version !== 1 || saved.savedAt !== commit.savedAt || !Array.isArray(saved.documents) || !saved.documents.length || saved.documents.length > 64) {
        clearStoredTabSession({ disable: true, warning: 'Ein beschädigter Tab-Zustand wurde beim Start entfernt.' });
        return false;
      }
      const usedIds = new Set();
      const documents = saved.documents.slice(0, 64).map((document) => restoredTabDocument(document, usedIds)).filter(Boolean);
      if (!documents.length) {
        clearStoredTabSession({ disable: true, warning: 'Der gespeicherte Tab-Zustand enthielt keine lesbaren Dokumente und wurde entfernt.' });
        return false;
      }
      state.documents = documents;
      state.activeDocumentId = documents.some((document) => document.id === saved.activeDocumentId) ? saved.activeDocumentId : documents[0].id;
      loadDocument(activeDocument());
      return true;
    } catch {
      clearStoredTabSession({ disable: true, warning: 'Ein beschädigter Tab-Zustand wurde beim Start entfernt.' });
      return false;
    }
  }

  function syncTabSessionControl() {
    const toggle = $('saveTabsToggle'); const label = $('saveTabsLabel');
    if (!toggle || !label) return;
    toggle.checked = state.saveTabsOnExit;
    label.title = state.saveTabsOnExit
      ? 'Geöffnete Tabs werden beim Beenden gespeichert und beim nächsten Start wiederhergestellt'
      : 'Geöffnete Tabs beim Beenden speichern und beim nächsten Start wiederherstellen';
  }

  function setTabSessionEnabled(enabled) {
    state.saveTabsOnExit = Boolean(enabled);
    try {
      localStorage.setItem(tabSessionEnabledStorageKey, String(state.saveTabsOnExit));
      if (!state.saveTabsOnExit) {
        localStorage.removeItem(tabSessionStorageKey);
        localStorage.removeItem(tabSessionCommitStorageKey);
        Object.values(tabSessionSlotStorageKeys).forEach((key) => localStorage.removeItem(key));
      }
    } catch { /* Browserspeicher ist optional. */ }
    if (state.saveTabsOnExit && !saveTabSession()) {
      state.saveTabsOnExit = false;
      try { localStorage.setItem(tabSessionEnabledStorageKey, 'false'); } catch { /* Browserspeicher ist optional. */ }
      toast('Die Tab-Sitzung ist zu groß für den Browserspeicher', 'warning');
    } else {
      toast(state.saveTabsOnExit ? 'Tabs werden beim Beenden wiederhergestellt' : 'Tab-Wiederherstellung deaktiviert');
    }
    syncTabSessionControl();
  }

  let documentOverviewPending = null;

  function applyDocumentOverviewStats(stats) {
    const objects = $('documentOverviewObjects'); const complexity = $('documentOverviewComplexity');
    const media = $('documentOverviewMedia'); const project = $('documentOverviewProjectSize');
    if (!objects || !complexity || !media || !project) return;
    objects.textContent = `${DocumentStats.formatNumber(stats.objects)} ${stats.objects === 1 ? 'Objekt' : 'Objekte'}`;
    complexity.textContent = `${DocumentStats.formatNumber(stats.paths)} ${stats.paths === 1 ? 'Pfad' : 'Pfade'} · ${DocumentStats.formatNumber(stats.nodes)} Knoten`;
    media.textContent = stats.mediaCount
      ? `${DocumentStats.formatNumber(stats.mediaCount)} ${stats.mediaCount === 1 ? 'Bild' : 'Bilder'} · ${DocumentStats.formatBytes(stats.mediaBytes)}`
      : 'Keine';
    project.textContent = `ca. ${DocumentStats.formatBytes(stats.projectBytes)}`;
    objects.title = 'Objekte auf oberster Ebene im aktiven Dokument';
    complexity.title = 'Vektorpfade und Ankerpunkte im aktiven Dokument';
    media.title = 'Eingebettete Bilder; identische mehrfach verwendete Bilder werden einmal gezählt';
    project.title = 'Geschätzter Speicherbedarf des aktuellen Projekts ohne Undo-Verlauf';
    $('documentOverviewPanel')?.removeAttribute('aria-busy');
  }

  function renderDocumentOverview() {
    const tabDocument = activeDocument(); const objects = $('documentOverviewObjects');
    const complexity = $('documentOverviewComplexity'); const media = $('documentOverviewMedia'); const project = $('documentOverviewProjectSize');
    if (!tabDocument || !objects || !complexity || !media || !project) return;
    const historyEntry = tabDocument.history?.[tabDocument.historyIndex];
    const cacheKey = historyEntry?.id || `${tabDocument.historyIndex}:${historyEntry?.value?.length || 0}`;
    const count = allObjects().length;
    objects.textContent = `${DocumentStats.formatNumber(count)} ${count === 1 ? 'Objekt' : 'Objekte'}`;
    if (tabDocument.overviewStats?.key === cacheKey) { applyDocumentOverviewStats(tabDocument.overviewStats.value); return; }

    complexity.textContent = 'Berechne…'; media.textContent = 'Berechne…'; project.textContent = 'Berechne…';
    $('documentOverviewPanel')?.setAttribute('aria-busy', 'true');
    if (documentOverviewPending?.documentId === tabDocument.id && documentOverviewPending.key === cacheKey) return;
    if (documentOverviewPending?.timer) window.clearTimeout(documentOverviewPending.timer);
    const pending = { documentId: tabDocument.id, key: cacheKey, timer: null };
    pending.timer = window.setTimeout(() => {
      if (documentOverviewPending !== pending) return;
      documentOverviewPending = null;
      try {
        const value = DocumentStats.analyze(tabDocument.doc, {
          snapshot: historyEntry?.value || '',
          assetStore: tabDocument.historyAssets,
          assetPrefix: HistoryAssets.ASSET_PREFIX
        });
        tabDocument.overviewStats = { key: cacheKey, value };
        if (activeDocument()?.id === tabDocument.id) applyDocumentOverviewStats(value);
      } catch (error) {
        if (activeDocument()?.id !== tabDocument.id) return;
        complexity.textContent = 'Nicht verfügbar'; media.textContent = 'Nicht verfügbar'; project.textContent = 'Nicht verfügbar';
        $('documentOverviewPanel')?.removeAttribute('aria-busy');
        debugLog('warning', 'Dokumentübersicht', 'PYRO_DOCUMENT_STATS_FAILED', 'Dokumentstatistiken konnten nicht berechnet werden', error);
      }
    }, 250);
    documentOverviewPending = pending;
  }

  function displayedDocumentTitle(tabDocument) {
    if (tabDocument?.exampleDocument) return I18n.t('example.documentTitle');
    if (!tabDocument?.automaticTitle) return tabDocument?.title || I18n.t('document.untitled');
    const suffix = String(tabDocument.title || '').match(/\s(\d+)$/)?.[1];
    return `${I18n.t('document.untitled')}${suffix ? ` ${suffix}` : ''}`;
  }

  let documentTabContextTargetId = null;
  let documentTabContextAnchor = null;
  let draggedDocumentTabId = null;

  function closeDocumentTabContextMenu({ restoreFocus = false } = {}) {
    if (!refs.tabContextMenu || refs.tabContextMenu.hidden) return;
    refs.tabContextMenu.hidden = true;
    refs.tabContextMenu.style.removeProperty('left');
    refs.tabContextMenu.style.removeProperty('top');
    refs.tabs?.querySelectorAll('.is-context-target').forEach((tab) => tab.classList.remove('is-context-target'));
    const anchor = documentTabContextAnchor;
    documentTabContextTargetId = null;
    documentTabContextAnchor = null;
    if (restoreFocus && anchor?.isConnected) anchor.querySelector('.document-tab__select')?.focus();
  }

  function openDocumentTabContextMenu(event, tab) {
    const id = tab?.dataset.documentId;
    const index = state.documents.findIndex((document) => document.id === id);
    if (!refs.tabContextMenu || index < 0) return;
    event.preventDefault();
    closeDocumentTabContextMenu();
    documentTabContextTargetId = id;
    documentTabContextAnchor = tab;
    tab.classList.add('is-context-target');
    const closeOthers = refs.tabContextMenu.querySelector('[data-document-tab-action="close-others"]');
    const closeRight = refs.tabContextMenu.querySelector('[data-document-tab-action="close-right"]');
    if (closeOthers) closeOthers.disabled = state.documents.length < 2;
    if (closeRight) closeRight.disabled = index >= state.documents.length - 1;
    refs.tabContextMenu.hidden = false;
    refs.tabContextMenu.style.left = '0px';
    refs.tabContextMenu.style.top = '0px';
    const bounds = refs.tabContextMenu.getBoundingClientRect();
    const left = clamp(event.clientX, 6, Math.max(6, window.innerWidth - bounds.width - 6));
    const top = clamp(event.clientY, 6, Math.max(6, window.innerHeight - bounds.height - 6));
    refs.tabContextMenu.style.left = `${left}px`;
    refs.tabContextMenu.style.top = `${top}px`;
    refs.tabContextMenu.querySelector('button:not(:disabled)')?.focus();
  }

  function clearDocumentTabDropMarkers() {
    refs.tabs?.querySelectorAll('.is-dragging, .is-drop-before, .is-drop-after').forEach((tab) => {
      tab.classList.remove('is-dragging', 'is-drop-before', 'is-drop-after');
    });
  }

  function markDocumentTabDropTarget(tab, after) {
    refs.tabs?.querySelectorAll('.is-drop-before, .is-drop-after').forEach((entry) => {
      entry.classList.remove('is-drop-before', 'is-drop-after');
    });
    if (tab?.dataset.documentId && tab.dataset.documentId !== draggedDocumentTabId) {
      tab.classList.add(after ? 'is-drop-after' : 'is-drop-before');
    }
  }

  function reorderDocumentTab(sourceId, targetId, after = false) {
    if (!sourceId || !targetId || sourceId === targetId) return false;
    const before = state.documents.map((document) => document.id);
    const sourceIndex = state.documents.findIndex((document) => document.id === sourceId);
    if (sourceIndex < 0 || !state.documents.some((document) => document.id === targetId)) return false;
    const [moved] = state.documents.splice(sourceIndex, 1);
    const targetIndex = state.documents.findIndex((document) => document.id === targetId);
    state.documents.splice(targetIndex + (after ? 1 : 0), 0, moved);
    const changed = before.some((id, index) => state.documents[index]?.id !== id);
    if (!changed) return false;
    renderDocumentTabs();
    saveTabSession();
    return true;
  }

  function onDocumentTabDragStart(event) {
    const tab = event.target.closest?.('.document-tab');
    if (!tab || event.target.closest?.('.document-tab__title-input, .document-tab__close')) {
      event.preventDefault();
      return;
    }
    closeDocumentTabContextMenu();
    draggedDocumentTabId = tab.dataset.documentId || null;
    if (!draggedDocumentTabId) { event.preventDefault(); return; }
    tab.classList.add('is-dragging');
    if (event.dataTransfer) {
      event.dataTransfer.effectAllowed = 'move';
      event.dataTransfer.setData('text/plain', draggedDocumentTabId);
    }
  }

  function onDocumentTabDragOver(event) {
    if (!draggedDocumentTabId) return;
    event.preventDefault();
    if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
    const target = event.target.closest?.('.document-tab');
    if (target) {
      const bounds = target.getBoundingClientRect();
      markDocumentTabDropTarget(target, event.clientX >= bounds.left + bounds.width / 2);
      return;
    }
    const last = [...(refs.tabs?.querySelectorAll('.document-tab') || [])]
      .findLast((tab) => tab.dataset.documentId !== draggedDocumentTabId);
    markDocumentTabDropTarget(last, true);
  }

  function onDocumentTabDrop(event) {
    if (!draggedDocumentTabId) return;
    event.preventDefault();
    const target = event.target.closest?.('.document-tab')
      || refs.tabs?.querySelector('.document-tab.is-drop-before, .document-tab.is-drop-after');
    const targetId = target?.dataset.documentId;
    const after = Boolean(target?.classList.contains('is-drop-after'));
    const sourceId = draggedDocumentTabId;
    draggedDocumentTabId = null;
    clearDocumentTabDropMarkers();
    reorderDocumentTab(sourceId, targetId, after);
  }

  function onDocumentTabDragEnd() {
    draggedDocumentTabId = null;
    clearDocumentTabDropMarkers();
  }

  function renderDocumentTabs() {
    if (!refs.tabs) return;
    closeDocumentTabContextMenu();
    persistActiveDocument(); refs.tabs.replaceChildren();
    state.documents.forEach((tabDocument) => {
      const displayedTitle = displayedDocumentTitle(tabDocument);
      const tab = document.createElement('div');
      tab.className = `document-tab${tabDocument.id === state.activeDocumentId ? ' is-active' : ''}`;
      tab.dataset.documentId = tabDocument.id;
      tab.draggable = true;
      tab.addEventListener('contextmenu', (event) => openDocumentTabContextMenu(event, tab));
      const select = document.createElement('button'); select.type = 'button'; select.className = 'document-tab__select';
      select.setAttribute('role', 'tab'); select.setAttribute('aria-selected', String(tabDocument.id === state.activeDocumentId)); select.setAttribute('aria-label', `${displayedTitle} aktivieren`); select.title = `${displayedTitle} aktivieren`;
      const tabIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); Tabler.set(tabIcon, 'file'); select.append(tabIcon);
      tab.addEventListener('click', (event) => {
        if (event.target.closest('.document-tab__title, .document-tab__title-input, .document-tab__close')) return;
        switchDocument(tabDocument.id);
      });
      const label = document.createElement('button'); label.type = 'button'; label.className = 'document-tab__title'; label.dataset.i18nIgnoreText = ''; label.textContent = displayedTitle; label.title = 'Dokumentnamen bearbeiten';
      label.addEventListener('click', (event) => { event.stopPropagation(); editDocumentTitle(tabDocument.id, label); });
      const saveState = document.createElement('i'); saveState.className = `document-tab__save-state is-${tabDocument.dirty ? 'dirty' : tabDocument.hasSavedVersion ? 'saved' : 'new'}`;
      saveState.textContent = tabDocument.dirty ? 'Geändert' : tabDocument.hasSavedVersion ? 'Gespeichert' : 'Neu';
      saveState.title = saveState.textContent;
      const close = document.createElement('button'); close.type = 'button'; close.className = 'document-tab__close'; close.setAttribute('aria-label', `${displayedTitle} schließen`); close.title = 'Tab schließen'; close.textContent = '×';
      close.addEventListener('click', (event) => { event.stopPropagation(); closeDocument(tabDocument.id); }); tab.append(select, label, saveState, close); refs.tabs.append(tab);
    });
    requestAnimationFrame(() => {
      const activeTab = refs.tabs.querySelector('.document-tab.is-active');
      syncDocumentTabScroller();
      if (activeTab) revealDocumentTab(activeTab);
      syncDocumentTabScroller();
    });
    renderDocumentOverview();
  }

  function syncActiveDocumentTabSaveState() {
    const tabDocument = activeDocument();
    const tab = refs.tabs?.querySelector('.document-tab.is-active');
    const saveState = tab?.querySelector('.document-tab__save-state');
    if (!tabDocument || !saveState) return;
    const stateName = tabDocument.dirty ? 'dirty' : tabDocument.hasSavedVersion ? 'saved' : 'new';
    saveState.className = `document-tab__save-state is-${stateName}`;
    saveState.textContent = tabDocument.dirty ? 'Geändert' : tabDocument.hasSavedVersion ? 'Gespeichert' : 'Neu';
    saveState.title = saveState.textContent;
  }

  function syncDocumentTabScroller() {
    if (!refs.tabs || !refs.tabsStrip || !refs.tabsScrollLeft || !refs.tabsScrollRight) return;
    const overflowing = refs.tabs.scrollWidth > refs.tabsStrip.clientWidth + 1;
    refs.tabsStrip.classList.toggle('is-overflowing', overflowing);
    const maximum = Math.max(0, refs.tabs.scrollWidth - refs.tabs.clientWidth);
    refs.tabsScrollLeft.disabled = !overflowing || refs.tabs.scrollLeft <= 1;
    refs.tabsScrollRight.disabled = !overflowing || refs.tabs.scrollLeft >= maximum - 1;
  }

  function revealDocumentTab(tab) {
    if (!refs.tabs || !tab) return;
    const viewport = refs.tabs.getBoundingClientRect(); const bounds = tab.getBoundingClientRect();
    if (bounds.left < viewport.left) refs.tabs.scrollLeft -= viewport.left - bounds.left;
    else if (bounds.right > viewport.right) refs.tabs.scrollLeft += bounds.right - viewport.right;
  }

  function scrollDocumentTabs(direction) {
    if (!refs.tabs) return;
    const distance = Math.max(160, Math.round(refs.tabs.clientWidth * .72));
    refs.tabs.scrollBy({ left: direction * distance, behavior: 'smooth' });
  }

  function renameDocument(id, title) {
    const tabDocument = state.documents.find((document) => document.id === id); if (!tabDocument) return;
    const nextTitle = normalizeDocumentTitle(title, tabDocument.title);
    if (nextTitle === tabDocument.title) { renderDocumentTabs(); return; }
    tabDocument.title = nextTitle;
    tabDocument.automaticTitle = false;
    tabDocument.exampleDocument = false;
    if (id === state.activeDocumentId) refreshDirtyState();
    else tabDocument.dirty = tabDocument.savedSnapshot == null || snapshot(tabDocument.doc, tabDocument.historyAssets) !== tabDocument.savedSnapshot || nextTitle !== tabDocument.savedTitle;
    renderDocumentTabs(); updateCommandState(); toast(`Dokument heißt jetzt „${nextTitle}“`);
  }

  function editDocumentTitle(id, label) {
    const tabDocument = state.documents.find((document) => document.id === id); if (!tabDocument) return;
    const initialTitle = displayedDocumentTitle(tabDocument);
    const input = document.createElement('input'); input.type = 'text'; input.className = 'document-tab__title-input'; input.value = initialTitle; input.maxLength = 80; input.setAttribute('aria-label', 'Dokumentnamen bearbeiten');
    let finished = false;
    const finish = (save) => {
      if (finished) return;
      finished = true;
      if (save && !(tabDocument.automaticTitle && input.value === initialTitle)) renameDocument(id, input.value); else renderDocumentTabs();
    };
    input.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); finish(true); }
      if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); finish(false); }
    });
    input.addEventListener('blur', () => finish(true)); label.replaceWith(input); input.focus(); input.select();
  }

  function switchDocument(id) {
    if (id === state.activeDocumentId) return;
    const document = state.documents.find((item) => item.id === id); if (!document) return;
    finishInlineText(); persistActiveDocument(); $('pixelDialog').hidden = true; $('cropDialog').hidden = true; closeColorPalette();
    state.activeDocumentId = id; loadDocument(document); renderAll();
    requestAnimationFrame(() => { refs.workspace.scrollLeft = document.scrollLeft || 0; refs.workspace.scrollTop = document.scrollTop || 0; scheduleRulerRender(); });
  }

  function closeDocuments(ids) {
    finishInlineText(); persistActiveDocument();
    const requestedIds = new Set(ids);
    const documentsBefore = [...state.documents];
    const documentsToClose = documentsBefore.filter((document) => requestedIds.has(document.id));
    if (!documentsToClose.length) return false;
    for (const documentToClose of documentsToClose) {
      if (documentToClose.dirty && !confirm(I18n.t('dialog.discardChanges', { title: documentToClose.title }))) return false;
    }
    const activeIndex = documentsBefore.findIndex((document) => document.id === state.activeDocumentId);
    const closingActive = requestedIds.has(state.activeDocumentId);
    state.documents = documentsBefore.filter((document) => !requestedIds.has(document.id));
    if (!state.documents.length) {
      const layer = makeLayer(); const doc = { width: 960, height: 640, dpi: 72, bg: '#ffffff', layers: [layer], activeLayerId: layer.id, guides: { horizontal: [], vertical: [], locked: false }, grid: { visible: false, snap: false, size: 10 } };
      const replacement = { id: uid('document'), title: 'Unbenannt', automaticTitle: true, doc, selected: [], history: [], historyIndex: -1, historyBytes: 0, historyAssets: makeHistoryAssetStore(), savedSnapshot: null, savedTitle: 'Unbenannt', clipboard: [], dirty: false, hasSavedVersion: false, autoSaveEnabled: false, expandedLayerIds: new Set([layer.id]), zoom: 1, scrollLeft: 0, scrollTop: 0, fileHandle: null };
      state.documents.push(replacement); state.activeDocumentId = replacement.id; loadDocument(replacement); initializeHistory('Neues Dokument');
    } else if (closingActive) {
      let replacement = null;
      for (let distance = 1; distance <= documentsBefore.length && !replacement; distance += 1) {
        const left = documentsBefore[activeIndex - distance];
        const right = documentsBefore[activeIndex + distance];
        if (left && !requestedIds.has(left.id)) replacement = left;
        else if (right && !requestedIds.has(right.id)) replacement = right;
      }
      replacement ||= state.documents[0];
      state.activeDocumentId = replacement.id;
      loadDocument(replacement);
    }
    $('pixelDialog').hidden = true; $('cropDialog').hidden = true; renderAll();
    saveTabSession();
    return true;
  }

  function closeDocument(id) {
    return closeDocuments([id]);
  }

  function runDocumentTabContextAction(action) {
    const targetId = documentTabContextTargetId;
    const targetIndex = state.documents.findIndex((document) => document.id === targetId);
    if (targetIndex < 0) { closeDocumentTabContextMenu(); return; }
    let ids = [];
    if (action === 'close') ids = [targetId];
    else if (action === 'close-all') ids = state.documents.map((document) => document.id);
    else if (action === 'close-others') ids = state.documents.filter((document) => document.id !== targetId).map((document) => document.id);
    else if (action === 'close-right') ids = state.documents.slice(targetIndex + 1).map((document) => document.id);
    closeDocumentTabContextMenu();
    if (ids.length) closeDocuments(ids);
  }

  function untitledDocumentName() {
    const count = state.documents.filter((document) => document.title.startsWith('Unbenannt')).length;
    return count ? `Unbenannt ${count + 1}` : 'Unbenannt';
  }

  function titleFromFile(file, fallback = 'Unbenannt') {
    const name = String(file?.name || '').trim()
      .replace(/\.pyro\.json$/i, '')
      .replace(/\.(?:pyro|paintstudio|json|svg|png|jpe?g|webp|gif)$/i, '');
    return name || fallback;
  }

  function openDocumentInTab(title, doc, { selected = [], historyLabel = 'Dokument geöffnet', zoomToFit = true, hasSavedVersion = false, fileHandle = null, automaticTitle = false, exampleDocument = false } = {}) {
    finishInlineText(); persistActiveDocument();
    const cleanedDoc = cleanDocument(doc);
    const document = {
      id: uid('document'), title, automaticTitle, exampleDocument, doc: cleanedDoc, selected: [...selected], history: [], historyIndex: -1, historyBytes: 0, historyAssets: makeHistoryAssetStore(), savedSnapshot: null, savedTitle: title, clipboard: [], dirty: false, hasSavedVersion, autoSaveEnabled: false,
      expandedLayerIds: new Set(cleanedDoc.layers.map((layer) => layer.id)), zoom: 1, scrollLeft: 0, scrollTop: 0, fileHandle
    };
    state.documents.push(document); state.activeDocumentId = document.id; loadDocument(document);
    initializeHistory(historyLabel || 'Dokument geöffnet');
    renderAll(); if (zoomToFit) zoomFit();
    return document;
  }

  function toast(message, tone = '') {
    refs.toast.textContent = I18n.translateSource(String(message));
    refs.toast.dataset.tone = tone;
    if (tone === 'warning' || tone === 'error') debugLog(tone === 'error' ? 'error' : 'warning', 'Oberfläche', tone === 'error' ? 'PYRO_UI_ERROR' : 'PYRO_UI_WARNING', message);
    clearTimeout(toast.timer);
    toast.timer = setTimeout(() => { refs.toast.textContent = ''; refs.toast.removeAttribute('data-tone'); }, 2300);
  }

  function debugDetails(value) {
    if (value == null) return null;
    if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack || '' };
    try { return JSON.parse(JSON.stringify(value)); }
    catch { return { value: String(value) }; }
  }

  function debugLog(level, source, code, message, details = null) {
    const entry = {
      id: uid('debug'), order: ++consoleEntrySequence, timestamp: new Date().toISOString(),
      level: ['info', 'warning', 'error'].includes(level) ? level : 'info',
      source: String(source || 'PYRO').slice(0, 80), code: String(code || 'PYRO_INFO').slice(0, 120),
      message: String(message || '').slice(0, 2000), details: debugDetails(details)
    };
    debugEntries.push(entry); if (debugEntries.length > 1000) debugEntries.splice(0, debugEntries.length - 1000);
    if (!$('debugConsole')?.hidden) renderDebugConsole(); return entry;
  }

  const allConsoleEntries = () => [...debugEntries, ...performanceEntries].sort((left, right) => left.order - right.order);

  function filteredDebugEntries() {
    const level = $('debugConsoleLevel')?.value || 'all'; const search = String($('debugConsoleSearch')?.value || '').trim().toLocaleLowerCase();
    return allConsoleEntries().filter((entry) => {
      if (level === 'performance' && entry.code !== 'PYRO_PERFORMANCE_ACTION') return false;
      if (!['all', 'performance'].includes(level) && entry.level !== level) return false;
      if (!search) return true;
      const haystack = `${entry.timestamp} ${entry.level} ${entry.source} ${entry.code} ${entry.message} ${I18n.translateSource(entry.message)} ${entry.details ? JSON.stringify(entry.details) : ''}`.toLocaleLowerCase();
      return haystack.includes(search);
    });
  }

  function renderDebugConsole() {
    const list = $('debugConsoleEntries'); if (!list) return;
    const entries = filteredDebugEntries(); const total = debugEntries.length + performanceEntries.length; list.replaceChildren();
    if (!entries.length) {
      const empty = document.createElement('li'); empty.className = 'debug-console__empty'; empty.textContent = I18n.t(total ? 'ui.noEntriesMatchTheFilter' : 'ui.noDiagnosticEntriesYet'); list.append(empty);
    }
    entries.forEach((entry) => {
      const item = document.createElement('li'); item.className = 'debug-console__entry'; item.dataset.level = entry.level;
      if (entry.code === 'PYRO_PERFORMANCE_ACTION') item.dataset.kind = 'performance';
      const head = document.createElement('div'); head.className = 'debug-console__entry-head';
      const time = document.createElement('time'); time.dateTime = entry.timestamp; time.textContent = new Date(entry.timestamp).toLocaleTimeString(I18n.language === 'de' ? 'de-DE' : 'en-GB');
      const level = document.createElement('span'); level.textContent = I18n.translateSource(entry.level === 'error' ? 'Fehler' : entry.level === 'warning' ? 'Warnung' : 'Info');
      const source = document.createElement('span'); source.textContent = I18n.translateSource(entry.source);
      const code = document.createElement('code'); code.textContent = entry.code;
      head.append(time, level, source, code);
      const message = document.createElement('div'); message.textContent = I18n.translateSource(entry.message); item.append(head, message);
      if (entry.details != null) {
        const details = document.createElement('details'); const summary = document.createElement('summary'); summary.textContent = I18n.translateSource('Details');
        const pre = document.createElement('pre'); pre.textContent = JSON.stringify(entry.details, null, 2); details.append(summary, pre); item.append(details);
      }
      list.append(item);
    });
    const status = $('debugConsoleStatus'); if (status) status.textContent = I18n.translateSource(`${entries.length} von ${total} Einträgen`);
    if ($('performanceProfilerExportBtn')) $('performanceProfilerExportBtn').disabled = performanceEntries.length === 0;
  }

  function openDebugConsole() {
    const panel = $('debugConsole'); if (!panel) return;
    panel.hidden = false; $('debugConsoleBtn')?.setAttribute('aria-expanded', 'true'); renderDebugConsole();
    requestAnimationFrame(() => $('debugConsoleSearch')?.focus({ preventScroll: true }));
  }

  function closeDebugConsole() {
    const panel = $('debugConsole'); if (!panel) return;
    panel.hidden = true; $('debugConsoleBtn')?.setAttribute('aria-expanded', 'false');
  }

  function toggleDebugConsole() {
    if ($('debugConsole')?.hidden) openDebugConsole(); else closeDebugConsole();
  }

  async function copyDebugConsoleJson() {
    try {
      const payload = JSON.stringify(filteredDebugEntries(), null, 2);
      if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(payload);
      else {
        const field = document.createElement('textarea'); field.value = payload; field.setAttribute('readonly', ''); field.style.cssText = 'position:fixed;opacity:0;pointer-events:none'; document.body.append(field); field.select();
        const copied = document.execCommand?.('copy'); field.remove(); if (!copied) throw new Error('Zwischenablage nicht verfügbar');
      }
      toast('Debugdaten als JSON kopiert');
    } catch { toast('Debugdaten konnten nicht kopiert werden', 'warning'); }
  }

  function clearDebugConsole() {
    debugEntries.length = 0; performanceEntries.length = 0; renderDebugConsole(); toast('Debugkonsole geleert');
  }

  const performanceGestureLabels = Object.freeze({
    marquee: 'Auswahlrahmen', 'vector-marquee': 'Vektorpunkt-Auswahlrahmen', move: 'Objekt auswählen',
    resize: 'Objekt skalieren', rotate: 'Objekt drehen', pan: 'Arbeitsfläche verschieben', guide: 'Hilfslinie verschieben',
    draw: 'Objekt zeichnen', 'vector-points': 'Vektorpunkte verschieben', 'vector-point': 'Vektorpunkt verschieben',
    'vector-bezier': 'Bézier-Griff verschieben', 'free-transform-control': 'Freie Transformation',
    'transform-pivot': 'Drehmittelpunkt verschieben', 'fill-handle': 'Füllung direkt anpassen',
    'auto-shape-control': 'Auto-Form anpassen', 'text-path-start': 'Textstartpunkt verschieben',
    'bitmap-marquee': 'Bitmap-Auswahlrahmen', 'bitmap-ellipse-marquee': 'Bitmap-Auswahlrahmen', 'bitmap-lasso': 'Bitmap-Lasso', 'bitmap-polygon-lasso': 'Bitmap-Polygon-Lasso',
    bitmap: 'Bitmap bearbeiten', 'path-reshape': 'Pfad verformen', 'path-scrub': 'Pfadkontur bearbeiten',
    'warp-control': 'Bitmap verformen', 'stretch-select': 'Verformungsbereich auswählen',
    'stretch-handle': 'Verformungsbereich anpassen', 'puppet-pin': 'Puppet-Pin verschieben',
    'canvas-resize': 'Leinwand manuell anpassen', pen: 'Zeichenfederpfad bearbeiten', polyline: 'Polylinie bearbeiten'
  });

  function performanceGestureLabel(gesture) {
    if (!gesture) return '';
    if (gesture.kind === 'move' && gesture.moved) return 'Objekte verschieben';
    if (gesture.kind === 'draw') return `${names[gesture.object?.type] || 'Objekt'} zeichnen`;
    if (gesture.kind === 'bitmap') return names[gesture.tool] ? `${names[gesture.tool]} auf Bitmap` : 'Bitmap bearbeiten';
    return performanceGestureLabels[gesture.kind] || I18n.translateSource(gesture.kind);
  }

  function safePerformanceLabel(value, fallback = 'Aktion') {
    const normalized = String(value || '').replace(/„[^“]*“/g, '…').replace(/"[^"]*"/g, '…').replace(/\s+/g, ' ').trim();
    return (normalized || fallback).slice(0, 100);
  }

  function staticPerformanceLabel(value) {
    const normalized = safePerformanceLabel(value, '');
    return performanceStaticUiLabels.has(normalized) ? normalized : '';
  }

  function performanceElementLabel(element, fallback = 'Aktion') {
    if (!element) return fallback;
    if (element.closest?.('.document-tab')) {
      if (element.closest('.document-tab__close')) return 'Dokumenttab schließen';
      if (element.closest('.document-tab__title')) return 'Dokumentnamen bearbeiten';
      return 'Dokumenttab wechseln';
    }
    const explicit = element.getAttribute?.('aria-label') || element.getAttribute?.('title');
    const explicitLabel = staticPerformanceLabel(explicit);
    if (explicitLabel) return explicitLabel;
    if (element.id) {
      const label = document.querySelector(`label[for="${CSS.escape(element.id)}"]`);
      const directText = label && [...label.childNodes].filter((node) => node.nodeType === Node.TEXT_NODE).map((node) => node.textContent).join(' ');
      const directLabel = staticPerformanceLabel(directText);
      if (directLabel) return directLabel;
    }
    const wrapper = element.closest?.('label');
    const wrapperText = wrapper && [...wrapper.childNodes].filter((node) => node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE && ['SPAN', 'STRONG'].includes(node.tagName))
      .map((node) => node.textContent).join(' ');
    return staticPerformanceLabel(wrapperText) || staticPerformanceLabel(element.textContent) || fallback;
  }

  function performanceKeyboardLabel(event) {
    if (event.repeat || event.isComposing) return '';
    const target = event.target; const editing = target?.matches?.('input, textarea, select, [contenteditable="true"]');
    if (editing && !(event.ctrlKey || event.metaKey || event.altKey)) return '';
    const key = event.key === ' ' ? 'Leertaste' : event.key;
    const special = ['Escape', 'Delete', 'Backspace', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'F1', '+', '-', '0'].includes(key);
    const toolKey = !editing && /^[a-z]$/i.test(key);
    if (!(event.ctrlKey || event.metaKey || event.altKey || special || toolKey)) return '';
    const parts = [];
    if (event.ctrlKey || event.metaKey) parts.push(navigator.platform?.includes('Mac') ? 'Cmd' : 'Strg');
    if (event.altKey) parts.push('Alt');
    if (event.shiftKey) parts.push('Umschalt');
    parts.push(key.length === 1 ? key.toUpperCase() : key);
    return `Tastaturbefehl ${parts.join('+')}`;
  }

  function beginPerformanceAction(label, category, trigger = '') {
    if (!performanceProfilerEnabled || !label) return null;
    const artboard = refs.artboard; const startedAt = performance.now();
    return {
      id: uid('performance-action'), label: safePerformanceLabel(label), category, trigger: safePerformanceLabel(trigger, ''),
      observedAt: startedAt, startedAt, handlerMs: null, phaseSnapshot: performancePhaseSnapshot(),
      fullRenders: Number(artboard?.dataset.fullRenderCount || 0),
      interactionRenders: Number(artboard?.dataset.interactionRenderCount || 0),
      selectionRenders: Number(artboard?.dataset.selectionRenderCount || 0),
      historyIndex: state.historyIndex
    };
  }

  function performanceActionPhases(action) {
    const current = performancePhaseSnapshot(); const phasesMs = {}; const phaseCounts = {};
    const names = new Set([...Object.keys(current.totals), ...Object.keys(action.phaseSnapshot?.totals || {})]);
    names.forEach((name) => {
      const duration = num(current.totals[name]) - num(action.phaseSnapshot?.totals?.[name]);
      const count = num(current.counts[name]) - num(action.phaseSnapshot?.counts?.[name]);
      if (duration > .005) phasesMs[name] = round(duration, 2);
      if (count > 0) phaseCounts[name] = count;
    });
    return { phasesMs, phaseCounts };
  }

  function finishPerformanceAction(action) {
    if (!action) return null;
    const endedAt = performance.now(); const artboard = refs.artboard;
    const longTaskStartedAt = num(action.observedAt, action.startedAt);
    const longTasks = performanceLongTasks.filter((entry) => entry.startTime < endedAt && entry.startTime + entry.duration > longTaskStartedAt);
    const selectionRenderCount = Number(artboard?.dataset.selectionRenderCount || 0);
    const durationMs = round(endedAt - action.startedAt, 2);
    const phaseDetails = performanceActionPhases(action);
    const cachedStats = activeDocument()?.overviewStats?.value;
    const details = {
      action: action.label, category: action.category, trigger: action.trigger || undefined,
      durationMs, handlerMs: round(action.handlerMs ?? durationMs, 2),
      frameWaitMs: round(durationMs - (action.handlerMs ?? durationMs), 2),
      gestureElapsedMs: action.gestureElapsedMs == null ? undefined : round(action.gestureElapsedMs, 2),
      longTaskCount: longTasks.length, longTaskMs: round(longTasks.reduce((sum, entry) => sum + entry.duration, 0), 2),
      fullRenders: Number(artboard?.dataset.fullRenderCount || 0) - action.fullRenders,
      interactionRenders: Number(artboard?.dataset.interactionRenderCount || 0) - action.interactionRenders,
      selectionRenders: selectionRenderCount - action.selectionRenders,
      tool: state.tool, selectedObjects: state.selected.length, topLevelObjects: allObjects().length,
      vectorPaths: Number.isFinite(cachedStats?.paths) ? cachedStats.paths : undefined,
      vectorNodes: Number.isFinite(cachedStats?.nodes) ? cachedStats.nodes : undefined,
      historyChanged: state.historyIndex !== action.historyIndex,
      phasesMs: phaseDetails.phasesMs,
      phaseCounts: phaseDetails.phaseCounts
    };
    if (selectionRenderCount !== action.selectionRenders) {
      details.selectionPhasesMs = {
        total: num(artboard?.dataset.selectionRenderMs), overlay: num(artboard?.dataset.selectionOverlayMs),
        layers: num(artboard?.dataset.selectionLayersMs), inspector: num(artboard?.dataset.selectionInspectorMs),
        library: num(artboard?.dataset.selectionLibraryMs), commands: num(artboard?.dataset.selectionCommandsMs)
      };
    }
    const entry = {
      id: action.id, order: ++consoleEntrySequence, timestamp: new Date().toISOString(),
      level: 'info', source: 'Leistung', code: 'PYRO_PERFORMANCE_ACTION',
      message: `${action.label} · ${durationMs} ms`, details
    };
    performanceEntries.push(entry);
    if (performanceEntries.length > 2000) performanceEntries.splice(0, performanceEntries.length - 2000);
    if (!$('debugConsole')?.hidden) renderDebugConsole();
    return entry;
  }

  function schedulePerformanceAction(action) {
    if (!action) return;
    setTimeout(() => {
      action.handlerMs = performance.now() - action.startedAt;
      requestAnimationFrame(() => requestAnimationFrame(() => finishPerformanceAction(action)));
    }, 0);
  }

  function performancePercentile(values, percentile) {
    if (!values.length) return 0;
    const sorted = [...values].sort((left, right) => left - right);
    return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentile / 100 * sorted.length) - 1))];
  }

  function performanceProfilerReport() {
    const durations = performanceEntries.map((entry) => num(entry.details?.durationMs));
    const slowest = [...performanceEntries].sort((left, right) => num(right.details?.durationMs) - num(left.details?.durationMs)).slice(0, 20)
      .map((entry) => ({ timestamp: entry.timestamp, action: entry.details.action, durationMs: entry.details.durationMs }));
    const phaseTotalsMs = {};
    performanceEntries.forEach((entry) => Object.entries(entry.details?.phasesMs || {}).forEach(([name, duration]) => {
      phaseTotalsMs[name] = num(phaseTotalsMs[name]) + num(duration);
    }));
    Object.keys(phaseTotalsMs).forEach((name) => { phaseTotalsMs[name] = round(phaseTotalsMs[name], 2); });
    const cachedStats = activeDocument()?.overviewStats?.value;
    return {
      format: 'pyro-performance-log', version: 2, exportedAt: new Date().toISOString(),
      environment: {
        language: I18n.language, userAgent: navigator.userAgent, viewport: { width: innerWidth, height: innerHeight },
        devicePixelRatio: window.devicePixelRatio || 1, hardwareConcurrency: navigator.hardwareConcurrency || null
      },
      document: {
        width: state.doc.width, height: state.doc.height, dpi: state.doc.dpi, zoom: round(state.zoom, 4),
        topLevelObjects: allObjects().length, selectedObjects: state.selected.length,
        vectorPaths: Number.isFinite(cachedStats?.paths) ? cachedStats.paths : undefined,
        vectorNodes: Number.isFinite(cachedStats?.nodes) ? cachedStats.nodes : undefined
      },
      summary: {
        actions: durations.length, averageMs: durations.length ? round(durations.reduce((sum, value) => sum + value, 0) / durations.length, 2) : 0,
        medianMs: round(performancePercentile(durations, 50), 2), p95Ms: round(performancePercentile(durations, 95), 2),
        maximumMs: round(Math.max(0, ...durations), 2), phaseTotalsMs, slowest
      },
      entries: deepClone(performanceEntries)
    };
  }

  function exportPerformanceProfilerReport() {
    if (!performanceEntries.length) { toast('Keine Leistungsmesswerte vorhanden', 'warning'); return false; }
    const report = performanceProfilerReport();
    const stamp = report.exportedAt.replace(/[:.]/g, '-');
    saveBlob(new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' }), `pyro-performance-${stamp}.json`);
    toast('Leistungsmesswerte exportiert'); return true;
  }

  function syncPerformanceProfilerControl() {
    const button = $('performanceProfilerToggleBtn'); if (!button) return;
    button.setAttribute('aria-pressed', String(performanceProfilerEnabled));
    button.textContent = performanceProfilerEnabled ? 'Messung pausieren' : 'Messung starten';
    button.title = performanceProfilerEnabled ? 'Aktionsmessung pausieren' : 'Aktionsmessung fortsetzen';
  }

  function setPerformanceProfilerEnabled(enabled, notify = true) {
    performanceProfilerEnabled = Boolean(enabled);
    try { localStorage.setItem(performanceProfilerStorageKey, String(performanceProfilerEnabled)); } catch { /* Lokale Einstellung ist optional. */ }
    syncPerformanceProfilerControl();
    if (notify) toast(performanceProfilerEnabled ? 'Leistungsmessung aktiv' : 'Leistungsmessung pausiert');
    return performanceProfilerEnabled;
  }

  function bindPerformanceProfiler() {
    if (performanceProfilerBound) return;
    performanceProfilerBound = true;
    if (typeof PerformanceObserver === 'function') {
      try {
        performanceLongTaskObserver = new PerformanceObserver((list) => {
          performanceLongTasks.push(...list.getEntries().map((entry) => ({ startTime: entry.startTime, duration: entry.duration })));
          if (performanceLongTasks.length > 300) performanceLongTasks.splice(0, performanceLongTasks.length - 300);
        });
        performanceLongTaskObserver.observe({ type: 'longtask', buffered: true });
      } catch { performanceLongTaskObserver = null; }
    }
    document.addEventListener('pointerdown', (event) => {
      if (!event.isTrusted || !refs.workspace?.contains(event.target)) return;
      const action = beginPerformanceAction('Aktion', 'gesture', 'pointer');
      if (!action) return;
      action.pointerId = event.pointerId; activePerformanceGestureAction = action;
      setTimeout(() => {
        if (activePerformanceGestureAction !== action) return;
        if (!state.gesture) { activePerformanceGestureAction = null; return; }
        action.label = safePerformanceLabel(performanceGestureLabel(state.gesture));
        action.trigger = safePerformanceLabel(state.gesture.kind, '');
      }, 0);
    }, true);
    document.addEventListener('pointerup', (event) => {
      if (!event.isTrusted || !state.gesture) return;
      const action = activePerformanceGestureAction?.pointerId === event.pointerId
        ? activePerformanceGestureAction
        : beginPerformanceAction(performanceGestureLabel(state.gesture), 'gesture', state.gesture.kind);
      activePerformanceGestureAction = null;
      if (!action) return;
      const releasedAt = performance.now();
      action.label = safePerformanceLabel(performanceGestureLabel(state.gesture));
      action.trigger = safePerformanceLabel(state.gesture.kind, '');
      action.gestureElapsedMs = releasedAt - action.observedAt;
      action.startedAt = releasedAt;
      schedulePerformanceAction(action);
    }, true);
    document.addEventListener('pointercancel', (event) => {
      if (activePerformanceGestureAction?.pointerId === event.pointerId) activePerformanceGestureAction = null;
    }, true);
    document.addEventListener('click', (event) => {
      if (!event.isTrusted) return;
      const control = event.target.closest?.('button, a[href], summary, [role="button"]');
      if (!control || control.disabled || control.getAttribute('aria-disabled') === 'true') return;
      schedulePerformanceAction(beginPerformanceAction(performanceElementLabel(control, 'Klick'), 'command', control.id || control.getAttribute('role') || control.tagName.toLowerCase()));
    }, true);
    document.addEventListener('change', (event) => {
      if (!event.isTrusted || !event.target.matches?.('input, select, textarea')) return;
      const control = event.target;
      schedulePerformanceAction(beginPerformanceAction(`${performanceElementLabel(control, 'Wert')} ändern`, 'input', control.id || control.type || control.tagName.toLowerCase()));
    }, true);
    document.addEventListener('keydown', (event) => {
      if (!event.isTrusted) return;
      const label = performanceKeyboardLabel(event); if (!label) return;
      schedulePerformanceAction(beginPerformanceAction(label, 'keyboard', event.code || event.key));
    }, true);
    syncPerformanceProfilerControl();
  }

  globalThis.PyroDebugConsole = Object.freeze({
    log(message, details = null, level = 'info', source = 'API', code = 'PYRO_API') { return debugLog(level, source, code, message, details); },
    entries() { return deepClone(debugEntries); },
    performanceEntries() { return deepClone(performanceEntries); },
    performanceReport() { return performanceProfilerReport(); },
    performanceEnabled() { return performanceProfilerEnabled; },
    setPerformanceEnabled(enabled) { return setPerformanceProfilerEnabled(enabled, false); },
    exportPerformance() { return exportPerformanceProfilerReport(); },
    lastSvgImport() { return deepClone(lastSvgImportReport); },
    open: openDebugConsole, close: closeDebugConsole, clear: clearDebugConsole
  });

  function setStatus(message = 'Bereit') {
    const translated = I18n.translateSource(String(message));
    refs.status.lastChild ? refs.status.lastChild.textContent = translated : refs.status.textContent = translated;
  }

  function applyTheme(theme, persist = true) {
    const nextTheme = Object.hasOwn(uiThemes, theme) ? theme : 'macos9';
    document.documentElement.dataset.theme = nextTheme; $('themeSelect').value = nextTheme;
    document.querySelector('meta[name="theme-color"]')?.setAttribute('content', uiThemes[nextTheme]);
    scheduleRulerRender();
    if (persist) {
      try { localStorage.setItem('paint-studio-theme', nextTheme); } catch { /* Speicherung ist optional. */ }
    }
  }

  function restoreTheme() {
    let saved = 'macos9';
    try { saved = localStorage.getItem('paint-studio-theme') || saved; } catch { /* Speicherung ist optional. */ }
    applyTheme(saved, false);
  }

  let rulerFrame = 0;

  function rulerSteps() {
    const target = 72 / Math.max(state.zoom, .001);
    const magnitude = 10 ** Math.floor(Math.log10(Math.max(target, .001)));
    const major = [1, 2, 5, 10].map((factor) => factor * magnitude).find((value) => value >= target) || 10 * magnitude;
    const minor = [major / 10, major / 5, major / 2, major].find((value) => value >= 1 && value * state.zoom >= 7) || major;
    return { major, minor };
  }

  function formatRulerValue(value) {
    const rounded = Math.round(value);
    return rounded === 0 ? '0 px' : String(rounded);
  }

  function drawRuler(canvas, ruler, orientation) {
    if (!canvas || !ruler) return;
    delete canvas.dataset.guideMarkerOrientation;
    delete canvas.dataset.guideMarkerValue;
    delete canvas.dataset.guideMarkerScreen;
    const rect = ruler.getBoundingClientRect(); const artboardRect = refs.artboard.getBoundingClientRect();
    if (rect.width < 1 || rect.height < 1 || artboardRect.width < 1 || artboardRect.height < 1) return;
    const ratio = window.devicePixelRatio || 1;
    const pixelWidth = Math.max(1, Math.round(rect.width * ratio)); const pixelHeight = Math.max(1, Math.round(rect.height * ratio));
    if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { canvas.width = pixelWidth; canvas.height = pixelHeight; }
    const context = canvas.getContext('2d'); if (!context) return;
    context.setTransform(ratio, 0, 0, ratio, 0, 0); context.clearRect(0, 0, rect.width, rect.height);
    const horizontal = orientation === 'horizontal';
    const scale = horizontal ? artboardRect.width / state.doc.width : artboardRect.height / state.doc.height;
    const rulerStart = horizontal ? rect.left : rect.top; const rulerEnd = horizontal ? rect.right : rect.bottom;
    const artboardStart = horizontal ? artboardRect.left : artboardRect.top;
    const start = (rulerStart - artboardStart) / scale; const end = (rulerEnd - artboardStart) / scale;
    const { major, minor } = rulerSteps(); const epsilon = minor / 1000;
    const first = Math.floor(start / minor) * minor;
    const theme = document.documentElement.dataset.theme;
    const graphite = theme === 'graphite'; const platinum = theme === 'macos9' || theme === 'modern-gray'; const modernGray = theme === 'modern-gray';
    const colors = graphite
      ? { originTick: '#73a7f5', majorTick: '#a7adb6', minorTick: '#676d76', originLabel: '#9bc1ff', label: '#c8ccd2' }
      : platinum
        ? { originTick: modernGray ? '#0a246a' : '#315fa9', majorTick: '#202020', minorTick: '#777777', originLabel: modernGray ? '#000080' : '#153f83', label: '#333333' }
        : { originTick: '#4f63e9', majorTick: '#69717e', minorTick: '#9aa2ae', originLabel: '#3158db', label: '#4d5664' };
    context.font = '9px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
    context.textBaseline = 'top'; context.lineWidth = 1;
    for (let value = first; value <= end + epsilon; value += minor) {
      const screen = artboardStart + value * scale - rulerStart;
      const isMajor = Math.abs(value / major - Math.round(value / major)) < .001;
      const isOrigin = Math.abs(value) < epsilon;
      const tick = isMajor ? 10 : 5;
      context.strokeStyle = isOrigin ? colors.originTick : isMajor ? colors.majorTick : colors.minorTick;
      context.beginPath();
      if (horizontal) { context.moveTo(Math.round(screen) + .5, rect.height); context.lineTo(Math.round(screen) + .5, rect.height - tick); }
      else { context.moveTo(rect.width, Math.round(screen) + .5); context.lineTo(rect.width - tick, Math.round(screen) + .5); }
      context.stroke();
      if (!isMajor) continue;
      context.fillStyle = isOrigin ? colors.originLabel : colors.label;
      const label = formatRulerValue(value);
      if (horizontal) context.fillText(label, Math.round(screen) + 3, 2);
      else {
        context.save(); context.translate(10, Math.round(screen) - 3); context.rotate(-Math.PI / 2); context.fillText(label, 0, 0); context.restore();
      }
    }
    const preview = state.guidePreview;
    const markerOrientation = horizontal ? 'vertical' : 'horizontal';
    if (preview?.orientation === markerOrientation) {
      const value = num(preview.value, NaN);
      const screen = artboardStart + value * scale - rulerStart;
      const extent = horizontal ? rect.width : rect.height;
      if (Number.isFinite(value) && screen >= 0 && screen <= extent) {
        const coordinate = Math.round(screen) + .5;
        const strokeMarker = () => {
          context.beginPath();
          if (horizontal) { context.moveTo(coordinate, 0); context.lineTo(coordinate, rect.height); }
          else { context.moveTo(0, coordinate); context.lineTo(rect.width, coordinate); }
          context.stroke();
        };
        context.save();
        context.lineWidth = 3;
        context.strokeStyle = graphite ? 'rgba(20,23,29,.88)' : 'rgba(255,255,255,.92)';
        strokeMarker();
        context.lineWidth = 1.5;
        context.strokeStyle = graphite ? '#8db4ff' : platinum ? (modernGray ? '#0a246a' : '#315fa9') : '#3158db';
        strokeMarker();
        context.restore();
        canvas.dataset.guideMarkerOrientation = preview.orientation;
        canvas.dataset.guideMarkerValue = String(round(value));
        canvas.dataset.guideMarkerScreen = String(round(screen));
      }
    }
    canvas.dataset.unit = 'px'; canvas.dataset.majorStep = String(Math.round(major)); canvas.dataset.minorStep = String(Math.round(minor));
  }

  function renderRulers() {
    if (!refs.horizontalRuler || !refs.verticalRuler) return;
    refs.verticalRuler.style.transform = `translate(${refs.workspace.scrollLeft}px, ${refs.workspace.scrollTop}px)`;
    drawRuler(refs.horizontalRulerCanvas, refs.horizontalRuler, 'horizontal');
    drawRuler(refs.verticalRulerCanvas, refs.verticalRuler, 'vertical');
  }

  function documentGuides() {
    if (!state.doc.guides || typeof state.doc.guides !== 'object') state.doc.guides = { horizontal: [], vertical: [], locked: false };
    if (!Array.isArray(state.doc.guides.horizontal)) state.doc.guides.horizontal = [];
    if (!Array.isArray(state.doc.guides.vertical)) state.doc.guides.vertical = [];
    return state.doc.guides;
  }

  function documentGrid() {
    if (!state.doc.grid || typeof state.doc.grid !== 'object') state.doc.grid = { visible: false, snap: false, size: 10 };
    state.doc.grid.visible = Boolean(state.doc.grid.visible);
    state.doc.grid.snap = Boolean(state.doc.grid.snap);
    state.doc.grid.size = clamp(Math.round(num(state.doc.grid.size, 10)), 1, 10000);
    return state.doc.grid;
  }

  function syncCanvasGridControls() {
    const grid = documentGrid();
    if ($('canvasGridVisible')) $('canvasGridVisible').checked = grid.visible;
    if ($('canvasGridSnap')) $('canvasGridSnap').checked = grid.snap;
    if ($('canvasGridSize')) $('canvasGridSize').value = grid.size;
  }

  function syncManualCanvasResizeButton() {
    const button = $('manualCanvasResizeBtn'); if (!button) return;
    const active = Boolean(state.canvasResizeMode);
    button.classList.toggle('is-active', active);
    button.setAttribute('aria-pressed', String(active));
    button.title = active
      ? 'Manuelles Anpassen beenden (Esc)'
      : 'Leinwand mit acht Anfassern direkt verändern';
  }

  function setManualCanvasResizeMode(enabled) {
    const next = Boolean(enabled);
    if (state.gesture) cancelActiveGesture({ render: false });
    refs.shell.style.transform = '';
    state.canvasResizeMode = next;
    if (next) {
      if (state.inlineTextSession) finishInlineText();
      state.selected = [];
      clearVectorSubselection();
      state.textClick = null;
      state.bitmapMarquee = null;
      if (state.tool !== 'select') chooseTool('select');
    }
    renderAll();
    toast(next ? 'Leinwand-Anfasser aktiv – Esc beendet den Modus' : 'Manuelles Anpassen beendet');
  }

  function syncGuideLockButton() {
    if (!refs.guideLock) return;
    const locked = Boolean(documentGuides().locked);
    const label = locked ? 'Alle Hilfslinien entsperren' : 'Alle Hilfslinien sperren';
    refs.guideLock.classList.toggle('is-locked', locked);
    refs.guideLock.setAttribute('aria-pressed', String(locked));
    refs.guideLock.setAttribute('aria-label', label);
    refs.guideLock.title = label;
  }

  function toggleGuideLock() {
    const guides = documentGuides();
    guides.locked = !guides.locked;
    state.guidePreview = null;
    commitHistory(guides.locked ? 'Hilfslinien gesperrt' : 'Hilfslinien entsperrt');
    renderAll();
    toast(guides.locked ? 'Alle Hilfslinien gesperrt' : 'Alle Hilfslinien entsperrt');
  }

  function scheduleRulerRender() {
    if (rulerFrame) return;
    rulerFrame = requestAnimationFrame(() => { rulerFrame = 0; renderRulers(); });
  }

  function pointFromEvent(event) {
    const rect = refs.artboard.getBoundingClientRect();
    return {
      x: clamp((event.clientX - rect.left) * state.doc.width / rect.width, -100000, 100000),
      y: clamp((event.clientY - rect.top) * state.doc.height / rect.height, -100000, 100000)
    };
  }

  function objectVisualOutset(object) {
    const strokeType = objectStrokeType(object);
    const visibleStyles = strokeStackFor(object).filter(StrokeStyle.isVisible);
    const hasStroke = visibleStyles.length > 0 || (object?.type === 'image'
      ? Boolean(normalizeColor(object.stroke)) && num(object.strokeWidth, 0) > 0
      : object?.type === 'text'
        ? strokeType !== 'none' && !['none', 'transparent', ''].includes(String(object.textStroke || 'none').toLowerCase())
        : strokeType !== 'none' && !['none', 'transparent', ''].includes(String(object?.stroke || 'none').toLowerCase()));
    let strokeWidth = visibleStyles.length ? Math.max(...visibleStyles.map((style) => {
      const base = style.width * (style.alignment === 'outside' ? 1 : .5);
      return base + (style.pattern === 'zigzag' ? StrokeStyle.zigzagMetrics(style).amplitude : 0);
    }))
      : (object?.type === 'text' ? num(object.textStrokeWidth, 0) : num(object?.strokeWidth, 0)) / 2;
    if (object?.type === 'brush') strokeWidth *= 2.4;
    const strokeProfile = normalizedStrokeProfile(object);
    if (strokeProfile) strokeWidth *= Math.max(...strokeProfile);
    const markerOutset = visibleStyles.length ? Math.max(...visibleStyles.flatMap((style) => [style.startMarker === 'none' ? 0 : style.startMarkerSize, style.endMarker === 'none' ? 0 : style.endMarkerSize])) : 0;
    const strokeOutset = hasStroke ? Math.max(0, strokeWidth, markerOutset) : 0;
    const effectOutset = Effects.visualOutset(object?.effects);
    const texture = supportsObjectTexture(object) ? textTextureSettings(object) : null;
    const textureOutset = texture ? ['washed', 'pixel'].includes(texture.mode) ? 0 : texture.mode === 'outline' ? texture.amount * .12 : texture.distortion : 0;
    return Math.max(strokeOutset, effectOutset, textureOutset);
  }

  function completeBounds(bounds) {
    const x = num(bounds?.x); const y = num(bounds?.y);
    const width = Math.max(0, num(bounds?.width)); const height = Math.max(0, num(bounds?.height));
    return { x, y, width, height, right: x + width, bottom: y + height, cx: x + width / 2, cy: y + height / 2 };
  }

  function expandedBounds(bounds, padding = 0) {
    const source = completeBounds(bounds); const amount = Math.max(0, num(padding));
    return completeBounds({
      x: source.x - amount, y: source.y - amount,
      width: source.width + amount * 2, height: source.height + amount * 2
    });
  }

  function matrixTransformedBounds(bounds, matrix) {
    const source = completeBounds(bounds);
    const corners = [[source.x, source.y], [source.right, source.y], [source.right, source.bottom], [source.x, source.bottom]]
      .map(([x, y]) => Core.applyMatrix(matrix, { x, y }));
    const left = Math.min(...corners.map((point) => point.x)); const top = Math.min(...corners.map((point) => point.y));
    const right = Math.max(...corners.map((point) => point.x)); const bottom = Math.max(...corners.map((point) => point.y));
    return completeBounds({ x: left, y: top, width: right - left, height: bottom - top });
  }

  function objectLocalFilterRegion(object, geometryBounds) {
    if (!objectHasFilter(object)) return null;
    const imported = object.svgFilterSettings?.region;
    if (imported && num(imported.width) > 0 && num(imported.height) > 0) return completeBounds(imported);
    if (!object.svgNativeFilter) return null;
    const attributes = object.svgNativeFilter.attributes || {};
    const units = String(attributes.filterUnits || attributes.filterunits || 'objectBoundingBox').toLowerCase();
    const geometry = completeBounds(geometryBounds);
    const coordinate = (value, size, origin, fallback, length = false) => {
      const source = String(value ?? '').trim();
      if (!source) return length ? size * fallback : origin + size * fallback;
      if (source.endsWith('%')) {
        const fraction = num(source.slice(0, -1)) / 100;
        return length ? size * fraction : origin + size * fraction;
      }
      const parsed = Number.parseFloat(source);
      if (!Number.isFinite(parsed)) return length ? size * fallback : origin + size * fallback;
      if (units === 'objectboundingbox') return length ? size * parsed : origin + size * parsed;
      return parsed;
    };
    const x = coordinate(attributes.x, geometry.width, geometry.x, -.1);
    const y = coordinate(attributes.y, geometry.height, geometry.y, -.1);
    const width = Math.max(0, coordinate(attributes.width, geometry.width, geometry.x, 1.2, true));
    const height = Math.max(0, coordinate(attributes.height, geometry.height, geometry.y, 1.2, true));
    return width > 0 && height > 0 ? completeBounds({ x, y, width, height }) : null;
  }

  function objectLocalVisualBounds(object, options = {}) {
    const depth = Math.max(0, num(options.depth)); const symbolIds = options.symbolIds || new Set();
    const gesture = state.gesture;
    if (!options.ignoreGesture && gesture?.kind === 'resize' && gesture.deferredGeometry
      && gesture.object?.id === object?.id && gesture.origin && object === gesture.object) {
      const source = objectLocalVisualBounds(gesture.origin, { ...options, ignoreGesture: true });
      const scaleX = Math.max(.0001, num(object.width, 1)) / Math.max(.0001, num(gesture.origin.width, 1));
      const scaleY = Math.max(.0001, num(object.height, 1)) / Math.max(.0001, num(gesture.origin.height, 1));
      return matrixTransformedBounds(source, [scaleX, 0, 0, scaleY, 0, 0]);
    }

    let geometry = options.geometryBounds ? completeBounds(options.geometryBounds) : objectLocalGeometryBounds(object);
    const transform = activeFreeTransform(object);
    if (!options.geometryBounds && transform) {
      const width = Math.max(.0001, num(object?.width, 1)); const height = Math.max(.0001, num(object?.height, 1));
      const warped = FreeTransform.bounds(transform);
      geometry = completeBounds({ x: warped.x * width, y: warped.y * height, width: warped.width * width, height: warped.height * height });
    }

    const contentBounds = [geometry];
    if (depth < 32 && object?.type === 'group' && Array.isArray(object.children)) {
      object.children.filter((child) => child?.visible !== false).forEach((child) => {
        const childBounds = objectLocalVisualBounds(child, { depth: depth + 1, symbolIds });
        contentBounds.push(matrixTransformedBounds(childBounds, Core.objectMatrix(child)));
      });
    } else if (depth < 32 && object?.type === 'symbol-instance' && object.symbolId && !symbolIds.has(object.symbolId)) {
      const definition = symbolDefinition(object.symbolId); const source = symbolRenderObject(object);
      if (definition?.object && source) {
        const nextIds = new Set(symbolIds); nextIds.add(object.symbolId);
        const sourceBounds = objectLocalVisualBounds(source, { depth: depth + 1, symbolIds: nextIds });
        const scaleX = Math.max(.0001, num(object.width, 1)) / Math.max(.0001, num(definition.object.width, 1));
        const scaleY = Math.max(.0001, num(object.height, 1)) / Math.max(.0001, num(definition.object.height, 1));
        contentBounds.push(matrixTransformedBounds(sourceBounds, [scaleX, 0, 0, scaleY, 0, 0]));
      }
    }
    const content = Core.unionBounds(contentBounds);
    const filterRegion = objectLocalFilterRegion(object, content);
    return expandedBounds(filterRegion ? Core.unionBounds([content, filterRegion]) : content, objectVisualOutset(object));
  }

  function activeFreeTransform(object, requestedMode = null) {
    if (!object) return null;
    const source = object.freeTransform || (requestedMode ? { mode: requestedMode } : null);
    if (!source) return null;
    return requestedMode ? FreeTransform.forMode(source, requestedMode) : FreeTransform.normalize(source);
  }

  function objectLocalToDocument(object, local, transform = activeFreeTransform(object)) {
    const width = Math.max(.0001, num(object?.width, 1)); const height = Math.max(.0001, num(object?.height, 1));
    let target = { x: num(local?.x ?? local?.[0]), y: num(local?.y ?? local?.[1]) };
    if (transform) {
      const warped = FreeTransform.mapPoint(transform, target.x / width, target.y / height);
      target = { x: warped.x * width, y: warped.y * height };
    }
    return Core.applyMatrix(Core.objectMatrix(object), target);
  }

  function objectParentMatrix(object) {
    const ancestors = []; const visited = new Set();
    let parent = objectEntry(object?.id)?.parent || null;
    while (parent && !visited.has(parent.id)) {
      visited.add(parent.id); ancestors.push(parent); parent = objectEntry(parent.id)?.parent || null;
    }
    return ancestors.reverse().reduce(
      (matrix, ancestor) => Core.multiplyMatrices(matrix, Core.objectMatrix(ancestor)),
      Core.identityMatrix()
    );
  }

  function transformedObjectBounds(object, outset = 0) {
    const transform = activeFreeTransform(object);
    if (!transform) {
      const local = objectLocalGeometryBounds(object); const matrix = Core.objectMatrix(object);
      const corners = [[local.x, local.y], [local.right, local.y], [local.right, local.bottom], [local.x, local.bottom]]
        .map(([x, y]) => Core.applyMatrix(matrix, { x, y }));
      const padding = Math.max(0, num(outset)); const left = Math.min(...corners.map((entry) => entry.x)) - padding; const top = Math.min(...corners.map((entry) => entry.y)) - padding;
      const right = Math.max(...corners.map((entry) => entry.x)) + padding; const bottom = Math.max(...corners.map((entry) => entry.y)) + padding;
      return { x: left, y: top, width: right - left, height: bottom - top, right, bottom, cx: (left + right) / 2, cy: (top + bottom) / 2 };
    }
    const width = Math.max(.0001, num(object.width, 1)); const height = Math.max(.0001, num(object.height, 1));
    const local = FreeTransform.bounds(transform); const matrix = Core.objectMatrix(object);
    const corners = [[local.x, local.y], [local.right, local.y], [local.right, local.bottom], [local.x, local.bottom]]
      .map(([x, y]) => Core.applyMatrix(matrix, { x: x * width, y: y * height }));
    const padding = Math.max(0, num(outset)); const left = Math.min(...corners.map((entry) => entry.x)) - padding; const top = Math.min(...corners.map((entry) => entry.y)) - padding;
    const right = Math.max(...corners.map((entry) => entry.x)) + padding; const bottom = Math.max(...corners.map((entry) => entry.y)) + padding;
    return { x: left, y: top, width: right - left, height: bottom - top, right, bottom, cx: (left + right) / 2, cy: (top + bottom) / 2 };
  }

  function boundsOf(objects, { visual = false } = {}) {
    return Core.unionBounds((objects || []).map((object) => visual
      ? matrixTransformedBounds(objectLocalVisualBounds(object), Core.objectMatrix(object))
      : transformedObjectBounds(object)));
  }

  function layoutBoundsOf(objects) {
    if (!objects.length) return { x: 0, y: 0, width: 0, height: 0, right: 0, bottom: 0, cx: 0, cy: 0 };
    const boxes = objects.map((object) => {
      const x = num(object.x); const y = num(object.y); const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
      return { x, y, width, height, right: x + width, bottom: y + height, cx: x + width / 2, cy: y + height / 2 };
    });
    return Core.unionBounds(boxes);
  }

  function supportsCornerRoundness(object) {
    return Boolean(object && ['rectangle', 'image'].includes(object.type));
  }

  function objectRoundnessLimit(object) {
    if (!supportsCornerRoundness(object)) return 0;
    return Math.max(0, Math.min(num(object.width, 1), num(object.height, 1)) / 2);
  }

  function objectRoundness(object) {
    if (!supportsCornerRoundness(object)) return 0;
    const limit = objectRoundnessLimit(object);
    if (object.type === 'image') return round(clamp(num(object.cornerRadius), 0, limit), 4);
    const rx = clamp(num(object.svgRx), 0, limit); const ry = clamp(num(object.svgRy), 0, limit);
    return round(rx && ry ? Math.min(rx, ry) : Math.max(rx, ry), 4);
  }

  function setObjectRoundness(object, value) {
    if (!supportsCornerRoundness(object)) return 0;
    const radius = round(clamp(num(value), 0, objectRoundnessLimit(object)), 4);
    if (object.type === 'image') {
      if (radius > 0) object.cornerRadius = radius; else delete object.cornerRadius;
    } else if (radius > 0) { object.svgRx = radius; object.svgRy = radius; }
    else { delete object.svgRx; delete object.svgRy; }
    return radius;
  }

  function clampObjectRoundness(object, source = object) {
    if (!supportsCornerRoundness(object)) return;
    if (object.type === 'image') {
      const radius = round(clamp(num(source?.cornerRadius), 0, objectRoundnessLimit(object)), 4);
      if (radius > 0) object.cornerRadius = radius; else delete object.cornerRadius;
      return;
    }
    const rx = round(clamp(num(source?.svgRx), 0, Math.max(1, num(object.width, 1)) / 2), 4);
    const ry = round(clamp(num(source?.svgRy), 0, Math.max(1, num(object.height, 1)) / 2), 4);
    if (rx || ry) { object.svgRx = rx; object.svgRy = ry; }
    else { delete object.svgRx; delete object.svgRy; }
  }

  function objectBase(type, x, y) {
    const noFill = $('noFill').checked || openVectorShapeTypes.has(type);
    const object = {
      id: uid('object'), type, name: I18n.translateSource(names[type] || 'Objekt'), x: round(x), y: round(y), width: 1, height: 1,
      rotation: 0, opacity: 100, visible: true, locked: false, blendMode: 'normal', stroke: $('strokeColor').value, fill: noFill ? 'none' : $('fillColor').value,
      strokeWidth: num($('strokeWidth').value, 3), scalePercent: 100
    };
    if (supportsStrokeType(object)) object.strokeType = DEFAULT_STROKE_TYPE;
    if (type === 'brush') object.smoothing = 50;
    return object;
  }

  const pointVectorTypes = new Set(['pencil', 'brush', 'eraser', 'line', 'polyline', 'arrow', 'measure', 'polygon', 'pen']);
  const boundedVectorTypes = new Set(['rectangle', 'rounded-rectangle', 'ellipse', 'block-arrow', 'beveled-rectangle', 'chamfer-rectangle', 'connector-line', 'doughnut', 'l-shape', 'pie', 'smart-polygon', 'spiral', 'star']);
  const outlineVectorTypes = new Set(['text-outline', 'compound-path']);
  const vectorCursor = 'url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2724%27 height=%2724%27 viewBox=%270 0 24 24%27%3E%3Cpath d=%27M5 3l13 9-6.3 1.5-2.8 6z%27 fill=%27white%27 stroke=%27%23172033%27 stroke-width=%271.4%27 stroke-linejoin=%27round%27/%3E%3C/svg%3E") 4 3, default';
  const knifeCursor = 'url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2724%27 height=%2724%27 viewBox=%270 0 24 24%27%3E%3Cpath d=%27m4 19 11.5-11.5M14.5 4.5l5 5M5.5 20.5l-2-2 3.2-3.2 2 2z%27 fill=%27white%27 stroke=%27%23172033%27 stroke-width=%271.4%27 stroke-linejoin=%27round%27/%3E%3C/svg%3E") 3 20, crosshair';
  const penCursor = 'url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2724%27 height=%2724%27 viewBox=%270 0 24 24%27%3E%3Cpath d=%27M12 2 5 9l4 10h6l4-10-7-7Z%27 fill=%27white%27 stroke=%27%23172033%27 stroke-width=%271.3%27/%3E%3Cpath d=%27M12 2v10%27 stroke=%27%23172033%27 stroke-width=%271.3%27/%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%271.7%27 fill=%27%23facc15%27 stroke=%27%23172033%27/%3E%3C/svg%3E") 12 12, crosshair';

  function polygonSideCount(value) {
    return clamp(Math.round(num(value, 5)), 3, 100);
  }

  function regularPolygonPoints(sides, width, height) {
    const count = polygonSideCount(sides); const w = Math.max(1, num(width, 1)); const h = Math.max(1, num(height, 1));
    const cx = w / 2; const cy = h / 2;
    return Array.from({ length: count }, (_, index) => {
      const angle = -Math.PI / 2 + index * Math.PI * 2 / count;
      return [round(cx + Math.cos(angle) * w / 2), round(cy + Math.sin(angle) * h / 2)];
    });
  }

  function setPolygonSides(object, sides) {
    if (object?.type !== 'polygon') return;
    object.sides = polygonSideCount(sides);
    object.points = regularPolygonPoints(object.sides, object.width, object.height);
    delete object.bezierSegments;
  }

  function outlinePathData(contours, bezierContours = []) {
    return (contours || []).filter((contour) => Array.isArray(contour) && contour.length >= 3)
      .map((contour, contourIndex) => {
        let data = `M ${round(contour[0][0], 3)} ${round(contour[0][1], 3)}`;
        contour.forEach((point, index) => {
          const next = contour[(index + 1) % contour.length]; const segment = bezierContours?.[contourIndex]?.[index];
          data += isBezierSegment(segment)
            ? ` C ${round(segment.c1[0], 3)} ${round(segment.c1[1], 3)}, ${round(segment.c2[0], 3)} ${round(segment.c2[1], 3)}, ${round(next[0], 3)} ${round(next[1], 3)}`
            : ` L ${round(next[0], 3)} ${round(next[1], 3)}`;
        });
        return `${data} Z`;
      }).join(' ');
  }

  function parseOutlinePathData(pathData) {
    const contours = [];
    for (const match of String(pathData || '').matchAll(/M\s*([^MZ]*)(?:Z|$)/gi)) {
      const values = (match[1].match(/-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi) || []).map(Number);
      const contour = [];
      for (let index = 0; index + 1 < values.length; index += 2) contour.push([round(values[index], 3), round(values[index + 1], 3)]);
      if (contour.length >= 3) contours.push(contour);
    }
    return contours;
  }

  function outlineContoursFor(object) {
    const existing = Array.isArray(object?.outlineContours) ? object.outlineContours
      .filter((contour) => Array.isArray(contour) && contour.length >= 3)
      .map((contour) => contour.map(([x, y]) => [num(x), num(y)])) : [];
    if (existing.length) return existing;
    const parsed = parseOutlinePathData(object?.pathData);
    if (parsed.length && object) object.outlineContours = deepClone(parsed);
    return parsed;
  }

  function outlineBezierContoursFor(object, contours = outlineContoursFor(object)) {
    return contours.map((contour, contourIndex) => Array.from({ length: contour.length }, (_, index) => {
      const segment = object?.outlineBezierContours?.[contourIndex]?.[index];
      return isBezierSegment(segment) ? { c1: [num(segment.c1[0]), num(segment.c1[1])], c2: [num(segment.c2[0]), num(segment.c2[1])] } : null;
    }));
  }

  function syncOutlinePathData(object) {
    object.pathData = outlinePathData(outlineContoursFor(object), outlineBezierContoursFor(object));
  }

  function outlinePointAddress(object, flatIndex) {
    const contours = outlineContoursFor(object); let offset = 0;
    for (let contourIndex = 0; contourIndex < contours.length; contourIndex += 1) {
      const contour = contours[contourIndex];
      if (flatIndex >= offset && flatIndex < offset + contour.length) return { contours, contourIndex, pointIndex: flatIndex - offset, offset };
      offset += contour.length;
    }
    return null;
  }

  function outlineControlAddress(object, flatIndex) {
    const address = outlinePointAddress(object, flatIndex); if (!address) return null;
    const contour = address.contours[address.contourIndex]; const segments = outlineBezierContoursFor(object, address.contours)[address.contourIndex];
    const previousPointIndex = (address.pointIndex - 1 + contour.length) % contour.length;
    return {
      ...address, contour, segments,
      previousFlatIndex: address.offset + previousPointIndex,
      incoming: isBezierSegment(segments[previousPointIndex]) ? { segmentIndex: address.offset + previousPointIndex, control: 'c2', point: segments[previousPointIndex].c2 } : null,
      outgoing: isBezierSegment(segments[address.pointIndex]) ? { segmentIndex: flatIndex, control: 'c1', point: segments[address.pointIndex].c1 } : null
    };
  }

  function outlineScale(object) {
    return {
      x: Math.max(.0001, num(object?.width, 1)) / Math.max(.0001, num(object?.pathWidth, object?.width || 1)),
      y: Math.max(.0001, num(object?.height, 1)) / Math.max(.0001, num(object?.pathHeight, object?.height || 1))
    };
  }

  function vectorEditablePoints(object) {
    if (!object || !isVectorObject(object)) return [];
    if (outlineVectorTypes.has(object.type)) {
      const scale = outlineScale(object);
      return outlineContoursFor(object).flatMap((contour) => contour.map(([x, y]) => [round(x * scale.x), round(y * scale.y)]));
    }
    if (pointVectorTypes.has(object.type)) {
      if (object.type === 'polygon' && (!Array.isArray(object.points) || !object.points.length)) setPolygonSides(object, object.sides);
      return (object.points || []).map(([x, y]) => [num(x), num(y)]);
    }
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    if (object.type === 'ellipse') return [[width / 2, 0], [width, height / 2], [width / 2, height], [0, height / 2]];
    return [[0, 0], [width, 0], [width, height], [0, height]];
  }

  function convertShapeToPolygon(object, origin = object, sourcePoints = null) {
    if (!polygonConvertibleTypes.has(origin?.type)) return false;
    const points = deepClone(sourcePoints || vectorEditablePoints(origin));
    if (points.length < 3) return false;
    object.type = 'polygon'; object.name = 'Polygon'; object.x = num(origin.x); object.y = num(origin.y);
    object.width = Math.max(1, num(origin.width, 1)); object.height = Math.max(1, num(origin.height, 1));
    object.rotation = num(origin.rotation); object.flipX = Boolean(origin.flipX); object.flipY = Boolean(origin.flipY);
    object.points = points; object.sides = points.length; delete object.bezierSegments; object.scalePercent = 100;
    return true;
  }

  function vectorSegmentCount(object, points = vectorEditablePoints(object)) {
    return isClosedVectorPath(object) ? points.length : Math.max(0, points.length - 1);
  }

  function isClosedVectorPath(object) {
    return object?.type === 'polygon' || (['pen', 'polyline'].includes(object?.type) && object.closed === true);
  }

  function vectorNextPointIndex(object, index, points = vectorEditablePoints(object)) {
    if (index < 0 || index >= points.length) return -1;
    if (index + 1 < points.length) return index + 1;
    return isClosedVectorPath(object) && points.length > 2 ? 0 : -1;
  }

  function isBezierSegment(segment) {
    return Array.isArray(segment?.c1) && Array.isArray(segment?.c2)
      && segment.c1.length >= 2 && segment.c2.length >= 2
      && segment.c1.every(Number.isFinite) && segment.c2.every(Number.isFinite);
  }

  function bezierControlsFor(object, index, points = vectorEditablePoints(object)) {
    if (object?.type === 'measure') return null;
    if (outlineVectorTypes.has(object?.type)) {
      const address = outlinePointAddress(object, index); if (!address) return null;
      const segment = outlineBezierContoursFor(object, address.contours)[address.contourIndex]?.[address.pointIndex]; if (!isBezierSegment(segment)) return null;
      const scale = outlineScale(object); const nextPointIndex = (address.pointIndex + 1) % address.contours[address.contourIndex].length;
      return {
        c1: [round(segment.c1[0] * scale.x, 4), round(segment.c1[1] * scale.y, 4)],
        c2: [round(segment.c2[0] * scale.x, 4), round(segment.c2[1] * scale.y, 4)],
        nextIndex: address.offset + nextPointIndex
      };
    }
    const nextIndex = vectorNextPointIndex(object, index, points);
    if (nextIndex < 0) return null;
    const segment = object?.bezierSegments?.[index];
    if (isBezierSegment(segment)) return { c1: [num(segment.c1[0]), num(segment.c1[1])], c2: [num(segment.c2[0]), num(segment.c2[1])], nextIndex };
    const start = points[index]; const end = points[nextIndex];
    return {
      c1: [round(num(start[0]) + (num(end[0]) - num(start[0])) / 3), round(num(start[1]) + (num(end[1]) - num(start[1])) / 3)],
      c2: [round(num(start[0]) + (num(end[0]) - num(start[0])) * 2 / 3), round(num(start[1]) + (num(end[1]) - num(start[1])) * 2 / 3)],
      nextIndex
    };
  }

  function hasBezierSegments(object) {
    return Array.isArray(object?.bezierSegments) && object.bezierSegments.some(isBezierSegment)
      || outlineBezierContoursFor(object || {}).some((segments) => segments.some(isBezierSegment));
  }

  function vectorPathData(object, points = vectorEditablePoints(object)) {
    if (!points.length) return 'M 0 0';
    const coordinate = (value) => round(value, 6);
    let d = `M ${coordinate(points[0][0])} ${coordinate(points[0][1])}`;
    const segmentCount = vectorSegmentCount(object, points);
    for (let index = 0; index < segmentCount; index += 1) {
      const nextIndex = vectorNextPointIndex(object, index, points); if (nextIndex < 0) continue;
      const next = points[nextIndex]; const controls = bezierControlsFor(object, index, points);
      if (isBezierSegment(object?.bezierSegments?.[index])) {
        d += ` C ${coordinate(controls.c1[0])} ${coordinate(controls.c1[1])}, ${coordinate(controls.c2[0])} ${coordinate(controls.c2[1])}, ${coordinate(next[0])} ${coordinate(next[1])}`;
      } else d += ` L ${coordinate(next[0])} ${coordinate(next[1])}`;
    }
    return isClosedVectorPath(object) ? `${d} Z` : d;
  }

  function isVectorObject(object) {
    return Boolean(object && (pointVectorTypes.has(object.type) || boundedVectorTypes.has(object.type) || outlineVectorTypes.has(object.type)));
  }

  function isPointEditableVector(object) {
    return Boolean(object && (pointVectorTypes.has(object.type) || outlineVectorTypes.has(object.type)));
  }

  function vectorEditObject() {
    const objects = selectedObjects();
    return objects.length === 1 && isVectorObject(objects[0]) ? objects[0] : null;
  }

  function vectorEditObjects() {
    return selectedObjects().filter((object) => isVectorObject(object));
  }

  function vectorPointSelections() {
    const source = Array.isArray(state.vectorPoints) && state.vectorPoints.length
      ? state.vectorPoints
      : state.vectorPoint ? [state.vectorPoint] : [];
    const seen = new Set(); const valid = source.filter((selection) => {
      const key = `${selection?.objectId}:${selection?.index}`; const object = objectEntry(selection?.objectId)?.object;
      if (seen.has(key) || !object || !Number.isInteger(selection?.index) || !vectorEditablePoints(object)[selection.index]) return false;
      seen.add(key); return true;
    }).map((selection) => ({ objectId: selection.objectId, index: selection.index }));
    state.vectorPoints = valid; state.vectorPoint = valid.at(-1) || null;
    return valid;
  }

  function setVectorPointSelections(selections = []) {
    state.vectorPoints = Array.isArray(selections) ? selections.map((selection) => ({ objectId: selection.objectId, index: selection.index })) : [];
    vectorPointSelections();
  }

  function vectorSegmentSelections() {
    const seen = new Set();
    state.vectorSegments = (Array.isArray(state.vectorSegments) ? state.vectorSegments : []).filter((selection) => {
      const object = objectEntry(selection?.objectId)?.object; const key = `${selection?.objectId}:${selection?.index}`;
      if (seen.has(key) || !object || !pointVectorTypes.has(object.type) || object.type === 'measure') return false;
      if (!Number.isInteger(selection.index) || selection.index < 0 || selection.index >= vectorSegmentCount(object)) return false;
      seen.add(key); return true;
    }).map((selection) => ({ objectId: selection.objectId, index: selection.index }));
    return state.vectorSegments;
  }

  function setVectorSegmentSelections(selections = []) {
    state.vectorSegments = Array.isArray(selections) ? selections.map((selection) => ({ objectId: selection.objectId, index: selection.index })) : [];
    vectorSegmentSelections();
  }

  function clearVectorSubselection() {
    state.vectorPoint = null; state.vectorPoints = []; state.vectorSegments = [];
  }

  function toggleVectorSelection(list, selection) {
    const key = `${selection.objectId}:${selection.index}`; const current = [...list];
    const index = current.findIndex((item) => `${item.objectId}:${item.index}` === key);
    if (index >= 0) current.splice(index, 1); else current.push(selection);
    return current;
  }

  function isVectorPointSelected(objectId, index) {
    return vectorPointSelections().some((selection) => selection.objectId === objectId && selection.index === index);
  }

  function isVectorSegmentSelected(objectId, index) {
    return vectorSegmentSelections().some((selection) => selection.objectId === objectId && selection.index === index);
  }

  function workbenchPathFromObject(object, { documentSpace = false } = {}) {
    const points = deepClone(object?.points || vectorEditablePoints(object));
    const segments = Array.from({ length: vectorSegmentCount(object, points) }, (_, index) => deepClone(object?.bezierSegments?.[index] || null));
    const path = { points, segments, pointModes: deepClone(object?.pointModes || []), closed: isClosedVectorPath(object) };
    if (!documentSpace) return PathWorkbench.normalizePath(path);
    const transform = ([x, y]) => {
      const point = objectLocalToDocument(object, { x: num(x), y: num(y) }); return [round(point.x, 5), round(point.y, 5)];
    };
    path.points = path.points.map(transform);
    path.segments = path.segments.map((segment) => isBezierSegment(segment) ? { c1: transform(segment.c1), c2: transform(segment.c2) } : null);
    return PathWorkbench.normalizePath(path);
  }

  function applyWorkbenchPath(object, value, { normalize = true, forcePathType = false } = {}) {
    const path = PathWorkbench.normalizePath(value);
    object.points = deepClone(path.points); object.pointModes = deepClone(path.pointModes);
    object.bezierSegments = deepClone(path.segments); object.closed = Boolean(path.closed);
    if (!object.bezierSegments.some(isBezierSegment)) delete object.bezierSegments;
    if (forcePathType || (object.closed && !['pen', 'polyline', 'polygon'].includes(object.type)) || (!object.closed && object.type === 'polygon')) {
      object.type = path.segments.some(isBezierSegment) ? 'pen' : 'polyline';
      object.name = object.closed ? (object.type === 'pen' ? 'Geschlossener Bézierpfad' : 'Geschlossene Polylinie') : (object.type === 'pen' ? 'Offener Bézierpfad' : 'Polylinie');
    }
    if (!object.closed) object.fill = 'none';
    if (object.type === 'pencil') object.rawPoints = deepClone(object.points);
    if (normalize) normalizeVectorPointBounds(object);
    object.scalePercent = 100;
    return object;
  }

  function vectorSegmentPathData(object, index, points = vectorEditablePoints(object)) {
    const next = vectorNextPointIndex(object, index, points); if (next < 0 || !points[index] || !points[next]) return '';
    const start = points[index]; const end = points[next]; const segment = object?.bezierSegments?.[index];
    if (isBezierSegment(segment)) return `M ${round(start[0])} ${round(start[1])} C ${round(segment.c1[0])} ${round(segment.c1[1])}, ${round(segment.c2[0])} ${round(segment.c2[1])}, ${round(end[0])} ${round(end[1])}`;
    return `M ${round(start[0])} ${round(start[1])} L ${round(end[0])} ${round(end[1])}`;
  }

  function pointToObjectLocal(object, point) {
    const local = Core.pointToObjectLocal(object, point); const transform = activeFreeTransform(object);
    if (!transform) return local;
    const width = Math.max(.0001, num(object?.width, 1)); const height = Math.max(.0001, num(object?.height, 1));
    const restored = FreeTransform.inversePoint(transform, { x: local.x / width, y: local.y / height });
    return { x: restored.x * width, y: restored.y * height };
  }

  function cubicCoordinateAt(start, control1, control2, end, t) {
    const inverse = 1 - t;
    return inverse ** 3 * start + 3 * inverse ** 2 * t * control1 + 3 * inverse * t ** 2 * control2 + t ** 3 * end;
  }

  function cubicExtremaParameters(start, control1, control2, end) {
    const a = -start + 3 * control1 - 3 * control2 + end;
    const b = 2 * (start - 2 * control1 + control2);
    const c = control1 - start;
    if (Math.abs(a) < 1e-10) {
      if (Math.abs(b) < 1e-10) return [];
      const t = -c / b; return t > 0 && t < 1 ? [t] : [];
    }
    const discriminant = b * b - 4 * a * c; if (discriminant < 0) return [];
    const root = Math.sqrt(Math.max(0, discriminant));
    return [...new Set([(-b + root) / (2 * a), (-b - root) / (2 * a)])].filter((t) => t > 0 && t < 1);
  }

  function pointVectorLocalGeometryBounds(object) {
    const points = Array.isArray(object?.points) ? object.points.filter((point) => Array.isArray(point) && point.length >= 2).map(([x, y]) => [num(x), num(y)]) : [];
    if (!points.length) return null;
    const samples = points.map(([x, y]) => ({ x, y }));
    const segmentCount = vectorSegmentCount(object, points);
    for (let index = 0; index < segmentCount; index += 1) {
      const nextIndex = vectorNextPointIndex(object, index, points); const segment = object?.bezierSegments?.[index];
      if (nextIndex < 0 || !isBezierSegment(segment)) continue;
      const start = points[index]; const end = points[nextIndex]; const c1 = segment.c1; const c2 = segment.c2;
      const parameters = new Set([
        ...cubicExtremaParameters(start[0], num(c1[0]), num(c2[0]), end[0]),
        ...cubicExtremaParameters(start[1], num(c1[1]), num(c2[1]), end[1])
      ]);
      parameters.forEach((t) => samples.push({
        x: cubicCoordinateAt(start[0], num(c1[0]), num(c2[0]), end[0], t),
        y: cubicCoordinateAt(start[1], num(c1[1]), num(c2[1]), end[1], t)
      }));
    }
    const left = Math.min(...samples.map((point) => point.x)); const top = Math.min(...samples.map((point) => point.y));
    const right = Math.max(...samples.map((point) => point.x)); const bottom = Math.max(...samples.map((point) => point.y));
    return { x: left, y: top, width: right - left, height: bottom - top, right, bottom, cx: (left + right) / 2, cy: (top + bottom) / 2 };
  }

  function objectLocalGeometryBounds(object) {
    const pathBounds = pointVectorTypes.has(object?.type) ? pointVectorLocalGeometryBounds(object) : null;
    if (pathBounds) return pathBounds;
    const width = Math.max(1, num(object?.width, 1)); const height = Math.max(1, num(object?.height, 1));
    return { x: 0, y: 0, width, height, right: width, bottom: height, cx: width / 2, cy: height / 2 };
  }

  const vectorNormalizationStateKeys = ['fillPaint', 'strokePaint', 'svgFilterSettings', 'transformPivot'];

  function restoreVectorNormalizationState(object, origin) {
    if (!object || !origin) return;
    vectorNormalizationStateKeys.forEach((key) => {
      if (Object.prototype.hasOwnProperty.call(origin, key)) object[key] = deepClone(origin[key]);
      else delete object[key];
    });
  }

  function normalizeVectorPointBounds(object) {
    const points = object.points || []; if (!points.length || object.freeTransform) return false;
    const bounds = pointVectorLocalGeometryBounds(object); if (!bounds) return false;
    const minX = Math.abs(bounds.x) < 1e-8 ? 0 : bounds.x; const minY = Math.abs(bounds.y) < 1e-8 ? 0 : bounds.y;
    const oldWidth = Math.max(1, num(object.width, 1)); const oldHeight = Math.max(1, num(object.height, 1));
    const newWidth = Math.max(1, bounds.width); const newHeight = Math.max(1, bounds.height);
    if (Math.abs(minX) < 1e-8 && Math.abs(minY) < 1e-8 && Math.abs(newWidth - oldWidth) < 1e-8 && Math.abs(newHeight - oldHeight) < 1e-8) return false;
    const oldCenter = { x: oldWidth / 2, y: oldHeight / 2 }; const newCenter = { x: newWidth / 2, y: newHeight / 2 };
    const delta = { x: minX + newCenter.x - oldCenter.x, y: minY + newCenter.y - oldCenter.y };
    const flipX = object.flipX ? -delta.x : delta.x; const flipY = object.flipY ? -delta.y : delta.y;
    const radians = num(object.rotation) * Math.PI / 180; const cosine = Math.cos(radians); const sine = Math.sin(radians);
    const rotated = { x: cosine * flipX - sine * flipY, y: sine * flipX + cosine * flipY };
    const pivot = object.transformPivot ? {
      x: (num(object.transformPivot.x, .5) * oldWidth - minX) / newWidth,
      y: (num(object.transformPivot.y, .5) * oldHeight - minY) / newHeight
    } : null;
    const fillPaint = object.fillPaint && typeof object.fillPaint === 'object' ? deepClone(object.fillPaint) : null;
    const strokePaint = object.strokePaint && typeof object.strokePaint === 'object' ? deepClone(object.strokePaint) : null;
    const shiftExactGradient = (paint) => {
      if (!paint?.gradientCoordinates || !Array.isArray(paint.gradientMatrix) || paint.gradientMatrix.length < 6) return;
      paint.gradientMatrix[4] = round(num(paint.gradientMatrix[4]) - minX, 8);
      paint.gradientMatrix[5] = round(num(paint.gradientMatrix[5]) - minY, 8);
    };
    const shiftExactPattern = (paint) => {
      if (!paint?.svgPattern || !Array.isArray(paint.svgPattern.matrix) || paint.svgPattern.matrix.length < 6) return;
      paint.svgPattern.matrix[4] = round(num(paint.svgPattern.matrix[4]) - minX, 8);
      paint.svgPattern.matrix[5] = round(num(paint.svgPattern.matrix[5]) - minY, 8);
    };
    shiftExactGradient(fillPaint); shiftExactGradient(strokePaint);
    shiftExactPattern(fillPaint); shiftExactPattern(strokePaint);
    if (fillPaint) {
      const horizontal = (value) => (oldWidth * num(value) / 100 - minX) / newWidth * 100;
      const vertical = (value) => (oldHeight * num(value) / 100 - minY) / newHeight * 100;
      if (fillPaint.type === 'linear') {
        fillPaint.x1 = round(horizontal(fillPaint.x1), 4); fillPaint.y1 = round(vertical(fillPaint.y1), 4);
        fillPaint.x2 = round(horizontal(fillPaint.x2), 4); fillPaint.y2 = round(vertical(fillPaint.y2), 4);
      } else if (fillPaint.type === 'radial' || fillPaint.type === 'conic') {
        fillPaint.cx = round(horizontal(fillPaint.cx), 4); fillPaint.cy = round(vertical(fillPaint.cy), 4);
        if (fillPaint.type === 'radial') fillPaint.radius = round(num(fillPaint.radius, 50) * Math.min(oldWidth, oldHeight) / Math.min(newWidth, newHeight), 4);
      } else if (fillPaint.type === 'image') {
        const imageWidth = Math.max(1, num(fillPaint.imageWidth, 1)); const imageHeight = Math.max(1, num(fillPaint.imageHeight, 1));
        const oldCover = Math.max(oldWidth / imageWidth, oldHeight / imageHeight); const nextCover = Math.max(newWidth / imageWidth, newHeight / imageHeight);
        fillPaint.scale = round(num(fillPaint.scale, 100) * oldCover / Math.max(.0001, nextCover), 4);
        fillPaint.offsetX = round(oldCenter.x + num(fillPaint.offsetX) - minX - newCenter.x, 4);
        fillPaint.offsetY = round(oldCenter.y + num(fillPaint.offsetY) - minY - newCenter.y, 4);
      } else if (fillPaint.type === 'pattern') {
        const angle = num(fillPaint.rotation) * Math.PI / 180; const cosineFill = Math.cos(-angle); const sineFill = Math.sin(-angle);
        const inverseX = cosineFill * (oldCenter.x - newCenter.x - minX) - sineFill * (oldCenter.y - newCenter.y - minY);
        const inverseY = sineFill * (oldCenter.x - newCenter.x - minX) + cosineFill * (oldCenter.y - newCenter.y - minY);
        fillPaint.offsetX = round(num(fillPaint.offsetX) + newCenter.x - oldCenter.x + inverseX, 4);
        fillPaint.offsetY = round(num(fillPaint.offsetY) + newCenter.y - oldCenter.y + inverseY, 4);
      }
    }
    if (strokePaint && ['linear', 'radial'].includes(strokePaint.type)) {
      const horizontal = (value) => (oldWidth * num(value) / 100 - minX) / newWidth * 100;
      const vertical = (value) => (oldHeight * num(value) / 100 - minY) / newHeight * 100;
      if (strokePaint.type === 'linear') {
        strokePaint.x1 = round(horizontal(strokePaint.x1), 4); strokePaint.y1 = round(vertical(strokePaint.y1), 4);
        strokePaint.x2 = round(horizontal(strokePaint.x2), 4); strokePaint.y2 = round(vertical(strokePaint.y2), 4);
      } else {
        strokePaint.cx = round(horizontal(strokePaint.cx), 4); strokePaint.cy = round(vertical(strokePaint.cy), 4);
        strokePaint.radius = round(num(strokePaint.radius, 50) * Math.min(oldWidth, oldHeight) / Math.min(newWidth, newHeight), 4);
      }
    }
    if (object.svgFilterSettings?.region) {
      object.svgFilterSettings.region.x = round(num(object.svgFilterSettings.region.x) - minX, 4);
      object.svgFilterSettings.region.y = round(num(object.svgFilterSettings.region.y) - minY, 4);
    }
    object.x = round(num(object.x) + oldCenter.x - newCenter.x + rotated.x, 4);
    object.y = round(num(object.y) + oldCenter.y - newCenter.y + rotated.y, 4);
    object.width = round(newWidth, 4); object.height = round(newHeight, 4);
    object.points = points.map(([x, y]) => [round(num(x) - minX, 4), round(num(y) - minY, 4)]);
    if (Array.isArray(object.bezierSegments)) {
      object.bezierSegments = object.bezierSegments.map((segment) => {
        if (!isBezierSegment(segment)) return null;
        return {
          c1: [round(num(segment.c1[0]) - minX, 4), round(num(segment.c1[1]) - minY, 4)],
          c2: [round(num(segment.c2[0]) - minX, 4), round(num(segment.c2[1]) - minY, 4)]
        };
      });
    }
    if (pivot) object.transformPivot = { x: round(pivot.x, 6), y: round(pivot.y, 6) };
    if (fillPaint) object.fillPaint = fillPaint;
    if (strokePaint) object.strokePaint = strokePaint;
    if (object.type === 'pencil') object.rawPoints = deepClone(object.points);
    return true;
  }

  function updateBoundedVectorPoint(object, index, point, origin) {
    const local = pointToObjectLocal(origin, point); const width = Math.max(1, num(origin.width, 1)); const height = Math.max(1, num(origin.height, 1));
    let left = 0; let top = 0; let right = width; let bottom = height;
    if (object.type === 'ellipse') {
      if (index === 0) top = local.y; if (index === 1) right = local.x;
      if (index === 2) bottom = local.y; if (index === 3) left = local.x;
    } else {
      if (index === 0 || index === 3) left = local.x; if (index === 1 || index === 2) right = local.x;
      if (index === 0 || index === 1) top = local.y; if (index === 2 || index === 3) bottom = local.y;
    }
    object.x = round(num(origin.x) + Math.min(left, right)); object.y = round(num(origin.y) + Math.min(top, bottom));
    object.width = Math.max(1, round(Math.abs(right - left))); object.height = Math.max(1, round(Math.abs(bottom - top)));
  }

  function updateTextOutlineVectorPoint(object, index, point, origin) {
    const address = outlinePointAddress(origin, index); if (!address) return;
    const local = pointToObjectLocal(origin, point); const scale = outlineScale(origin); const contours = deepClone(address.contours); const bezierContours = outlineBezierContoursFor(origin, address.contours);
    const previous = contours[address.contourIndex][address.pointIndex]; const next = [round(local.x / scale.x, 3), round(local.y / scale.y, 3)];
    const dx = next[0] - previous[0]; const dy = next[1] - previous[1]; contours[address.contourIndex][address.pointIndex] = next;
    const segments = bezierContours[address.contourIndex]; const previousIndex = (address.pointIndex - 1 + contours[address.contourIndex].length) % contours[address.contourIndex].length;
    if (isBezierSegment(segments[address.pointIndex])) segments[address.pointIndex].c1 = [round(segments[address.pointIndex].c1[0] + dx, 3), round(segments[address.pointIndex].c1[1] + dy, 3)];
    if (isBezierSegment(segments[previousIndex])) segments[previousIndex].c2 = [round(segments[previousIndex].c2[0] + dx, 3), round(segments[previousIndex].c2[1] + dy, 3)];
    object.x = num(origin.x); object.y = num(origin.y); object.width = Math.max(.1, num(origin.width, 1)); object.height = Math.max(.1, num(origin.height, 1));
    object.outlineContours = contours; object.outlineBezierContours = bezierContours; object.pathData = outlinePathData(contours, bezierContours); object.scalePercent = 100;
  }

  function updateVectorPoint(object, index, point, origin) {
    if (outlineVectorTypes.has(object?.type)) { updateTextOutlineVectorPoint(object, index, point, origin); return; }
    if (polygonConvertibleTypes.has(origin?.type)) {
      if (!convertShapeToPolygon(object, origin)) return;
      const local = pointToObjectLocal(origin, point); const points = deepClone(object.points);
      if (!points[index]) return;
      points[index] = [round(local.x), round(local.y)]; object.points = points;
      restoreVectorNormalizationState(object, origin);
      normalizeVectorPointBounds(object);
      object.scalePercent = 100; return;
    }
    if (pointVectorTypes.has(object.type)) {
      const local = pointToObjectLocal(origin, point); const points = deepClone(origin.points || vectorEditablePoints(origin));
      if (!points[index]) return;
      // Jeder Pointer-Move wird aus dem unveränderten Objektzustand berechnet.
      // Nach dem Normalisieren können x/y bereits verschoben sein; ohne das
      // Zurücksetzen würden die unveränderten Punkte beim nächsten Move relativ
      // zu dieser neuen Position gelesen und das Polygon sichtbar wandern.
      object.x = num(origin.x); object.y = num(origin.y); object.width = Math.max(1, num(origin.width, 1)); object.height = Math.max(1, num(origin.height, 1));
      restoreVectorNormalizationState(object, origin);
      const originalPoint = points[index]; const dx = round(local.x - num(originalPoint[0])); const dy = round(local.y - num(originalPoint[1]));
      points[index] = [round(local.x), round(local.y)]; object.points = points;
      if (Array.isArray(origin.bezierSegments)) {
        const segments = deepClone(origin.bezierSegments);
        const move = (segmentIndex, control) => {
          const segment = segments[segmentIndex]; if (!isBezierSegment(segment)) return;
          segment[control] = [round(num(segment[control][0]) + dx), round(num(segment[control][1]) + dy)];
        };
        move(index, 'c1');
        const previous = index > 0 ? index - 1 : isClosedVectorPath(origin) ? points.length - 1 : -1;
        if (previous >= 0) move(previous, 'c2');
        object.bezierSegments = segments;
      }
      if (object.type === 'pencil') object.rawPoints = deepClone(points);
      normalizeVectorPointBounds(object);
    } else updateBoundedVectorPoint(object, index, point, origin);
    object.scalePercent = 100;
  }

  function updateVectorPointsGesture(gesture, point) {
    const changedObjects = [];
    gesture.items.forEach((item) => {
      const { object, origin, indices } = item; if (!object || !origin || !indices.length) return;
      const startLocal = pointToObjectLocal(origin, gesture.start); const currentLocal = pointToObjectLocal(origin, point);
      const dx = round(currentLocal.x - startLocal.x, 4); const dy = round(currentLocal.y - startLocal.y, 4);
      if (outlineVectorTypes.has(origin.type)) {
        const scale = outlineScale(origin); const contours = deepClone(outlineContoursFor(origin)); const bezierContours = outlineBezierContoursFor(origin); const movedControls = new Set();
        indices.forEach((index) => {
          const address = outlinePointAddress(origin, index); if (!address) return;
          const anchor = contours[address.contourIndex]?.[address.pointIndex]; if (!anchor) return;
          anchor[0] = round(anchor[0] + dx / scale.x, 3); anchor[1] = round(anchor[1] + dy / scale.y, 3);
          const segments = bezierContours[address.contourIndex]; const previousIndex = (address.pointIndex - 1 + contours[address.contourIndex].length) % contours[address.contourIndex].length;
          [[address.pointIndex, 'c1'], [previousIndex, 'c2']].forEach(([segmentIndex, control]) => {
            const key = `${address.contourIndex}:${segmentIndex}:${control}`; const segment = segments[segmentIndex];
            if (movedControls.has(key) || !isBezierSegment(segment)) return;
            segment[control] = [round(segment[control][0] + dx / scale.x, 3), round(segment[control][1] + dy / scale.y, 3)]; movedControls.add(key);
          });
        });
        object.x = num(origin.x); object.y = num(origin.y); object.width = num(origin.width, 1); object.height = num(origin.height, 1);
        object.outlineContours = contours; object.outlineBezierContours = bezierContours; object.pathData = outlinePathData(contours, bezierContours); object.scalePercent = 100; changedObjects.push(object); return;
      }
      if (!pointVectorTypes.has(origin.type)) return;
      if (object.type !== origin.type) {
        object.type = origin.type; object.name = origin.name || names[origin.type] || 'Polygon';
        object.sides = origin.sides;
      }
      object.x = num(origin.x); object.y = num(origin.y); object.width = Math.max(1, num(origin.width, 1)); object.height = Math.max(1, num(origin.height, 1));
      restoreVectorNormalizationState(object, origin);
      object.points = deepClone(origin.points || vectorEditablePoints(origin));
      const selected = new Set(indices); const movedControls = new Set();
      indices.forEach((index) => {
        const anchor = object.points[index]; if (!anchor) return;
        anchor[0] = round(anchor[0] + dx, 4); anchor[1] = round(anchor[1] + dy, 4);
      });
      if (Array.isArray(origin.bezierSegments)) {
        object.bezierSegments = deepClone(origin.bezierSegments);
        indices.forEach((index) => {
          const controls = [[index, 'c1'], [index > 0 ? index - 1 : isClosedVectorPath(origin) ? object.points.length - 1 : -1, 'c2']];
          controls.forEach(([segmentIndex, control]) => {
            const key = `${segmentIndex}:${control}`; const segment = object.bezierSegments[segmentIndex];
            if (segmentIndex < 0 || movedControls.has(key) || !isBezierSegment(segment)) return;
            segment[control] = [round(segment[control][0] + dx, 4), round(segment[control][1] + dy, 4)]; movedControls.add(key);
          });
        });
      }
      if (object.type === 'pencil') object.rawPoints = deepClone(object.points);
      normalizeVectorPointBounds(object);
      object.scalePercent = 100; changedObjects.push(object);
    });
    return changedObjects;
  }

  function updateVectorBezierControl(object, index, control, point, origin) {
    if (outlineVectorTypes.has(origin?.type)) {
      const address = outlinePointAddress(origin, index); if (!address || !['c1', 'c2'].includes(control)) return;
      const local = pointToObjectLocal(origin, point); const scale = outlineScale(origin); const contours = deepClone(address.contours); const bezierContours = outlineBezierContoursFor(origin, address.contours);
      const segment = bezierContours[address.contourIndex][address.pointIndex]; if (!isBezierSegment(segment)) return;
      segment[control] = [round(local.x / scale.x, 4), round(local.y / scale.y, 4)];
      object.x = num(origin.x); object.y = num(origin.y); object.width = Math.max(.1, num(origin.width, 1)); object.height = Math.max(.1, num(origin.height, 1));
      object.outlineContours = contours; object.outlineBezierContours = bezierContours; object.pathData = outlinePathData(contours, bezierContours); object.scalePercent = 100; return;
    }
    const points = deepClone(origin.points || vectorEditablePoints(origin)); const controls = bezierControlsFor(origin, index, points);
    if (!controls || !['c1', 'c2'].includes(control)) return;
    const local = pointToObjectLocal(origin, point);
    object.x = num(origin.x); object.y = num(origin.y); object.width = Math.max(1, num(origin.width, 1)); object.height = Math.max(1, num(origin.height, 1));
    restoreVectorNormalizationState(object, origin);
    const path = PathWorkbench.moveHandle(workbenchPathFromObject(origin), index, control, [round(local.x), round(local.y)]);
    applyWorkbenchPath(object, path, { normalize: false });
    normalizeVectorPointBounds(object);
    object.scalePercent = 100;
  }

  const lerpPoint = (a, b, t) => [round(num(a[0]) + (num(b[0]) - num(a[0])) * t, 4), round(num(a[1]) + (num(b[1]) - num(a[1])) * t, 4)];
  const distanceBetweenPoints = (a, b) => Math.hypot(num(b?.[0]) - num(a?.[0]), num(b?.[1]) - num(a?.[1]));

  function cubicPoint(a, c1, c2, b, t) {
    const q0 = lerpPoint(a, c1, t); const q1 = lerpPoint(c1, c2, t); const q2 = lerpPoint(c2, b, t);
    return lerpPoint(lerpPoint(q0, q1, t), lerpPoint(q1, q2, t), t);
  }

  function closestPointOnLine(point, a, b) {
    const dx = num(b[0]) - num(a[0]); const dy = num(b[1]) - num(a[1]); const lengthSquared = dx * dx + dy * dy;
    const t = lengthSquared > .00001 ? clamp(((point[0] - num(a[0])) * dx + (point[1] - num(a[1])) * dy) / lengthSquared, 0, 1) : 0;
    const hit = lerpPoint(a, b, t);
    return { t, point: hit, distance: Math.hypot(point[0] - hit[0], point[1] - hit[1]) };
  }

  function closestPointOnCubic(point, a, c1, c2, b) {
    const steps = 36; let best = { distance: Infinity, t: 0, point: a };
    let previous = cubicPoint(a, c1, c2, b, 0);
    for (let index = 1; index <= steps; index += 1) {
      const current = cubicPoint(a, c1, c2, b, index / steps); const hit = closestPointOnLine(point, previous, current);
      if (hit.distance < best.distance) {
        const t = ((index - 1) + hit.t) / steps;
        best = { distance: hit.distance, t, point: cubicPoint(a, c1, c2, b, t) };
      }
      previous = current;
    }
    return best;
  }

  function vectorSegmentHit(object, point) {
    if (!pointVectorTypes.has(object?.type) || object.type === 'measure') return null;
    const points = vectorEditablePoints(object); const local = pointToObjectLocal(object, point);
    const profile = normalizedStrokeProfile(object, points); const pressure = profile ? Math.max(...profile) : 1;
    const tolerance = Math.max(4 / state.zoom, (object.type === 'brush' ? num(object.strokeWidth, 3) * 1.2 : num(object.strokeWidth, 3) / 2) * pressure + 3 / state.zoom);
    let best = null;
    for (let index = 0; index < vectorSegmentCount(object, points); index += 1) {
      const nextIndex = vectorNextPointIndex(object, index, points); const a = points[index]; const b = points[nextIndex];
      if (!a || !b) continue;
      const controls = bezierControlsFor(object, index, points);
      const hit = isBezierSegment(object?.bezierSegments?.[index]) && controls
        ? closestPointOnCubic([local.x, local.y], a, controls.c1, controls.c2, b)
        : closestPointOnLine([local.x, local.y], a, b);
      if (hit.distance <= tolerance && (!best || hit.distance < best.distance)) best = { ...hit, index, nextIndex, points, closed: isClosedVectorPath(object) };
    }
    return best;
  }

  function sampledClosedShapePoints(object) {
    const width = Math.max(1, num(object?.width, 1)); const height = Math.max(1, num(object?.height, 1));
    if (object.type === 'polygon' || (['pen', 'polyline'].includes(object.type) && object.closed)) {
      const points = vectorEditablePoints(object); const sampled = [];
      for (let index = 0; index < points.length; index += 1) {
        const nextIndex = vectorNextPointIndex(object, index, points); const controls = bezierControlsFor(object, index, points);
        if (isBezierSegment(object?.bezierSegments?.[index]) && controls) {
          const length = distanceBetweenPoints(points[index], controls.c1) + distanceBetweenPoints(controls.c1, controls.c2) + distanceBetweenPoints(controls.c2, points[nextIndex]);
          const steps = clamp(Math.ceil(length / 6), 8, 64);
          for (let step = 0; step < steps; step += 1) sampled.push(cubicPoint(points[index], controls.c1, controls.c2, points[nextIndex], step / steps));
        } else sampled.push(deepClone(points[index]));
      }
      return sampled;
    }
    if (AutoShapes.isAutoShape(object)) return AutoShapes.outlinePoints(object);
    if (object.type === 'rectangle') return [[0, 0], [width, 0], [width, height], [0, height]];
    if (object.type === 'ellipse') return Array.from({ length: 64 }, (_, index) => {
      const angle = -Math.PI / 2 + index * Math.PI * 2 / 64;
      return [round(width / 2 + Math.cos(angle) * width / 2, 3), round(height / 2 + Math.sin(angle) * height / 2, 3)];
    });
    return [];
  }

  function closedShapeHit(object, point) {
    const points = sampledClosedShapePoints(object); if (points.length < 3) return null;
    const local = pointToObjectLocal(object, point); const tolerance = Math.max(4 / state.zoom, num(object.strokeWidth, 3) / 2 + 3 / state.zoom);
    let best = null;
    points.forEach((anchor, index) => {
      const nextIndex = (index + 1) % points.length; const hit = closestPointOnLine([local.x, local.y], anchor, points[nextIndex]);
      if (hit.distance <= tolerance && (!best || hit.distance < best.distance)) best = { ...hit, index, nextIndex, points, closed: true };
    });
    return best;
  }

  function outlineSegmentHit(object, point) {
    if (!outlineVectorTypes.has(object?.type)) return null;
    const local = pointToObjectLocal(object, point); const scale = outlineScale(object);
    const probe = [local.x / scale.x, local.y / scale.y]; const tolerance = 4 / state.zoom / Math.min(scale.x, scale.y);
    const contours = outlineContoursFor(object); const bezierContours = outlineBezierContoursFor(object, contours); let best = null;
    contours.forEach((contour, contourIndex) => contour.forEach((anchor, index) => {
      const nextIndex = (index + 1) % contour.length; const segment = bezierContours[contourIndex][index];
      const hit = isBezierSegment(segment) ? closestPointOnCubic(probe, anchor, segment.c1, segment.c2, contour[nextIndex]) : closestPointOnLine(probe, anchor, contour[nextIndex]);
      if (hit.distance <= tolerance && (!best || hit.distance < best.distance)) best = { ...hit, contourIndex, index, nextIndex, contours };
    }));
    return best;
  }

  function vectorHitAt(point, { cuttable = false } = {}) {
    let best = null;
    for (let layerIndex = state.doc.layers.length - 1; layerIndex >= 0; layerIndex -= 1) {
      const layer = state.doc.layers[layerIndex]; if (!layer.visible || layer.locked) continue;
      for (let index = layer.objects.length - 1; index >= 0; index -= 1) {
        const object = layer.objects[index];
        if (object.visible === false || object.locked) continue;
        const hit = cuttable
          ? (cuttableVectorTypes.has(object.type) ? vectorSegmentHit(object, point) : closedKnifeTypes.has(object.type) ? closedShapeHit(object, point) : null)
          : (outlineVectorTypes.has(object.type) ? outlineSegmentHit(object, point) : pointVectorTypes.has(object.type) ? vectorSegmentHit(object, point) : polygonConvertibleTypes.has(object.type) ? closedShapeHit(object, point) : null);
        if (hit && (!best || hit.distance < best.hit.distance)) best = { layer, index, object, hit };
      }
    }
    return best;
  }

  const pathScrubberTypes = new Set(['pencil', 'brush', 'line', 'polyline', 'pen']);

  function pathModifierSettings() {
    return {
      size: clamp(num($('pathModifierSize')?.value, 80), 1, 500),
      strength: clamp(num($('pathModifierStrength')?.value, 70), 1, 100)
    };
  }

  function canDeformWithPathModifier(object) {
    if (!object || object.locked || object.visible === false || object.type === 'measure') return false;
    if (pointVectorTypes.has(object.type) || outlineVectorTypes.has(object.type)) return true;
    return boundedVectorTypes.has(object.type) && object.type !== 'doughnut' && sampledClosedShapePoints(object).length >= 3;
  }

  function canScrubPath(object) {
    return Boolean(object && pathScrubberTypes.has(object.type) && vectorEditablePoints(object).length >= 2 && objectStrokeType(object) !== 'none');
  }

  function closestPathModifierSegment(object, point) {
    if (!object) return null;
    if (outlineVectorTypes.has(object.type)) {
      const local = pointToObjectLocal(object, point); const scale = outlineScale(object);
      const probe = [local.x / scale.x, local.y / scale.y]; let best = null;
      outlineContoursFor(object).forEach((contour, contourIndex) => contour.forEach((anchor, index) => {
        const nextIndex = (index + 1) % contour.length; const hit = closestPointOnLine(probe, anchor, contour[nextIndex]);
        const distance = hit.distance * Math.min(scale.x, scale.y);
        if (!best || distance < best.distance) best = { ...hit, distance, contourIndex, index, nextIndex, contours: outlineContoursFor(object), closed: true };
      }));
      return best;
    }
    const points = pointVectorTypes.has(object.type) ? vectorEditablePoints(object) : sampledClosedShapePoints(object);
    if (points.length < 2) return null;
    const local = pointToObjectLocal(object, point); const closed = pointVectorTypes.has(object.type) ? isClosedVectorPath(object) : true;
    const segmentCount = closed ? points.length : points.length - 1; let best = null;
    for (let index = 0; index < segmentCount; index += 1) {
      const nextIndex = index + 1 < points.length ? index + 1 : 0; const a = points[index]; const b = points[nextIndex];
      const controls = pointVectorTypes.has(object.type) ? bezierControlsFor(object, index, points) : null;
      const hit = pointVectorTypes.has(object.type) && isBezierSegment(object?.bezierSegments?.[index]) && controls
        ? closestPointOnCubic([local.x, local.y], a, controls.c1, controls.c2, b)
        : closestPointOnLine([local.x, local.y], a, b);
      if (!best || hit.distance < best.distance) best = { ...hit, index, nextIndex, points, closed };
    }
    return best;
  }

  function pathModifierTargetAt(point, { scrub = false } = {}) {
    const eligible = scrub ? canScrubPath : canDeformWithPathModifier;
    const direct = objectAtPoint(point);
    if (eligible(direct)) return objectEntry(direct.id);
    const radius = pathModifierSettings().size / 2;
    const selected = selectedObjects().filter(eligible).map((object) => ({ entry: objectEntry(object.id), hit: closestPathModifierSegment(object, point) }))
      .filter(({ entry, hit }) => entry && hit && hit.distance <= radius + 8 / state.zoom)
      .sort((left, right) => left.hit.distance - right.hit.distance);
    if (selected[0]) return selected[0].entry;
    let best = null;
    for (const layer of [...state.doc.layers].reverse()) {
      if (!layer.visible || layer.locked) continue;
      for (let index = layer.objects.length - 1; index >= 0; index -= 1) {
        const object = layer.objects[index]; if (!eligible(object)) continue;
        const hit = closestPathModifierSegment(object, point);
        if (hit && hit.distance <= Math.max(8 / state.zoom, radius * .35) && (!best || hit.distance < best.hit.distance)) best = { object, layer, index, hit };
      }
    }
    return best;
  }

  function preparePathModifierObject(object) {
    if (pointVectorTypes.has(object?.type) || outlineVectorTypes.has(object?.type)) return true;
    const points = sampledClosedShapePoints(object);
    if (!boundedVectorTypes.has(object?.type) || object.type === 'doughnut' || points.length < 3) return false;
    object.type = 'polygon'; object.points = deepClone(points); object.sides = points.length; object.closed = true;
    delete object.bezierSegments; delete object.strokeProfile; object.scalePercent = 100;
    return true;
  }

  function areaReshapeSubdivisionTargets(object, focus, radius) {
    const targets = []; const spacing = clamp(radius * .18, 6, 18);
    const appendSegment = (start, control1, control2, end) => {
      const curved = Boolean(control1 && control2); const pointAt = (t) => curved ? cubicPoint(start, control1, control2, end, t) : lerpPoint(start, end, t);
      let previous = objectLocalToDocument(object, { x: start[0], y: start[1] }); let length = 0;
      for (let step = 1; step <= 12; step += 1) {
        const local = pointAt(step / 12); const world = objectLocalToDocument(object, { x: local[0], y: local[1] });
        length += Math.hypot(world.x - previous.x, world.y - previous.y); previous = world;
      }
      const divisions = Math.round(clamp(Math.ceil(length / spacing), 1, 24));
      for (let step = 1; step < divisions; step += 1) {
        const local = pointAt(step / divisions); const world = objectLocalToDocument(object, { x: local[0], y: local[1] });
        const distance = Math.hypot(world.x - focus.x, world.y - focus.y);
        if (distance < radius) targets.push({ x: world.x, y: world.y, distance });
      }
    };
    if (outlineVectorTypes.has(object?.type)) {
      const scale = outlineScale(object); const contours = outlineContoursFor(object); const bezierContours = outlineBezierContoursFor(object);
      contours.forEach((contour, contourIndex) => contour.forEach((anchor, index) => {
        const next = contour[(index + 1) % contour.length]; const segment = bezierContours[contourIndex]?.[index];
        appendSegment(
          [anchor[0] * scale.x, anchor[1] * scale.y],
          isBezierSegment(segment) ? [segment.c1[0] * scale.x, segment.c1[1] * scale.y] : null,
          isBezierSegment(segment) ? [segment.c2[0] * scale.x, segment.c2[1] * scale.y] : null,
          [next[0] * scale.x, next[1] * scale.y]
        );
      }));
    } else {
      const points = vectorEditablePoints(object); const segmentCount = vectorSegmentCount(object, points);
      for (let index = 0; index < segmentCount; index += 1) {
        const nextIndex = vectorNextPointIndex(object, index, points); if (nextIndex < 0) continue;
        const segment = object.bezierSegments?.[index];
        appendSegment(points[index], isBezierSegment(segment) ? segment.c1 : null, isBezierSegment(segment) ? segment.c2 : null, points[nextIndex]);
      }
    }
    return targets.sort((left, right) => left.distance - right.distance).slice(0, 80);
  }

  function refineAreaReshapeObject(object, focus, radius) {
    const targets = areaReshapeSubdivisionTargets(object, focus, radius); let inserted = 0;
    targets.forEach((target) => {
      const hit = closestPathModifierSegment(object, target);
      if (!hit || hit.t <= .018 || hit.t >= .982 || hit.distance > Math.max(1, radius * .025)) return;
      if (insertVectorAnchor(object, hit) >= 0) inserted += 1;
    });
    return inserted;
  }

  function modifierFalloff(distance, radius, mode) {
    if (distance >= radius) return 0;
    const inner = mode === 'reshape-area' ? radius * .34 : 0;
    if (distance <= inner) return 1;
    const t = clamp((distance - inner) / Math.max(.001, radius - inner), 0, 1);
    return 1 - t * t * (3 - 2 * t);
  }

  function deformedLocalPoint(origin, localPoint, focus, dx, dy, radius, strength, mode) {
    const world = objectLocalToDocument(origin, { x: num(localPoint[0]), y: num(localPoint[1]) });
    const weight = modifierFalloff(Math.hypot(world.x - focus.x, world.y - focus.y), radius, mode) * strength;
    if (weight <= 0) return { point: [num(localPoint[0]), num(localPoint[1])], movement: 0 };
    const moved = pointToObjectLocal(origin, { x: world.x + dx * weight, y: world.y + dy * weight });
    return { point: [round(moved.x, 3), round(moved.y, 3)], movement: Math.hypot(dx * weight, dy * weight) };
  }

  function deformPathModifierObject(object, origin, focus, dx, dy, radius, strength, mode) {
    object.x = num(origin.x); object.y = num(origin.y); object.width = Math.max(1, num(origin.width, 1)); object.height = Math.max(1, num(origin.height, 1));
    restoreVectorNormalizationState(object, origin);
    if (outlineVectorTypes.has(origin.type)) {
      const scale = outlineScale(origin); let maximum = 0;
      object.outlineContours = outlineContoursFor(origin).map((contour) => contour.map(([x, y]) => {
        const result = deformedLocalPoint(origin, [x * scale.x, y * scale.y], focus, dx, dy, radius, strength, mode);
        maximum = Math.max(maximum, result.movement); return [round(result.point[0] / scale.x, 3), round(result.point[1] / scale.y, 3)];
      }));
      object.outlineBezierContours = outlineBezierContoursFor(origin).map((segments) => segments.map((segment) => {
        if (!isBezierSegment(segment)) return null;
        const c1 = deformedLocalPoint(origin, [segment.c1[0] * scale.x, segment.c1[1] * scale.y], focus, dx, dy, radius, strength, mode);
        const c2 = deformedLocalPoint(origin, [segment.c2[0] * scale.x, segment.c2[1] * scale.y], focus, dx, dy, radius, strength, mode);
        maximum = Math.max(maximum, c1.movement, c2.movement);
        return { c1: [round(c1.point[0] / scale.x, 3), round(c1.point[1] / scale.y, 3)], c2: [round(c2.point[0] / scale.x, 3), round(c2.point[1] / scale.y, 3)] };
      }));
      syncOutlinePathData(object); object.scalePercent = 100; return maximum;
    }
    const points = deepClone(origin.points || vectorEditablePoints(origin)); let maximum = 0;
    object.points = points.map((point) => {
      const result = deformedLocalPoint(origin, point, focus, dx, dy, radius, strength, mode);
      maximum = Math.max(maximum, result.movement); return result.point;
    });
    if (Array.isArray(origin.bezierSegments)) {
      object.bezierSegments = origin.bezierSegments.map((segment) => {
        if (!isBezierSegment(segment)) return null;
        const c1 = deformedLocalPoint(origin, segment.c1, focus, dx, dy, radius, strength, mode);
        const c2 = deformedLocalPoint(origin, segment.c2, focus, dx, dy, radius, strength, mode);
        maximum = Math.max(maximum, c1.movement, c2.movement); return { c1: c1.point, c2: c2.point };
      });
    }
    if (origin.type === 'pencil') object.rawPoints = deepClone(object.points);
    normalizeVectorPointBounds(object);
    object.scalePercent = 100; return maximum;
  }

  function ensurePathScrubberAnchor(object, point, radius) {
    const hit = closestPathModifierSegment(object, point);
    if (!hit || hit.distance > radius || hit.t <= .025 || hit.t >= .975) return -1;
    return insertVectorAnchor(object, hit);
  }

  function applyPathScrubberStamp(gesture, point, event) {
    const object = gesture.object; const settings = gesture.settings; const radius = settings.size / 2;
    if (!object || !canScrubPath(object)) return false;
    const pointsBefore = vectorEditablePoints(object);
    if (!normalizedStrokeProfile(object, pointsBefore)) object.strokeProfile = pointsBefore.map(() => 1);
    ensurePathScrubberAnchor(object, point, radius);
    const points = vectorEditablePoints(object); const profile = normalizedStrokeProfile(object, points) || points.map(() => 1);
    const pressure = event?.pointerType === 'pen' && num(event.pressure) > 0 ? clamp(num(event.pressure), .1, 1) : 1;
    const direction = gesture.mode === 'path-scrubber-add' ? 1 : -1;
    const amount = direction * settings.strength / 100 * .34 * pressure; let changed = false;
    points.forEach((anchor, index) => {
      const world = objectLocalToDocument(object, { x: anchor[0], y: anchor[1] });
      const weight = modifierFalloff(Math.hypot(world.x - point.x, world.y - point.y), radius, 'freeform');
      if (weight <= 0) return;
      const next = clamp(profile[index] + amount * weight, .05, 4);
      changed ||= Math.abs(next - profile[index]) > .0001; profile[index] = round(next, 4);
    });
    object.strokeProfile = profile; return changed;
  }

  function beginPathModifierGesture(event, point) {
    const mode = state.tool; const scrub = mode.startsWith('path-scrubber-');
    const entry = pathModifierTargetAt(point, { scrub });
    if (!entry) {
      toast(scrub ? 'Ziehe den Pfadschrubber über eine Vektorkontur' : 'Ziehe das Werkzeug über einen editierbaren Vektorpfad', 'warning');
      return false;
    }
    const transaction = beginDocumentTransaction(scrub ? 'Pfadstärke schrubben' : 'Vektorpfad formen');
    state.doc.activeLayerId = entry.layer.id;
    if (scrub) {
      state.selected = [entry.object.id];
      const gesture = { kind: 'path-scrub', mode, object: entry.object, settings: pathModifierSettings(), transaction, changed: false, lastStamp: point };
      gesture.changed = applyPathScrubberStamp(gesture, point, event); state.gesture = gesture;
    } else {
      const candidates = mode === 'reshape-area' && state.selected.includes(entry.object.id)
        ? selectedObjects().filter(canDeformWithPathModifier)
        : [entry.object];
      if (!candidates.length || candidates.some((object) => !preparePathModifierObject(object))) {
        rollbackDocumentTransaction(transaction); toast('Diese Vektorform kann noch nicht mit dem Pfadformer bearbeitet werden', 'warning'); renderAll(); return false;
      }
      if (mode === 'reshape-area') {
        const radius = pathModifierSettings().size / 2;
        candidates.forEach((object) => refineAreaReshapeObject(object, point, radius));
      }
      state.selected = candidates.map((object) => object.id);
      if (mode === 'freeform') {
        const hit = closestPathModifierSegment(entry.object, point);
        if (hit && hit.distance <= pathModifierSettings().size / 2) insertVectorAnchor(entry.object, hit);
      }
      state.gesture = {
        kind: 'path-reshape', mode, start: point, focus: point, settings: pathModifierSettings(),
        objects: candidates, origins: candidates.map((object) => ({ object, origin: deepClone(object) })), transaction, moved: false
      };
    }
    refs.artboard.setPointerCapture?.(event.pointerId); event.preventDefault(); scheduleInteractionRender(scrub ? [entry.object] : state.gesture.objects, { inspector: true });
    return true;
  }

  function updatePathModifierGesture(gesture, point, event) {
    if (gesture.kind === 'path-reshape') {
      const dx = point.x - gesture.start.x; const dy = point.y - gesture.start.y;
      const radius = gesture.settings.size / 2; const strength = gesture.settings.strength / 100;
      let maximum = 0;
      gesture.origins.forEach(({ object, origin }) => { maximum = Math.max(maximum, deformPathModifierObject(object, origin, gesture.focus, dx, dy, radius, strength, gesture.mode)); });
      gesture.moved = maximum > .05; scheduleInteractionRender(gesture.objects, { inspector: true }); return;
    }
    if (gesture.kind === 'path-scrub') {
      const spacing = Math.max(1 / state.zoom, gesture.settings.size * .08);
      if (Math.hypot(point.x - gesture.lastStamp.x, point.y - gesture.lastStamp.y) < spacing) return;
      gesture.changed = applyPathScrubberStamp(gesture, point, event) || gesture.changed; gesture.lastStamp = point;
      scheduleInteractionRender([gesture.object], { inspector: true });
    }
  }

  function insertVectorAnchor(object, hit) {
    if (outlineVectorTypes.has(object?.type)) {
      const contours = deepClone(outlineContoursFor(object)); const bezierContours = outlineBezierContoursFor(object); const contour = contours[hit.contourIndex]; const segments = bezierContours[hit.contourIndex];
      if (!contour) return -1;
      const insertAt = hit.index + 1; let inserted = hit.point.map((value) => round(value, 3)); const segment = segments[hit.index];
      if (isBezierSegment(segment)) {
        const start = contour[hit.index]; const end = contour[(hit.index + 1) % contour.length]; const t = clamp(num(hit.t, .5), .001, .999);
        const q0 = lerpPoint(start, segment.c1, t); const q1 = lerpPoint(segment.c1, segment.c2, t); const q2 = lerpPoint(segment.c2, end, t);
        const r0 = lerpPoint(q0, q1, t); const r1 = lerpPoint(q1, q2, t); inserted = lerpPoint(r0, r1, t);
        segments[hit.index] = { c1: q0, c2: r0 }; segments.splice(insertAt, 0, { c1: r1, c2: q2 });
      } else segments.splice(insertAt, 0, null);
      contour.splice(insertAt, 0, inserted); object.outlineContours = contours; object.outlineBezierContours = bezierContours; syncOutlinePathData(object);
      const offset = contours.slice(0, hit.contourIndex).reduce((sum, item) => sum + item.length, 0);
      return offset + insertAt;
    }
    if (!pointVectorTypes.has(object?.type) || object.type === 'measure') return -1;
    const points = deepClone(object.points || vectorEditablePoints(object)); const segmentIndex = hit.index;
    const nextIndex = vectorNextPointIndex(object, segmentIndex, points);
    if (nextIndex < 0 || hit.t <= .012 || hit.t >= .988) return -1;
    const insertAt = isClosedVectorPath(object) && nextIndex === 0 ? points.length : nextIndex;
    const strokeProfile = normalizedStrokeProfile(object, points);
    const insertedPressure = strokeProfile
      ? strokeProfile[segmentIndex] + (strokeProfile[nextIndex] - strokeProfile[segmentIndex]) * hit.t
      : 1;
    let inserted = hit.point.map((value) => round(value, 3));
    const segments = Array.from({ length: vectorSegmentCount(object, points) }, (_, index) => deepClone(object.bezierSegments?.[index] || null));
    const curve = isBezierSegment(segments[segmentIndex]);
    if (curve) {
      const a = points[segmentIndex]; const b = points[nextIndex]; const c1 = segments[segmentIndex].c1; const c2 = segments[segmentIndex].c2; const t = hit.t;
      const q0 = lerpPoint(a, c1, t); const q1 = lerpPoint(c1, c2, t); const q2 = lerpPoint(c2, b, t);
      const r0 = lerpPoint(q0, q1, t); const r1 = lerpPoint(q1, q2, t); inserted = lerpPoint(r0, r1, t);
      segments[segmentIndex] = { c1: q0, c2: r0 };
      segments.splice(segmentIndex + 1, 0, { c1: r1, c2: q2 });
    } else if (Array.isArray(object.bezierSegments)) {
      segments.splice(segmentIndex + 1, 0, null);
    }
    points.splice(insertAt, 0, inserted); object.points = points;
    const pointModes = Array.isArray(object.pointModes) ? deepClone(object.pointModes) : Array.from({ length: points.length - 1 }, () => 'corner');
    pointModes.splice(insertAt, 0, curve ? 'smooth' : 'corner'); object.pointModes = pointModes;
    if (strokeProfile) {
      strokeProfile.splice(insertAt, 0, round(insertedPressure, 4));
      object.strokeProfile = strokeProfile;
    }
    if (curve || Array.isArray(object.bezierSegments)) object.bezierSegments = segments;
    if (object.type === 'pencil') object.rawPoints = deepClone(points);
    normalizeVectorPointBounds(object);
    object.scalePercent = 100;
    return insertAt;
  }

  function addVectorAnchorAt(point) {
    const entry = vectorHitAt(point); if (!entry) return false;
    if (polygonConvertibleTypes.has(entry.object.type)) convertShapeToPolygon(entry.object, entry.object, entry.hit.closed ? entry.hit.points : null);
    const index = insertVectorAnchor(entry.object, entry.hit); if (index < 0) return false;
    state.selected = [entry.object.id]; setVectorPointSelections([{ objectId: entry.object.id, index }]); setVectorSegmentSelections([]);
    state.doc.activeLayerId = entry.layer.id; commitHistory('Vektoranker hinzugefügt'); renderAll(); return true;
  }

  function openClosedShapeAt(entry) {
    const { object, hit, layer, index } = entry;
    if (!hit?.closed || hit.t <= .012 || hit.t >= .988) { toast('Schneide auf eine Kante, nicht direkt auf einen Eckpunkt', 'warning'); return false; }
    const contour = hit.points; const tail = [...contour.slice(hit.nextIndex), ...contour.slice(0, hit.nextIndex)];
    const opened = deepClone(object); opened.type = 'line'; opened.name = `${names[object.type] || object.name} – geöffnet`;
    opened.fill = 'none'; opened.points = [deepClone(hit.point), ...deepClone(tail), deepClone(hit.point)];
    const strokeProfile = normalizedStrokeProfile(object, contour);
    if (strokeProfile) {
      const pressure = strokeProfile[hit.index] + (strokeProfile[hit.nextIndex] - strokeProfile[hit.index]) * hit.t;
      opened.strokeProfile = [round(pressure, 4), ...strokeProfile.slice(hit.nextIndex), ...strokeProfile.slice(0, hit.nextIndex), round(pressure, 4)];
    }
    delete opened.bezierSegments; delete opened.rawPoints; opened.scalePercent = 100;
    layer.objects.splice(index, 1, opened);
    state.selected = [opened.id]; clearVectorSubselection(); state.doc.activeLayerId = layer.id;
    commitHistory('Geschlossene Form geöffnet'); renderAll(); return true;
  }

  function splitVectorAt(point) {
    const entry = vectorHitAt(point, { cuttable: true });
    if (!entry) { toast('Schneide eine Vektorlinie oder geschlossene Form', 'warning'); return false; }
    if (entry.hit.closed) return openClosedShapeAt(entry);
    const working = deepClone(entry.object); const insertedIndex = insertVectorAnchor(working, entry.hit);
    if (insertedIndex < 1 || insertedIndex >= working.points.length - 1) { toast('Nicht direkt auf einen Endpunkt schneiden', 'warning'); return false; }
    const first = deepClone(working); const second = deepClone(working); const segments = Array.isArray(working.bezierSegments) ? deepClone(working.bezierSegments) : null;
    first.points = deepClone(working.points.slice(0, insertedIndex + 1)); second.points = deepClone(working.points.slice(insertedIndex));
    const strokeProfile = normalizedStrokeProfile(working, working.points);
    if (strokeProfile) {
      first.strokeProfile = strokeProfile.slice(0, insertedIndex + 1);
      second.strokeProfile = strokeProfile.slice(insertedIndex);
    }
    if (segments) { first.bezierSegments = segments.slice(0, insertedIndex); second.bezierSegments = segments.slice(insertedIndex); }
    if (first.type === 'arrow') first.type = 'line';
    first.name = `${names[first.type] || working.name} – Teil 1`; second.name = `${names[second.type] || working.name} – Teil 2`;
    second.id = uid('object');
    [first, second].forEach((object) => {
      if (object.type === 'pencil') object.rawPoints = deepClone(object.points);
      normalizeVectorPointBounds(object);
      object.scalePercent = 100;
    });
    entry.layer.objects.splice(entry.index, 1, first, second);
    state.selected = [first.id, second.id]; clearVectorSubselection(); state.doc.activeLayerId = entry.layer.id;
    commitHistory('Vektorlinie geteilt'); renderAll(); return true;
  }

  function selectedVectorPointIndex(object) {
    const selected = vectorPointSelections().filter((selection) => selection.objectId === object?.id).at(-1);
    const points = vectorEditablePoints(object);
    return state.tool === 'vector-select' && selected?.objectId === object?.id && Number.isInteger(selected.index) && points[selected.index] ? selected.index : -1;
  }

  const DEFAULT_FONT_FAMILY = "'Arimo', Arial, sans-serif";
  const textDefaults = Object.freeze({ letterSpacing: 0, lineHeight: 120, horizontalScale: 100, verticalScale: 100, baselineShift: 0, align: 'left', antiAlias: 'crisp', autoKern: true, underline: false, strike: false });
  const textTextureDefaults = Object.freeze({ mode: 'none', scale: 100, distortion: 0, distribution: 0, pixelSize: 6, gap: 1, pixelShape: 'square', pixelFullCells: false, amount: 45, seed: 1, transparent: false });
  const textPixelTextureCache = new Map();
  const textTextureModeLabels = Object.freeze({
    none: 'Keine', grain: 'Körnung', waves: 'Wellen', paper: 'Papierfaser', canvas: 'Leinengewebe', wood: 'Holzmaserung',
    grunge: 'Grunge', distressed: 'Ausgefressen', cracks: 'Risse', washed: 'Verwaschen', outline: 'Kontur', pixel: 'Pixel'
  });
  const surfaceTextTextureModes = new Set(['grain', 'waves', 'paper', 'canvas', 'wood']);
  const proceduralTextTextureModes = new Set([...surfaceTextTextureModes, 'grunge', 'distressed', 'cracks', 'washed']);
  const supportsObjectTexture = (object) => Boolean(object && (object.type === 'text' || supportsAdvancedFill(object)));
  const isProceduralTextTextureMode = (mode) => proceduralTextTextureModes.has(mode);
  const textTextureAmountModes = new Set([...proceduralTextTextureModes, 'outline']);
  const isTextTextureAmountMode = (mode) => textTextureAmountModes.has(mode);
  const fontCategoryOrder = Object.freeze(['standard', 'sans', 'serif', 'mono', 'display', 'script', 'pixel', 'retro', 'futuristic', 'kids', 'poster', 'logo']);
  const fontCategoryLabels = Object.freeze({ standard: 'Standard', sans: 'Sans', serif: 'Serif', mono: 'Mono', display: 'Display', script: 'Script', pixel: 'Pixel', retro: 'Retro', futuristic: 'Futuristic', kids: 'Kids', poster: 'Poster', logo: 'Logo' });
  const googleFontDatabaseName = 'pyro-google-fonts-v1';
  const googleFontStoreName = 'font-faces';
  const googleFontStorageByteBudget = 96 * 1024 * 1024;
  const installedGoogleFontFaces = new Map();
  const embeddedSvgFontFaces = new Map();
  const startupIdleTurn = () => new Promise((resolve) => {
    if (typeof requestIdleCallback === 'function') requestIdleCallback(() => resolve(), { timeout: 160 });
    else setTimeout(resolve, 0);
  });
  const systemFontOptions = Object.freeze([
    ['Arial, sans-serif', 'Arial'], ['Verdana, sans-serif', 'Verdana'], ['Georgia, serif', 'Georgia'], ["'Courier New', monospace", 'Courier New'], ['system-ui, sans-serif', 'Systemschrift']
  ]);

  const localFontValue = (family) => `"${String(family || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}", sans-serif`;

  function applyFontPreview(select) {
    if (!select) return;
    [...select.options].forEach((option) => { option.style.fontFamily = option.value || 'system-ui, sans-serif'; });
    select.style.fontFamily = select.value || 'system-ui, sans-serif';
  }

  function refreshFontPreviews() {
    ['fontFamily', 'propFontFamily'].forEach((id) => applyFontPreview($(id)));
  }

  function syncFontSelectors() {
    const compareFamilies = (a, b) => a[1].localeCompare(b[1], undefined, { sensitivity: 'base' });
    const bundled = [...new Set(state.bundledFontFamilies.map((family) => String(family || '').trim()).filter(Boolean))];
    const bundledNames = new Set(bundled.map((family) => family.toLocaleLowerCase()));
    const installed = [...new Set(state.installedGoogleFontFamilies.map((family) => String(family || '').trim()).filter(Boolean))];
    const installedNames = new Set(installed.map((family) => family.toLocaleLowerCase()));
    const projectGroups = new Map(fontCategoryOrder.map((category) => [category, []]));
    const otherProjectFonts = [];
    bundled.forEach((family) => {
      const category = String(state.bundledFontCategories?.[family] || '').toLowerCase();
      const entry = [family === 'Arimo' ? DEFAULT_FONT_FAMILY : localFontValue(family), family];
      if (projectGroups.has(category)) projectGroups.get(category).push(entry);
      else otherProjectFonts.push(entry);
    });
    if (!projectGroups.get('standard').some(([, family]) => family === 'Arimo')) projectGroups.get('standard').push([DEFAULT_FONT_FAMILY, 'Arimo']);
    projectGroups.forEach((entries, category) => entries.sort((a, b) => category === 'standard' && a[1] === 'Arimo' ? -1 : category === 'standard' && b[1] === 'Arimo' ? 1 : compareFamilies(a, b)));
    otherProjectFonts.sort(compareFamilies);
    const googleFontEntries = installed.filter((family) => !bundledNames.has(family.toLocaleLowerCase())).map((family) => [localFontValue(family), family]).sort(compareFamilies);

    const systemLabels = new Set(systemFontOptions.map(([, label]) => label.toLocaleLowerCase()));
    const localFonts = [...new Set(state.localFontFamilies.map((family) => String(family || '').trim()).filter(Boolean))]
      .filter((family) => !bundledNames.has(family.toLocaleLowerCase()) && !installedNames.has(family.toLocaleLowerCase()) && !systemLabels.has(family.toLocaleLowerCase()))
      .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
      .map((family) => [localFontValue(family), family]);
    const systemEntries = [...systemFontOptions, ...localFonts];

    const makeOption = ([value, label]) => {
      const option = document.createElement('option'); option.value = value; option.textContent = label; return option;
    };
    const makeGroup = (label, entries) => {
      const group = document.createElement('optgroup'); group.label = label; group.append(...entries.map(makeOption)); return group;
    };
    ['fontFamily', 'propFontFamily'].forEach((id) => {
      const select = $(id); if (!select) return;
      const selected = select.value;
      const groups = fontCategoryOrder.map((category) => [fontCategoryLabels[category], projectGroups.get(category)]).filter(([, entries]) => entries.length);
      if (otherProjectFonts.length) groups.push(['Weitere Projektfonts', otherProjectFonts]);
      if (googleFontEntries.length) groups.push(['Google Fonts', googleFontEntries]);
      if (systemEntries.length) groups.push(['System', systemEntries]);
      const values = new Set(groups.flatMap(([, entries]) => entries.map(([value]) => value)));
      if (selected && !values.has(selected)) {
        groups.push(['Dokumentschrift', [[selected, selected]]]); values.add(selected);
      }
      select.replaceChildren(...groups.map(([label, entries]) => makeGroup(label, entries)));
      select.value = selected && values.has(selected) ? selected : DEFAULT_FONT_FAMILY;
      applyFontPreview(select);
    });
  }

  function openGoogleFontDatabase() {
    if (!globalThis.indexedDB) return Promise.reject(new Error('Dieser Browser unterstützt keine lokale Font-Speicherung.'));
    return new Promise((resolveDatabase, rejectDatabase) => {
      const request = indexedDB.open(googleFontDatabaseName, 1);
      request.onupgradeneeded = () => {
        const database = request.result;
        if (!database.objectStoreNames.contains(googleFontStoreName)) database.createObjectStore(googleFontStoreName, { keyPath: 'id' });
      };
      request.onsuccess = () => resolveDatabase(request.result);
      request.onerror = () => rejectDatabase(request.error || new Error('Font-Speicher konnte nicht geöffnet werden.'));
      request.onblocked = () => rejectDatabase(new Error('Font-Speicher wird von einem anderen PYRO-Fenster blockiert.'));
    });
  }

  async function readInstalledGoogleFontRecords() {
    const database = await openGoogleFontDatabase();
    return new Promise((resolveRecords, rejectRecords) => {
      const transaction = database.transaction(googleFontStoreName, 'readonly');
      const request = transaction.objectStore(googleFontStoreName).getAll();
      let records = [];
      request.onsuccess = () => { records = Array.isArray(request.result) ? request.result : []; };
      request.onerror = () => transaction.abort();
      transaction.oncomplete = () => { database.close(); resolveRecords(records); };
      transaction.onerror = () => { database.close(); rejectRecords(transaction.error || request.error || new Error('Installierte Fonts konnten nicht gelesen werden.')); };
      transaction.onabort = () => { database.close(); rejectRecords(transaction.error || request.error || new Error('Installierte Fonts konnten nicht gelesen werden.')); };
    });
  }

  async function replaceInstalledGoogleFontRecords(records) {
    const database = await openGoogleFontDatabase();
    const families = new Set(records.map((record) => record.family));
    return new Promise((resolveWrite, rejectWrite) => {
      const transaction = database.transaction(googleFontStoreName, 'readwrite');
      const store = transaction.objectStore(googleFontStoreName);
      const request = store.getAll();
      request.onsuccess = () => {
        request.result.filter((record) => families.has(record.family)).forEach((record) => store.delete(record.id));
        records.forEach((record) => store.put(record));
      };
      request.onerror = () => transaction.abort();
      transaction.oncomplete = () => { database.close(); resolveWrite(); };
      transaction.onerror = () => { database.close(); rejectWrite(transaction.error || request.error || new Error('Google-Schrift konnte nicht gespeichert werden.')); };
      transaction.onabort = () => { database.close(); rejectWrite(transaction.error || request.error || new Error('Google-Schrift konnte nicht gespeichert werden.')); };
    });
  }

  function googleFontCssUrl(value) {
    const input = String(value || '').trim().replace(/&amp;/gi, '&');
    const extracted = input.match(/https:\/\/(?:fonts\.googleapis\.com|fonts\.google\.com)\/[^\s"'<>]+/i)?.[0] || input;
    let url;
    try { url = new URL(extracted); }
    catch { throw new Error('Bitte einen vollständigen Google-Fonts-Link einfügen.'); }
    if (url.protocol !== 'https:') throw new Error('Google Fonts muss über HTTPS geladen werden.');
    const host = url.hostname.toLowerCase();
    if (host === 'fonts.googleapis.com' && /^\/css(?:2)?$/i.test(url.pathname)) return url.href;
    if (host === 'fonts.google.com') {
      const specimen = url.pathname.match(/^\/specimen\/([^/]+)/i)?.[1];
      if (!specimen) throw new Error('Bitte den Link zu einer Google-Fonts-Schriftseite verwenden.');
      const family = decodeURIComponent(specimen.replace(/\+/g, ' ')).trim();
      if (!family) throw new Error('Im Link wurde keine Schriftfamilie gefunden.');
      const queryFamily = family.split(/\s+/).map((part) => encodeURIComponent(part)).join('+');
      return `https://fonts.googleapis.com/css2?family=${queryFamily}&display=swap`;
    }
    throw new Error('Erlaubt sind nur Links von fonts.google.com oder fonts.googleapis.com.');
  }

  function parseGoogleFontCss(css, cssUrl) {
    const definitions = []; const seen = new Set();
    const property = (block, name) => block.match(new RegExp(`(?:^|;)\\s*${name}\\s*:\\s*([^;]+)`, 'i'))?.[1]?.trim() || '';
    for (const match of String(css || '').matchAll(/@font-face\s*\{([\s\S]*?)\}/gi)) {
      const block = match[1];
      const rawFamily = property(block, 'font-family');
      const family = rawFamily.replace(/^(['"])([\s\S]*)\1$/, '$2').replace(/\\(['"])/g, '$1').trim();
      const source = property(block, 'src');
      const sourceMatch = source.match(/url\(\s*(['"]?)([^'")]+)\1\s*\)/i);
      if (!family || !sourceMatch?.[2]) continue;
      const sourceUrl = new URL(sourceMatch[2], cssUrl);
      if (sourceUrl.protocol !== 'https:' || sourceUrl.hostname.toLowerCase() !== 'fonts.gstatic.com') throw new Error('Der Google-Font-Link verweist auf eine unerwartete Downloadquelle.');
      const definition = {
        family,
        style: property(block, 'font-style') || 'normal',
        weight: property(block, 'font-weight') || '400',
        stretch: property(block, 'font-stretch') || 'normal',
        unicodeRange: property(block, 'unicode-range'),
        sourceUrl: sourceUrl.href
      };
      const key = [definition.family, definition.style, definition.weight, definition.stretch, definition.unicodeRange, definition.sourceUrl].join('|');
      if (!seen.has(key)) { seen.add(key); definitions.push(definition); }
    }
    if (!definitions.length) throw new Error('Unter diesem Link wurden keine installierbaren Fontdateien gefunden.');
    if (definitions.length > 128) throw new Error('Der Link enthält zu viele Schriftschnitte. Bitte weniger Varianten auswählen.');
    return definitions;
  }

  async function downloadGoogleFontRecords(link) {
    const cssUrl = googleFontCssUrl(link);
    const cssResponse = await fetch(cssUrl, { mode: 'cors', cache: 'no-store' });
    if (!cssResponse.ok) throw new Error(`Google Fonts antwortet mit Fehler ${cssResponse.status}.`);
    const css = await cssResponse.text();
    if (css.length > 500_000) throw new Error('Die Google-Fonts-Beschreibung ist unerwartet groß.');
    const definitions = parseGoogleFontCss(css, cssUrl);
    const files = new Map(); let totalBytes = 0;
    for (const definition of definitions) {
      if (files.has(definition.sourceUrl)) continue;
      const response = await fetch(definition.sourceUrl, { mode: 'cors', cache: 'force-cache' });
      if (!response.ok) throw new Error(`Eine Fontdatei konnte nicht geladen werden (${response.status}).`);
      const data = await response.arrayBuffer();
      if (!data.byteLength || data.byteLength > 8 * 1024 * 1024) throw new Error('Eine Fontdatei ist leer oder größer als 8 MB.');
      totalBytes += data.byteLength;
      if (totalBytes > 48 * 1024 * 1024) throw new Error('Die ausgewählten Fontvarianten sind zusammen größer als 48 MB.');
      files.set(definition.sourceUrl, data);
    }
    const installedAt = Date.now();
    return definitions.map((definition, index) => ({
      id: `${definition.family}\u0000${installedAt}\u0000${index}`,
      ...definition,
      cssUrl,
      installedAt,
      data: files.get(definition.sourceUrl).slice(0)
    }));
  }

  async function registerGoogleFontRecords(records, { reset = false, replace = false } = {}) {
    if (reset) {
      installedGoogleFontFaces.forEach((faces) => faces.forEach((face) => document.fonts.delete(face)));
      installedGoogleFontFaces.clear(); state.installedGoogleFontFamilies = [];
    }
    const requestedFamilies = [...new Set(records.map((record) => String(record?.family || '').trim()).filter(Boolean))];
    if (replace) requestedFamilies.forEach((family) => {
      installedGoogleFontFaces.get(family)?.forEach((face) => document.fonts.delete(face)); installedGoogleFontFaces.delete(family);
    });
    const loadedFamilies = new Set();
    for (let index = 0; index < records.length; index += 1) {
      const record = records[index];
      if (!(record?.data instanceof ArrayBuffer) || !record.family) continue;
      const descriptors = { style: record.style || 'normal', weight: String(record.weight || '400'), stretch: record.stretch || 'normal', display: 'swap' };
      if (record.unicodeRange) descriptors.unicodeRange = record.unicodeRange;
      try {
        const face = new FontFace(record.family, record.data.slice(0), descriptors);
        await face.load(); document.fonts.add(face);
        if (!installedGoogleFontFaces.has(record.family)) installedGoogleFontFaces.set(record.family, []);
        installedGoogleFontFaces.get(record.family).push(face); loadedFamilies.add(record.family);
      } catch { /* Eine defekte Teilmenge soll die übrigen Schriftschnitte nicht blockieren. */ }
      if (index % 4 === 3) await startupIdleTurn();
    }
    if (records.length && !loadedFamilies.size) throw new Error('Die geladenen Dateien enthalten keine verwendbare Schrift.');
    const retained = state.installedGoogleFontFamilies.filter((family) => !requestedFamilies.includes(family));
    state.installedGoogleFontFamilies = [...new Set([...retained, ...loadedFamilies])];
    syncFontSelectors();
    return [...loadedFamilies];
  }

  async function registerInstalledGoogleFonts() {
    try {
      const records = await readInstalledGoogleFontRecords();
      if (records.length) await registerGoogleFontRecords(records, { reset: true });
    } catch { /* Ohne IndexedDB bleiben die mitgelieferten und Systemfonts nutzbar. */ }
  }

  function setGoogleFontStatus(message = '', tone = '') {
    const output = $('googleFontStatus'); output.textContent = I18n.translateSource(String(message));
    if (tone) output.dataset.tone = tone; else output.removeAttribute('data-tone');
  }

  function openGoogleFontDialog() {
    setGoogleFontStatus();
    if (!$('googleFontDialog').open) $('googleFontDialog').showModal();
    requestAnimationFrame(() => $('googleFontUrl').focus());
  }

  function closeGoogleFontDialog() {
    if ($('googleFontDialog').open) $('googleFontDialog').close();
  }

  async function installGoogleFontFromDialog() {
    if (!$('googleFontForm').reportValidity()) return;
    const confirm = $('googleFontInstallConfirm'); const cancel = $('googleFontCancelBtn'); const close = $('googleFontCloseBtn');
    confirm.disabled = true; cancel.disabled = true; close.disabled = true; setGoogleFontStatus('Font wird geladen und geprüft …');
    try {
      const records = await downloadGoogleFontRecords($('googleFontUrl').value);
      const requestedFamilies = new Set(records.map((record) => record.family));
      const retainedRecords = (await readInstalledGoogleFontRecords()).filter((record) => !requestedFamilies.has(record.family));
      const storedBytes = [...retainedRecords, ...records].reduce((sum, record) => sum + (record?.data?.byteLength || 0), 0);
      if (storedBytes > googleFontStorageByteBudget) throw new Error('Die installierten Google Fonts würden zusammen das Speicherlimit von 96 MB überschreiten.');
      await replaceInstalledGoogleFontRecords(records);
      const families = await registerGoogleFontRecords(records, { replace: true });
      setGoogleFontStatus(`${families.join(', ')} wurde installiert.`, 'success');
      $('googleFontUrl').value = ''; closeGoogleFontDialog();
      toast(`${families.join(', ')} unter Google Fonts installiert`);
    } catch (error) {
      setGoogleFontStatus(error?.message || 'Google-Schrift konnte nicht installiert werden.', 'error');
    } finally {
      confirm.disabled = false; cancel.disabled = false; close.disabled = false;
    }
  }

  async function loadSystemFonts() {
    const button = $('loadSystemFontsBtn');
    if (typeof window.queryLocalFonts !== 'function') {
      toast('Lokale Schriftlisten werden von diesem Browser nicht unterstützt. Nutze Chrome oder Edge über HTTPS beziehungsweise localhost.', 'warning');
      return;
    }
    const label = button.textContent; button.disabled = true; button.textContent = 'Lade Schriften…';
    try {
      const fonts = await window.queryLocalFonts();
      state.localFontFamilies = [...new Set(fonts.map((font) => font.family).filter(Boolean))];
      syncFontSelectors();
      toast(`${state.localFontFamilies.length} installierte Schriftfamilien geladen`);
    } catch (error) {
      const denied = error?.name === 'NotAllowedError' || error?.name === 'SecurityError';
      toast(denied ? 'Der Zugriff auf lokale Schriften wurde nicht erlaubt' : 'Lokale Schriften konnten nicht geladen werden', 'warning');
    } finally {
      button.disabled = false; button.textContent = label;
    }
  }

  async function registerBundledFonts() {
    try {
      let manifest = globalThis.PYRO_FONT_MANIFEST;
      if (!Array.isArray(manifest?.fonts)) {
        const response = await fetch(new URL('fonts/manifest.json', document.baseURI), { cache: 'no-cache' });
        if (!response.ok) return;
        manifest = await response.json();
      }
      const entries = Array.isArray(manifest?.fonts) ? manifest.fonts : [];
      const registered = []; const categories = {};
      for (let index = 0; index < entries.length; index += 1) {
        const entry = entries[index];
        const file = String(entry?.file || ''); const family = String(entry?.family || '').trim();
        if (!family || !/^fonts\/.+\.(?:ttf|otf)$/i.test(file)) continue;
        try {
          const source = `url(${JSON.stringify(new URL(file, document.baseURI).href)})`;
          const face = new FontFace(family, source, {
            style: entry.style === 'italic' ? 'italic' : 'normal', weight: String(entry.weight || 400), display: 'swap'
          });
          document.fonts.add(face); registered.push(family);
          const category = file.split('/')[1]; if (fontCategoryOrder.includes(category)) categories[family] = category;
        } catch { /* Eine defekte Datei darf die übrigen Projektfonts nicht blockieren. */ }
        if (index % 16 === 15) await startupIdleTurn();
      }
      if (!registered.length) return;
      state.bundledFontFamilies = [...new Set(registered)]; state.bundledFontCategories = categories; syncFontSelectors();
    } catch { /* Ein Projekt ohne fonts/manifest.json bleibt uneingeschränkt nutzbar. */ }
  }

  const textSettings = (object) => {
    const align = ['left', 'center', 'right', 'justify'].includes(object?.align) ? object.align : textDefaults.align;
    const antiAlias = ['crisp', 'smooth', 'auto', 'none'].includes(object?.antiAlias) ? object.antiAlias : textDefaults.antiAlias;
    return {
      letterSpacing: clamp(num(object?.letterSpacing, textDefaults.letterSpacing), -50, 200),
      lineHeight: clamp(num(object?.lineHeight, textDefaults.lineHeight), 40, 400),
      horizontalScale: clamp(num(object?.horizontalScale, textDefaults.horizontalScale), 25, 400),
      verticalScale: clamp(num(object?.verticalScale, textDefaults.verticalScale), 25, 400),
      baselineShift: clamp(num(object?.baselineShift, textDefaults.baselineShift), -1000, 1000),
      align, antiAlias, autoKern: object?.autoKern !== false,
      underline: Boolean(object?.underline), strike: Boolean(object?.strike)
    };
  };

  const textTextureSettings = (object) => {
    const raw = supportsObjectTexture(object) && object.textTexture && typeof object.textTexture === 'object' ? object.textTexture : textTextureDefaults;
    const mode = Object.prototype.hasOwnProperty.call(textTextureModeLabels, raw.mode) ? raw.mode : textTextureDefaults.mode;
    return {
      mode,
      scale: clamp(num(raw.scale, textTextureDefaults.scale), 20, 400),
      distortion: clamp(num(raw.distortion, textTextureDefaults.distortion), 0, 40),
      distribution: clamp(num(raw.distribution, textTextureDefaults.distribution), 0, 100),
      pixelSize: clamp(num(raw.pixelSize, textTextureDefaults.pixelSize), 1, 128),
      gap: clamp(num(raw.gap, textTextureDefaults.gap), 0, 64),
      pixelShape: raw.pixelShape === 'round' ? 'round' : 'square',
      pixelFullCells: object?.type === 'text' && Boolean(raw.pixelFullCells),
      amount: clamp(num(raw.amount, textTextureDefaults.amount), 0, 100),
      seed: clamp(Math.round(num(raw.seed, textTextureDefaults.seed)), 1, 9999),
      transparent: surfaceTextTextureModes.has(mode) && Boolean(raw.transparent)
    };
  };

  function textTextureSummary(texture, formatValue) {
    const label = I18n.translateSource(textTextureModeLabels[texture.mode]); let effect = I18n.t('ui.none');
    if (texture.mode === 'washed') effect = I18n.t('ui.washedTextureSummary', { label, amount: formatValue(texture.amount), distribution: formatValue(texture.distribution) });
    else if (texture.mode === 'outline') effect = I18n.t('ui.textureAmountSummary', { label, amount: formatValue(texture.amount) });
    if (texture.mode === 'pixel') {
      const shape = I18n.t(texture.pixelShape === 'round' ? 'ui.round' : 'ui.angular');
      effect = I18n.t(texture.pixelFullCells ? 'ui.pixelTextureFullEdgeSummary' : 'ui.pixelTextureSummary', {
        label, size: formatValue(texture.pixelSize), gap: formatValue(texture.gap), shape
      });
    } else if (isProceduralTextTextureMode(texture.mode)) effect = I18n.t('ui.proceduralTextureSummary', {
      label, amount: formatValue(texture.amount), distortion: formatValue(texture.distortion)
    });
    if (texture.transparent) effect = `${effect} · ${I18n.t('ui.transparent')}`;
    return effect;
  }

  function syncObjectTextureInspector(object) {
    if (!supportsObjectTexture(object)) return;
    const panel = document.querySelector('.text-texture-panel');
    $('fillPaintPanel').after(panel);

    const texture = textTextureSettings(object); const textureActive = texture.mode !== 'none';
    setField('propTextTextureMode', texture.mode);
    const washedTexture = texture.mode === 'washed'; const pixelTexture = texture.mode === 'pixel';
    const scaleValue = pixelTexture ? texture.pixelSize : texture.scale;
    const distortionValue = pixelTexture ? texture.gap : washedTexture ? texture.distribution : texture.distortion;
    $('propTextTextureScale').min = pixelTexture ? '1' : '20'; $('propTextTextureScale').max = pixelTexture ? '128' : '400';
    $('propTextTextureDistortion').max = pixelTexture ? '64' : washedTexture ? '100' : '40';
    setField('propTextTextureScale', scaleValue); setField('propTextTextureDistortion', distortionValue); setField('propTextTextureAmount', texture.amount);
    $('propTextTextureScale').disabled = !textureActive || texture.mode === 'outline'; $('propTextTextureDistortion').disabled = !textureActive;
    $('propTextTextureAmount').disabled = !isTextTextureAmountMode(texture.mode);
    $('propTextTextureAmount').closest('.text-texture-slider').hidden = pixelTexture;
    $('propTextTextureScaleLabel').textContent = pixelTexture ? 'Pixelgröße' : 'Größe';
    $('propTextTextureScaleValue').textContent = `${Math.round(scaleValue)}${pixelTexture ? ' px' : ' %'}`;
    $('propTextTextureDistortionLabel').textContent = pixelTexture ? 'Zwischenabstand' : washedTexture ? 'Verteilung' : 'Verformung';
    $('propTextTextureDistortionValue').textContent = `${Math.round(distortionValue)}${washedTexture ? ' %' : ' px'}`;
    $('propTextTextureAmountValue').textContent = `${Math.round(texture.amount)} %`;
    $('propTextTextureAmountLabel').textContent = texture.mode === 'outline' ? 'Konturstärke' : surfaceTextTextureModes.has(texture.mode) ? I18n.t('ui.textureAmount') : 'Abnutzung';
    const transparentOption = $('propTextTextureTransparentOption');
    transparentOption.hidden = !surfaceTextTextureModes.has(texture.mode);
    $('propTextTextureTransparent').checked = texture.transparent;
    $('propTextTexturePixelOptions').hidden = !pixelTexture; $('propTextTextureFullCells').checked = texture.pixelFullCells;
    const fullCellsLabel = $('propTextTextureFullCells').closest('.pixel-complete-toggle');
    if (fullCellsLabel) fullCellsLabel.hidden = object.type !== 'text';
    setField('propTextTexturePixelShape', texture.pixelShape);
    $('randomizeTextTextureBtn').hidden = !isProceduralTextTextureMode(texture.mode);
  }

  function hasActiveTextTexture(object) {
    return textTextureSettings(object).mode !== 'none';
  }

  function applyCurrentTextSettings(object) {
    Object.assign(object, {
      fontFamily: $('fontFamily').value, fontSize: num($('fontSize').value, 32),
      bold: $('fontBold').classList.contains('is-active'), italic: $('fontItalic').classList.contains('is-active'),
      textStroke: 'none', textStrokeWidth: 1, nameManuallyEdited: false,
      ...textDefaults
    });
  }

  function textObjectName(text) {
    const value = String(text ?? '').replace(/[\u0000-\u001f\u007f]/g, ' ').trim().slice(0, 256);
    return value || names.text || 'Text';
  }

  function syncTextObjectName(object, previousText = object?.text) {
    if (object?.type !== 'text') return false;
    const defaultName = names.text || 'Text';
    const previousAutomaticName = textObjectName(previousText);
    const inferredManual = object.nameManuallyEdited == null
      && ![defaultName, 'text', previousAutomaticName].includes(String(object.name || ''));
    if (object.nameManuallyEdited === true || inferredManual) {
      object.nameManuallyEdited = true;
      return false;
    }
    const nextName = textObjectName(object.text);
    object.nameManuallyEdited = false;
    if (object.name === nextName) return false;
    object.name = nextName;
    return true;
  }

  const automaticObjectNames = new Set([
    ...Object.values(names), 'Polygon', 'Bézierpfad', 'Offener Bézierpfad', 'Geschlossener Bézierpfad', 'Polylinie', 'Geschlossene Polylinie',
    'SVG-Pfad', 'SVG-Text', 'Strukturierter SVG-Text', 'Inline-Pfad', 'SVG-Bild'
  ]);
  const localizedObjectNameKeys = new Set(['example.backgroundCardName', 'example.sunName', 'example.textObjectName', 'ui.arrow', 'ui.bitmap']);
  const isLegacyFlattenedBitmapName = (object, value = String(object?.name || '')) => object?.type === 'image'
    && object.nameManuallyEdited !== true && /^Gruppe(?:\s+\(\d+\))?\s+\(flach\)$/.test(value);

  function isAutomaticObjectName(object) {
    if (!object || object.nameManuallyEdited === true) return false;
    if (localizedObjectNameKeys.has(object.nameKey)) return true;
    const value = String(object.name || names[object.type] || 'Objekt');
    if (isLegacyFlattenedBitmapName(object, value)) return true;
    return automaticObjectNames.has(value) || /^Bild:\s+.+$/.test(value);
  }

  function displayedObjectName(object) {
    if (localizedObjectNameKeys.has(object?.nameKey)) return I18n.t(object.nameKey);
    const value = String(object?.name || names[object?.type] || 'Objekt');
    if (isLegacyFlattenedBitmapName(object, value)) return I18n.t('ui.bitmap');
    return isAutomaticObjectName(object) ? I18n.translateSource(value) : value;
  }

  function displayedLayerName(layer) {
    const value = String(layer?.name || I18n.t('document.layer'));
    if (layer?.nameManuallyEdited === true) return value;
    const sourceKey = I18n.keyForSource(value);
    if (sourceKey === 'example.layerName') return I18n.t(sourceKey);
    const regular = value.match(/^Ebene(?:\s+(\d+))?$/);
    if (regular) return `${I18n.t('document.layer')}${regular[1] ? ` ${regular[1]}` : ''}`;
    const svg = value.match(/^SVG-Ebene(?:\s+(\d+))?$/);
    if (svg) return `${I18n.t('ui.svgLayer')}${svg[1] ? ` ${svg[1]}` : ''}`;
    return value;
  }

  function refreshObjectNamePresentation(object) {
    if (!object) return;
    const objectName = displayedObjectName(object);
    if (state.selected.includes(object.id) && refs.propertyDock) {
      refs.propertyDock.dataset.objectName = objectName;
      if (state.tool === 'select' && state.selected.length === 1 && !document.querySelector('.property-dock__heading-input')) refs.propertyDockHeading.textContent = objectName;
      else if (state.tool !== 'select') refs.propertyDockSummary.textContent = objectName;
    }
    const row = [...refs.layerList.querySelectorAll('.layer-object-row')].find((entry) => entry.dataset.objectId === object.id);
    const label = row?.querySelector('.layer-object-name');
    if (label) { label.textContent = objectName; row.title = `${objectName} · ${I18n.translateSource(names[object.type] || object.type)}`; }
  }

  const textPathDefaults = Object.freeze({ placement: 'outside', distance: 0, offset: 0, orientation: 'vertical', spacing: 0, reversed: false, targetVisible: true });

  function objectTransform(object) {
    if (Array.isArray(object?.svgTransformMatrix) && object.svgTransformMatrix.length >= 6) {
      return `matrix(${Core.objectMatrix(object).map((value) => round(value, 12)).join(' ')})`;
    }
    const x = num(object?.x); const y = num(object?.y); const width = Math.max(1, num(object?.width, 1)); const height = Math.max(1, num(object?.height, 1));
    const base = `translate(${x} ${y}) rotate(${num(object?.rotation)} ${width / 2} ${height / 2})`;
    if (!object?.flipX && !object?.flipY) return base;
    return `${base} translate(${width / 2} ${height / 2}) scale(${object.flipX ? -1 : 1} ${object.flipY ? -1 : 1}) translate(${-width / 2} ${-height / 2})`;
  }

  function interactionObjectTransform(object) {
    const gesture = state.gesture;
    if (gesture?.kind !== 'resize' || !gesture.deferredGeometry || gesture.object?.id !== object?.id) return objectTransform(object);
    const originWidth = Math.max(.0001, num(gesture.origin?.width, 1)); const originHeight = Math.max(.0001, num(gesture.origin?.height, 1));
    const scaleX = Math.max(.0001, num(object.width, 1) / originWidth); const scaleY = Math.max(.0001, num(object.height, 1) / originHeight);
    return `${objectTransform(object)} scale(${round(scaleX, 12)} ${round(scaleY, 12)})`;
  }

  function textPathBinding(object) {
    if (object?.type !== 'text' || !object.pathBinding?.targetId) return null;
    const target = objectEntry(object.pathBinding.targetId)?.object;
    if (!target || target.id === object.id || target.type === 'text' || !snapPathDefinition(target).some((path) => path?.points?.length >= 2)) return null;
    return {
      target,
      binding: {
        placement: object.pathBinding.placement === 'inside' ? 'inside' : textPathDefaults.placement,
        distance: clamp(num(object.pathBinding.distance, textPathDefaults.distance), 0, 1000),
        offset: clamp(num(object.pathBinding.offset, textPathDefaults.offset), -360, 360),
        orientation: object.pathBinding.orientation === 'horizontal' ? 'horizontal' : textPathDefaults.orientation,
        spacing: clamp(num(object.pathBinding.spacing, textPathDefaults.spacing), -100, 500),
        reversed: Boolean(object.pathBinding.reversed),
        targetVisible: object.pathBinding.targetVisible !== false,
        svgBaselineOnPath: target.svgTextPathSource === true
      }
    };
  }

  function textPathContext() {
    const selected = state.selected.map((id) => objectEntry(id)?.object).filter(Boolean);
    const text = selected.find((object) => object.type === 'text');
    if (!text) return null;
    const binding = textPathBinding(text);
    const target = binding?.target || [...selected].reverse().find((object) => object.id !== text.id && object.type !== 'text' && snapPathDefinition(object).some((path) => path?.points?.length >= 2));
    return target ? { text, target, binding: binding?.binding || null } : null;
  }

  function syncBoundTextBounds(text, target) {
    if (!text || !target) return;
    const settings = textSettings(text); const pathDistance = textPathBinding(text)?.binding.distance || 0;
    const padding = num(text.fontSize, 32) * Math.max(settings.horizontalScale, settings.verticalScale) / 100 * .75 + pathDistance;
    text.x = round(num(target.x) - padding); text.y = round(num(target.y) - padding);
    text.width = round(Math.max(1, num(target.width, 1) + padding * 2)); text.height = round(Math.max(1, num(target.height, 1) + padding * 2));
  }

  function bindTextToShape() {
    const context = textPathContext();
    if (!context) { toast('Wähle einen Text und eine Form aus', 'warning'); return; }
    const { text, target } = context;
    text.pathBinding = { targetId: target.id, ...textPathDefaults, targetVisible: target.visible !== false };
    syncBoundTextBounds(text, target); state.selected = [text.id]; commitHistory('Text an Form ausgerichtet'); renderAll();
  }

  function unbindTextFromShape() {
    const context = textPathContext(); if (!context?.binding) return;
    delete context.text.pathBinding; commitHistory('Text von Form gelöst'); renderAll();
  }

  function setTextPathProperty(property, value, commit = false) {
    const context = textPathContext(); if (!context?.binding) return;
    context.text.pathBinding[property] = value;
    if (property === 'targetVisible') context.target.visible = Boolean(value);
    syncBoundTextBounds(context.text, context.target);
    if (commit) { commitHistory('Text an Form geändert'); renderAll(); } else renderArtboard();
  }

  let textMeasurementContext = null;

  function textPathMetric(target, owner = null) {
    const targetParentMatrix = objectParentMatrix(target);
    const ownerParentInverse = Core.invertMatrix(owner ? objectParentMatrix(owner) : Core.identityMatrix()) || Core.identityMatrix();
    const candidates = snapPathDefinition(target).map((path, index) => ({
      index,
      path,
      metric: PathMetrics.measurePath(path, {
        tolerance: .18,
        transform: ([x, y]) => {
          const inTargetParent = objectLocalToDocument(target, { x, y });
          const inDocument = Core.applyMatrix(targetParentMatrix, inTargetParent);
          const inOwnerParent = Core.applyMatrix(ownerParentInverse, inDocument);
          return [inOwnerParent.x, inOwnerParent.y];
        }
      })
    })).filter((entry) => entry.metric.length > .001);
    return candidates.sort((left, right) => right.metric.length - left.metric.length)[0] || null;
  }

  function textCharacterAdvances(object, characters, binding) {
    const settings = textSettings(object); const fontSize = num(object.fontSize, 32); const scaleX = settings.horizontalScale / 100;
    if (!textMeasurementContext) textMeasurementContext = document.createElement('canvas').getContext('2d');
    const context = textMeasurementContext;
    if (context) {
      context.font = `${object.italic ? 'italic ' : ''}${object.bold ? '700 ' : '400 '}${fontSize}px ${object.fontFamily || DEFAULT_FONT_FAMILY}`;
      if ('fontKerning' in context) context.fontKerning = settings.autoKern ? 'normal' : 'none';
    }
    let prefix = ''; let previousWidth = 0;
    return characters.map((character) => {
      prefix += character;
      const cumulativeWidth = context ? context.measureText(prefix).width : previousWidth + (character === ' ' ? fontSize * .34 : fontSize * .62);
      const glyphWidth = Math.max(fontSize * .04, cumulativeWidth - previousWidth); previousWidth = cumulativeWidth;
      const renderedGlyphWidth = Math.max(fontSize * .04, glyphWidth * scaleX);
      return { glyphWidth: renderedGlyphWidth, advance: Math.max(fontSize * .04, (glyphWidth + settings.letterSpacing + binding.spacing) * scaleX) };
    });
  }

  function boundTextFlow(binding, metric) {
    const direction = binding.reversed ? -1 : 1; const rawStart = binding.offset / 360;
    return {
      direction,
      cursor: binding.reversed && !metric.closed ? 1 - rawStart : rawStart,
      rotationOffset: binding.reversed ? 180 : 0
    };
  }

  function offsetTextPathPoint(sample, metric, binding, fontSize) {
    const outward = metric.closed ? (metric.area >= 0 ? -1 : 1) : -1;
    const placement = binding.placement === 'inside' ? -outward : outward;
    const distance = (binding.svgBaselineOnPath ? 0 : Math.max(2, fontSize * .48)) + clamp(num(binding.distance), 0, 1000);
    return {
      ...sample, pathX: sample.x, pathY: sample.y,
      x: sample.x + sample.normal.x * placement * distance, y: sample.y + sample.normal.y * placement * distance,
      normalDistance: distance, normalDirection: placement
    };
  }

  function boundTextLayout(object, pathBinding) {
    const { target, binding } = pathBinding; const fontSize = num(object.fontSize, 32); const path = textPathMetric(target, object);
    if (!path) return null;
    const characters = [...String(object.text || '').replace(/\n/g, ' ')]; const advances = textCharacterAdvances(object, characters, binding);
    const flow = boundTextFlow(binding, path.metric); const anchor = textSettings(object).align;
    const anchorFactor = anchor === 'center' ? .5 : anchor === 'right' ? 1 : 0;
    flow.cursor -= flow.direction * advances.reduce((sum, entry) => sum + entry.advance, 0) / path.metric.length * anchorFactor;
    const startSample = offsetTextPathPoint(path.metric.at(flow.cursor), path.metric, binding, fontSize);
    const placements = characters.map((character, index) => {
      const { glyphWidth, advance } = advances[index]; const offset = flow.cursor + flow.direction * glyphWidth / path.metric.length / 2;
      const sample = offsetTextPathPoint(path.metric.at(offset), path.metric, binding, fontSize);
      flow.cursor += flow.direction * advance / path.metric.length;
      if (binding.svgBaselineOnPath && (offset < 0 || offset > 1)) return null;
      return { character, advance, offset, point: sample, rotation: binding.orientation === 'horizontal' ? 0 : sample.tangent.angle + flow.rotationOffset };
    }).filter(Boolean);
    return { ...path, placements, advances, startPoint: startSample, direction: flow.direction };
  }

  function boundTextShape(object, pathBinding, rawStrokeStyle = primaryStrokeStyle(object)) {
    const { target, binding } = pathBinding; const settings = textSettings(object); const fontSize = num(object.fontSize, 32);
    const { style: strokeStyle, strokeType, attributes: strokeAttributes } = strokeStyleAttributes(object, rawStrokeStyle);
    const layout = boundTextLayout(object, pathBinding); if (!layout) return E('g');
    const scaleX = settings.horizontalScale / 100; const scaleY = settings.verticalScale / 100;
    const textRendering = { crisp: 'geometricPrecision', smooth: 'optimizeLegibility', auto: 'auto', none: 'optimizeSpeed' }[settings.antiAlias];
    const decorations = [settings.underline && 'underline', settings.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
    const glowOnlyText = hasActiveGlow(object) && !hasActiveTextTexture(object) && !normalizedObjectFillPaint(object) && (object.fill === 'none' || object.fill === 'transparent');
    const compatibilityRadiusX = target.type === 'ellipse' ? Math.max(2, num(target.width, 1) / 2 + layout.startPoint.normalDistance * (binding.placement === 'inside' ? -1 : 1)) : 0;
    const compatibilityRadiusY = target.type === 'ellipse' ? Math.max(2, num(target.height, 1) / 2 + layout.startPoint.normalDistance * (binding.placement === 'inside' ? -1 : 1)) : 0;
    const root = E('g', {
      'data-text-path-flow': binding.reversed ? 'reversed' : 'normal',
      'data-text-path-source': 'vector',
      'data-text-path-length': round(layout.metric.length, 3),
      'data-text-path-index': layout.index,
      'data-text-path-distance': binding.distance,
      'data-text-path-spacing': binding.spacing,
      'data-text-path-radius-x': compatibilityRadiusX ? round(compatibilityRadiusX) : undefined,
      'data-text-path-radius-y': compatibilityRadiusY ? round(compatibilityRadiusY) : undefined
    });
    layout.placements.forEach(({ character, point, rotation }) => {
      const group = E('g', { transform: `translate(${round(point.x, 4)} ${round(point.y, 4)}) rotate(${round(rotation, 4)}) scale(${scaleX} ${scaleY})` });
      const glyph = E('text', {
        x: 0, y: settings.baselineShift, fill: glowOnlyText ? '#000000' : objectFillValue(object),
        ...strokeAttributes, stroke: strokeAttributes.stroke, 'paint-order': 'stroke fill',
        'fill-opacity': object.svgFillOpacity == null ? undefined : clamp(num(object.svgFillOpacity, 1), 0, 1),
        'font-family': object.fontFamily || DEFAULT_FONT_FAMILY, 'font-size': fontSize,
        'font-weight': object.bold ? '700' : '400', 'font-style': object.italic ? 'italic' : 'normal', 'text-anchor': 'middle',
        'font-kerning': settings.autoKern ? 'normal' : 'none', 'text-decoration': decorations, 'text-rendering': textRendering, 'xml:space': 'preserve'
      });
      glyph.textContent = character || ' '; group.append(glyph); root.append(group);
    });
    return root;
  }

  function effectStack(object) { return Effects.normalizeStack(object?.effects); }
  function activeEffectStack(object) { return effectStack(object).filter((effect) => effect.enabled); }
  function activeFilterStack(object) { return activeEffectStack(object).filter((effect) => Effects.definitionFor(effect.type)?.category !== 'face'); }
  function activeFaceStack(object) { return activeEffectStack(object).filter((effect) => Effects.definitionFor(effect.type)?.category === 'face'); }
  function hasActiveGlow(object) { return activeEffectStack(object).some((effect) => ['outer-glow', 'inner-glow'].includes(effect.type)); }
  function objectFilterId(object) { return `object-filter-${String(object?.id || 'object').replace(/[^a-z0-9_-]/gi, '-')}`; }
  function objectHasFilter(object) { return activeFilterStack(object).length > 0 || Boolean(object?.svgNativeFilter); }

  const nativeSvgFilterTags = new Set([
    'filter', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting',
    'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR',
    'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight',
    'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence'
  ]);

  function nativeSvgFilterNode(value, depth = 0) {
    if (!value || typeof value !== 'object' || depth > 16 || !nativeSvgFilterTags.has(value.tag)) return null;
    const attributes = {};
    Object.entries(value.attributes || {}).slice(0, 128).forEach(([name, rawValue]) => {
      const lower = String(name).toLowerCase(); const attributeValue = String(rawValue ?? '').slice(0, 262144);
      if (!/^[a-z_][\w:.-]*$/i.test(name) || lower.startsWith('on') || lower.startsWith('xmlns') || lower === 'id') return;
      if ((lower === 'href' || lower === 'xlink:href') && (/^data:image\/svg\+xml/i.test(attributeValue) || !attributeValue.startsWith('#') && !/^data:image\/(?:png|jpe?g|webp|gif)(?:;[^,]*)?,/i.test(attributeValue))) return;
      if (/(?:javascript:|expression\s*\(|url\(\s*["']?(?!#))/i.test(attributeValue)) return;
      attributes[lower === 'xlink:href' ? 'href' : name] = attributeValue;
    });
    const node = E(value.tag, attributes);
    (Array.isArray(value.children) ? value.children : []).slice(0, 256).forEach((child) => { const safe = nativeSvgFilterNode(child, depth + 1); if (safe) node.append(safe); });
    return node;
  }

  function binaryAlphaMask(input, result, attributes = {}) {
    const threshold = E('feComponentTransfer', { in: input, result, ...attributes });
    threshold.append(E('feFuncA', { type: 'discrete', tableValues: '0 0 1 1' }));
    return threshold;
  }

  const blurDisplacementMapCache = new Map();

  function blurDisplacementMapHref(object, effect, mode) {
    const width = Math.max(1, num(object?.width, 1)); const height = Math.max(1, num(object?.height, 1));
    const centerX = width * clamp(num(effect?.centerX, 50), 0, 100) / 100;
    const centerY = height * clamp(num(effect?.centerY, 50), 0, 100) / 100;
    const span = Math.max(1, Math.max(centerX, width - centerX, centerY, height - centerY) * 2);
    const key = [mode, round(width / span, 4), round(height / span, 4), round(centerX / span, 4), round(centerY / span, 4)].join(':');
    if (blurDisplacementMapCache.has(key)) return blurDisplacementMapCache.get(key);
    const size = 64; const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size;
    const context = canvas.getContext('2d'); const pixels = context.createImageData(size, size);
    for (let y = 0; y < size; y += 1) {
      const localY = height * y / (size - 1);
      for (let x = 0; x < size; x += 1) {
        const localX = width * x / (size - 1); const index = (y * size + x) * 4;
        const displacementX = mode === 'radial' ? -(localY - centerY) / span : (localX - centerX) / span;
        const displacementY = mode === 'radial' ? (localX - centerX) / span : (localY - centerY) / span;
        pixels.data[index] = Math.round(clamp(.5 + displacementX, 0, 1) * 255);
        pixels.data[index + 1] = Math.round(clamp(.5 + displacementY, 0, 1) * 255);
        pixels.data[index + 2] = 128; pixels.data[index + 3] = 255;
      }
    }
    context.putImageData(pixels, 0, 0);
    const href = canvas.toDataURL('image/png');
    blurDisplacementMapCache.set(key, href);
    if (blurDisplacementMapCache.size > 96) blurDisplacementMapCache.delete(blurDisplacementMapCache.keys().next().value);
    return href;
  }

  function appendAveragedFilterSamples(filter, samples, result, effectType) {
    if (!samples.length) {
      filter.append(E('feOffset', { in: 'SourceGraphic', dx: 0, dy: 0, result, 'data-effect-type': effectType }));
      return result;
    }
    let current = samples[0];
    for (let index = 1; index < samples.length; index += 1) {
      const next = index === samples.length - 1 ? result : `${result}-average-${index}`;
      filter.append(E('feComposite', {
        in: current, in2: samples[index], operator: 'arithmetic',
        k1: 0, k2: round(index / (index + 1), 6), k3: round(1 / (index + 1), 6), k4: 0,
        result: next, 'data-effect-type': index === samples.length - 1 ? effectType : undefined
      }));
      current = next;
    }
    if (samples.length === 1) filter.append(E('feOffset', { in: current, dx: 0, dy: 0, result, 'data-effect-type': effectType }));
    return result;
  }

  function appendEffectPrimitive(filter, effect, input, index, object, context) {
    const result = `effect-${index}`;
    if (effect.type === 'outer-glow') {
      const spread = `${result}-spread`; const roundedSpread = `${result}-rounded-spread`; const roundedMask = `${result}-rounded-mask`;
      const softGlow = `${result}-soft`; const color = `${result}-color`; const glow = `${result}-glow`;
      const roundness = effect.size * effect.rounding / 100 * .5;
      const deviationValues = effect.svgFilter?.stdDeviation || [effect.softness / 2];
      const hardEdge = deviationValues.every((value) => num(value) <= 0);
      const sourceMask = `${result}-source-mask`; const rawSpreadInput = effect.svgFilter?.input || 'SourceAlpha';
      const spreadInput = hardEdge ? sourceMask : rawSpreadInput;
      if (hardEdge) filter.append(binaryAlphaMask(rawSpreadInput, sourceMask, { 'data-hard-glow-source': effect.type }));
      filter.append(E('feMorphology', { in: spreadInput, operator: 'dilate', radius: effect.size, result: spread, 'data-effect-type': effect.type }));
      let glowInput = spread;
      if (roundness > .01) {
        filter.append(
          E('feGaussianBlur', { in: spread, stdDeviation: roundness, result: roundedSpread }),
          binaryAlphaMask(roundedSpread, roundedMask)
        );
        glowInput = roundedMask;
      }
      if (hardEdge) filter.append(binaryAlphaMask(glowInput, softGlow, { 'data-hard-glow-mask': effect.type }));
      else filter.append(E('feGaussianBlur', {
        in: glowInput, stdDeviation: deviationValues.join(' '), result: softGlow, 'data-effect-type': effect.type
      }));
      filter.append(
        E('feFlood', { 'flood-color': effect.color, 'flood-opacity': effect.opacity / 100, result: color }),
        E('feComposite', { in: color, in2: softGlow, operator: 'in', result: glow })
      );
      const merge = E('feMerge', { result, 'data-effect-result': effect.type }); merge.append(E('feMergeNode', { in: glow }));
      const glowOnlyText = (object?.type === 'text' || object?.glowOnly) && !hasActiveTextTexture(object) && (object?.fill === 'none' || object?.fill === 'transparent');
      if (!glowOnlyText || context.transparentSourceHidden) merge.append(E('feMergeNode', { in: input }));
      else context.transparentSourceHidden = true;
      filter.append(merge); return result;
    }
    if (effect.type === 'inner-glow') {
      const roundedSoft = `${result}-rounded-soft`; const roundedMask = `${result}-rounded-mask`;
      const eroded = `${result}-eroded`; const innerBand = `${result}-inner-band`; const softGlow = `${result}-soft`;
      const clippedGlow = `${result}-clipped`; const color = `${result}-color`; const glow = `${result}-glow`;
      const roundness = effect.size * effect.rounding / 100 * .5;
      const hardEdge = effect.softness <= 0; const sourceMask = `${result}-source-mask`;
      const originalMask = hardEdge ? sourceMask : 'SourceAlpha';
      if (hardEdge) filter.append(binaryAlphaMask('SourceAlpha', sourceMask, { 'data-hard-glow-source': effect.type }));
      let maskInput = originalMask;
      if (roundness > .01) {
        filter.append(
          E('feGaussianBlur', { in: originalMask, stdDeviation: roundness, result: roundedSoft }),
          binaryAlphaMask(roundedSoft, roundedMask)
        );
        maskInput = roundedMask;
      }
      filter.append(
        E('feMorphology', { in: maskInput, operator: 'erode', radius: effect.size, result: eroded, 'data-effect-type': effect.type }),
        E('feComposite', { in: maskInput, in2: eroded, operator: 'out', result: innerBand })
      );
      if (hardEdge) filter.append(binaryAlphaMask(innerBand, softGlow, { 'data-hard-glow-mask': effect.type }));
      else filter.append(E('feGaussianBlur', {
        in: innerBand, stdDeviation: effect.softness / 2, result: softGlow, 'data-effect-type': effect.type
      }));
      filter.append(
        E('feComposite', { in: softGlow, in2: originalMask, operator: 'in', result: clippedGlow, 'data-inner-glow-clip': 'true' }),
        E('feFlood', { 'flood-color': effect.color, 'flood-opacity': effect.opacity / 100, result: color }),
        E('feComposite', { in: color, in2: clippedGlow, operator: 'in', result: glow })
      );
      const merge = E('feMerge', { result, 'data-effect-result': effect.type });
      const glowOnlyText = (object?.type === 'text' || object?.glowOnly) && !hasActiveTextTexture(object) && (object?.fill === 'none' || object?.fill === 'transparent');
      if (!glowOnlyText || context.transparentSourceHidden) merge.append(E('feMergeNode', { in: input }));
      else context.transparentSourceHidden = true;
      merge.append(E('feMergeNode', { in: glow }));
      filter.append(merge); return result;
    }
    if (['blur', 'blur-more', 'gaussian-blur'].includes(effect.type)) {
      if (effect.svgFilter?.blendMode) {
        const blur = `${result}-blur`;
        filter.append(
          E('feGaussianBlur', { in: effect.svgFilter.input || input, stdDeviation: effect.svgFilter.stdDeviation?.join(' ') || effect.radius, result: blur, 'data-effect-type': effect.type }),
          E('feBlend', { in: blur, in2: effect.svgFilter.blendInput2 || 'SourceGraphic', mode: effect.svgFilter.blendMode, result, 'data-imported-svg-blend': 'true' })
        );
      } else filter.append(E('feGaussianBlur', { in: effect.svgFilter?.input || input, stdDeviation: effect.svgFilter?.stdDeviation?.join(' ') || effect.radius, result, 'data-effect-type': effect.type }));
      return result;
    }
    if (effect.type === 'motion-blur') {
      if (effect.distance <= 0) {
        filter.append(E('feOffset', { in: input, dx: 0, dy: 0, result, 'data-effect-type': effect.type }));
        return result;
      }
      const samples = Effects.motionBlurOffsets(effect).map((offset, sampleIndex) => {
        const sample = `${result}-sample-${sampleIndex}`;
        filter.append(E('feOffset', { in: input, dx: offset.x, dy: offset.y, result: sample, 'data-blur-sample': sampleIndex }));
        return sample;
      });
      return appendAveragedFilterSamples(filter, samples, result, effect.type);
    }
    if (['radial-blur', 'zoom-blur'].includes(effect.type)) {
      if (effect.amount <= 0) {
        filter.append(E('feOffset', { in: input, dx: 0, dy: 0, result, 'data-effect-type': effect.type }));
        return result;
      }
      const width = Math.max(1, num(object?.width, 1)); const height = Math.max(1, num(object?.height, 1));
      const map = `${result}-map`; const mode = effect.type === 'radial-blur' ? 'radial' : 'zoom';
      filter.append(E('feImage', {
        href: blurDisplacementMapHref(object, effect, mode), x: 0, y: 0, width, height,
        preserveAspectRatio: 'none', result: map, 'data-blur-map': mode
      }));
      const count = Effects.blurSampleCount(effect);
      const samples = Array.from({ length: count }, (_entry, sampleIndex) => {
        const progress = count <= 1 ? 0 : sampleIndex / (count - 1);
        const signedProgress = mode === 'radial' ? progress * 2 - 1 : -progress;
        const sample = `${result}-sample-${sampleIndex}`;
        filter.append(E('feDisplacementMap', {
          in: input, in2: map, scale: round(effect.amount * 2 * signedProgress, 4),
          xChannelSelector: 'R', yChannelSelector: 'G', result: sample, 'data-blur-sample': sampleIndex
        }));
        return sample;
      });
      return appendAveragedFilterSamples(filter, samples, result, effect.type);
    }
    if (effect.type === 'add-noise') {
      const rawNoise = `${result}-raw`; const monochromeNoise = `${result}-monochrome`;
      const clippedNoise = `${result}-clipped`; const weightedNoise = `${result}-weighted`;
      const seed = [...String(effect.id || result)].reduce((value, character) => ((value * 33) ^ character.charCodeAt(0)) >>> 0, 5381) % 32767 + 1;
      filter.append(E('feTurbulence', {
        type: 'fractalNoise', baseFrequency: .75, numOctaves: 1, seed, stitchTiles: 'noStitch',
        result: rawNoise, 'data-effect-type': effect.type, 'data-noise-color': String(effect.color),
        'data-noise-amount': effect.amount, 'color-interpolation-filters': 'sRGB'
      }));
      let noiseInput = rawNoise;
      if (!effect.color) {
        filter.append(E('feColorMatrix', {
          in: rawNoise, type: 'saturate', values: 0, result: monochromeNoise,
          'data-noise-monochrome': 'true', 'color-interpolation-filters': 'sRGB'
        }));
        noiseInput = monochromeNoise;
      }
      filter.append(E('feComposite', { in: noiseInput, in2: input, operator: 'in', result: clippedNoise }));
      const opacity = E('feComponentTransfer', {
        in: clippedNoise, result: weightedNoise, 'data-noise-opacity': round(effect.amount / 400, 4)
      });
      opacity.append(E('feFuncA', { type: 'linear', slope: round(effect.amount / 400, 4) }));
      filter.append(
        opacity,
        E('feBlend', {
          in: input, in2: weightedNoise, mode: 'soft-light', result,
          'data-effect-result': effect.type, 'color-interpolation-filters': 'sRGB'
        })
      );
      return result;
    }
    if (effect.type === 'dither') {
      const amount = clamp(num(effect.amount, 50), 0, 100);
      if (amount <= 0) return input;
      const pattern = ditherPattern(effect.method); const pointSize = clamp(Math.round(num(effect.size, 1)), 1, 16);
      const channelLevels = clamp(Math.round(num(effect.levels, 5)), 2, 16);
      const channelStep = round(1 / (channelLevels - 1), 6);
      const channelTable = Array.from({ length: channelLevels }, (_entry, level) => round(level / (channelLevels - 1), 6)).join(' ');
      const tile = `${result}-tile`; const repeated = `${result}-repeated`;
      const thresholds = `${result}-thresholds`; const prepared = `${result}-prepared`; const quantized = `${result}-quantized`;
      const quantizer = E('feComponentTransfer', {
        in: prepared, result: quantized,
        'data-dither-quantize': 'rgb-multilevel',
        'data-dither-channel-levels': channelLevels
      });
      quantizer.append(
        E('feFuncR', { type: 'discrete', tableValues: channelTable }),
        E('feFuncG', { type: 'discrete', tableValues: channelTable }),
        E('feFuncB', { type: 'discrete', tableValues: channelTable }),
        E('feFuncA', { type: 'identity' })
      );
      filter.append(
        E('feImage', {
          x: 0, y: 0, width: pattern.width * pointSize, height: pattern.height * pointSize,
          href: ditherThresholdTile(pattern.method), preserveAspectRatio: 'none', result: tile,
          'image-rendering': 'pixelated', 'data-dither-pattern': pattern.method,
          'data-dither-pixel-size': pointSize, 'data-dither-matrix-width': pattern.width,
          'data-dither-matrix-height': pattern.height, 'data-dither-threshold-levels': pattern.thresholdLevels
        }),
        E('feTile', { in: tile, result: repeated }),
        E('feColorMatrix', {
          in: repeated, type: 'matrix',
          values: '1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 0 .5',
          result: thresholds
        }),
        E('feComposite', {
          in: input, in2: thresholds, operator: 'arithmetic',
          k1: 0, k2: 1, k3: -channelStep, k4: round(channelStep / 2, 6), result: prepared,
          'data-dither-threshold': 'true', 'data-dither-step': channelStep
        }),
        quantizer,
        E('feComposite', {
          in: input, in2: quantized, operator: 'arithmetic',
          k1: 0, k2: round(1 - amount / 100, 6), k3: round(amount / 100, 6), k4: 0, result,
          'data-effect-type': effect.type, 'data-dither-result': 'true',
          'data-dither-amount': amount, 'data-dither-method': pattern.method,
          'data-dither-size': pointSize, 'data-dither-levels': channelLevels
        })
      );
      return result;
    }
    if (effect.type === 'drop-shadow') {
      const blur = `${result}-blur`; const offset = `${result}-offset`; const color = `${result}-color`; const shadow = `${result}-shadow`;
      const shadowInput = effect.svgFilter?.input || input;
      filter.append(
        E('feGaussianBlur', { in: shadowInput, stdDeviation: effect.svgFilter?.stdDeviation?.join(' ') || effect.blur, result: blur, 'data-effect-type': effect.type }),
        E('feOffset', { in: blur, dx: effect.offsetX, dy: effect.offsetY, result: offset }),
        E('feFlood', { 'flood-color': effect.color, 'flood-opacity': effect.opacity / 100, result: color }),
        E('feComposite', { in: color, in2: offset, operator: 'in', result: shadow })
      );
      const merge = E('feMerge', { result, 'data-effect-result': effect.type });
      merge.append(E('feMergeNode', { in: shadow }), E('feMergeNode', { in: input })); filter.append(merge); return result;
    }
    if (effect.type === 'inner-shadow') {
      const blur = `${result}-blur`; const offset = `${result}-offset`; const mask = `${result}-mask`; const color = `${result}-color`; const shadow = `${result}-shadow`;
      const direction = Effects.shadowOffset(effect);
      filter.append(
        E('feGaussianBlur', { in: input, stdDeviation: effect.softness, result: blur, 'data-effect-type': effect.type }),
        E('feOffset', { in: blur, dx: -direction.x, dy: -direction.y, result: offset }),
        E('feComposite', { in: input, in2: offset, operator: 'out', result: mask }),
        E('feFlood', { 'flood-color': effect.color, 'flood-opacity': effect.opacity / 100, result: color }),
        E('feComposite', { in: color, in2: mask, operator: 'in', result: shadow }),
        E('feComposite', { in: shadow, in2: input, operator: 'over', result, 'data-effect-result': effect.type })
      );
      return result;
    }
    if (effect.type === 'solid-shadow') {
      const mask = `${result}-mask`; const color = `${result}-color`; const shadow = `${result}-shadow`;
      const mergeMask = E('feMerge', { result: mask });
      Effects.solidShadowOffsets(effect).forEach((offset, offsetIndex) => {
        const offsetResult = `${result}-offset-${offsetIndex + 1}`;
        filter.append(E('feOffset', { in: input, dx: offset.x, dy: offset.y, result: offsetResult }));
        mergeMask.append(E('feMergeNode', { in: offsetResult }));
      });
      filter.append(
        mergeMask,
        E('feFlood', { 'flood-color': effect.color, 'flood-opacity': effect.opacity / 100, result: color }),
        E('feComposite', { in: color, in2: mask, operator: 'in', result: shadow, 'data-effect-type': effect.type })
      );
      const merge = E('feMerge', { result, 'data-effect-result': effect.type });
      merge.append(E('feMergeNode', { in: shadow }), E('feMergeNode', { in: input })); filter.append(merge); return result;
    }
    if (['inner-bevel', 'outer-bevel', 'inset-emboss', 'raised-emboss'].includes(effect.type)) {
      const embossed = effect.type === 'inset-emboss' || effect.type === 'raised-emboss';
      const inner = effect.type === 'inner-bevel'; const outer = effect.type === 'outer-bevel';
      const lighting = Effects.bevelLighting(effect); const expanded = `${result}-expanded`; const eroded = `${result}-eroded`;
      const innerBand = `${result}-inner-band`; const outerBand = `${result}-outer-band`;
      filter.append(
        E('feMorphology', { in: input, operator: 'dilate', radius: effect.width, result: expanded, 'data-effect-type': effect.type }),
        E('feMorphology', { in: input, operator: 'erode', radius: effect.width, result: eroded }),
        E('feComposite', { in: input, in2: eroded, operator: 'out', result: innerBand }),
        E('feComposite', { in: expanded, in2: input, operator: 'out', result: outerBand })
      );
      const materialMask = outer ? outerBand : innerBand;
      const heightMap = `${result}-height-map`; const rawLight = `${result}-raw-light`; const normalizedLight = `${result}-normalized-light`;
      const boostedLight = `${result}-boosted-light`;
      const clippedLight = `${result}-clipped-light`;
      filter.append(E('feGaussianBlur', { in: input, stdDeviation: lighting.blur, result: heightMap }));
      const diffuse = E('feDiffuseLighting', {
        in: heightMap, surfaceScale: lighting.surfaceScale, diffuseConstant: 1, 'lighting-color': '#ffffff',
        kernelUnitLength: 1, result: rawLight, 'data-effect-type': effect.type
      });
      diffuse.append(E('feDistantLight', { azimuth: lighting.azimuth, elevation: lighting.elevation }));
      const transfer = E('feComponentTransfer', { in: rawLight, result: normalizedLight });
      ['R', 'G', 'B'].forEach((channel) => transfer.append(E(`feFunc${channel}`, {
        type: 'linear', slope: lighting.slope, intercept: lighting.intercept
      })));
      transfer.append(E('feFuncA', { type: 'identity' }));
      const highlightBoost = E('feComponentTransfer', { in: normalizedLight, result: boostedLight, 'data-bevel-highlight': 'true' });
      ['R', 'G', 'B'].forEach((channel) => highlightBoost.append(E(`feFunc${channel}`, {
        type: 'table', tableValues: lighting.highlightTable
      })));
      highlightBoost.append(E('feFuncA', { type: 'identity' }));
      filter.append(
        diffuse,
        transfer,
        highlightBoost,
        E('feComposite', { in: boostedLight, in2: materialMask, operator: 'in', result: clippedLight })
      );
      const innerMaterial = `${result}-inner-material`; const outerMaterial = `${result}-outer-material`;
      if (inner || embossed) filter.append(E('feComposite', { in: input, in2: innerBand, operator: 'in', result: innerMaterial }));
      else filter.append(E('feComposite', { in: expanded, in2: outerBand, operator: 'in', result: outerMaterial }));
      const showObject = !embossed || effect.showObject;
      const material = `${result}-material`;
      if (inner || embossed && showObject) filter.append(E('feComposite', { in: innerMaterial, in2: innerBand, operator: 'in', result: material }));
      else if (outer) filter.append(E('feComposite', { in: outerMaterial, in2: outerBand, operator: 'in', result: material }));
      else {
        const neutral = `${result}-neutral`;
        filter.append(
          E('feFlood', { 'flood-color': '#808080', result: neutral }),
          E('feComposite', { in: neutral, in2: innerBand, operator: 'in', result: material })
        );
      }
      const litMaterial = `${result}-lit-material`; const litOverlay = `${result}-lit-overlay`;
      filter.append(E('feBlend', {
        in: clippedLight, in2: material, mode: 'hard-light', result: litMaterial, 'data-bevel-lighting': 'diffuse'
      }));
      const overlayOpacity = E('feComponentTransfer', { in: litMaterial, result: litOverlay, 'data-bevel-opacity': lighting.opacity });
      overlayOpacity.append(E('feFuncA', { type: 'linear', slope: lighting.opacity }));
      filter.append(overlayOpacity);
      const merge = E('feMerge', {
        result, 'data-effect-result': effect.type, 'data-show-object': String(showObject),
        'data-bevel-scope': inner ? 'inner' : outer ? 'outer' : 'emboss'
      });
      if (showObject) merge.append(E('feMergeNode', { in: input }));
      merge.append(E('feMergeNode', { in: litOverlay }));
      filter.append(merge); return result;
    }
    if (effect.type === 'color-fill') {
      const color = `${result}-color`; const filled = `${result}-filled`; const opacity = round(effect.opacity / 100, 3);
      filter.append(
        E('feFlood', { 'flood-color': effect.color, result: color }),
        E('feComposite', { in: color, in2: input, operator: 'in', result: filled }),
        E('feComposite', {
          in: filled, in2: input, operator: 'arithmetic', k1: 0, k2: opacity, k3: round(1 - opacity, 3), k4: 0,
          result, 'data-effect-type': effect.type
        })
      );
      return result;
    }
    if (effect.type === 'find-edges') {
      const edges = `${result}-edges`;
      filter.append(E('feConvolveMatrix', {
        in: input, order: 3, kernelMatrix: Effects.findEdgesKernel().join(' '), divisor: 1, bias: 0,
        edgeMode: 'duplicate', preserveAlpha: 'true', result: edges, 'data-find-edges': 'true'
      }));
      const transfer = E('feComponentTransfer', { in: edges, result, 'data-effect-type': effect.type });
      ['R', 'G', 'B'].forEach((channel) => transfer.append(E(`feFunc${channel}`, { type: 'table', tableValues: '1 0' })));
      transfer.append(E('feFuncA', { type: 'identity' })); filter.append(transfer); return result;
    }
    if (effect.type === 'convert-to-alpha') {
      const alpha = `${result}-alpha`;
      filter.append(
        E('feColorMatrix', {
          in: input, type: 'matrix', values: Effects.convertToAlphaMatrix().join(' '),
          result: alpha, 'data-convert-to-alpha': 'luminance'
        }),
        E('feComposite', {
          in: alpha, in2: input, operator: 'in', result, 'data-effect-type': effect.type
        })
      );
      return result;
    }
    if (effect.type === 'auto-color') {
      const values = Effects.autoColorTransfer(effect); const transfer = E('feComponentTransfer', { in: input, result, 'data-effect-type': effect.type });
      transfer.append(
        E('feFuncR', { type: 'linear', slope: values.red.slope, intercept: values.red.intercept }),
        E('feFuncG', { type: 'linear', slope: values.green.slope, intercept: values.green.intercept }),
        E('feFuncB', { type: 'linear', slope: values.blue.slope, intercept: values.blue.intercept }),
        E('feFuncA', { type: 'identity' })
      );
      filter.append(transfer); return result;
    }
    if (effect.type === 'curves' || effect.type === 'levels') {
      const transfer = E('feComponentTransfer', { in: input, result, 'data-effect-type': effect.type });
      ['R', 'G', 'B'].forEach((channel) => {
        const values = effect.type === 'curves'
          ? Effects.curveTable(effect, { R: 'red', G: 'green', B: 'blue' }[channel])
          : Effects.levelsTable(effect);
        transfer.append(E(`feFunc${channel}`, { type: 'table', tableValues: values.join(' ') }));
      });
      transfer.append(E('feFuncA', { type: 'identity' })); filter.append(transfer); return result;
    }
    if (effect.type === 'invert') {
      const transfer = E('feComponentTransfer', { in: input, result, 'data-effect-type': effect.type });
      ['R', 'G', 'B'].forEach((channel) => transfer.append(E(`feFunc${channel}`, { type: 'table', tableValues: '1 0' })));
      transfer.append(E('feFuncA', { type: 'identity' })); filter.append(transfer); return result;
    }
    if (effect.type === 'brightness-contrast') {
      const values = Effects.brightnessContrastValues(effect); const transfer = E('feComponentTransfer', { in: input, result, 'data-effect-type': effect.type });
      ['R', 'G', 'B'].forEach((channel) => transfer.append(E(`feFunc${channel}`, { type: 'linear', slope: values.slope, intercept: values.intercept })));
      transfer.append(E('feFuncA', { type: 'identity' })); filter.append(transfer); return result;
    }
    if (effect.type === 'hue-saturation') {
      const hueResult = `${result}-hue`;
      filter.append(
        E('feColorMatrix', { in: input, type: 'hueRotate', values: effect.hue, result: hueResult, 'data-effect-type': effect.type }),
        E('feColorMatrix', { in: hueResult, type: 'saturate', values: effect.saturation / 100, result })
      );
      return result;
    }
    if (effect.type === 'sharpen' || effect.type === 'sharpen-more') {
      filter.append(E('feConvolveMatrix', {
        in: input, order: 3, kernelMatrix: Effects.sharpenKernel(effect).join(' '), divisor: 1, bias: 0,
        edgeMode: 'duplicate', preserveAlpha: 'true', result, 'data-effect-type': effect.type
      }));
      return result;
    }
    if (effect.type === 'unsharp-mask') {
      const values = Effects.unsharpMaskValues(effect);
      const blurred = `${result}-blur`; const difference = `${result}-difference`;
      const differenceAlpha = `${result}-difference-alpha`; const thresholdMask = `${result}-threshold`;
      const clippedMask = `${result}-threshold-clipped`; const sharpened = `${result}-sharpened`;
      const sharpenedMasked = `${result}-sharpened-masked`; const originalOutside = `${result}-original-outside`;
      filter.append(
        E('feGaussianBlur', {
          in: input, stdDeviation: values.radius, result: blurred, 'data-unsharp-stage': 'blur'
        }),
        E('feBlend', {
          in: input, in2: blurred, mode: 'difference', result: difference, 'data-unsharp-stage': 'difference'
        }),
        E('feColorMatrix', {
          in: difference, type: 'matrix',
          values: '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2126 0.7152 0.0722 0 0',
          result: differenceAlpha, 'data-unsharp-stage': 'luminance'
        })
      );
      const threshold = E('feComponentTransfer', {
        in: differenceAlpha, result: thresholdMask, 'data-unsharp-stage': 'threshold',
        'data-unsharp-threshold': values.threshold
      });
      threshold.append(E('feFuncA', { type: 'discrete', tableValues: values.thresholdTable.join(' ') }));
      filter.append(
        threshold,
        E('feComposite', { in: thresholdMask, in2: input, operator: 'in', result: clippedMask }),
        E('feComposite', {
          in: input, in2: blurred, operator: 'arithmetic',
          k1: 0, k2: round(1 + values.amount, 5), k3: round(-values.amount, 5), k4: 0,
          result: sharpened, 'data-unsharp-stage': 'contrast', 'data-unsharp-amount': effect.amount
        }),
        E('feComposite', { in: sharpened, in2: clippedMask, operator: 'in', result: sharpenedMasked }),
        E('feComposite', { in: input, in2: clippedMask, operator: 'out', result: originalOutside })
      );
      const merge = E('feMerge', { result, 'data-effect-result': effect.type, 'data-effect-type': effect.type });
      merge.append(E('feMergeNode', { in: originalOutside }), E('feMergeNode', { in: sharpenedMasked }));
      filter.append(merge); return result;
    }
    return input;
  }

  function appendObjectFilter(defs, object) {
    const effects = activeFilterStack(object);
    if (object?.svgNativeFilter) {
      const nativeFilter = nativeSvgFilterNode(object.svgNativeFilter);
      if (nativeFilter?.localName === 'filter' && nativeFilter.childElementCount) {
        if (object.svgFilterImage) {
          const nativeImage = nativeFilter.querySelector('feImage');
          if (nativeImage) {
            nativeImage.setAttribute('href', object.href || '');
            nativeImage.setAttribute('preserveAspectRatio', object.svgImageSettings?.preserveAspectRatio || 'xMidYMid meet');
          }
        }
        nativeFilter.id = objectFilterId(object); nativeFilter.dataset.objectFilter = object.id;
        nativeFilter.dataset.nativeSvgFilter = 'true'; defs.append(nativeFilter); return;
      }
    }
    if (!effects.length) return;
    const bound = textPathBinding(object);
    const extent = Math.max(1, Effects.visualOutset(effects));
    const textPaddingX = object.type === 'text' ? Math.max(0, num(object.fontSize, 32) * .8 + num(object.textStrokeWidth, 0) * 2) : 0;
    const textPaddingY = object.type === 'text' ? Math.max(0, num(object.fontSize, 32) * .45 + num(object.textStrokeWidth, 0) * 2) : 0;
    const extentX = extent + textPaddingX; const extentY = extent + textPaddingY;
    const imported = object.svgFilterSettings; const region = imported?.region;
    const filter = E('filter', {
      id: objectFilterId(object), filterUnits: 'userSpaceOnUse', primitiveUnits: 'userSpaceOnUse',
      x: region ? region.x : bound ? num(object.x) - extentX : -extentX,
      y: region ? region.y : bound ? num(object.y) - extentY : -extentY,
      width: region ? region.width : Math.max(1, num(object.width, 1)) + extentX * 2,
      height: region ? region.height : Math.max(1, num(object.height, 1)) + extentY * 2,
      'color-interpolation-filters': imported?.colorInterpolationFilters || 'sRGB',
      'data-object-filter': object.id, 'data-imported-svg-filter': imported ? 'true' : undefined,
      'data-svg-filter-units': imported?.filterUnits, 'data-svg-primitive-units': imported?.primitiveUnits
    });
    let current = 'SourceGraphic'; const context = { transparentSourceHidden: false };
    effects.forEach((effect, index) => { current = appendEffectPrimitive(filter, effect, current, index, object, context); });
    defs.append(filter);
  }

  function faceEffectShape(object, effect) {
    const width = Math.max(1, num(object?.width, 1)); const height = Math.max(1, num(object?.height, 1));
    const basis = Math.max(1, Math.min(width, height)); const scale = clamp(num(effect.size, 100) / 100, .25, 2);
    const positionX = width / 2 + width * clamp(num(effect.positionX), -50, 50) / 100;
    const positionY = height * clamp(num(effect.positionY), -50, 50) / 100;
    const spacing = clamp(num(effect.spacing, 100) / 100, .35, 2);
    const thickness = Math.max(.5, basis * .032 * scale * clamp(num(effect.thickness, 100) / 100, .25, 2));
    const root = E('g', {
      'data-face-effect': effect.type, 'data-effect-id': effect.id, 'data-face-feature': effect.type.includes('eyes') ? 'eyes' : 'mouth',
      'pointer-events': 'none'
    });
    const strokeAttrs = (color) => ({
      fill: 'none', stroke: color, 'stroke-width': round(thickness, 3), 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'pointer-events': 'none'
    });

    if (effect.type === 'face-line-eyes') {
      const centerY = height * .39 + positionY; const gap = basis * .24 * scale * spacing; const eyeHeight = basis * .18 * scale;
      [-1, 1].forEach((side) => root.append(E('path', {
        ...strokeAttrs(effect.color), d: `M ${round(positionX + side * gap / 2)} ${round(centerY - eyeHeight / 2)} L ${round(positionX + side * gap / 2)} ${round(centerY + eyeHeight / 2)}`
      })));
    } else if (effect.type === 'face-dot-eyes') {
      const centerY = height * .39 + positionY; const gap = basis * .24 * scale * spacing; const radius = basis * .055 * scale;
      [-1, 1].forEach((side) => root.append(E('circle', {
        cx: round(positionX + side * gap / 2), cy: round(centerY), r: round(radius, 3), fill: effect.color, 'pointer-events': 'none'
      })));
    } else if (effect.type === 'face-comic-eyes') {
      const centerY = height * .39 + positionY; const gap = basis * .25 * scale * spacing;
      const radiusX = basis * .09 * scale; const radiusY = basis * .135 * scale; const border = Math.max(.5, basis * .014 * scale);
      const pupilX = clamp(num(effect.gazeX), -100, 100) / 100 * radiusX * .42;
      const pupilY = clamp(num(effect.gazeY), -100, 100) / 100 * radiusY * .38;
      [-1, 1].forEach((side) => {
        const eyeX = positionX + side * gap / 2;
        root.append(
          E('ellipse', {
            cx: round(eyeX), cy: round(centerY), rx: round(radiusX, 3), ry: round(radiusY, 3), fill: effect.eyeColor,
            stroke: effect.pupilColor, 'stroke-width': round(border, 3), 'pointer-events': 'none'
          }),
          E('ellipse', {
            cx: round(eyeX + pupilX), cy: round(centerY + pupilY), rx: round(radiusX * .37, 3), ry: round(radiusY * .42, 3),
            fill: effect.pupilColor, 'pointer-events': 'none'
          })
        );
      });
    } else if (effect.type === 'face-wink-eyes') {
      const centerY = height * .39 + positionY; const gap = basis * .24 * scale * spacing; const eyeHeight = basis * .17 * scale;
      const leftX = positionX - gap / 2; const rightX = positionX + gap / 2; const halfWidth = basis * .058 * scale;
      root.append(
        E('path', { ...strokeAttrs(effect.color), d: `M ${round(leftX)} ${round(centerY - eyeHeight / 2)} L ${round(leftX)} ${round(centerY + eyeHeight / 2)}` }),
        E('path', {
          ...strokeAttrs(effect.color),
          d: `M ${round(rightX - halfWidth)} ${round(centerY + eyeHeight * .05)} Q ${round(rightX)} ${round(centerY + eyeHeight * .45)} ${round(rightX + halfWidth)} ${round(centerY + eyeHeight * .05)}`
        })
      );
    } else if (effect.type === 'face-smile') {
      const centerY = height * .6 + positionY; const mouthWidth = basis * .34 * scale;
      const depth = basis * .13 * scale * clamp(num(effect.curvature, 100), -100, 200) / 100;
      root.append(E('path', {
        ...strokeAttrs(effect.color),
        d: `M ${round(positionX - mouthWidth / 2)} ${round(centerY)} Q ${round(positionX)} ${round(centerY + depth)} ${round(positionX + mouthWidth / 2)} ${round(centerY)}`
      }));
    } else if (effect.type === 'face-grin') {
      const centerY = height * .58 + positionY; const mouthWidth = basis * .38 * scale; const mouthHeight = basis * .18 * scale;
      const left = positionX - mouthWidth / 2; const right = positionX + mouthWidth / 2;
      root.append(E('path', {
        d: `M ${round(left)} ${round(centerY)} Q ${round(positionX)} ${round(centerY + mouthHeight * 1.25)} ${round(right)} ${round(centerY)} Q ${round(positionX)} ${round(centerY + mouthHeight * .5)} ${round(left)} ${round(centerY)} Z`,
        fill: effect.toothColor, stroke: effect.color, 'stroke-width': round(Math.max(.5, basis * .022 * scale), 3),
        'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'pointer-events': 'none'
      }));
      [-.5, 0, .5].forEach((part) => root.append(E('line', {
        x1: round(positionX + mouthWidth * part / 2), y1: round(centerY + mouthHeight * .2),
        x2: round(positionX + mouthWidth * part / 2), y2: round(centerY + mouthHeight * .73),
        stroke: effect.color, 'stroke-width': round(Math.max(.35, basis * .009 * scale), 3), 'stroke-linecap': 'round', 'pointer-events': 'none'
      })));
    } else if (effect.type === 'face-tongue') {
      const centerY = height * .57 + positionY; const mouthWidth = basis * .34 * scale; const mouthHeight = basis * .22 * scale;
      const left = positionX - mouthWidth / 2; const right = positionX + mouthWidth / 2;
      root.append(E('path', {
        d: `M ${round(left)} ${round(centerY)} Q ${round(positionX)} ${round(centerY + mouthHeight * 1.45)} ${round(right)} ${round(centerY)} Q ${round(positionX)} ${round(centerY + mouthHeight * .25)} ${round(left)} ${round(centerY)} Z`,
        fill: effect.mouthColor, 'pointer-events': 'none'
      }));
      const tongueWidth = mouthWidth * .48; const tongueTop = centerY + mouthHeight * .56; const tongueBottom = centerY + mouthHeight * 1.18;
      root.append(
        E('path', {
          d: `M ${round(positionX - tongueWidth / 2)} ${round(tongueTop)} L ${round(positionX + tongueWidth / 2)} ${round(tongueTop)} L ${round(positionX + tongueWidth / 2)} ${round(tongueBottom - tongueWidth * .18)} A ${round(tongueWidth / 2)} ${round(tongueWidth * .24)} 0 0 1 ${round(positionX - tongueWidth / 2)} ${round(tongueBottom - tongueWidth * .18)} Z`,
          fill: effect.tongueColor, 'pointer-events': 'none'
        }),
        E('line', {
          x1: round(positionX), y1: round(tongueTop + mouthHeight * .08), x2: round(positionX), y2: round(tongueBottom - tongueWidth * .2),
          stroke: effect.mouthColor, 'stroke-opacity': .35, 'stroke-width': round(Math.max(.35, basis * .009 * scale), 3), 'stroke-linecap': 'round', 'pointer-events': 'none'
        })
      );
    }
    return root;
  }

  function appendFaceEffectOverlays(target, object, boundText = false) {
    const effects = activeFaceStack(object); if (!effects.length) return;
    const overlay = E('g', {
      'data-face-effects': object.id, transform: boundText ? objectTransform(object) : undefined, 'pointer-events': 'none'
    });
    effects.forEach((effect) => overlay.append(faceEffectShape(object, effect)));
    target.append(overlay);
  }

  function cacheTextPixelTexture(key, value) {
    if (textPixelTextureCache.has(key)) textPixelTextureCache.delete(key);
    textPixelTextureCache.set(key, value);
    while (textPixelTextureCache.size > 16) textPixelTextureCache.delete(textPixelTextureCache.keys().next().value);
    return value;
  }

  function textPixelTextureOverlay(object, texture) {
    if (!texture.pixelFullCells) return null;
    const pixelSize = clamp(Math.round(texture.pixelSize), 1, 128); const gap = clamp(Math.round(texture.gap), 0, 64);
    const cellSize = pixelSize + gap; const fontSize = Math.max(1, num(object.fontSize, 32)); const settings = textSettings(object);
    const pathBinding = textPathBinding(object); const padding = Math.ceil(Math.max(fontSize, cellSize * 2) / cellSize) * cellSize;
    let originX; let originY; let canvasWidth; let canvasHeight;
    if (pathBinding) {
      originX = Math.floor((num(object.x) - padding) / cellSize) * cellSize;
      originY = Math.floor((num(object.y) - padding) / cellSize) * cellSize;
      canvasWidth = Math.ceil((num(object.x) + Math.max(1, num(object.width, 1)) + padding - originX) / cellSize) * cellSize;
      canvasHeight = Math.ceil((num(object.y) + Math.max(1, num(object.height, 1)) + padding - originY) / cellSize) * cellSize;
    } else {
      originX = -padding; originY = -padding;
      canvasWidth = Math.ceil((Math.max(1, num(object.width, 1)) + padding * 2) / cellSize) * cellSize;
      canvasHeight = Math.ceil((Math.max(1, num(object.height, 1)) + padding * 2) / cellSize) * cellSize;
    }
    canvasWidth = Math.max(cellSize, Math.ceil(canvasWidth)); canvasHeight = Math.max(cellSize, Math.ceil(canvasHeight));
    // Sehr große importierte Textboxen dürfen den synchronen Editor-Render nicht blockieren.
    // In diesem Sonderfall bleibt die normale, an der Glyphenkante beschnittene Pixeltextur aktiv.
    if (canvasWidth > 4096 || canvasHeight > 4096 || canvasWidth * canvasHeight > 4_000_000) return null;
    const bindingKey = pathBinding ? {
      target: [pathBinding.target.id, pathBinding.target.x, pathBinding.target.y, pathBinding.target.width, pathBinding.target.height, pathBinding.target.rotation],
      binding: pathBinding.binding
    } : null;
    const key = JSON.stringify([
      object.text, object.width, object.height, object.fontFamily, fontSize, object.bold, object.italic,
      settings.letterSpacing, settings.lineHeight, settings.horizontalScale, settings.verticalScale, settings.baselineShift,
      settings.align, settings.autoKern, settings.underline, settings.strike, pixelSize, gap, texture.pixelShape,
      originX, originY, canvasWidth, canvasHeight, bindingKey
    ]);
    let rendered = textPixelTextureCache.get(key);
    if (!rendered) {
      const canvas = document.createElement('canvas'); canvas.width = canvasWidth; canvas.height = canvasHeight;
      const context = canvas.getContext('2d', { willReadFrequently: true }); if (!context) return null;
      context.fillStyle = '#ffffff'; context.textBaseline = 'alphabetic'; context.font = `${object.italic ? 'italic ' : ''}${object.bold ? '700 ' : '400 '}${fontSize}px ${object.fontFamily || DEFAULT_FONT_FAMILY}`;
      context.fontKerning = settings.autoKern ? 'normal' : 'none';
      if ('letterSpacing' in context) context.letterSpacing = `${settings.letterSpacing}px`;
      const drawDecoration = (line, x, y) => {
        if (!settings.underline && !settings.strike) return;
        const width = context.measureText(line).width; let start = x;
        if (context.textAlign === 'center') start -= width / 2; else if (context.textAlign === 'right') start -= width;
        context.lineWidth = Math.max(1, fontSize * .055); context.strokeStyle = '#ffffff';
        context.beginPath();
        if (settings.underline) { context.moveTo(start, y + fontSize * .12); context.lineTo(start + width, y + fontSize * .12); }
        if (settings.strike) { context.moveTo(start, y - fontSize * .3); context.lineTo(start + width, y - fontSize * .3); }
        context.stroke();
      };
      if (pathBinding) {
        const { target, binding } = pathBinding; const geometry = boundPathGeometry(target, binding.placement, fontSize, binding.distance);
        const characters = [...String(object.text || '').replace(/\n/g, ' ')]; const scaleX = settings.horizontalScale / 100; const scaleY = settings.verticalScale / 100;
        const advances = characters.map((character) => Math.max(fontSize * .04, (character === ' ' ? fontSize * .34 : fontSize * .62) + settings.letterSpacing + binding.spacing));
        const total = advances.reduce((sum, width) => sum + width, 0); const flow = boundTextFlow(binding, total, geometry.circumference);
        characters.forEach((character, index) => {
          const advance = advances[index]; const point = pointOnBoundPath(target, geometry, flow.cursor + flow.direction * advance / geometry.circumference / 2);
          const rotation = binding.orientation === 'horizontal' ? 0 : point.tangent + flow.rotationOffset;
          context.save(); context.translate(point.x - originX, point.y - originY); context.rotate(rotation * Math.PI / 180); context.scale(scaleX, scaleY);
          context.textAlign = 'center'; context.fillText(character || ' ', 0, settings.baselineShift); context.restore(); flow.cursor += flow.direction * advance / geometry.circumference;
        });
      } else {
        const scaleX = settings.horizontalScale / 100; const scaleY = settings.verticalScale / 100;
        const naturalWidth = Math.max(1, num(object.width, 1) / scaleX); const anchor = settings.align === 'center' ? naturalWidth / 2 : settings.align === 'right' ? naturalWidth : 0;
        context.save(); context.translate(-originX, -originY); context.scale(scaleX, scaleY);
        context.textAlign = settings.align === 'center' ? 'center' : settings.align === 'right' ? 'right' : 'left';
        String(object.text || '').split('\n').forEach((line, index) => {
          const y = fontSize + settings.baselineShift + index * fontSize * settings.lineHeight / 100;
          context.fillText(line || ' ', anchor, y); drawDecoration(line, anchor, y);
        });
        context.restore();
      }
      const source = context.getImageData(0, 0, canvasWidth, canvasHeight).data;
      const columns = Math.ceil(canvasWidth / cellSize); const rows = Math.ceil(canvasHeight / cellSize); const touched = new Uint8Array(columns * rows);
      for (let y = 0; y < canvasHeight; y += 1) {
        for (let x = 0; x < canvasWidth; x += 1) {
          if (source[(y * canvasWidth + x) * 4 + 3] > 8) touched[Math.floor(y / cellSize) * columns + Math.floor(x / cellSize)] = 1;
        }
      }
      let minColumn = columns; let minRow = rows; let maxColumn = -1; let maxRow = -1;
      touched.forEach((active, index) => {
        if (!active) return; const column = index % columns; const row = Math.floor(index / columns);
        minColumn = Math.min(minColumn, column); minRow = Math.min(minRow, row); maxColumn = Math.max(maxColumn, column); maxRow = Math.max(maxRow, row);
      });
      if (maxColumn < minColumn || maxRow < minRow) return null;
      const output = document.createElement('canvas'); output.width = (maxColumn - minColumn + 1) * cellSize; output.height = (maxRow - minRow + 1) * cellSize;
      const outputContext = output.getContext('2d'); if (!outputContext) return null;
      outputContext.fillStyle = '#ffffff';
      touched.forEach((active, index) => {
        if (!active) return; const x = (index % columns - minColumn) * cellSize; const y = (Math.floor(index / columns) - minRow) * cellSize;
        if (texture.pixelShape === 'round') {
          outputContext.beginPath(); outputContext.arc(x + pixelSize / 2, y + pixelSize / 2, pixelSize / 2, 0, Math.PI * 2); outputContext.fill();
        } else outputContext.fillRect(x, y, pixelSize, pixelSize);
      });
      rendered = cacheTextPixelTexture(key, {
        href: output.toDataURL('image/png'), x: originX + minColumn * cellSize, y: originY + minRow * cellSize,
        width: output.width, height: output.height
      });
    }
    return rendered;
  }

  function applyTextTexture(shape, target, object) {
    const texture = textTextureSettings(object);
    if (texture.mode === 'none') return;
    const safeId = String(object.id || 'text').replace(/[^a-z0-9_-]/gi, '-');
    const definitions = E('defs'); let textureFill = shape.getAttribute('fill') || objectFillValue(object); let pixelOverlay = null; let pixelMaskId = null;
    if (['none', 'transparent', ''].includes(String(textureFill || '').toLowerCase())) textureFill = '#000000';

    if (texture.mode === 'pixel') {
      const renderedMask = textPixelTextureOverlay(object, texture);
      if (renderedMask) {
        pixelMaskId = `text-pixel-mask-${safeId}`;
        const mask = E('mask', {
          id: pixelMaskId, maskUnits: 'userSpaceOnUse', x: renderedMask.x, y: renderedMask.y,
          width: renderedMask.width, height: renderedMask.height, 'mask-type': 'alpha'
        });
        mask.append(E('image', {
          x: renderedMask.x, y: renderedMask.y, width: renderedMask.width, height: renderedMask.height, href: renderedMask.href,
          preserveAspectRatio: 'none', 'image-rendering': 'pixelated', 'data-text-texture-pixel-mask': 'true'
        }));
        definitions.append(mask);
        pixelOverlay = E('rect', {
          x: renderedMask.x, y: renderedMask.y, width: renderedMask.width, height: renderedMask.height, fill: textureFill,
          mask: `url(#${pixelMaskId})`, 'pointer-events': 'none', 'data-text-texture-mode': 'pixel',
          'data-text-texture-pixel-overlay': 'true', 'data-text-texture-pixel-shape': texture.pixelShape,
          'data-text-texture-full-cells': 'true', 'data-text-texture-fill': normalizedObjectFillPaint(object)?.type || 'solid'
        });
      } else {
        const patternId = `text-pixel-pattern-${safeId}`; const pixelSize = Math.round(texture.pixelSize); const cellSize = pixelSize + Math.round(texture.gap);
        const pattern = E('pattern', {
          id: patternId, patternUnits: 'userSpaceOnUse', x: 0, y: 0,
          width: cellSize, height: cellSize, 'data-text-texture-mode': texture.mode,
          'data-text-texture-pixel-shape': texture.pixelShape
        });
        pattern.append(texture.pixelShape === 'round'
          ? E('circle', { cx: pixelSize / 2, cy: pixelSize / 2, r: pixelSize / 2, fill: '#ffffff', 'data-text-texture-pixel': 'true' })
          : E('rect', {
            x: 0, y: 0, width: pixelSize, height: pixelSize, fill: '#ffffff',
            'shape-rendering': 'crispEdges', 'data-text-texture-pixel': 'true'
          }));
        const fontPadding = Math.max(8, num(object.fontSize, 32)); const bound = textPathBinding(object);
        const maskX = bound ? num(object.x) - fontPadding : -fontPadding; const maskY = bound ? num(object.y) - fontPadding : -fontPadding;
        const maskWidth = Math.max(1, num(object.width, 1)) + fontPadding * 2; const maskHeight = Math.max(1, num(object.height, 1)) + fontPadding * 2;
        pixelMaskId = `text-pixel-mask-${safeId}`;
        const mask = E('mask', { id: pixelMaskId, maskUnits: 'userSpaceOnUse', x: maskX, y: maskY, width: maskWidth, height: maskHeight, 'mask-type': 'alpha' });
        mask.append(E('rect', { x: maskX, y: maskY, width: maskWidth, height: maskHeight, fill: `url(#${patternId})` }));
        definitions.append(pattern, mask);
      }
    }

    const paintNodes = object.type === 'text'
      ? (shape.matches?.('text') ? [shape] : [...shape.querySelectorAll?.('text') || []])
      : [shape];
    const outlineMode = texture.mode === 'outline'; const paintOutline = outlineMode && Boolean(normalizedObjectFillPaint(object)); const outlineWidth = texture.amount * .12;
    paintNodes.forEach((node) => {
      node.setAttribute('fill', pixelOverlay || paintOutline ? 'transparent' : textureFill);
      if (pixelMaskId && !pixelOverlay) node.setAttribute('mask', `url(#${pixelMaskId})`);
      if (paintOutline) {
        node.setAttribute('stroke', textureFill); node.setAttribute('stroke-width', Math.max(.02, outlineWidth * 2)); node.setAttribute('paint-order', 'stroke');
      } else if (outlineMode) node.setAttribute('stroke', 'none');
      if (texture.mode === 'pixel') { node.setAttribute('shape-rendering', 'crispEdges'); node.setAttribute('text-rendering', 'optimizeSpeed'); }
    });

    const procedural = isProceduralTextTextureMode(texture.mode);
    const filterOutlineMode = outlineMode && !paintOutline;
    const appliedDistortion = ['washed', 'pixel'].includes(texture.mode) ? 0 : texture.distortion;
    if (procedural || filterOutlineMode || appliedDistortion > 0) {
      const filterId = `text-texture-filter-${safeId}`;
      const fontSize = Math.max(1, num(object.fontSize, 32)); const strokePadding = num(object.textStrokeWidth, 0) * 2;
      const extentX = Math.max(6, appliedDistortion + 3, outlineWidth + 2, fontSize * .8 + strokePadding);
      const extentY = Math.max(6, appliedDistortion + 3, outlineWidth + 2, fontSize * .45 + strokePadding);
      const frequencyScale = Math.max(.2, texture.scale / 100);
      const baseFrequency = texture.mode === 'waves'
        ? `${round(.012 / frequencyScale, 5)} ${round(.065 / frequencyScale, 5)}`
        : texture.mode === 'canvas'
          ? `${round(.075 / frequencyScale, 5)} ${round(.018 / frequencyScale, 5)}`
          : texture.mode === 'wood'
            ? `${round(.008 / frequencyScale, 5)} ${round(.055 / frequencyScale, 5)}`
            : texture.mode === 'paper'
              ? round(.035 / frequencyScale, 5)
              : ['grunge', 'distressed'].includes(texture.mode)
                ? round(.026 / frequencyScale, 5)
                : texture.mode === 'washed'
                  ? round(.032 / frequencyScale, 5)
                  : texture.mode === 'cracks' ? round(.038 / frequencyScale, 5) : round(.075 / frequencyScale, 5);
      const filter = E('filter', {
        id: filterId, filterUnits: 'userSpaceOnUse', x: -extentX, y: -extentY,
        width: Math.max(1, num(object.width, 1)) + extentX * 2, height: Math.max(1, num(object.height, 1)) + extentY * 2,
        'color-interpolation-filters': 'sRGB', 'data-text-texture-mode': texture.mode,
        'data-text-texture-amount': texture.amount,
        'data-text-texture-transparent': String(texture.transparent)
      });
      const noiseResult = `${filterId}-noise`;
      if (procedural || appliedDistortion > 0) {
        filter.append(E('feTurbulence', {
          type: ['waves', 'canvas', 'wood'].includes(texture.mode) ? 'turbulence' : 'fractalNoise', baseFrequency,
          numOctaves: texture.mode === 'canvas' ? 1 : texture.mode === 'paper' ? 5 : ['waves'].includes(texture.mode) ? 2
            : ['grunge', 'distressed', 'cracks', 'washed'].includes(texture.mode) ? 4 : 3,
          seed: texture.seed, result: noiseResult, 'data-text-texture-seed': texture.seed,
          ...(surfaceTextTextureModes.has(texture.mode) ? { 'data-text-texture-surface': texture.mode } : {}),
          ...(texture.mode === 'canvas' ? { 'data-text-texture-thread': 'warp' } : {})
        }));
      }
      let surfaceNoise = noiseResult;
      if (texture.mode === 'canvas') {
        const crossNoise = `${filterId}-cross-noise`; const wovenNoise = `${filterId}-woven-noise`;
        const crossSeed = texture.seed % 9999 + 1;
        filter.append(
          E('feTurbulence', {
            type: 'turbulence',
            baseFrequency: `${round(.018 / frequencyScale, 5)} ${round(.075 / frequencyScale, 5)}`,
            numOctaves: 1, seed: crossSeed, result: crossNoise,
            'data-text-texture-surface': texture.mode, 'data-text-texture-thread': 'weft'
          }),
          E('feBlend', {
            in: noiseResult, in2: crossNoise, mode: 'multiply', result: wovenNoise,
            'data-text-texture-weave': 'true'
          })
        );
        surfaceNoise = wovenNoise;
      }
      let current = 'SourceGraphic';
      if (appliedDistortion > 0) {
        const displaced = `${filterId}-displaced`;
        filter.append(E('feDisplacementMap', {
          in: current, in2: noiseResult, scale: appliedDistortion, xChannelSelector: 'R', yChannelSelector: 'G', result: displaced,
          'data-text-texture-distortion': appliedDistortion
        }));
        current = displaced;
      }
      if (filterOutlineMode) {
        const alpha = `${filterId}-alpha`; const expanded = `${filterId}-expanded`; const ring = `${filterId}-ring`; const color = `${filterId}-color`;
        const outlineColor = normalizeColor(object.textStroke) || normalizeColor(object.fill) || '#000000';
        filter.append(
          E('feColorMatrix', {
            in: current, type: 'matrix', values: '0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0', result: alpha
          }),
          E('feMorphology', { in: alpha, operator: 'dilate', radius: round(Math.max(.01, outlineWidth), 3), result: expanded }),
          E('feComposite', { in: expanded, in2: alpha, operator: 'out', result: ring }),
          E('feFlood', { 'flood-color': outlineColor, result: color }),
          E('feComposite', { in: color, in2: ring, operator: 'in', result: `${filterId}-result`, 'data-text-texture-outline': 'true' })
        );
      } else if (surfaceTextTextureModes.has(texture.mode)) {
        const mono = `${filterId}-mono`; const tone = `${filterId}-tone`; const clipped = `${filterId}-clipped`;
        filter.append(E('feColorMatrix', { in: surfaceNoise, type: 'saturate', values: 0, result: mono }));
        const transfer = E('feComponentTransfer', { in: mono, result: tone, 'data-text-texture-surface': texture.mode });
        const linearTone = texture.mode === 'paper'
          ? { slope: .5, intercept: .25, alpha: .38 }
          : texture.mode === 'canvas'
            ? { slope: .9, intercept: .05, alpha: .5 }
            : { slope: .72, intercept: .14, alpha: texture.mode === 'waves' ? .42 : .58 };
        if (texture.mode === 'wood') {
          const rings = '0.28 0.82 0.38 0.74 0.3 0.88 0.42 0.7 0.26 0.84';
          transfer.append(
            E('feFuncR', { type: 'table', tableValues: rings }),
            E('feFuncG', { type: 'table', tableValues: rings }),
            E('feFuncB', { type: 'table', tableValues: rings })
          );
          if (!texture.transparent) transfer.append(E('feFuncA', { type: 'linear', slope: .48 * texture.amount / 100 }));
        } else {
          transfer.append(
            E('feFuncR', { type: 'linear', slope: linearTone.slope, intercept: linearTone.intercept }),
            E('feFuncG', { type: 'linear', slope: linearTone.slope, intercept: linearTone.intercept }),
            E('feFuncB', { type: 'linear', slope: linearTone.slope, intercept: linearTone.intercept })
          );
          if (!texture.transparent) transfer.append(E('feFuncA', { type: 'linear', slope: linearTone.alpha * texture.amount / 100 }));
        }
        filter.append(transfer);
        if (texture.transparent) {
          const transparencyMask = `${filterId}-transparency-mask`;
          const transparency = texture.amount / 100; const channelWeight = round(-transparency / 3, 6);
          filter.append(
            E('feColorMatrix', {
              in: texture.mode === 'wood' ? tone : mono, type: 'matrix',
              values: `1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  ${channelWeight} ${channelWeight} ${channelWeight} 0 1`,
              result: transparencyMask, 'data-text-texture-transparent': 'true',
              'data-text-texture-transparency-amount': texture.amount
            }),
            E('feComposite', {
              in: current, in2: transparencyMask, operator: 'in', result: `${filterId}-result`,
              'data-text-texture-transparent': 'true'
            })
          );
        } else {
          filter.append(
            E('feComposite', { in: tone, in2: current, operator: 'in', result: clipped }),
            E('feBlend', {
              in: current, in2: clipped, mode: texture.mode === 'wood' ? 'multiply' : 'soft-light',
              result: `${filterId}-result`, 'data-text-texture-surface': texture.mode
            })
          );
        }
      } else if (texture.mode === 'washed') {
        const fadeAlpha = `${filterId}-fade-alpha`; const fadeMask = `${filterId}-fade-mask`;
        const spreadInk = `${filterId}-spread-ink`; const softInk = `${filterId}-soft-ink`; const fadedInk = `${filterId}-faded-ink`;
        const fineNoise = `${filterId}-fine-noise`; const fineAlpha = `${filterId}-fine-alpha`;
        const rawChips = `${filterId}-raw-chips`; const spreadChips = `${filterId}-spread-chips`; const softChips = `${filterId}-soft-chips`;
        const rawGate = `${filterId}-raw-gate`; const wearGate = `${filterId}-wear-gate`;
        const wornInk = `${filterId}-worn-ink`; const wornPart = `${filterId}-worn-part`; const intactPart = `${filterId}-intact-part`;
        const intensity = texture.amount / 100;
        filter.append(E('feColorMatrix', {
          in: noiseResult, type: 'matrix',
          values: '0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  .3333 .3333 .3333 0 0', result: fadeAlpha
        }));
        const fadeTransfer = E('feComponentTransfer', { in: fadeAlpha, result: fadeMask });
        fadeTransfer.append(E('feFuncA', { type: 'linear', slope: round(1.25 * intensity, 4), intercept: round(1 - .95 * intensity, 4) }));
        const bins = 64; const distributionRatio = texture.distribution / 100;
        const gateThreshold = clamp(.48 - texture.distribution * .0003, .45, .48); const gateFloor = (1 - distributionRatio) ** 2;
        const gateTable = Array.from({ length: bins }, (_, index) => index / (bins - 1) <= gateThreshold ? 1 : round(gateFloor, 4));
        const gateTransfer = E('feComponentTransfer', { in: fadeAlpha, result: rawGate, 'data-text-texture-distribution': texture.distribution });
        gateTransfer.append(E('feFuncA', { type: 'discrete', tableValues: gateTable.join(' ') }));
        const fineSeed = (texture.seed + 7918) % 9999 + 1;
        filter.append(
          fadeTransfer,
          gateTransfer,
          E('feGaussianBlur', { in: rawGate, stdDeviation: round(texture.distribution <= 0 ? .001 : .25 + texture.distribution / 90, 3), result: wearGate }),
          E('feMorphology', { in: current, operator: 'dilate', radius: round(Math.max(.001, texture.amount / 250), 3), result: spreadInk }),
          E('feGaussianBlur', { in: spreadInk, stdDeviation: round(Math.max(.001, texture.amount / 320), 3), result: softInk }),
          E('feComposite', { in: softInk, in2: fadeMask, operator: 'in', result: fadedInk }),
          E('feTurbulence', {
            type: 'fractalNoise', baseFrequency: round(.18 / frequencyScale, 5), numOctaves: 2,
            seed: fineSeed, result: fineNoise, 'data-text-texture-fine-seed': fineSeed
          }),
          E('feColorMatrix', {
            in: fineNoise, type: 'matrix',
            values: '0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  .3333 .3333 .3333 0 0', result: fineAlpha
          })
        );
        const chipThreshold = texture.amount <= 0 ? 2 : clamp(.78 - texture.amount * .0035, .4, .78);
        const chipTable = Array.from({ length: bins }, (_, index) => index / (bins - 1) >= chipThreshold ? 1 : 0);
        const chipTransfer = E('feComponentTransfer', { in: fineAlpha, result: rawChips });
        chipTransfer.append(E('feFuncA', { type: 'discrete', tableValues: chipTable.join(' ') }));
        filter.append(
          chipTransfer,
          E('feMorphology', { in: rawChips, operator: 'dilate', radius: round(Math.max(.001, texture.amount / 550), 3), result: spreadChips }),
          E('feGaussianBlur', { in: spreadChips, stdDeviation: round(Math.max(.001, texture.amount / 700), 3), result: softChips }),
          E('feComposite', { in: fadedInk, in2: softChips, operator: 'out', result: wornInk }),
          E('feComposite', { in: wornInk, in2: wearGate, operator: 'in', result: wornPart }),
          E('feComposite', { in: current, in2: wearGate, operator: 'out', result: intactPart })
        );
        const merge = E('feMerge', { 'data-text-texture-washed': 'true', 'data-text-texture-distribution': texture.distribution });
        merge.append(E('feMergeNode', { in: intactPart }), E('feMergeNode', { in: wornPart })); filter.append(merge);
      } else if (['grunge', 'distressed', 'cracks'].includes(texture.mode)) {
        const alphaNoise = `${filterId}-alpha-noise`; const rawMask = `${filterId}-raw-mask`; const wearMask = `${filterId}-wear-mask`;
        filter.append(E('feColorMatrix', {
          in: noiseResult, type: 'matrix',
          values: '0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  .3333 .3333 .3333 0 0', result: alphaNoise
        }));
        const bins = 64; let table;
        if (texture.mode === 'cracks') {
          const halfWidth = texture.amount <= 0 ? -1 : .25 + texture.amount / 55;
          const centers = [21, 34, 46];
          table = Array.from({ length: bins }, (_, index) => centers.some((center) => Math.abs(index - center) <= halfWidth) ? 1 : 0);
        } else {
          const threshold = texture.amount <= 0 ? 2 : clamp(.97 - texture.amount * .0064, .27, .97);
          table = Array.from({ length: bins }, (_, index) => index / (bins - 1) >= threshold ? 1 : 0);
        }
        const threshold = E('feComponentTransfer', { in: alphaNoise, result: rawMask, 'data-text-texture-amount': texture.amount });
        threshold.append(E('feFuncA', { type: 'discrete', tableValues: table.join(' ') })); filter.append(threshold);
        if (texture.mode === 'cracks') {
          filter.append(E('feMorphology', { in: rawMask, operator: 'dilate', radius: round(texture.amount / 180, 3), result: wearMask }));
        } else {
          filter.append(E('feGaussianBlur', { in: rawMask, stdDeviation: round(Math.max(.08, texture.scale / 850), 3), result: wearMask }));
        }
        if (texture.mode === 'distressed') {
          filter.append(E('feComposite', { in: current, in2: wearMask, operator: 'out', result: `${filterId}-result`, 'data-text-texture-cutout': 'true' }));
        } else {
          const dark = `${filterId}-dark`; const marked = `${filterId}-marked`; const clippedMark = `${filterId}-clipped-mark`;
          filter.append(
            E('feFlood', { 'flood-color': '#050505', 'flood-opacity': round(.32 + texture.amount * .0055, 3), result: dark }),
            E('feComposite', { in: dark, in2: wearMask, operator: 'in', result: marked }),
            E('feComposite', { in: marked, in2: current, operator: 'in', result: clippedMark }),
            E('feBlend', { in: current, in2: clippedMark, mode: 'multiply', result: `${filterId}-result` })
          );
        }
      }
      definitions.append(filter); shape.setAttribute('filter', `url(#${filterId})`);
    }
    shape.dataset.textTextureMode = texture.mode;
    shape.dataset.textTextureTransparent = String(texture.transparent);
    target.append(definitions);
    if (pixelOverlay) target.append(pixelOverlay);
  }

  function normalizedStrokeProfile(object, points = vectorEditablePoints(object)) {
    if (!Array.isArray(object?.strokeProfile) || object.strokeProfile.length !== points.length) return null;
    return object.strokeProfile.map((value) => clamp(num(value, 1), .05, 4));
  }

  function variableStrokeShape(object, points, common, baseWidth = common['stroke-width']) {
    const profile = normalizedStrokeProfile(object, points);
    if (!profile || !profile.some((value) => Math.abs(value - 1) > .001) || points.length < 2 || objectStrokeType(object) === 'none') return null;
    const root = E('g', { 'data-variable-stroke': 'true' });
    const segmentCount = vectorSegmentCount(object, points);
    for (let index = 0; index < segmentCount; index += 1) {
      const nextIndex = vectorNextPointIndex(object, index, points); if (nextIndex < 0) continue;
      const a = points[index]; const b = points[nextIndex]; const controls = bezierControlsFor(object, index, points);
      const curved = isBezierSegment(object?.bezierSegments?.[index]) && controls;
      const steps = curved ? 10 : 6;
      const pointAt = (t) => curved ? cubicPoint(a, controls.c1, controls.c2, b, t) : lerpPoint(a, b, t);
      for (let step = 0; step < steps; step += 1) {
        const fromT = step / steps; const toT = (step + 1) / steps;
        const from = pointAt(fromT); const to = pointAt(toT);
        const fromPressure = profile[index] + (profile[nextIndex] - profile[index]) * fromT;
        const toPressure = profile[index] + (profile[nextIndex] - profile[index]) * toT;
        root.append(E('path', {
          ...common, d: `M ${round(from[0], 3)} ${round(from[1], 3)} L ${round(to[0], 3)} ${round(to[1], 3)}`,
          fill: 'none', 'stroke-width': round(Math.max(.05, num(baseWidth, 1) * (fromPressure + toPressure) / 2), 3),
          'stroke-linecap': common['stroke-linecap'] === 'round' ? 'round' : 'butt', 'data-variable-stroke-segment': `${index}:${step}`
        }));
      }
    }
    return root;
  }

  function closedVariableStrokeShape(object, points, common, fillShape) {
    const stroke = variableStrokeShape(object, points, common);
    if (!stroke) return null;
    const root = E('g', { 'data-variable-stroke-closed': 'true' });
    fillShape.setAttribute('stroke', 'none'); root.append(fillShape, stroke); return root;
  }

  function arrowHeadShape(tip, tangent, size, style, common) {
    if (!tip || !tangent || style === 'none') return null;
    const dx = num(tip[0]) - num(tangent[0]); const dy = num(tip[1]) - num(tangent[1]);
    const length = Math.max(.001, Math.hypot(dx, dy)); const ux = dx / length; const uy = dy / length;
    const nx = -uy; const ny = ux; const baseX = num(tip[0]) - ux * size; const baseY = num(tip[1]) - uy * size;
    const side = size * .48; const left = [baseX + nx * side, baseY + ny * side]; const right = [baseX - nx * side, baseY - ny * side];
    if (style === 'bar') return E('path', { ...common, d: `M ${round(left[0])} ${round(left[1])} L ${round(right[0])} ${round(right[1])}`, fill: 'none', 'data-stroke-marker-shape': 'true' });
    if (style === 'circle') return E('circle', { ...common, cx: round(num(tip[0]) - ux * size * .38), cy: round(num(tip[1]) - uy * size * .38), r: round(size * .36), fill: common.stroke, 'data-stroke-marker-shape': 'true' });
    if (style === 'diamond') {
      const middleX = num(tip[0]) - ux * size * .5; const middleY = num(tip[1]) - uy * size * .5;
      const back = [num(tip[0]) - ux * size, num(tip[1]) - uy * size];
      return E('path', { ...common, d: `M ${round(tip[0])} ${round(tip[1])} L ${round(middleX + nx * side)} ${round(middleY + ny * side)} L ${round(back[0])} ${round(back[1])} L ${round(middleX - nx * side)} ${round(middleY - ny * side)} Z`, fill: common.stroke, 'data-stroke-marker-shape': 'true' });
    }
    const path = `M ${round(left[0])} ${round(left[1])} L ${round(tip[0])} ${round(tip[1])} L ${round(right[0])} ${round(right[1])}`;
    return E('path', { ...common, d: style === 'triangle' ? `${path} Z` : path, fill: style === 'triangle' ? common.stroke : 'none', 'data-stroke-marker-shape': 'true' });
  }

  const fillPaintId = (object) => `object-fill-${String(object?.id || 'paint').replace(/[^a-z0-9_-]/gi, '-')}`;
  const strokePaintId = (object) => `object-stroke-${String(object?.id || 'paint').replace(/[^a-z0-9_-]/gi, '-')}`;

  function normalizedObjectFillPaint(object) {
    if (!supportsAdvancedFill(object)) return null;
    return FillPaint.normalizePaint(object.fillPaint, {
      fallbackColor: normalizeColor(object.fill) || '#4f6df5'
    });
  }

  function normalizedObjectStrokePaint(object) {
    if (!supportsStrokeType(object)) return null;
    return FillPaint.normalizePaint(object.strokePaint, {
      fallbackColor: normalizeColor(object.type === 'text' ? object.textStroke : object.stroke) || '#172033'
    });
  }

  function objectFillValue(object) {
    const paint = normalizedObjectFillPaint(object);
    if (!paint || (FillPaint.isImage(paint.type) && !paint.href)) return object.fill || 'none';
    return `url(#${fillPaintId(object)})`;
  }

  function wrappedInlineTextLines(object, maximumWidth) {
    const sourceLines = String(object.text || '').split('\n');
    if (!(maximumWidth > 0)) return sourceLines;
    if (!textMeasurementContext) textMeasurementContext = document.createElement('canvas').getContext('2d');
    const context = textMeasurementContext; const settings = textSettings(object); const fontSize = Math.max(1, num(object.fontSize, 32));
    if (context) {
      context.font = `${object.italic ? 'italic ' : ''}${object.bold ? '700 ' : '400 '}${fontSize}px ${object.fontFamily || DEFAULT_FONT_FAMILY}`;
      if ('fontKerning' in context) context.fontKerning = settings.autoKern ? 'normal' : 'none';
      if ('letterSpacing' in context) context.letterSpacing = `${settings.letterSpacing}px`;
    }
    const widthOf = (value) => context ? context.measureText(value).width : value.length * fontSize * .62 + Math.max(0, value.length - 1) * settings.letterSpacing;
    return sourceLines.flatMap((source) => {
      const words = source.trim().split(/\s+/).filter(Boolean); if (!words.length) return [' '];
      const lines = []; let line = '';
      words.forEach((word) => {
        const candidate = line ? `${line} ${word}` : word;
        if (line && widthOf(candidate) > maximumWidth) { lines.push(line); line = word; }
        else line = candidate;
      });
      if (line) lines.push(line); return lines;
    });
  }

  function wrappedInlineTextRunLines(object, maximumWidth, runs) {
    if (!(maximumWidth > 0) || !Array.isArray(runs) || !runs.length) return [];
    if (!textMeasurementContext) textMeasurementContext = document.createElement('canvas').getContext('2d');
    const context = textMeasurementContext; const settings = textSettings(object);
    const baseFontSize = Math.max(1, num(object.fontSize, 32));
    const normalizedRun = (run = {}) => {
      const fontSize = num(run.fontSize) > 0 ? num(run.fontSize) : baseFontSize;
      return {
        text: String(run.text || ''), opacity: num(run.opacity, 100), fill: run.fill || '',
        fontFamily: run.fontFamily || '', fontSize, explicitFontSize: num(run.fontSize) > 0,
        lineHeight: run.lineHeightExplicit ? Math.max(0, num(run.lineHeight)) : fontSize * settings.lineHeight / 100,
        bold: Boolean(run.bold), italic: Boolean(run.italic), direction: run.direction === 'rtl' ? 'rtl' : ''
      };
    };
    const base = normalizedRun({
      text: '', opacity: 100, fontSize: baseFontSize, lineHeight: baseFontSize * settings.lineHeight / 100,
      lineHeightExplicit: true, bold: object.bold, italic: object.italic
    });
    const widthOf = (fragment) => {
      if (!fragment.text) return 0;
      if (context) {
        context.font = `${fragment.italic ? 'italic ' : ''}${fragment.bold ? '700 ' : '400 '}${fragment.fontSize}px ${fragment.fontFamily || object.fontFamily || DEFAULT_FONT_FAMILY}`;
        if ('fontKerning' in context) context.fontKerning = settings.autoKern ? 'normal' : 'none';
        if ('letterSpacing' in context) context.letterSpacing = `${settings.letterSpacing}px`;
        return context.measureText(fragment.text).width;
      }
      return fragment.text.length * fragment.fontSize * .62 + Math.max(0, fragment.text.length - 1) * settings.letterSpacing;
    };
    const widthStyleKey = (fragment) => [fragment.fontFamily, fragment.fontSize, fragment.bold, fragment.italic, fragment.direction].join('\u0000');
    const widthOfFragments = (fragments) => {
      const merged = [];
      fragments.forEach((fragment) => {
        const previous = merged.at(-1);
        if (previous && widthStyleKey(previous) === widthStyleKey(fragment)) previous.text += fragment.text;
        else merged.push({ ...fragment });
      });
      return merged.reduce((sum, fragment) => sum + widthOf(fragment), 0);
    };
    const words = []; let word = [];
    runs.map(normalizedRun).forEach((run) => {
      for (const token of run.text.split(/(\s+)/u).filter(Boolean)) {
        if (/^\s+$/u.test(token)) { if (word.length) { words.push(word); word = []; } continue; }
        word.push({ ...run, text: token });
      }
    });
    if (word.length) words.push(word);
    const space = { ...base, text: ' ' }; const spaceWidth = widthOf(space); const lines = [];
    const runMeasurementTolerance = Math.max(.25, baseFontSize * .5);
    let line = { fragments: [], width: 0 };
    const finishLine = () => { if (line.fragments.length) lines.push(line); line = { fragments: [], width: 0 }; };
    words.forEach((parts) => {
      const candidate = line.fragments.length ? [...line.fragments, space, ...parts] : parts;
      if (line.fragments.length && widthOfFragments(candidate) > maximumWidth + runMeasurementTolerance) finishLine();
      if (line.fragments.length) { line.fragments.push({ ...space }); line.width += spaceWidth; }
      line.fragments.push(...parts); line.width = widthOfFragments(line.fragments);
    });
    finishLine();
    const extents = (current) => {
      let top = base.fontSize * .8 + (base.lineHeight - base.fontSize) / 2;
      let bottom = base.fontSize * .2 + (base.lineHeight - base.fontSize) / 2;
      current.fragments.forEach((fragment) => {
        const leading = (fragment.lineHeight - fragment.fontSize) / 2;
        top = Math.max(top, fragment.fontSize * .8 + leading);
        bottom = Math.max(bottom, fragment.fontSize * .2 + leading);
      });
      return { top, bottom };
    };
    lines.forEach((current, index) => {
      Object.assign(current, extents(current));
      current.dy = index ? lines[index - 1].bottom + current.top : 0;
      if (index < lines.length - 1) current.fragments.push({ ...space });
    });
    return lines;
  }

  function justifiedInlineWords(object, line, maximumWidth) {
    const words = String(line || '').trim().split(/\s+/).filter(Boolean); if (words.length < 2) return [];
    if (!textMeasurementContext) textMeasurementContext = document.createElement('canvas').getContext('2d');
    const context = textMeasurementContext; const settings = textSettings(object); const fontSize = Math.max(1, num(object.fontSize, 32));
    if (context) {
      context.font = `${object.italic ? 'italic ' : ''}${object.bold ? '700 ' : '400 '}${fontSize}px ${object.fontFamily || DEFAULT_FONT_FAMILY}`;
      if ('fontKerning' in context) context.fontKerning = settings.autoKern ? 'normal' : 'none';
      if ('letterSpacing' in context) context.letterSpacing = `${settings.letterSpacing}px`;
    }
    const widthOf = (word) => context ? context.measureText(word).width : word.length * fontSize * .62 + Math.max(0, word.length - 1) * settings.letterSpacing;
    const widths = words.map(widthOf); const gap = Math.max(0, (maximumWidth - widths.reduce((sum, width) => sum + width, 0)) / (words.length - 1));
    let x = 0; return words.map((word, index) => {
      const positioned = { text: word, x }; x += widths[index] + gap; return positioned;
    });
  }

  function appendGradientStops(gradient, stops) {
    FillPaint.normalizeStops(stops).forEach((stop) => gradient.append(E('stop', {
      offset: `${stop.offset}%`, 'stop-color': stop.color, 'stop-opacity': round(stop.opacity / 100, 4),
      'data-fill-stop-id': stop.id
    })));
  }

  function conicGradientPattern(object, paint, id) {
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    const centerX = width * paint.cx / 100; const centerY = height * paint.cy / 100;
    const radius = Math.max(
      Math.hypot(centerX, centerY), Math.hypot(width - centerX, centerY),
      Math.hypot(centerX, height - centerY), Math.hypot(width - centerX, height - centerY)
    ) * 1.04;
    const pattern = E('pattern', {
      id, x: 0, y: 0, width, height, patternUnits: 'userSpaceOnUse',
      'data-fill-paint': 'conic', 'data-fill-angle': paint.angle
    });
    const slices = 144; const start = (paint.angle - 90) * Math.PI / 180;
    for (let index = 0; index < slices; index += 1) {
      const angleOne = start + index * Math.PI * 2 / slices;
      const angleTwo = start + (index + 1.035) * Math.PI * 2 / slices;
      const sampled = FillPaint.colorAt(paint.stops, (index + .5) * 100 / slices);
      pattern.append(E('path', {
        d: `M ${round(centerX)} ${round(centerY)} L ${round(centerX + Math.cos(angleOne) * radius)} ${round(centerY + Math.sin(angleOne) * radius)} A ${round(radius)} ${round(radius)} 0 0 1 ${round(centerX + Math.cos(angleTwo) * radius)} ${round(centerY + Math.sin(angleTwo) * radius)} Z`,
        fill: sampled.color, 'fill-opacity': round(sampled.opacity / 100, 4), stroke: 'none'
      }));
    }
    return pattern;
  }

  function imageFillPattern(object, paint, id) {
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    if (paint.type === 'pattern') {
      if (paint.svgPattern?.matrix) {
        const source = paint.svgPattern; const factor = paint.scale / 100;
        const tileWidth = Math.max(.00000001, source.width * factor); const tileHeight = Math.max(.00000001, source.height * factor);
        const viewBox = Array.isArray(source.viewBox) && source.viewBox.length >= 4 ? source.viewBox : [source.x, source.y, source.width, source.height];
        const matrix = `matrix(${source.matrix.slice(0, 6).map((value) => round(value, 8)).join(' ')})`;
        const userTransform = paint.rotation ? ` rotate(${round(paint.rotation)} ${round(width / 2)} ${round(height / 2)})` : '';
        const pattern = E('pattern', {
          id, x: source.x + paint.offsetX, y: source.y + paint.offsetY, width: tileWidth, height: tileHeight,
          patternUnits: 'userSpaceOnUse', patternContentUnits: 'userSpaceOnUse', patternTransform: `${matrix}${userTransform}`,
          viewBox: viewBox.map((value) => round(value, 8)).join(' '), preserveAspectRatio: source.preserveAspectRatio || 'xMidYMid meet',
          'data-fill-paint': 'pattern', 'data-svg-pattern': 'true', 'data-svg-pattern-units': source.patternUnits,
          'data-svg-pattern-content-units': source.patternContentUnits, 'data-fill-scale': paint.scale, 'data-fill-rotation': paint.rotation
        });
        pattern.append(E('image', {
          x: viewBox[0], y: viewBox[1], width: viewBox[2], height: viewBox[3], href: paint.href,
          preserveAspectRatio: 'none', opacity: clamp(num(source.opacity, 100) / 100, 0, 1)
        }));
        return pattern;
      }
      const tileWidth = Math.max(.25, paint.imageWidth * paint.scale / 100);
      const tileHeight = Math.max(.25, paint.imageHeight * paint.scale / 100);
      const pattern = E('pattern', {
        id, x: paint.offsetX, y: paint.offsetY, width: tileWidth, height: tileHeight,
        patternUnits: 'userSpaceOnUse', patternTransform: paint.rotation ? `rotate(${round(paint.rotation)} ${round(width / 2)} ${round(height / 2)})` : undefined,
        'data-fill-paint': 'pattern', 'data-fill-scale': paint.scale, 'data-fill-rotation': paint.rotation
      });
      pattern.append(E('image', { x: 0, y: 0, width: tileWidth, height: tileHeight, href: paint.href, preserveAspectRatio: 'none' }));
      return pattern;
    }
    const coverScale = Math.max(width / Math.max(1, paint.imageWidth), height / Math.max(1, paint.imageHeight));
    const imageWidth = paint.imageWidth * coverScale * paint.scale / 100;
    const imageHeight = paint.imageHeight * coverScale * paint.scale / 100;
    const imageX = (width - imageWidth) / 2 + paint.offsetX; const imageY = (height - imageHeight) / 2 + paint.offsetY;
    const pattern = E('pattern', {
      id, x: 0, y: 0, width: 1, height: 1, patternUnits: 'objectBoundingBox',
      viewBox: `0 0 ${round(width)} ${round(height)}`, preserveAspectRatio: 'none',
      'data-fill-paint': 'image', 'data-fill-scale': paint.scale, 'data-fill-rotation': paint.rotation
    });
    pattern.append(E('image', {
      x: round(imageX), y: round(imageY), width: round(imageWidth), height: round(imageHeight), href: paint.href,
      preserveAspectRatio: 'none', transform: paint.rotation ? `rotate(${round(paint.rotation)} ${round(width / 2 + paint.offsetX)} ${round(height / 2 + paint.offsetY)})` : undefined
    }));
    return pattern;
  }

  function gradientMatrixAttribute(paint) {
    if (!Array.isArray(paint.gradientMatrix) || paint.gradientMatrix.length < 6) return undefined;
    return `matrix(${paint.gradientMatrix.slice(0, 6).map((value) => round(value, 8)).join(' ')})`;
  }

  function objectPaintDefinitions(object, paint, id, kind = 'fill') {
    if (!paint || (FillPaint.isImage(paint.type) && !paint.href)) return null;
    const definitions = E('defs', { [`data-object-${kind}-definitions`]: object.id });
    const exact = paint.gradientCoordinates && paint.gradientMatrix;
    if (paint.type === 'linear') {
      const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
      const coordinates = exact ? paint.gradientCoordinates : {
        x1: width * paint.x1 / 100, y1: height * paint.y1 / 100,
        x2: width * paint.x2 / 100, y2: height * paint.y2 / 100
      };
      const gradient = E('linearGradient', {
        id, gradientUnits: 'userSpaceOnUse', ...coordinates,
        spreadMethod: paint.spreadMethod, 'color-interpolation': paint.colorInterpolation || undefined, gradientTransform: exact ? gradientMatrixAttribute(paint) : undefined,
        [`data-${kind}-paint`]: 'linear'
      });
      appendGradientStops(gradient, paint.stops); definitions.append(gradient);
    } else if (paint.type === 'radial') {
      const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
      const coordinates = exact ? paint.gradientCoordinates : {
        cx: width * paint.cx / 100, cy: height * paint.cy / 100,
        fx: width * paint.cx / 100, fy: height * paint.cy / 100,
        r: Math.min(width, height) * paint.radius / 100, fr: 0
      };
      const gradient = E('radialGradient', {
        id, gradientUnits: 'userSpaceOnUse', ...coordinates,
        spreadMethod: paint.spreadMethod, 'color-interpolation': paint.colorInterpolation || undefined, gradientTransform: exact ? gradientMatrixAttribute(paint) : undefined,
        [`data-${kind}-paint`]: 'radial'
      });
      appendGradientStops(gradient, paint.stops); definitions.append(gradient);
    } else if (paint.type === 'conic') definitions.append(conicGradientPattern(object, paint, id));
    else definitions.append(imageFillPattern(object, paint, id));
    return definitions;
  }

  function objectFillDefinitions(object) {
    return objectPaintDefinitions(object, normalizedObjectFillPaint(object), fillPaintId(object), 'fill');
  }

  function objectStrokeDefinitions(object) {
    return objectPaintDefinitions(object, normalizedObjectStrokePaint(object), strokePaintId(object), 'stroke');
  }

  const ditherThresholdTileHrefs = new Map();

  function ditherPattern(method) {
    return DitherPatterns.pattern(method);
  }

  function ditherThresholdTile(method) {
    const pattern = ditherPattern(method);
    if (ditherThresholdTileHrefs.has(pattern.method)) return ditherThresholdTileHrefs.get(pattern.method);
    const canvas = document.createElement('canvas'); canvas.width = pattern.width; canvas.height = pattern.height;
    const context = canvas.getContext('2d'); const pixels = context.createImageData(pattern.width, pattern.height);
    pattern.thresholds.forEach((threshold, index) => {
      const value = Math.round((threshold + .5) / pattern.thresholdLevels * 255); const offset = index * 4;
      pixels.data[offset] = value; pixels.data[offset + 1] = value; pixels.data[offset + 2] = value; pixels.data[offset + 3] = 255;
    });
    context.putImageData(pixels, 0, 0);
    const href = canvas.toDataURL('image/png'); ditherThresholdTileHrefs.set(pattern.method, href); return href;
  }

  function shapeFor(object, interactive = true, rawStrokeStyle = primaryStrokeStyle(object)) {
    const { style: strokeStyle, strokeType, attributes: strokeAttributes } = strokeStyleAttributes(object, rawStrokeStyle);
    const common = {
      ...strokeAttributes,
      fill: objectFillValue(object),
      'fill-opacity': object.svgFillOpacity == null ? undefined : clamp(num(object.svgFillOpacity, 1), 0, 1),
      pathLength: Number.isFinite(Number(object.svgPathLength)) && Number(object.svgPathLength) >= 0 ? Number(object.svgPathLength) : undefined
    };
    let shape;
    const autoGeometry = AutoShapes.geometry(object);
    if (autoGeometry) {
      shape = E(autoGeometry.tag, { ...common, ...(autoGeometry.attrs || {}) });
    } else if (['pencil', 'brush', 'eraser'].includes(object.type)) {
      const points = object.points || [[0, 0]];
      const d = hasBezierSegments(object) ? vectorPathData(object, points) : (object.type === 'brush' ? pencilPathData(points, object.smoothing) : points.map((p, index) => `${index ? 'L' : 'M'} ${round(p[0])} ${round(p[1])}`).join(' '));
      const width = object.type === 'brush' ? Math.max(2, num(object.strokeWidth, 3) * 2.4) : common['stroke-width'];
      shape = variableStrokeShape(object, points, common, width) || E('path', { ...common, d, fill: 'none', 'stroke-width': width });
    } else if (object.type === 'pen') {
      const points = object.points?.length ? object.points : [[0, 0]];
      const path = E('path', {
        ...common, d: vectorPathData(object, points), fill: object.closed || object.svgOpenFill ? common.fill : 'none',
        'fill-rule': object.fillRule === 'nonzero' ? 'nonzero' : 'evenodd'
      });
      shape = object.closed ? (closedVariableStrokeShape(object, points, common, path) || path) : (variableStrokeShape(object, points, common) || path);
    } else if (object.type === 'line') {
      const points = object.points?.length ? object.points : [[0, 0], [object.width, object.height]];
      const [a, b] = points;
      shape = variableStrokeShape(object, points, common) || (hasBezierSegments(object) || points.length > 2
        ? E('path', { ...common, d: vectorPathData(object, points), fill: 'none' })
        : E('line', { ...common, x1: a[0], y1: a[1], x2: b[0], y2: b[1], fill: 'none' }));
    } else if (object.type === 'polyline') {
      const points = object.points?.length ? object.points : [[0, 0]];
      const path = hasBezierSegments(object)
        ? E('path', { ...common, d: vectorPathData(object, points), fill: object.closed || object.svgOpenFill ? common.fill : 'none' })
        : E(object.closed ? 'polygon' : 'polyline', { ...common, points: points.map(([x, y]) => `${round(x, 6)},${round(y, 6)}`).join(' '), fill: object.closed || object.svgOpenFill ? common.fill : 'none' });
      shape = object.closed ? (closedVariableStrokeShape(object, points, common, path) || path) : (variableStrokeShape(object, points, common) || path);
    } else if (object.type === 'arrow') {
      const points = object.points?.length ? object.points : [[0, 0], [object.width, object.height]];
      const a = points[0]; const b = points.at(-1); const finalSegment = Math.max(0, points.length - 2); const params = AutoShapes.ensure(object);
      const finalControls = bezierControlsFor(object, finalSegment, points); const firstControls = bezierControlsFor(object, 0, points);
      const endTangent = isBezierSegment(object?.bezierSegments?.[finalSegment]) && finalControls ? finalControls.c2 : points.at(-2);
      const startTangent = isBezierSegment(object?.bezierSegments?.[0]) && firstControls ? firstControls.c1 : points[1];
      const shaft = hasBezierSegments(object) || points.length > 2 ? vectorPathData(object, points) : `M ${a[0]} ${a[1]} L ${b[0]} ${b[1]}`;
      shape = E('g');
      shape.append(variableStrokeShape(object, points, common) || E('path', { ...common, d: shaft, fill: 'none' }));
      const startHead = strokeStyle.startMarker === 'none' ? arrowHeadShape(a, startTangent, params.headSize, params.startHead, common) : null;
      const endHead = strokeStyle.endMarker === 'none' ? arrowHeadShape(b, endTangent, params.headSize, params.endHead, common) : null;
      if (startHead) shape.append(startHead); if (endHead) shape.append(endHead);
    } else if (object.type === 'rectangle') {
      const width = Math.max(1, object.width); const height = Math.max(1, object.height);
      const rx = clamp(num(object.svgRx), 0, width / 2); const ry = clamp(num(object.svgRy), 0, height / 2);
      shape = E('rect', { ...common, x: 0, y: 0, width, height, rx: rx || undefined, ry: ry || undefined });
    } else if (object.type === 'ellipse') {
      shape = object.svgPrimitiveType === 'circle' && Math.abs(num(object.width) - num(object.height)) < 1e-8
        ? E('circle', { ...common, cx: object.width / 2, cy: object.height / 2, r: Math.max(.5, object.width / 2) })
        : E('ellipse', { ...common, cx: object.width / 2, cy: object.height / 2, rx: Math.max(.5, object.width / 2), ry: Math.max(.5, object.height / 2) });
    } else if (object.type === 'polygon') {
      const points = object.points?.length ? object.points : regularPolygonPoints(object.sides, object.width, object.height);
      shape = hasBezierSegments(object)
        ? E('path', { ...common, d: vectorPathData(object, points) })
        : E('polygon', { ...common, points: points.map(([x, y]) => `${round(x, 6)},${round(y, 6)}`).join(' ') });
    } else if (object.type === 'measure') {
      const [a, b] = object.points || [[0, 0], [object.width, object.height]];
      const params = AutoShapes.ensure(object);
      const dx = num(b[0]) - num(a[0]); const dy = num(b[1]) - num(a[1]); const length = Math.max(.01, Math.hypot(dx, dy));
      const unitX = dx / length; const unitY = dy / length; const normalX = unitY; const normalY = -unitX;
      const arrowSize = Math.max(7, num(object.strokeWidth, 3) * 2.8); const fontSize = clamp(Math.round(Math.max(11, num(object.strokeWidth, 3) * 3.2)), 11, 18);
      const label = params.unit === 'in' ? `${round(length / Math.max(1, num(state.doc.dpi, 72)), 2)} in` : `${Math.round(length)} px`;
      const gap = Math.min(length * .34, Math.max(fontSize * 1.8, label.length * fontSize * .31));
      const middle = [(num(a[0]) + num(b[0])) / 2, (num(a[1]) + num(b[1])) / 2];
      const gapStart = [middle[0] - unitX * gap, middle[1] - unitY * gap]; const gapEnd = [middle[0] + unitX * gap, middle[1] + unitY * gap];
      const head = (point, direction) => {
        const baseX = point[0] + direction * unitX * arrowSize; const baseY = point[1] + direction * unitY * arrowSize; const side = arrowSize * .52;
        return `M ${round(baseX + normalX * side)} ${round(baseY + normalY * side)} L ${round(point[0])} ${round(point[1])} L ${round(baseX - normalX * side)} ${round(baseY - normalY * side)}`;
      };
      const line = length > gap * 2 + arrowSize * 2
        ? `M ${round(a[0])} ${round(a[1])} L ${round(gapStart[0])} ${round(gapStart[1])} M ${round(gapEnd[0])} ${round(gapEnd[1])} L ${round(b[0])} ${round(b[1])}`
        : `M ${round(a[0])} ${round(a[1])} L ${round(b[0])} ${round(b[1])}`;
      const root = E('g');
      root.append(E('path', { ...common, d: `${line} ${head(a, 1)} ${head(b, -1)}`, fill: 'none', 'data-measure-line': 'true' }));
      const labelOffset = Math.max(11, fontSize * .72);
      const text = E('text', {
        x: round(middle[0] + normalX * labelOffset), y: round(middle[1] + normalY * labelOffset), fill: 'none', stroke: common.stroke === 'none' ? 'none' : (object.stroke || '#172033'),
        'stroke-width': 1, 'stroke-linecap': common['stroke-linecap'], 'stroke-linejoin': common['stroke-linejoin'], 'paint-order': 'stroke', 'data-stroke-type': strokeType,
        'font-family': 'Arial, Helvetica, sans-serif', 'font-size': fontSize, 'font-weight': '400', 'letter-spacing': '.1px',
        'text-anchor': 'middle', 'dominant-baseline': 'middle', 'data-measure-label': 'true', 'pointer-events': 'none'
      });
      text.textContent = label; root.append(text); shape = root;
    } else if (object.type === 'text') {
      const binding = textPathBinding(object);
      if (binding) shape = boundTextShape(object, binding, strokeStyle);
      else {
        const settings = textSettings(object); const scaleX = settings.horizontalScale / 100; const scaleY = settings.verticalScale / 100;
        const naturalWidth = Math.max(1, num(object.width, 1) / scaleX); const verticalWriting = Boolean(object.svgWritingMode);
        const anchor = verticalWriting ? 0 : settings.align === 'center' ? naturalWidth / 2 : settings.align === 'right' ? naturalWidth : 0;
        const textAnchor = { left: 'start', center: 'middle', right: 'end', justify: 'start' }[settings.align];
        const textRendering = { crisp: 'geometricPrecision', smooth: 'optimizeLegibility', auto: 'auto', none: 'optimizeSpeed' }[settings.antiAlias];
        const decorations = [settings.underline && 'underline', settings.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
        const glowOnlyText = hasActiveGlow(object) && !hasActiveTextTexture(object) && !normalizedObjectFillPaint(object) && (object.fill === 'none' || object.fill === 'transparent');
        shape = E('text', {
          x: anchor, y: num(object.fontSize, 32) + settings.baselineShift, fill: glowOnlyText ? '#000000' : objectFillValue(object),
          'fill-opacity': object.svgFillOpacity == null ? undefined : clamp(num(object.svgFillOpacity, 1), 0, 1),
          ...strokeAttributes, stroke: strokeAttributes.stroke, 'paint-order': 'stroke fill', 'font-family': object.fontFamily || DEFAULT_FONT_FAMILY,
          'font-size': Number.isFinite(Number(object.svgImplicitFontSize)) && Math.abs(num(object.fontSize, 32) - num(object.svgImplicitFontSize)) < .0001 ? undefined : num(object.fontSize, 32), 'font-weight': object.bold ? '700' : '400',
          'font-style': object.italic ? 'italic' : 'normal', 'text-anchor': textAnchor,
          'dominant-baseline': object.svgDominantBaseline || undefined,
          'letter-spacing': settings.letterSpacing, 'font-kerning': settings.autoKern ? 'normal' : 'none', 'text-decoration': decorations,
          textLength: Number.isFinite(Number(object.svgTextLength)) ? Number(object.svgTextLength) : undefined,
          lengthAdjust: Number.isFinite(Number(object.svgTextLength)) ? object.svgLengthAdjust || 'spacing' : undefined,
          'text-rendering': textRendering, transform: `scale(${scaleX} ${scaleY})`, 'xml:space': object.svgInlineSize && object.svgXmlSpace !== 'preserve' ? 'default' : 'preserve',
          lang: object.svgLanguage || undefined, 'xml:lang': object.svgLanguage || undefined
        });
        if (object.svgWritingMode) shape.style.writingMode = object.svgWritingMode;
        if (object.svgDirection) shape.style.direction = object.svgDirection;
        if (object.svgTextOrientation) shape.style.textOrientation = object.svgTextOrientation;
        if (object.svgTextAlign) shape.style.textAlign = object.svgTextAlign;
        if (object.svgFontSizeAdjust) shape.style.fontSizeAdjust = object.svgFontSizeAdjust;
        const preservedRuns = Array.isArray(object.svgTextRuns) && object.svgTextRunsSource === object.text
          && object.svgTextRuns.map((run) => String(run?.text || '')).join('') === String(object.text || '') ? object.svgTextRuns : null;
        if (preservedRuns && object.svgInlineSize) {
          const runLines = wrappedInlineTextRunLines(object, num(object.svgInlineSize) / Math.max(.0001, scaleX), preservedRuns);
          runLines.forEach((line) => {
            const tspan = E('tspan', { x: anchor, dy: line.dy || 0 });
            line.fragments.forEach((run) => {
              const fragment = E('tspan', {
                opacity: num(run.opacity, 100) === 100 ? undefined : clamp(num(run.opacity, 100) / 100, 0, 1),
                fill: run.fill || undefined, 'font-family': run.fontFamily || undefined,
                'font-size': run.explicitFontSize ? num(run.fontSize) : undefined,
                'font-weight': run.bold ? '700' : undefined, 'font-style': run.italic ? 'italic' : undefined,
                direction: run.direction === 'rtl' ? 'rtl' : undefined
              });
              fragment.textContent = run.text; tspan.append(fragment);
            });
            shape.append(tspan);
          });
        }
        else if (preservedRuns) preservedRuns.forEach((run) => {
          const tspan = E('tspan', {
            opacity: num(run.opacity, 100) === 100 ? undefined : clamp(num(run.opacity, 100) / 100, 0, 1),
            fill: run.fill || undefined, 'font-family': run.fontFamily || undefined,
            'font-size': num(run.fontSize) > 0 ? num(run.fontSize) : undefined,
            'font-weight': run.bold ? '700' : undefined, 'font-style': run.italic ? 'italic' : undefined,
            direction: run.direction === 'rtl' ? 'rtl' : undefined
          });
          tspan.textContent = String(run.text || ''); shape.append(tspan);
        });
        else {
          const lines = wrappedInlineTextLines(object, object.svgInlineSize ? num(object.svgInlineSize) / Math.max(.0001, scaleX) : 0);
          lines.forEach((line, index) => {
          const justify = settings.align === 'justify' && index < lines.length - 1 && /\s/.test(line.trim());
          const justifiedWidth = object.svgInlineSize ? num(object.svgInlineSize) / Math.max(.0001, scaleX) : naturalWidth;
          if (justify && !verticalWriting) {
            justifiedInlineWords(object, line, justifiedWidth).forEach((word, wordIndex) => {
              const tspan = E('tspan', { x: word.x, dy: wordIndex ? undefined : index ? `${settings.lineHeight / 100}em` : 0 });
              tspan.textContent = word.text; shape.append(tspan);
            });
            return;
          }
          if (justify && verticalWriting) {
            justifiedInlineWords(object, line, justifiedWidth).forEach((word) => {
              const tspan = E('tspan', { x: -index * num(object.fontSize, 32) * settings.lineHeight / 100, y: num(object.fontSize, 32) + word.x });
              tspan.textContent = word.text; shape.append(tspan);
            });
            return;
          }
          const tspan = E('tspan', verticalWriting
            ? { x: -index * num(object.fontSize, 32) * settings.lineHeight / 100, y: num(object.fontSize, 32), textLength: justify ? justifiedWidth : undefined, lengthAdjust: justify ? 'spacing' : undefined }
            : { x: justify ? 0 : anchor, dy: index ? `${settings.lineHeight / 100}em` : 0, textLength: justify ? justifiedWidth : undefined, lengthAdjust: justify ? 'spacing' : undefined });
          // SVG text-anchor richtet auch das beim Umbruch verworfene Leerzeichen
          // am Zeilenende mit aus. Es bleibt unsichtbar, bewahrt aber bei
          // middle/end den originalen SVG-Zeilenanker.
          tspan.textContent = `${line || ' '}${index < lines.length - 1 ? ' ' : ''}`;
          shape.append(tspan);
          });
        }
      }
    } else if (outlineVectorTypes.has(object.type)) {
      const pathWidth = Math.max(1, num(object.pathWidth, object.width)); const pathHeight = Math.max(1, num(object.pathHeight, object.height));
      const scaleX = Math.max(1, num(object.width, 1)) / pathWidth; const scaleY = Math.max(1, num(object.height, 1)) / pathHeight;
      shape = E('path', {
        ...common, d: object.pathData || outlinePathData(outlineContoursFor(object)), fill: object.glowOnly && hasActiveGlow(object) && !normalizedObjectFillPaint(object) && (object.fill === 'none' || object.fill === 'transparent') ? '#000000' : common.fill,
        'fill-rule': object.fillRule === 'nonzero' ? 'nonzero' : 'evenodd',
        transform: scaleX === 1 && scaleY === 1 ? undefined : `scale(${scaleX} ${scaleY})`
      });
    } else if (object.type === 'resource-placeholder') {
      const width = Math.max(1, object.width); const height = Math.max(1, object.height);
      shape = E('g', { 'data-ui-overlay': 'missing-svg-resource', 'data-missing-resource': object.resourceKind || 'missing' });
      shape.append(
        E('rect', { x: 0, y: 0, width, height, fill: '#fff7ed', stroke: '#d97706', 'stroke-width': Math.max(1, Math.min(width, height) * .025), 'stroke-dasharray': '7 5' }),
        E('path', { d: `M 0 0 L ${round(width)} ${round(height)} M ${round(width)} 0 L 0 ${round(height)}`, fill: 'none', stroke: '#d97706', 'stroke-width': Math.max(1, Math.min(width, height) * .018) })
      );
      const label = E('text', { x: width / 2, y: height / 2, fill: '#7c2d12', 'font-family': 'Arial, sans-serif', 'font-size': Math.max(8, Math.min(14, height * .18)), 'text-anchor': 'middle', 'dominant-baseline': 'middle', 'pointer-events': 'none' });
      label.textContent = object.resourceKind === 'blocked' ? 'Externe SVG-Ressource blockiert' : 'SVG-Ressource fehlt'; shape.append(label);
    } else if (object.type === 'image') {
      const width = Math.max(1, object.width); const height = Math.max(1, object.height);
      const imported = object.svgImageSettings?.importedViewport;
      const importedMatrix = Array.isArray(imported?.matrix) ? imported.matrix.slice(0, 6).map(Number) : [];
      const hasImportedViewport = importedMatrix.length === 6 && importedMatrix.every(Number.isFinite)
        && num(imported?.width) > 0 && num(imported?.height) > 0 && num(imported?.baseWidth) > 0 && num(imported?.baseHeight) > 0;
      const primary = E('image', {
        x: hasImportedViewport ? num(imported.x) : 0, y: hasImportedViewport ? num(imported.y) : 0,
        width: hasImportedViewport ? num(imported.width) : width, height: hasImportedViewport ? num(imported.height) : height,
        href: object.href || '', preserveAspectRatio: object.svgImageSettings?.preserveAspectRatio || 'none'
      });
      primary.style.imageRendering = bitmapInterpolationFor(object) === 'nearest' ? 'pixelated' : 'auto';
      let importedShape = primary;
      if (hasImportedViewport) {
        const scaleX = width / num(imported.baseWidth, width); const scaleY = height / num(imported.baseHeight, height);
        const [a, b, c, d, e, f] = importedMatrix;
        importedShape = E('g', {
          transform: `matrix(${round(a * scaleX, 10)} ${round(b * scaleY, 10)} ${round(c * scaleX, 10)} ${round(d * scaleY, 10)} ${round(e * scaleX, 10)} ${round(f * scaleY, 10)})`,
          'data-svg-image-viewport': 'preserved'
        });
        importedShape.append(primary);
      }
      if (object.marqueeBleed) {
        // SVG filtert die Außenkante eines separat gerasterten Ausschnitts
        // gegen Transparenz. Ein nur im Editor sichtbarer, hinterlegter
        // Überlappungsring schließt diese Haarlinie, ohne den scharfen bzw.
        // weich gefilterten Bildkern umzurendern.
        const bleed = 1.1 / Math.max(state.zoom, .01);
        const backing = E('image', {
          x: -bleed, y: -bleed, width: width + bleed * 2, height: height + bleed * 2,
          href: object.href || '', preserveAspectRatio: 'none', 'data-bitmap-bleed': 'true', 'pointer-events': 'none'
        });
        backing.style.pointerEvents = 'none'; backing.style.imageRendering = primary.style.imageRendering;
        shape = E('g'); shape.append(backing, importedShape);
      } else shape = importedShape;
      shape = bitmapAppearanceShape(object, shape, width, height);
    } else {
      shape = E('rect', { ...common, x: 0, y: 0, width: Math.max(1, object.width), height: Math.max(1, object.height) });
    }
    if (interactive) shape.dataset.objectId = object.id;
    shape.style.pointerEvents = object.type === 'measure' || supportsObjectFill(object) ? 'all' : (openVectorShapeTypes.has(object.type) ? 'stroke' : 'all');
    return shape;
  }

  function bitmapAppearanceShape(object, source, width, height) {
    const fill = normalizeColor(object.fill); const stroke = normalizeColor(object.stroke);
    const strokeWidth = Math.max(0, num(object.strokeWidth, 0)); const hasStroke = Boolean(stroke && strokeWidth > 0);
    const cornerRadius = objectRoundness(object); const definitions = E('defs'); let visibleSource = source;
    if (cornerRadius > 0) {
      const clipId = uid('bitmap-corners');
      const clip = E('clipPath', { id: clipId, clipPathUnits: 'userSpaceOnUse', 'data-bitmap-corner-definition': 'true' });
      clip.append(E('rect', { x: 0, y: 0, width, height, rx: cornerRadius, ry: cornerRadius }));
      definitions.append(clip);
      visibleSource = E('g', {
        'clip-path': `url(#${clipId})`, 'data-bitmap-corner-clip': 'true', 'data-bitmap-corner-radius': cornerRadius
      });
      visibleSource.append(source);
    }
    if (!fill && !hasStroke) {
      if (!cornerRadius) return source;
      const rounded = E('g', { 'data-bitmap-corner-radius': cornerRadius });
      rounded.append(definitions, visibleSource); return rounded;
    }
    const root = E('g', {
      'data-bitmap-appearance': 'true',
      'data-bitmap-fill-color': fill || 'none',
      'data-bitmap-stroke-color': hasStroke ? stroke : 'none',
      'data-bitmap-stroke-width': hasStroke ? strokeWidth : 0,
      'data-bitmap-corner-radius': cornerRadius
    });
    const extent = Math.max(2, strokeWidth);
    const filterRegion = {
      filterUnits: 'userSpaceOnUse', primitiveUnits: 'userSpaceOnUse',
      x: -extent, y: -extent, width: width + extent * 2, height: height + extent * 2,
      'color-interpolation-filters': 'sRGB'
    };
    if (hasStroke) {
      const filterId = uid('bitmap-contour'); const expanded = `${filterId}-expanded`; const outside = `${filterId}-outside`; const color = `${filterId}-color`;
      const filter = E('filter', { id: filterId, ...filterRegion, 'data-bitmap-contour-filter': 'true' });
      filter.append(
        E('feMorphology', { in: 'SourceAlpha', operator: 'dilate', radius: round(strokeWidth / 2, 3), result: expanded }),
        E('feComposite', { in: expanded, in2: 'SourceAlpha', operator: 'out', result: outside }),
        E('feFlood', { 'flood-color': stroke, result: color }),
        E('feComposite', { in: color, in2: outside, operator: 'in', 'data-bitmap-contour': 'true' })
      );
      definitions.append(filter);
      const contourSource = visibleSource.cloneNode(true); contourSource.removeAttribute('data-object-id');
      const contour = cornerRadius
        ? E('g', { filter: `url(#${filterId})`, 'data-bitmap-contour-layer': 'true' })
        : contourSource;
      if (cornerRadius) contour.append(contourSource); else {
        contour.setAttribute('filter', `url(#${filterId})`);
        contour.setAttribute('data-bitmap-contour-layer', 'true');
      }
      contour.style.pointerEvents = 'none'; root.append(contour);
    }
    if (fill) {
      const filterId = uid('bitmap-fill'); const color = `${filterId}-color`;
      const filter = E('filter', { id: filterId, ...filterRegion, 'data-bitmap-fill-filter': 'true' });
      filter.append(
        E('feFlood', { 'flood-color': fill, result: color }),
        E('feComposite', { in: color, in2: 'SourceAlpha', operator: 'in', 'data-bitmap-fill': 'true' })
      );
      definitions.append(filter);
      if (cornerRadius) {
        const fillLayer = E('g', { filter: `url(#${filterId})`, 'data-bitmap-fill-layer': 'true' });
        fillLayer.append(visibleSource); visibleSource = fillLayer;
      } else {
        visibleSource.setAttribute('filter', `url(#${filterId})`); visibleSource.setAttribute('data-bitmap-fill-layer', 'true');
      }
    }
    root.prepend(definitions); root.append(visibleSource); return root;
  }

  function strokePaintNodes(root) {
    return [root, ...root.querySelectorAll('*')].filter((node) => {
      const stroke = String(node.getAttribute?.('stroke') || '').toLowerCase();
      return stroke && !['none', 'transparent'].includes(stroke);
    });
  }

  const markerShapeTags = new Set(['path', 'line', 'polyline', 'polygon']);
  const fillShapeTags = new Set(['path', 'rect', 'ellipse', 'circle', 'polygon', 'text']);

  function strokeMarkerDefinition(id, markerStyle, size, color, strokeWidth, imported = null) {
    const marker = E('marker', {
      id, markerUnits: 'userSpaceOnUse', markerWidth: size, markerHeight: size,
      viewBox: '0 0 10 10', refX: imported?.refX ?? 9, refY: imported?.refY ?? 5, orient: imported?.orient || 'auto-start-reverse', overflow: 'visible',
      'data-stroke-marker-definition': markerStyle
    });
    const markerStroke = clamp(strokeWidth * 10 / Math.max(1, size), .8, 2.8);
    if (markerStyle === 'open-arrow') marker.append(E('path', { d: 'M 1 1 L 9 5 L 1 9', fill: 'none', stroke: color, 'stroke-width': markerStroke, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }));
    else if (markerStyle === 'triangle') marker.append(E('path', { d: 'M 0 0 L 10 5 L 0 10 Z', fill: color, stroke: color, 'stroke-width': Math.min(1, markerStroke) }));
    else if (markerStyle === 'circle') marker.append(E('circle', { cx: 5, cy: 5, r: 4, fill: color, stroke: color, 'stroke-width': Math.min(1, markerStroke) }));
    else if (markerStyle === 'diamond') marker.append(E('path', { d: 'M 0 5 L 5 0 L 10 5 L 5 10 Z', fill: color, stroke: color, 'stroke-width': Math.min(1, markerStroke) }));
    else marker.append(E('path', { d: 'M 8 0 L 8 10', fill: 'none', stroke: color, 'stroke-width': markerStroke, 'stroke-linecap': 'butt' }));
    return marker;
  }

  function markerTargetFor(shape) {
    if (markerShapeTags.has(shape.tagName?.toLowerCase())) return shape;
    return [...shape.querySelectorAll('path, line, polyline, polygon')].find((node) => node.dataset.strokeMarkerShape !== 'true' && node.dataset.measureLabel !== 'true') || null;
  }

  function appendStrokeMarkers(wrapper, visual, object, style, index) {
    const target = markerTargetFor(visual); if (!target) return;
    const definitions = E('defs'); const safeObject = String(object.id || 'object').replace(/[^a-z0-9_-]/gi, '-'); const safeStroke = String(style.id || index).replace(/[^a-z0-9_-]/gi, '-');
    const add = (edge, markerStyle, size) => {
      if (markerStyle === 'none') return;
      const id = `stroke-marker-${safeObject}-${safeStroke}-${edge}`;
      definitions.append(strokeMarkerDefinition(id, markerStyle, size, style.color, style.width, object.svgMarkerSettings?.[edge]));
      target.setAttribute(`marker-${edge}`, `url(#${id})`); wrapper.setAttribute(`data-stroke-${edge}-marker`, markerStyle);
    };
    add('start', style.startMarker, style.startMarkerSize); add('end', style.endMarker, style.endMarkerSize);
    if (definitions.childElementCount) wrapper.prepend(definitions);
  }

  function alignmentMaskSource(object, style, color) {
    const source = shapeFor(object, false, { ...style, enabled: false, strokeType: 'none', startMarker: 'none', endMarker: 'none' });
    [source, ...source.querySelectorAll('*')].forEach((node) => {
      node.removeAttribute?.('data-object-id'); node.removeAttribute?.('filter'); node.removeAttribute?.('mask'); node.removeAttribute?.('clip-path');
      node.removeAttribute?.('marker-start'); node.removeAttribute?.('marker-end');
      if (node.hasAttribute?.('stroke')) node.setAttribute('stroke', 'none');
      if (fillShapeTags.has(node.tagName?.toLowerCase())) node.setAttribute('fill', color);
    });
    return source;
  }

  function supportsStrokeAlignment(object) {
    return Boolean(object && supportsObjectFill(object) && object.type !== 'image');
  }

  function applyStrokeAlignment(wrapper, visual, object, style) {
    if (style.alignment === 'center' || !supportsStrokeAlignment(object)) return;
    const safeObject = String(object.id || 'object').replace(/[^a-z0-9_-]/gi, '-'); const safeStroke = String(style.id || 'stroke').replace(/[^a-z0-9_-]/gi, '-');
    const id = `stroke-${style.alignment}-${safeObject}-${safeStroke}`; const definitions = wrapper.querySelector(':scope > defs') || E('defs');
    if (!definitions.parentNode) wrapper.prepend(definitions);
    strokePaintNodes(visual).forEach((node) => node.setAttribute('stroke-width', round(num(node.getAttribute('stroke-width'), style.width) * 2, 3)));
    wrapper.setAttribute('stroke-width', round(style.width * 2, 3));
    if (style.alignment === 'inside') {
      const clip = E('clipPath', { id, clipPathUnits: 'userSpaceOnUse', 'data-stroke-alignment-clip': 'inside' });
      clip.append(alignmentMaskSource(object, style, '#ffffff')); definitions.append(clip); visual.setAttribute('clip-path', `url(#${id})`);
    } else {
      const extent = Math.max(32, num(object.width, 1), num(object.height, 1), style.width * 4, style.startMarkerSize * 2, style.endMarkerSize * 2);
      const mask = E('mask', {
        id, maskUnits: 'userSpaceOnUse', x: -extent, y: -extent,
        width: num(object.width, 1) + extent * 2, height: num(object.height, 1) + extent * 2,
        'mask-type': 'luminance', 'data-stroke-alignment-mask': 'outside'
      });
      mask.append(E('rect', { x: -extent, y: -extent, width: num(object.width, 1) + extent * 2, height: num(object.height, 1) + extent * 2, fill: '#ffffff' }));
      mask.append(alignmentMaskSource(object, style, '#000000')); definitions.append(mask); visual.setAttribute('mask', `url(#${id})`);
    }
  }

  function zigzagStrokeVisual(object, style, fallbackVisual) {
    if (['text', 'measure'].includes(object?.type)) return null;
    const centerlines = strokeCenterlinesFor(object, { ...style, pattern: 'solid' });
    if (!centerlines.length) return null;
    const attributes = strokeStyleAttributes(object, style).attributes;
    const root = E('g', { 'data-zigzag-stroke': 'true' });
    centerlines.forEach((centerline, index) => {
      const points = zigzagStrokePoints(centerline.points, centerline.closed, style);
      if (points.length < 2) return;
      const d = points.map((point, pointIndex) => `${pointIndex ? 'L' : 'M'} ${round(point[0], 3)} ${round(point[1], 3)}`).join(' ');
      root.append(E('path', {
        ...attributes, d: `${d}${centerline.closed ? ' Z' : ''}`, fill: 'none',
        'data-zigzag-centerline': String(index), 'data-zigzag-vertices': points.length
      }));
    });
    if (!root.childElementCount) return null;
    if (object.type === 'arrow') fallbackVisual?.querySelectorAll('[data-stroke-marker-shape="true"]').forEach((marker) => root.append(marker.cloneNode(true)));
    return root;
  }

  function appendStrokeRenderLayer(target, shape, object) {
    const mainPaintNodes = strokePaintNodes(shape);
    mainPaintNodes.forEach((node) => node.setAttribute('stroke', 'none'));
    [shape, ...shape.querySelectorAll('[data-stroke-marker-shape="true"]')].forEach((node) => {
      if (node.dataset?.strokeMarkerShape === 'true') node.setAttribute('fill', 'none');
    });
    shape.setAttribute('data-stroke-fill-layer', 'true');
    const styles = strokeStackFor(object).filter(StrokeStyle.isVisible);
    [...styles].reverse().forEach((rawStyle) => {
      const style = StrokeStyle.normalizeStyle(rawStyle, { fallback: StrokeStyle.legacyStyle(object) });
      const definition = strokeTypeDefinition(style.strokeType); const baseVisual = shapeFor(object, false, style);
      const visual = style.pattern === 'zigzag' ? (zigzagStrokeVisual(object, style, baseVisual) || baseVisual) : baseVisual;
      [visual, ...visual.querySelectorAll('*')].forEach((node) => {
        node.removeAttribute?.('data-object-id'); node.removeAttribute?.('filter');
        if (node.hasAttribute?.('fill') && node.dataset?.strokeMarkerShape !== 'true') node.setAttribute('fill', 'none');
      });
      const paintNodes = strokePaintNodes(visual); if (!paintNodes.length) return;
      const importedShapeRendering = ['auto', 'optimizespeed', 'crispedges', 'geometricprecision'].includes(String(object.svgShapeRendering || '').toLowerCase())
        ? String(object.svgShapeRendering).toLowerCase().replace('optimizespeed', 'optimizeSpeed').replace('crispedges', 'crispEdges').replace('geometricprecision', 'geometricPrecision')
        : null;
      const shapeRendering = importedShapeRendering === 'auto' ? '' : importedShapeRendering || (definition.antialias ? 'geometricPrecision' : 'crispEdges');
      paintNodes.forEach((node) => {
        if (shapeRendering) node.setAttribute('shape-rendering', shapeRendering); else node.removeAttribute('shape-rendering');
        node.setAttribute('data-stroke-antialias', String(definition.antialias));
        if (node.tagName.toLowerCase() === 'text') {
          const importedTextRendering = textSettings(object).antiAlias === 'auto';
          node.setAttribute('text-rendering', importedTextRendering ? 'auto' : definition.antialias ? 'geometricPrecision' : 'optimizeSpeed');
        }
      });
      const attributes = strokeStyleAttributes(object, style).attributes;
      const wrapper = E('g', {
        ...attributes, fill: 'none', 'data-stroke-render-layer': 'true', 'data-stroke-id': style.id,
        'data-stroke-index': strokeStackFor(object).findIndex((entry) => entry.id === style.id),
        'data-stroke-antialias': String(definition.antialias), 'shape-rendering': shapeRendering || undefined, 'pointer-events': 'stroke'
      });
      wrapper.append(visual); applyStrokeAlignment(wrapper, visual, object, style); appendStrokeMarkers(wrapper, visual, object, style, wrapper.dataset.strokeIndex);
      target.append(wrapper);
    });
  }

  function appendTextAntialiasLayer(target, visual, object) {
    const settings = textSettings(object); const mode = settings.antiAlias;
    visual.dataset.textAntialias = mode;
    [...visual.querySelectorAll('text')].forEach((node) => node.setAttribute('data-text-antialias', mode));
    if (mode === 'auto') return;

    const safeId = String(object.id || 'text').replace(/[^a-z0-9_-]/gi, '-');
    const filterId = `text-antialias-${safeId}`;
    const filter = E('filter', {
      id: filterId, x: '-12%', y: '-18%', width: '124%', height: '136%',
      'color-interpolation-filters': 'sRGB', 'data-text-antialias-filter': mode
    });
    if (mode === 'smooth') {
      filter.append(E('feGaussianBlur', {
        in: 'SourceGraphic', stdDeviation: .38, 'data-text-antialias-softness': .38
      }));
    } else {
      const transfer = E('feComponentTransfer', { in: 'SourceGraphic' });
      transfer.append(mode === 'none'
        ? E('feFuncA', { type: 'discrete', tableValues: '0 1', 'data-text-antialias-threshold': .5 })
        : E('feFuncA', { type: 'linear', slope: 1.6, intercept: -.3, 'data-text-antialias-contrast': 1.6 }));
      filter.append(transfer);
    }
    const definitions = E('defs'); definitions.append(filter); target.append(definitions);
    visual.setAttribute('filter', `url(#${filterId})`);
  }

  function warpedObjectContent(content, object) {
    const transform = activeFreeTransform(object);
    if (!transform || FreeTransform.isIdentity(transform)) return content;
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    const safeObject = String(object.id || 'object').replace(/[^a-z0-9_-]/gi, '-'); const sourceId = `free-transform-source-${safeObject}-${uid('mesh')}`;
    content.id = sourceId; content.setAttribute('data-free-transform-source', transform.mode);
    const definitions = E('defs'); definitions.append(content);
    const warped = E('g', { 'data-free-transform-render': transform.mode, 'data-free-transform-triangles': transform.mode === 'envelope' ? 128 : 72 });
    FreeTransform.triangles(transform, width, height).forEach((triangle, index) => {
      const clipId = `${sourceId}-clip-${index}`; const clip = E('clipPath', { id: clipId, clipPathUnits: 'userSpaceOnUse' });
      clip.append(E('polygon', { points: triangle.source.map((entry) => `${round(entry.x, 5)},${round(entry.y, 5)}`).join(' ') })); definitions.append(clip);
      const use = E('use', { href: `#${sourceId}`, 'clip-path': `url(#${clipId})`, 'pointer-events': 'all' });
      const [a, b, c, d, e, f] = triangle.matrix; use.setAttribute('transform', `matrix(${round(a, 8)} ${round(b, 8)} ${round(c, 8)} ${round(d, 8)} ${round(e, 8)} ${round(f, 8)})`);
      warped.append(use);
    });
    const root = E('g', { 'data-free-transform-mesh': transform.mode }); root.append(definitions, warped); return root;
  }

  function objectGroup(object, interactive = true) {
    const binding = object.type === 'text' ? textPathBinding(object) : null;
    const nativeFilterCoordinates = Boolean(object?.svgNativeFilter);
    const group = E('g', {
      'data-object-id': interactive ? object.id : undefined,
      transform: binding || nativeFilterCoordinates ? undefined : objectTransform(object),
      opacity: clamp(num(object.opacity, 100) / 100, 0, 1),
      filter: objectHasFilter(object) ? `url(#${objectFilterId(object)})` : undefined
    });
    const blendMode = normalizeBlendMode(object.blendMode);
    group.style.mixBlendMode = blendMode;
    if (object.svgIsolation === 'isolate') group.style.isolation = 'isolate';
    group.dataset.blendMode = blendMode;
    if (object.visible === false) group.style.display = 'none';
    if (state.inlineTextSession?.objectId === object.id) group.style.visibility = 'hidden';
    if (object.locked) group.style.pointerEvents = 'none';
    const content = E('g', { 'data-object-content': object.type });
    if (object.type === 'group') (object.children || []).forEach((child) => content.append(objectGroup(child, false)));
    else if (object.type === 'mask-group') appendMaskGroupContents(content, object);
    else if (object.type === 'clip-group') appendClipGroupContents(content, object);
    else if (object.type === 'symbol-instance') {
      const source = symbolRenderObject(object); const definition = symbolDefinition(object.symbolId);
      if (source && definition) {
        const rendered = objectGroup(source, false);
        const scaleX = Math.max(.0001, num(object.width, 1)) / Math.max(.0001, num(definition.object.width, 1));
        const scaleY = Math.max(.0001, num(object.height, 1)) / Math.max(.0001, num(definition.object.height, 1));
        rendered.setAttribute('transform', `scale(${scaleX} ${scaleY})`); rendered.dataset.symbolContent = definition.id; content.append(rendered);
      } else {
        content.append(
          E('rect', { x: 0, y: 0, width: Math.max(1, num(object.width, 1)), height: Math.max(1, num(object.height, 1)), fill: 'none', stroke: '#d14343', 'stroke-dasharray': '6 4' }),
          E('path', { d: `M 0 0 L ${Math.max(1, num(object.width, 1))} ${Math.max(1, num(object.height, 1))} M ${Math.max(1, num(object.width, 1))} 0 L 0 ${Math.max(1, num(object.height, 1))}`, fill: 'none', stroke: '#d14343' })
        );
      }
    }
    else {
      const fillDefinitions = objectFillDefinitions(object);
      if (fillDefinitions) content.append(fillDefinitions);
      const strokeDefinitions = objectStrokeDefinitions(object);
      if (strokeDefinitions) content.append(strokeDefinitions);
      const shape = shapeFor(object, interactive);
      const paintOrder = String(object.svgPaintOrder || 'fill stroke markers').split(/\s+/);
      const strokeBeforeFill = paintOrder.indexOf('stroke') >= 0 && paintOrder.indexOf('stroke') < paintOrder.indexOf('fill');
      if (object.type === 'text') {
        const visual = E('g');
        applyTextTexture(shape, visual, object);
        if (strokeBeforeFill) { appendStrokeRenderLayer(visual, shape, object); visual.append(shape); }
        else { visual.append(shape); appendStrokeRenderLayer(visual, shape, object); }
        appendTextAntialiasLayer(content, visual, object); content.append(visual);
      } else if (supportsObjectTexture(object) && hasActiveTextTexture(object)) {
        const visual = E('g');
        applyTextTexture(shape, visual, object);
        if (strokeBeforeFill) { appendStrokeRenderLayer(visual, shape, object); visual.append(shape); }
        else { visual.append(shape); appendStrokeRenderLayer(visual, shape, object); }
        content.append(visual);
      } else {
        if (strokeBeforeFill) { appendStrokeRenderLayer(content, shape, object); content.append(shape); }
        else { content.append(shape); appendStrokeRenderLayer(content, shape, object); }
      }
    }
    appendFaceEffectOverlays(content, object, Boolean(binding));
    const renderedContent = binding ? content : warpedObjectContent(content, object);
    if (nativeFilterCoordinates) {
      const positioned = E('g', { transform: objectTransform(object), 'data-native-filter-coordinates': 'document' });
      positioned.append(renderedContent); group.append(positioned);
    } else group.append(renderedContent);
    return group;
  }

  function refreshObjectFilterDefinitions(object) {
    if (!artboardScene.defs || !object) return;
    objectTree(object).flatMap((entry) => objectTree(symbolRenderObject(entry) || entry)).forEach((entry) => {
      const id = objectFilterId(entry);
      [...artboardScene.defs.children].find((node) => node.id === id)?.remove();
      appendObjectFilter(artboardScene.defs, entry);
    });
  }

  function boundTextDependents(objectId) {
    return allObjects().filter((object) => object.type === 'text' && object.pathBinding?.targetId === objectId);
  }

  function renderDependents(objectId) {
    const source = objectEntry(objectId)?.object; const dependents = boundTextDependents(objectId);
    if (source?.symbolMasterId) dependents.push(...renderableObjects().filter((object) => object.type === 'symbol-instance' && object.symbolId === source.symbolMasterId));
    return dependents;
  }

  const artboardScene = SceneController.createSceneController({
    getObject: (id) => objectEntry(id)?.object,
    getDependents: renderDependents,
    renderObject: objectGroup,
    patchObjectTransform: (object, node) => {
      if (object.type === 'text' && textPathBinding(object)) return false;
      if (object.svgNativeFilter) {
        const positioned = node.querySelector(':scope > [data-native-filter-coordinates="document"]');
        if (!positioned) return false;
        positioned.setAttribute('transform', interactionObjectTransform(object));
        return true;
      }
      node.setAttribute('transform', interactionObjectTransform(object));
      return true;
    },
    refreshObject: refreshObjectFilterDefinitions,
    renderOverlays: renderEditorOverlays,
    renderFallback: renderArtboard,
    renderInspector,
    renderTransformInspector: syncMovedObjectInspector,
    measure: measurePerformancePhase,
    onInteractionFrame: ({ changed, total }) => {
      refs.artboard.dataset.interactionRenderCount = String(total);
      refs.artboard.dataset.lastInteractionObjects = String(changed);
      if (changed) {
        invalidateDocumentPixelPreview();
        scheduleDocumentPixelPreview();
      }
    },
    onFullRender: ({ total }) => { refs.artboard.dataset.fullRenderCount = String(total); }
  });
  const scheduleInteractionRender = (objects = [], options = {}) => {
    const source = Array.isArray(objects) ? objects.filter(Boolean) : [];
    syncSymbolMasters(source);
    const connectors = syncBoundConnectors(source.map((object) => object.id));
    return artboardScene.scheduleInteractionRender([...new Map([...source, ...connectors].map((object) => [object.id, object])).values()], options);
  };
  const scheduleMoveRender = (objects = [], options = {}) => {
    const source = Array.isArray(objects) ? objects.filter(Boolean) : [];
    syncSymbolMasters(source);
    const connectors = syncBoundConnectors(source.map((object) => object.id));
    const replacements = source.filter((object) => object.type === 'text' && textPathBinding(object));
    const transforms = source.filter((object) => !replacements.includes(object));
    if (transforms.length) artboardScene.scheduleTransformRender(transforms, options);
    if (replacements.length || connectors.length) artboardScene.scheduleInteractionRender([...replacements, ...connectors], options);
  };

  function renderedTextBounds(object) {
    const node = artboardScene.objectNodes.get(object.id) || [...refs.artboard.querySelectorAll('g[data-object-id]')].find((candidate) => candidate.dataset.objectId === object.id);
    const artboardBounds = refs.artboard.getBoundingClientRect(); const bounds = node?.getBoundingClientRect();
    if (!node || !bounds || !artboardBounds.width || !artboardBounds.height || bounds.width <= 0 || bounds.height <= 0) return null;
    const x = (bounds.left - artboardBounds.left) * state.doc.width / artboardBounds.width;
    const y = (bounds.top - artboardBounds.top) * state.doc.height / artboardBounds.height;
    const width = bounds.width * state.doc.width / artboardBounds.width;
    const height = bounds.height * state.doc.height / artboardBounds.height;
    return [x, y, width, height].every(Number.isFinite) && width > 0 && height > 0 ? { x, y, width, height } : null;
  }

  function appendTextPathStartHandle(group, object) {
    if (object?.type !== 'text' || object.id !== state.selected.at(-1)) return;
    const binding = textPathBinding(object); const layout = binding && boundTextLayout(object, binding);
    if (!layout?.startPoint || !layout.metric.points.length) return;
    const guideData = layout.metric.points.map(([x, y], index) => `${index ? 'L' : 'M'} ${round(x, 4)} ${round(y, 4)}`).join(' ');
    group.append(E('path', {
      d: `${guideData}${layout.metric.closed ? ' Z' : ''}`, fill: 'none', stroke: '#f59e0b',
      'stroke-width': 1.35 / state.zoom, 'stroke-dasharray': `${5 / state.zoom} ${3 / state.zoom}`,
      'vector-effect': 'non-scaling-stroke', 'data-text-path-guide': object.id, 'pointer-events': 'none'
    }));
    const point = layout.startPoint; const size = 7 / state.zoom;
    group.append(E('line', {
      x1: point.pathX, y1: point.pathY, x2: point.x, y2: point.y, stroke: '#f59e0b',
      'stroke-width': 1.15 / state.zoom, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none'
    }));
    const handle = E('path', {
      d: `M ${round(point.x, 4)} ${round(point.y - size, 4)} L ${round(point.x + size, 4)} ${round(point.y, 4)} L ${round(point.x, 4)} ${round(point.y + size, 4)} L ${round(point.x - size, 4)} ${round(point.y, 4)} Z`,
      fill: '#facc15', stroke: '#b45309', 'stroke-width': 1.5 / state.zoom, 'vector-effect': 'non-scaling-stroke',
      'data-text-path-start': 'true', 'data-object-id': object.id, 'aria-label': 'Textstartpunkt auf dem Pfad verschieben',
      role: 'button', 'pointer-events': 'all'
    });
    const title = E('title'); title.textContent = 'Textstartpunkt auf dem Pfad verschieben'; handle.append(title); handle.style.cursor = 'grab'; group.append(handle);
  }

  function appendFillPaintHandles(group, object) {
    const paint = normalizedObjectFillPaint(object);
    if (!paint || object.id !== state.selected.at(-1) || object.type === 'text') return;
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    const line = (from, to, dashed = false) => group.append(E('line', {
      x1: from[0], y1: from[1], x2: to[0], y2: to[1], stroke: '#f97316',
      'stroke-width': 1.7 / state.zoom, 'stroke-dasharray': dashed ? `${5 / state.zoom} ${3 / state.zoom}` : undefined,
      'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none', 'data-fill-guide': paint.type
    }));
    const handle = (kind, point, shape, label) => {
      const size = 9 / state.zoom; let node;
      if (shape === 'diamond') node = E('path', {
        d: `M ${round(point[0])} ${round(point[1] - size / 2)} L ${round(point[0] + size / 2)} ${round(point[1])} L ${round(point[0])} ${round(point[1] + size / 2)} L ${round(point[0] - size / 2)} ${round(point[1])} Z`
      });
      else if (shape === 'square') node = E('rect', { x: point[0] - size / 2, y: point[1] - size / 2, width: size, height: size, rx: 1 / state.zoom });
      else node = E('circle', { cx: point[0], cy: point[1], r: size / 2 });
      Object.entries({ fill: shape === 'diamond' ? '#facc15' : '#ffffff', stroke: '#ea580c', 'stroke-width': 1.6 / state.zoom, 'vector-effect': 'non-scaling-stroke', 'data-fill-handle': kind, 'data-object-id': object.id, 'pointer-events': 'all' })
        .forEach(([key, value]) => node.setAttribute(key, value));
      const title = E('title'); title.textContent = label; node.append(title); node.style.cursor = kind.includes('rotate') || kind === 'conic-angle' ? 'grab' : 'move'; group.append(node);
    };
    if (paint.type === 'linear') {
      const start = [width * paint.x1 / 100, height * paint.y1 / 100]; const end = [width * paint.x2 / 100, height * paint.y2 / 100];
      line(start, end); handle('linear-start', start, 'circle', 'Verlaufsanfang'); handle('linear-end', end, 'square', 'Verlaufsende');
    } else if (paint.type === 'radial') {
      const center = [width * paint.cx / 100, height * paint.cy / 100]; const radius = Math.min(width, height) * paint.radius / 100;
      const edge = [center[0] + radius, center[1]]; line(center, edge); handle('radial-center', center, 'circle', 'Verlaufsmittelpunkt'); handle('radial-radius', edge, 'square', 'Verlaufsradius');
    } else if (paint.type === 'conic') {
      const center = [width * paint.cx / 100, height * paint.cy / 100]; const radius = Math.max(18 / state.zoom, Math.min(width, height) * .42);
      const radians = (paint.angle - 90) * Math.PI / 180; const edge = [center[0] + Math.cos(radians) * radius, center[1] + Math.sin(radians) * radius];
      line(center, edge); handle('conic-center', center, 'circle', 'Verlaufsmittelpunkt'); handle('conic-angle', edge, 'diamond', 'Verlaufswinkel');
    } else if (FillPaint.isImage(paint.type)) {
      const center = [width / 2 + paint.offsetX, height / 2 + paint.offsetY]; const radians = paint.rotation * Math.PI / 180;
      const scaleRadius = Math.max(14 / state.zoom, Math.min(width, height) * .35 * paint.scale / 100);
      const scale = [center[0] + Math.cos(radians) * scaleRadius, center[1] + Math.sin(radians) * scaleRadius];
      const rotateRadius = Math.max(28 / state.zoom, Math.min(width, height) * .48);
      const rotate = [center[0] + Math.cos(radians - Math.PI / 2) * rotateRadius, center[1] + Math.sin(radians - Math.PI / 2) * rotateRadius];
      line(center, scale); line(center, rotate, true);
      handle('image-offset', center, 'circle', 'Füllung verschieben'); handle('image-scale', scale, 'square', 'Füllung skalieren'); handle('image-rotate', rotate, 'diamond', 'Füllung drehen');
    }
  }

  function appendTransformPivot(group, object, transform = activeFreeTransform(object)) {
    if (object.id !== state.selected.at(-1)) return;
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    const pivot = object.transformPivot || { x: .5, y: .5 }; const mapped = transform ? FreeTransform.mapPoint(transform, pivot.x, pivot.y) : pivot;
    const x = mapped.x * width; const y = mapped.y * height; const radius = 3.5 / state.zoom; const hitRadius = 10.15 / state.zoom;
    const target = E('g', { 'data-transform-pivot': 'true', 'data-object-id': object.id, 'pointer-events': 'all', role: 'button', 'aria-label': 'Drehmittelpunkt verschieben' });
    target.append(
      E('circle', { cx: x, cy: y, r: hitRadius, fill: 'transparent', stroke: 'none', 'pointer-events': 'all', 'data-transform-pivot-hit': 'true' }),
      E('circle', { cx: x, cy: y, r: radius, fill: 'rgba(255,255,255,.82)', stroke: '#e11d48', 'stroke-width': 1.45 / state.zoom, 'vector-effect': 'non-scaling-stroke' }),
      E('line', { x1: x - radius * 1.45, y1: y, x2: x + radius * 1.45, y2: y, stroke: '#e11d48', 'stroke-width': 1.35 / state.zoom, 'vector-effect': 'non-scaling-stroke' }),
      E('line', { x1: x, y1: y - radius * 1.45, x2: x, y2: y + radius * 1.45, stroke: '#e11d48', 'stroke-width': 1.35 / state.zoom, 'vector-effect': 'non-scaling-stroke' })
    );
    target.style.cursor = 'move'; group.append(target);
  }

  function displayedLocalVectorPoint(object, value, transform = activeFreeTransform(object)) {
    const x = num(value?.[0]); const y = num(value?.[1]); if (!transform) return [x, y];
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1)); const mapped = FreeTransform.mapPoint(transform, x / width, y / height);
    return [mapped.x * width, mapped.y * height];
  }

  function displayedVectorSegmentPath(object, index, sourcePoints, transform = activeFreeTransform(object)) {
    if (!transform) return vectorSegmentPathData(object, index, sourcePoints);
    const next = vectorNextPointIndex(object, index, sourcePoints); if (next < 0) return '';
    const start = sourcePoints[index]; const end = sourcePoints[next]; const controls = bezierControlsFor(object, index, sourcePoints); const curved = isBezierSegment(object?.bezierSegments?.[index]) && controls;
    return Array.from({ length: 17 }, (_, step) => {
      const progress = step / 16; const source = curved ? cubicPoint(start, controls.c1, controls.c2, end, progress) : lerpPoint(start, end, progress);
      const [x, y] = displayedLocalVectorPoint(object, source, transform); return `${step ? 'L' : 'M'} ${round(x, 4)} ${round(y, 4)}`;
    }).join(' ');
  }

  function appendFreeTransformOverlay(group, object, requestedMode) {
    const mode = requestedMode || object.freeTransform?.mode || 'free'; const transform = activeFreeTransform(object, mode) || FreeTransform.normalize({ mode });
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    const mapped = (u, v) => { const result = FreeTransform.mapPoint(transform, u, v); return [result.x * width, result.y * height]; };
    const pathFor = (axis, value) => Array.from({ length: 25 }, (_, index) => {
      const progress = index / 24; const [x, y] = axis === 'u' ? mapped(value, progress) : mapped(progress, value);
      return `${index ? 'L' : 'M'} ${round(x, 4)} ${round(y, 4)}`;
    }).join(' ');
    const gridValues = mode === 'envelope' ? [0, 1 / 3, 2 / 3, 1] : [0, .5, 1];
    gridValues.forEach((value) => {
      group.append(E('path', { d: pathFor('u', value), fill: 'none', stroke: '#0f67d8', 'stroke-width': (value === 0 || value === 1 ? 1.7 : 1) / state.zoom, 'stroke-dasharray': value === 0 || value === 1 ? undefined : `${4 / state.zoom} ${3 / state.zoom}`, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none', 'data-free-transform-grid': mode }));
      group.append(E('path', { d: pathFor('v', value), fill: 'none', stroke: '#0f67d8', 'stroke-width': (value === 0 || value === 1 ? 1.7 : 1) / state.zoom, 'stroke-dasharray': value === 0 || value === 1 ? undefined : `${4 / state.zoom} ${3 / state.zoom}`, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none', 'data-free-transform-grid': mode }));
    });
    const controls = mode === 'envelope' ? transform.mesh : transform.corners;
    controls.forEach((control, index) => {
      const x = control.x * width; const y = control.y * height; const isCorner = mode !== 'envelope' || [0, 3, 12, 15].includes(index); const size = (isCorner ? 9 : 7) / state.zoom;
      const handle = isCorner
        ? E('rect', { x: x - size / 2, y: y - size / 2, width: size, height: size, rx: 1.2 / state.zoom })
        : E('circle', { cx: x, cy: y, r: size / 2 });
      Object.entries({ fill: isCorner ? '#ffffff' : '#fde68a', stroke: '#0f67d8', 'stroke-width': 1.45 / state.zoom, 'vector-effect': 'non-scaling-stroke', 'data-free-transform-control': index, 'data-free-transform-mode': mode, 'data-object-id': object.id, 'pointer-events': 'all' }).forEach(([key, value]) => handle.setAttribute(key, value));
      handle.style.cursor = 'move'; group.append(handle);
    });
    const top = mapped(.5, 0); const rotate = mapped(.5, -28 / Math.max(height * state.zoom, 1));
    group.append(E('line', { x1: top[0], y1: top[1], x2: rotate[0], y2: rotate[1], stroke: '#4f63e9', 'stroke-width': 1.2 / state.zoom, 'vector-effect': 'non-scaling-stroke' }));
    const rotateHandle = E('circle', { cx: rotate[0], cy: rotate[1], r: 5 / state.zoom, fill: '#4f63e9', stroke: '#fff', 'stroke-width': 1.4 / state.zoom, 'data-handle': 'rotate', 'data-object-id': object.id, 'pointer-events': 'all' });
    rotateHandle.style.cursor = 'grab'; group.append(rotateHandle); appendTransformPivot(group, object, transform);
  }

  function appendAlignmentKeyBadge(root, object) {
    const bounds = boundsOf([object], { visual: true }); const label = I18n.t('ui.keyObject');
    const scale = 1 / Math.max(state.zoom, .01); const height = 18 * scale; const gap = 4 * scale;
    const width = Math.max(48, label.length * 6.2 + 16) * scale;
    const x = bounds.x; const y = bounds.y >= height + gap ? bounds.y - height - gap : bounds.y + gap;
    const badge = E('g', {
      'data-key-object-badge': object.id, 'pointer-events': 'none',
      role: 'status', 'aria-label': label
    });
    badge.append(
      E('rect', {
        x, y, width, height, rx: 4 * scale,
        fill: '#f59e0b', stroke: '#ffffff', 'stroke-width': 1.2 * scale,
        'vector-effect': 'non-scaling-stroke'
      }),
      E('text', {
        x: x + 8 * scale, y: y + 12.5 * scale,
        fill: '#1f2937', 'font-size': 10 * scale, 'font-family': 'Arial, sans-serif',
        'font-weight': '700', 'letter-spacing': .15 * scale
      })
    );
    badge.lastElementChild.textContent = label; root.append(badge);
  }

  function renderSelection(root) {
    const selectionRoot = E('g', { 'data-ui-overlay': 'selection', 'pointer-events': 'none' });
    const marquee = state.gesture?.kind === 'marquee' ? state.gesture : null;
    if (marquee) {
      const start = marquee.start; const current = marquee.current || start;
      const x = Math.min(start.x, current.x); const y = Math.min(start.y, current.y);
      const width = Math.abs(current.x - start.x); const height = Math.abs(current.y - start.y);
      selectionRoot.append(E('rect', {
        x, y, width, height, fill: 'rgba(79,99,233,.13)', stroke: '#3158db',
        'stroke-width': 2 / state.zoom, 'stroke-dasharray': `${7 / state.zoom} ${4 / state.zoom}`,
        'data-marquee': 'true'
      }));
    }
    const vectorMarquee = state.gesture?.kind === 'vector-marquee' ? state.gesture : null;
    if (vectorMarquee) {
      const start = vectorMarquee.start; const current = vectorMarquee.current || start;
      selectionRoot.append(E('rect', {
        x: Math.min(start.x, current.x), y: Math.min(start.y, current.y),
        width: Math.abs(current.x - start.x), height: Math.abs(current.y - start.y),
        fill: 'none', stroke: '#2563eb', 'stroke-width': 2 / state.zoom,
        'stroke-dasharray': `${6 / state.zoom} ${3 / state.zoom}`,
        'data-vector-marquee': 'true', 'pointer-events': 'none'
      }));
    }
    const pen = activePenGesture();
    if (pen?.object?.points?.length && pen.previewPoint) {
      const object = pen.object; const last = object.points.at(-1); const preview = penLocalPoint(object, pen.previewPoint);
      const previewPath = pen.nextControl
        ? `M ${round(last[0])} ${round(last[1])} C ${round(pen.nextControl[0])} ${round(pen.nextControl[1])}, ${round(preview[0])} ${round(preview[1])}, ${round(preview[0])} ${round(preview[1])}`
        : `M ${round(last[0])} ${round(last[1])} L ${round(preview[0])} ${round(preview[1])}`;
      selectionRoot.append(E('path', {
        d: previewPath, transform: objectTransform(object), fill: 'none', stroke: object.stroke || '#3158db',
        'stroke-width': Math.max(1, num(object.strokeWidth, 1)) / state.zoom, 'stroke-dasharray': `${5 / state.zoom} ${3 / state.zoom}`,
        'data-pen-preview': 'true', 'pointer-events': 'none'
      }));
    }
    const polyline = activePolylineGesture();
    if (polyline?.object?.points?.length && polyline.previewPoint) {
      const object = polyline.object; const last = object.points.at(-1); const preview = penLocalPoint(object, polyline.previewPoint);
      if (Math.hypot(num(preview[0]) - num(last[0]), num(preview[1]) - num(last[1])) > .01) {
        selectionRoot.append(E('path', {
          d: `M ${round(last[0])} ${round(last[1])} L ${round(preview[0])} ${round(preview[1])}`,
          transform: objectTransform(object), fill: 'none', stroke: object.stroke || '#3158db',
          'stroke-width': Math.max(1, num(object.strokeWidth, 1)) / state.zoom,
          'stroke-dasharray': `${5 / state.zoom} ${3 / state.zoom}`,
          'data-polyline-preview': 'true', 'pointer-events': 'none'
        }));
      }
    }
    state.snapGuides.forEach((guide) => {
      if (guide.axis) {
        selectionRoot.append(E('line', guide.axis === 'x' ? {
          x1: guide.value, y1: 0, x2: guide.value, y2: state.doc.height
        } : {
          x1: 0, y1: guide.value, x2: state.doc.width, y2: guide.value
        }));
        const line = selectionRoot.lastElementChild;
        line.setAttribute('stroke', '#ec4899');
        line.setAttribute('stroke-width', 1.2 / state.zoom);
        line.setAttribute('stroke-dasharray', `${5 / state.zoom} ${3 / state.zoom}`);
        return;
      }
      if (!guide.kind || !Number.isFinite(guide.x) || !Number.isFinite(guide.y)) return;
      const colors = { anchor: '#0f67d8', segment: '#0f9f7a', intersection: '#d946ef', midpoint: '#f59e0b', center: '#64748b', tangent: '#e11d48', connection: '#7c3aed' };
      const labels = { anchor: 'Anker', segment: 'Segment', intersection: 'Schnittpunkt', midpoint: 'Pfadmitte', center: 'Objektzentrum', tangent: 'Tangente', connection: 'Anschluss' };
      const color = colors[guide.kind] || '#ec4899'; const radius = 6 / state.zoom; const marker = E('g', {
        'data-snap-target': guide.kind, 'data-snap-object-id': guide.objectId, 'pointer-events': 'none'
      });
      if (guide.kind === 'tangent' && guide.origin) marker.append(E('line', {
        x1: guide.origin.x, y1: guide.origin.y, x2: guide.x, y2: guide.y, stroke: color,
        'stroke-width': 1.25 / state.zoom, 'stroke-dasharray': `${4 / state.zoom} ${3 / state.zoom}`, 'vector-effect': 'non-scaling-stroke'
      }));
      marker.append(E('circle', { cx: guide.x, cy: guide.y, r: radius, fill: 'rgba(255,255,255,.88)', stroke: color, 'stroke-width': 1.7 / state.zoom, 'vector-effect': 'non-scaling-stroke' }));
      if (guide.kind === 'intersection') marker.append(
        E('line', { x1: guide.x - radius * .6, y1: guide.y - radius * .6, x2: guide.x + radius * .6, y2: guide.y + radius * .6, stroke: color, 'stroke-width': 1.4 / state.zoom }),
        E('line', { x1: guide.x + radius * .6, y1: guide.y - radius * .6, x2: guide.x - radius * .6, y2: guide.y + radius * .6, stroke: color, 'stroke-width': 1.4 / state.zoom })
      );
      else if (guide.kind === 'midpoint') marker.append(E('rect', { x: guide.x - radius * .42, y: guide.y - radius * .42, width: radius * .84, height: radius * .84, fill: color }));
      else marker.append(E('circle', { cx: guide.x, cy: guide.y, r: 1.7 / state.zoom, fill: color }));
      const label = E('text', {
        x: guide.x + 9 / state.zoom, y: guide.y - 8 / state.zoom, fill: color, stroke: '#ffffff', 'stroke-width': 3 / state.zoom,
        'paint-order': 'stroke', 'font-size': 9 / state.zoom, 'font-family': 'Arial, sans-serif', 'font-weight': '700'
      });
      label.textContent = labels[guide.kind] || guide.kind; marker.append(label); selectionRoot.append(marker);
    });
    const activeDrawObjectId = state.gesture?.kind === 'draw' ? state.gesture.object?.id : null;
    const objects = selectedObjects().filter((object) => state.inlineTextSession?.objectId !== object.id && object.id !== activeDrawObjectId);
    const keyObjectId = alignmentKeyObjectId(objects);
    objects.forEach((object) => {
      const isKeyObject = object.id === keyObjectId;
      const selectionStroke = isKeyObject ? '#f59e0b' : '#3158db';
      const selectionStrokeWidth = (isKeyObject ? 3 : 1.65) / state.zoom;
      const selectionDash = isKeyObject ? undefined : `${5 / state.zoom} ${3 / state.zoom}`;
      const editingObjectTransform = object.id === state.selected.at(-1) && (state.freeTransformMode || object.freeTransform?.mode);
      const textBounds = object.type === 'text' && !editingObjectTransform ? renderedTextBounds(object) : null;
      const bitmapBounds = object.type === 'image' ? liveBitmapSelectionBounds(object) : undefined;
      if (bitmapBounds === null) return;
      const group = E('g', { transform: textBounds ? undefined : objectTransform(object) });
      const localBounds = textBounds
        ? expandedBounds(textBounds, objectVisualOutset(object))
        : objectLocalVisualBounds(object, bitmapBounds ? { geometryBounds: bitmapBounds } : undefined);
      const x = localBounds.x; const y = localBounds.y;
      const w = Math.max(1, localBounds.width); const h = Math.max(1, localBounds.height);
      const drawingPen = pen?.object?.id === object.id;
      const drawingPolyline = polyline?.object?.id === object.id;
      const drawingPointPath = drawingPen || drawingPolyline;
      const editingVectorPoints = drawingPointPath || (state.tool === 'vector-select' && isVectorObject(object));
      const freeTransformMode = !textBounds && editingObjectTransform;
      if (!editingVectorPoints && freeTransformMode) {
        group.append(E('rect', {
          x, y, width: w, height: h, fill: 'none',
          stroke: selectionStroke, 'stroke-width': selectionStrokeWidth, 'stroke-dasharray': selectionDash,
          'data-selection-object-id': object.id, 'data-selection-visual-bounds': 'true',
          'data-key-object': isKeyObject || undefined
        }));
        appendFreeTransformOverlay(group, object, state.freeTransformMode);
      }
      if (!editingVectorPoints && !freeTransformMode) {
        const box = E('rect', {
          x, y, width: w, height: h, fill: 'none',
          stroke: selectionStroke, 'stroke-width': selectionStrokeWidth, 'stroke-dasharray': selectionDash,
          'data-selection-object-id': object.id, 'data-selection-visual-bounds': 'true',
          'data-key-object': isKeyObject || undefined
        });
        group.append(box);
        const handles = { nw: [x, y], n: [x + w / 2, y], ne: [x + w, y], e: [x + w, y + h / 2], se: [x + w, y + h], s: [x + w / 2, y + h], sw: [x, y + h], w: [x, y + h / 2] };
        const size = 8 / state.zoom;
        Object.entries(handles).forEach(([name, [handleX, handleY]]) => {
          const handle = E('rect', { x: handleX - size / 2, y: handleY - size / 2, width: size, height: size, rx: 1.5 / state.zoom, fill: isKeyObject ? '#fef3c7' : '#ffffff', stroke: isKeyObject ? '#d97706' : '#4f63e9', 'stroke-width': 1.3 / state.zoom, 'data-handle': name, 'data-object-id': object.id, 'pointer-events': 'all' });
          handle.style.cursor = `${name}-resize`;
          group.append(handle);
        });
        const rotateY = -28 / state.zoom;
        group.append(E('line', { x1: x + w / 2, y1: y, x2: x + w / 2, y2: y + rotateY, stroke: isKeyObject ? '#d97706' : '#4f63e9', 'stroke-width': 1.2 / state.zoom }));
        const rotate = E('circle', { cx: x + w / 2, cy: y + rotateY, r: 5 / state.zoom, fill: isKeyObject ? '#f59e0b' : '#4f63e9', stroke: '#fff', 'stroke-width': 1.4 / state.zoom, 'data-handle': 'rotate', 'data-object-id': object.id, 'pointer-events': 'all' });
        rotate.style.cursor = 'grab';
        group.append(rotate);
        if (AutoShapes.isAutoShape(object) && object.id === state.selected.at(-1)) {
          const size = 5.1 / state.zoom;
          const controls = AutoShapes.controls(object);
          if (object.type === 'pie') {
            controls.filter((control) => /^segment-\d+$/.test(control.id)).forEach((control) => {
              group.append(E('line', {
                x1: num(object.width, 1) / 2, y1: num(object.height, 1) / 2, x2: control.x, y2: control.y,
                stroke: '#d59b00', 'stroke-width': 1.2 / state.zoom, 'stroke-dasharray': `${4 / state.zoom} ${3 / state.zoom}`,
                'data-pie-segment-guide': control.id, 'pointer-events': 'none'
              }));
            });
          }
          controls.forEach((control) => {
            const diamond = E('path', {
              d: `M ${round(control.x)} ${round(control.y - size)} L ${round(control.x + size)} ${round(control.y)} L ${round(control.x)} ${round(control.y + size)} L ${round(control.x - size)} ${round(control.y)} Z`,
              fill: '#facc15', stroke: '#28c6df', 'stroke-width': 1.25 / state.zoom,
              'data-auto-shape-control': control.id, 'data-auto-shape-behavior': control.behavior,
              'data-l-shape-control': object.type === 'l-shape' ? control.id : undefined,
              'data-object-id': object.id, 'aria-label': control.label, title: control.label, 'pointer-events': 'all'
            });
            const title = E('title'); title.textContent = control.label; diamond.append(title);
            diamond.style.cursor = control.cursor || 'move'; group.append(diamond);
          });
        }
        appendFillPaintHandles(group, object);
        if (!textBounds) appendTransformPivot(group, object);
      }
      if (!editingVectorPoints && !freeTransformMode) appendTextPathStartHandle(group, object);
      if (editingVectorPoints) {
        const radius = 4.6 / state.zoom;
        const sourcePoints = vectorEditablePoints(object); const vectorTransform = activeFreeTransform(object);
        const points = sourcePoints.map((point) => displayedLocalVectorPoint(object, point, vectorTransform));
        const objectPointSelections = drawingPointPath ? [] : vectorPointSelections().filter((selection) => selection.objectId === object.id);
        const selectedPoint = drawingPen ? pen.anchorIndex : drawingPolyline ? points.length - 1 : objectPointSelections.at(-1)?.index ?? -1;
        if (!drawingPointPath && pointVectorTypes.has(object.type)) {
          vectorSegmentSelections().filter((selection) => selection.objectId === object.id).forEach((selection) => {
            const d = displayedVectorSegmentPath(object, selection.index, sourcePoints, vectorTransform); if (!d) return;
            group.append(E('path', {
              d, fill: 'none', stroke: '#f59e0b', 'stroke-width': Math.max(3.2, num(object.strokeWidth, 1) + 2.8) / state.zoom,
              'stroke-linecap': 'round', 'stroke-linejoin': 'round', opacity: .9,
              'data-vector-selected-segment': selection.index, 'data-vector-object-id': object.id, 'pointer-events': 'none'
            }));
          });
        }
        if (drawingPen && selectedPoint >= 0) {
          const anchor = points[selectedPoint];
          const incoming = selectedPoint === 0 ? pen.firstIncoming : object.bezierSegments?.[selectedPoint - 1]?.c2;
          [incoming, pen.nextControl].filter((control) => Array.isArray(control)).forEach((control) => {
            group.append(E('line', { x1: anchor[0], y1: anchor[1], x2: control[0], y2: control[1], stroke: '#d97706', 'stroke-width': 1.2 / state.zoom, 'pointer-events': 'none' }));
            group.append(E('circle', { cx: control[0], cy: control[1], r: 3.8 / state.zoom, fill: '#fff7d6', stroke: '#d97706', 'stroke-width': 1.3 / state.zoom, 'data-pen-handle': 'true', 'pointer-events': 'none' }));
          });
        }
        if (selectedPoint >= 0) {
          const anchor = points[selectedPoint]; const handleEntries = [];
          if (pointVectorTypes.has(object.type) && anchor) {
            const previous = selectedPoint > 0 ? selectedPoint - 1 : isClosedVectorPath(object) ? points.length - 1 : -1;
            const incoming = previous >= 0 ? bezierControlsFor(object, previous, sourcePoints) : null;
            const outgoing = bezierControlsFor(object, selectedPoint, sourcePoints);
            if (incoming) handleEntries.push({ segmentIndex: previous, control: 'c2', point: displayedLocalVectorPoint(object, incoming.c2, vectorTransform) });
            if (outgoing) handleEntries.push({ segmentIndex: selectedPoint, control: 'c1', point: displayedLocalVectorPoint(object, outgoing.c1, vectorTransform) });
          } else if (outlineVectorTypes.has(object.type) && anchor) {
            const address = outlineControlAddress(object, selectedPoint);
            if (address?.incoming) {
              const incoming = bezierControlsFor(object, address.incoming.segmentIndex, sourcePoints);
              if (incoming) handleEntries.push({ segmentIndex: address.incoming.segmentIndex, control: 'c2', point: displayedLocalVectorPoint(object, incoming.c2, vectorTransform) });
            }
            if (address?.outgoing) {
              const outgoing = bezierControlsFor(object, address.outgoing.segmentIndex, sourcePoints);
              if (outgoing) handleEntries.push({ segmentIndex: address.outgoing.segmentIndex, control: 'c1', point: displayedLocalVectorPoint(object, outgoing.c1, vectorTransform) });
            }
          }
          handleEntries.forEach(({ segmentIndex, control, point: [controlX, controlY] }) => {
            group.append(E('line', { x1: anchor[0], y1: anchor[1], x2: controlX, y2: controlY, stroke: '#f59e0b', 'stroke-width': 1.25 / state.zoom, 'stroke-dasharray': `${4 / state.zoom} ${3 / state.zoom}`, 'pointer-events': 'none' }));
            const handle = E('circle', {
              cx: controlX, cy: controlY, r: 4.2 / state.zoom, fill: '#fff7d6', stroke: '#d97706', 'stroke-width': 1.4 / state.zoom,
              'data-vector-bezier-control': control, 'data-vector-segment-index': segmentIndex, 'data-vector-anchor-index': selectedPoint, 'data-vector-object-id': object.id, 'pointer-events': 'all'
            });
            handle.style.cursor = vectorCursor; group.append(handle);
          });
        }
        const appendPoint = ([pointX, pointY], index) => {
          const isSelected = drawingPointPath ? index === selectedPoint : isVectorPointSelected(object.id, index);
          const isPolylineCloseTarget = drawingPolyline && index === 0 && points.length >= 3;
          const point = E('circle', {
            cx: pointX, cy: pointY, r: isSelected || isPolylineCloseTarget ? radius * 1.12 : radius,
            fill: isSelected ? '#facc15' : isPolylineCloseTarget ? '#bbf7d0' : '#ffffff',
            stroke: isSelected ? '#a16207' : isPolylineCloseTarget ? '#15803d' : '#2563eb', 'stroke-width': 1.45 / state.zoom,
            'data-vector-point': index, 'data-vector-object-id': object.id,
            'data-pen-close-anchor': drawingPen && index === 0 && points.length >= 3 ? 'true' : undefined,
            'data-polyline-close-anchor': isPolylineCloseTarget ? 'true' : undefined,
            'aria-label': isPolylineCloseTarget ? 'Startpunkt zum Schließen der Polylinie' : undefined,
            'pointer-events': drawingPointPath ? 'none' : 'all'
          });
          point.style.cursor = vectorCursor; group.append(point);
        };
        points.forEach((point, index) => { if (!isVectorPointSelected(object.id, index) && index !== selectedPoint) appendPoint(point, index); });
        objectPointSelections.forEach((selection) => { if (points[selection.index]) appendPoint(points[selection.index], selection.index); });
        if (drawingPointPath && selectedPoint >= 0) appendPoint(points[selectedPoint], selectedPoint);
      }
      selectionRoot.append(group);
      if (isKeyObject && !editingVectorPoints) appendAlignmentKeyBadge(selectionRoot, object);
    });
    root.append(selectionRoot);
  }

  function renderGuides(root) {
    const guideRoot = E('g', { 'data-ui-overlay': 'guides' });
    const guides = documentGuides(); const locked = Boolean(guides.locked);
    guideRoot.dataset.guidesLocked = String(locked);
    const appendGuide = (orientation, value, index, preview = false) => {
      const vertical = orientation === 'vertical';
      const lineAttrs = vertical
        ? { x1: value, y1: 0, x2: value, y2: state.doc.height }
        : { x1: 0, y1: value, x2: state.doc.width, y2: value };
      const visible = E('line', {
        ...lineAttrs, stroke: preview ? 'rgba(49,88,219,.72)' : '#4f63e9',
        'stroke-width': 1 / state.zoom, 'stroke-dasharray': preview ? `${5 / state.zoom} ${3 / state.zoom}` : undefined,
        'pointer-events': 'none', 'data-guide-preview': preview || undefined
      });
      guideRoot.append(visible);
      if (preview || locked) return;
      const hitArea = E('line', {
        ...lineAttrs, stroke: 'transparent', 'stroke-width': 12 / state.zoom,
        'pointer-events': 'stroke',
        'data-guide-axis': orientation, 'data-guide-index': index, 'data-guide-value': value
      });
      hitArea.style.cursor = vertical ? 'col-resize' : 'row-resize';
      guideRoot.append(hitArea);
    };
    ['horizontal', 'vertical'].forEach((orientation) => (guides[orientation] || []).forEach((value, index) => appendGuide(orientation, value, index)));
    const preview = state.guidePreview;
    if (preview) appendGuide(preview.orientation, preview.value, -1, true);
    root.append(guideRoot);
  }

  function renderCanvasResizeHandles(root) {
    if (!state.canvasResizeMode) return;
    const width = state.doc.width; const height = state.doc.height;
    const group = E('g', { 'data-ui-overlay': 'canvas-resize', 'pointer-events': 'none' });
    group.append(E('rect', {
      x: 0, y: 0, width, height, fill: 'none', stroke: '#f59e0b',
      'stroke-width': 2 / state.zoom, 'stroke-dasharray': `${7 / state.zoom} ${4 / state.zoom}`,
      'data-canvas-resize-frame': 'true', 'pointer-events': 'none'
    }));
    const points = {
      nw: [0, 0], n: [width / 2, 0], ne: [width, 0],
      e: [width, height / 2], se: [width, height], s: [width / 2, height],
      sw: [0, height], w: [0, height / 2]
    };
    const cursors = {
      nw: 'nwse-resize', n: 'ns-resize', ne: 'nesw-resize', e: 'ew-resize',
      se: 'nwse-resize', s: 'ns-resize', sw: 'nesw-resize', w: 'ew-resize'
    };
    const size = 11 / state.zoom;
    Object.entries(points).forEach(([name, [x, y]]) => {
      const handle = E('rect', {
        x: x - size / 2, y: y - size / 2, width: size, height: size,
        rx: 1.8 / state.zoom, fill: '#fff7d6', stroke: '#d97706',
        'stroke-width': 1.5 / state.zoom, 'data-canvas-resize-handle': name,
        'pointer-events': 'all'
      });
      handle.style.cursor = cursors[name];
      group.append(handle);
    });
    root.append(group);
  }

  function wandMaskContains(selection, x, y) {
    if (x < 0 || y < 0 || x >= selection.sourceWidth || y >= selection.sourceHeight) return false;
    return Boolean(selection.mask?.[y * selection.sourceWidth + x]);
  }

  function wandMaskCanvas(selection, overlay = false) {
    const width = Math.max(1, num(selection.width, 1)); const height = Math.max(1, num(selection.height, 1));
    const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height;
    const context = canvas.getContext('2d'); const image = context.createImageData(width, height); const data = image.data;
    const edge = marqueeEdgeSettings(selection);
    for (let y = 0; y < height; y += 1) {
      for (let x = 0; x < width; x += 1) {
        const sourceX = selection.x + x; const sourceY = selection.y + y;
        if (!wandMaskContains(selection, sourceX, sourceY)) continue;
        const boundary = !wandMaskContains(selection, sourceX - 1, sourceY) || !wandMaskContains(selection, sourceX + 1, sourceY)
          || !wandMaskContains(selection, sourceX, sourceY - 1) || !wandMaskContains(selection, sourceX, sourceY + 1);
        const offset = (y * width + x) * 4;
        if (overlay) { data[offset] = 49; data[offset + 1] = 88; data[offset + 2] = 219; }
        const alpha = edge.edge === 'antialias' && boundary ? .5 : 1;
        data[offset + 3] = Math.round((overlay ? 78 : 255) * alpha);
      }
    }
    context.putImageData(image, 0, 0);
    if (edge.edge !== 'feather') return canvas;
    const blurred = document.createElement('canvas'); blurred.width = width; blurred.height = height;
    const blurredContext = blurred.getContext('2d');
    try { blurredContext.filter = `blur(${edge.feather}px)`; blurredContext.drawImage(canvas, 0, 0); } catch { blurredContext.drawImage(canvas, 0, 0); }
    return blurred;
  }

  function wandOverlayHref(selection) {
    if (!selection.overlayHref) selection.overlayHref = wandMaskCanvas(selection, true).toDataURL('image/png');
    return selection.overlayHref;
  }

  function wandMarqueePath(selection) {
    if (selection.marqueePath !== undefined) return selection.marqueePath;
    if (selection.width * selection.height > 1_500_000) { selection.marqueePath = ''; return selection.marqueePath; }
    const commands = []; const add = (command) => { if (commands.length < 60_000) commands.push(command); };
    for (let y = selection.y; y < selection.y + selection.height; y += 1) {
      for (let x = selection.x; x < selection.x + selection.width; x += 1) {
        if (!wandMaskContains(selection, x, y)) continue;
        if (!wandMaskContains(selection, x, y - 1)) add(`M${x} ${y}H${x + 1}`);
        if (!wandMaskContains(selection, x + 1, y)) add(`M${x + 1} ${y}V${y + 1}`);
        if (!wandMaskContains(selection, x, y + 1)) add(`M${x} ${y + 1}H${x + 1}`);
        if (!wandMaskContains(selection, x - 1, y)) add(`M${x} ${y}V${y + 1}`);
      }
    }
    selection.marqueePath = commands.join(' '); return selection.marqueePath;
  }

  function renderBitmapMarquee(root) {
    const gesture = ['bitmap-marquee', 'bitmap-ellipse-marquee', 'bitmap-lasso', 'bitmap-polygon-lasso'].includes(state.gesture?.kind) ? state.gesture : null;
    const object = objectEntry(gesture?.objectId || state.bitmapMarquee?.objectId)?.object;
    let selection = state.bitmapMarquee;
    if (gesture && object) {
      if (['bitmap-marquee', 'bitmap-ellipse-marquee'].includes(gesture.kind)) {
        const settings = marqueeSettings();
        const bounds = bitmapMarqueeRect(gesture.start, gesture.current, bitmapPixelDimensions(object), { ...settings, clip: false });
        selection = {
          objectId: gesture.objectId, ...bounds, edge: settings.edge, feather: settings.feather,
          ...(gesture.kind === 'bitmap-ellipse-marquee' ? { kind: 'ellipse-marquee', ellipse: bounds } : {})
        };
      } else {
        const points = [...gesture.points]; const last = points.at(-1);
        if (gesture.current && (!last || Math.hypot(gesture.current.x - last.x, gesture.current.y - last.y) > .1)) points.push(gesture.current);
        selection = {
          objectId: gesture.objectId, kind: gesture.kind === 'bitmap-lasso' ? 'lasso' : 'polygon-lasso',
          ...bitmapLassoBounds(points, bitmapPixelDimensions(object)), points, edge: marqueeSettings().edge, feather: marqueeSettings().feather,
          preview: true, closed: gesture.kind === 'bitmap-lasso'
        };
      }
    }
    if (!selection || object?.type !== 'image') return;
    const pixelWidth = Math.max(1, num(object.pixelWidth, 1)); const pixelHeight = Math.max(1, num(object.pixelHeight, 1));
    const group = E('g', { transform: objectTransform(object), 'data-ui-overlay': 'bitmap-marquee', 'pointer-events': 'none' });
    const x = selection.x * num(object.width, 1) / pixelWidth; const y = selection.y * num(object.height, 1) / pixelHeight;
    const width = selection.width * num(object.width, 1) / pixelWidth; const height = selection.height * num(object.height, 1) / pixelHeight;
    if (selection.kind === 'ellipse-marquee') {
      const ellipse = selection.ellipse || selection;
      const ellipseX = num(ellipse.x) * num(object.width, 1) / pixelWidth; const ellipseY = num(ellipse.y) * num(object.height, 1) / pixelHeight;
      const ellipseWidth = num(ellipse.width, 1) * num(object.width, 1) / pixelWidth; const ellipseHeight = num(ellipse.height, 1) * num(object.height, 1) / pixelHeight;
      group.append(E('ellipse', {
        cx: ellipseX + ellipseWidth / 2, cy: ellipseY + ellipseHeight / 2, rx: ellipseWidth / 2, ry: ellipseHeight / 2,
        fill: 'rgba(255,255,255,.08)', stroke: '#172033', 'stroke-width': 2 / state.zoom,
        'stroke-dasharray': selection.edge === 'hard' ? `${5 / state.zoom} ${3 / state.zoom}` : selection.edge === 'antialias' ? `${2 / state.zoom} ${2 / state.zoom}` : `${8 / state.zoom} ${3 / state.zoom}`,
        'data-bitmap-marquee': 'true', 'data-bitmap-marquee-edge': selection.edge || 'hard', 'data-bitmap-selection-type': selection.kind
      }));
      root.append(group); return;
    }
    if (['lasso', 'polygon-lasso'].includes(selection.kind) && Array.isArray(selection.points)) {
      const scaleX = num(object.width, 1) / pixelWidth; const scaleY = num(object.height, 1) / pixelHeight;
      const points = selection.points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));
      const closed = selection.closed !== false && points.length >= 3;
      const path = points.length ? `${points.map((point, index) => `${index ? 'L' : 'M'}${point.x} ${point.y}`).join(' ')}${closed ? ' Z' : ''}` : '';
      if (path) {
        if (closed) group.append(E('path', { d: `${points.map((point, index) => `${index ? 'L' : 'M'}${point.x} ${point.y}`).join(' ')} Z`, fill: 'rgba(49,88,219,.13)', stroke: 'none', 'data-bitmap-selection-fill': 'true' }));
        group.append(E('path', { d: path, fill: 'none', stroke: '#ffffff', 'stroke-width': 3.8 / state.zoom, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', 'vector-effect': 'non-scaling-stroke' }));
        group.append(E('path', { d: path, fill: 'none', stroke: '#172033', 'stroke-width': 1.35 / state.zoom, 'stroke-dasharray': `${7 / state.zoom} ${4 / state.zoom}`, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', 'vector-effect': 'non-scaling-stroke', 'data-bitmap-marquee': 'true', 'data-bitmap-marquee-edge': selection.edge || 'hard', 'data-bitmap-selection-type': selection.kind }));
      }
      root.append(group); return;
    }
    if (selection.kind === 'wand' && selection.mask) {
      group.append(E('image', { x, y, width, height, href: wandOverlayHref(selection), preserveAspectRatio: 'none', opacity: .86 }));
      const path = wandMarqueePath(selection);
      if (path) {
        const scaleX = num(object.width, 1) / pixelWidth; const scaleY = num(object.height, 1) / pixelHeight;
        group.append(E('path', { d: path, transform: `scale(${scaleX} ${scaleY})`, fill: 'none', stroke: '#ffffff', 'stroke-width': 2.8 / state.zoom, 'vector-effect': 'non-scaling-stroke', 'data-wand-marquee': 'true' }));
        group.append(E('path', { d: path, transform: `scale(${scaleX} ${scaleY})`, fill: 'none', stroke: '#172033', 'stroke-width': 1.2 / state.zoom, 'stroke-dasharray': `${4 / state.zoom} ${3 / state.zoom}`, 'vector-effect': 'non-scaling-stroke', 'data-wand-marquee-dashes': 'true' }));
      }
    }
    group.append(E('rect', {
      x, y, width, height,
      fill: selection.kind === 'wand' ? 'rgba(49,88,219,.04)' : 'rgba(255,255,255,.08)', stroke: selection.kind === 'wand' ? '#3158db' : '#172033', 'stroke-width': selection.kind === 'wand' ? 2.4 / state.zoom : 2 / state.zoom,
      'stroke-dasharray': selection.edge === 'hard' ? `${5 / state.zoom} ${3 / state.zoom}` : selection.edge === 'antialias' ? `${2 / state.zoom} ${2 / state.zoom}` : `${8 / state.zoom} ${3 / state.zoom}`,
      'data-bitmap-marquee': 'true', 'data-bitmap-marquee-edge': selection.edge || 'hard', 'data-bitmap-selection-type': selection.kind || 'marquee'
    }));
    root.append(group);
  }

  function renderRetouchSource(root) {
    const source = state.retouchSource; const object = objectEntry(source?.objectId)?.object;
    if (!['clone', 'heal'].includes(state.tool) || !source || object?.type !== 'image') return;
    const dimensions = bitmapPixelDimensions(object); const x = (source.x + .5) * num(object.width, 1) / dimensions.width; const y = (source.y + .5) * num(object.height, 1) / dimensions.height;
    const size = 6 / state.zoom; const group = E('g', { transform: objectTransform(object), 'data-ui-overlay': 'retouch-source', 'pointer-events': 'none' });
    group.append(E('circle', { cx: x, cy: y, r: size, fill: 'rgba(255,255,255,.72)', stroke: '#172033', 'stroke-width': 1.5 / state.zoom }));
    group.append(E('path', { d: `M ${x - size * 1.6} ${y} H ${x + size * 1.6} M ${x} ${y - size * 1.6} V ${y + size * 1.6}`, stroke: '#172033', 'stroke-width': 1.5 / state.zoom }));
    root.append(group);
  }

  function renderLiquifyCursor(root) {
    const object = primaryObject(); const point = state.warpCursorPoint;
    if (state.tool !== 'liquify') return;
    if (!state.spaceDown) refs.artboard.style.cursor = cursorForTool();
    if (object?.type !== 'image' || !point) return;
    const local = bitmapLocalPoint(object, point); if (!local) return;
    const dimensions = bitmapPixelDimensions(object); const settings = liquifySettings();
    const radiusX = settings.size * num(object.width, 1) / dimensions.width / 2;
    const radiusY = settings.size * num(object.height, 1) / dimensions.height / 2;
    const group = E('g', { transform: objectTransform(object), 'data-liquify-cursor': 'true', 'pointer-events': 'none' });
    group.append(
      E('ellipse', { cx: local.x, cy: local.y, rx: radiusX, ry: radiusY, fill: 'none', stroke: 'rgba(23,32,51,.92)', 'stroke-width': 3 / state.zoom, 'vector-effect': 'non-scaling-stroke' }),
      E('ellipse', { cx: local.x, cy: local.y, rx: radiusX, ry: radiusY, fill: 'none', stroke: 'rgba(255,255,255,.96)', 'stroke-width': 1 / state.zoom, 'vector-effect': 'non-scaling-stroke' }),
      E('circle', { cx: local.x, cy: local.y, r: 1.7 / state.zoom, fill: '#ffffff', stroke: '#172033', 'stroke-width': 1 / state.zoom, 'vector-effect': 'non-scaling-stroke' })
    );
    root.append(group); refs.artboard.style.cursor = 'none';
  }

  function warpOverlayPoint(session, object, point) {
    return {
      x: point.x * num(object.width, 1) / session.width,
      y: point.y * num(object.height, 1) / session.height
    };
  }

  function warpGridPath(session, object, points) {
    const local = points.map((point) => warpOverlayPoint(session, object, point)); const commands = [];
    for (let row = 0; row < session.rows; row += 1) {
      for (let column = 0; column < session.columns; column += 1) {
        const point = local[row * session.columns + column]; commands.push(`${column ? 'L' : 'M'}${round(point.x, 3)} ${round(point.y, 3)}`);
      }
    }
    for (let column = 0; column < session.columns; column += 1) {
      for (let row = 0; row < session.rows; row += 1) {
        const point = local[row * session.columns + column]; commands.push(`${row ? 'L' : 'M'}${round(point.x, 3)} ${round(point.y, 3)}`);
      }
    }
    return commands.join(' ');
  }

  function stretchOverlaySelection(session) {
    const gesture = state.gesture;
    if (gesture?.kind === 'stretch-select' && gesture.session === session) {
      const start = gesture.axis === 'vertical' ? gesture.start.y : gesture.start.x;
      const end = gesture.axis === 'vertical' ? gesture.current.y : gesture.current.x;
      return { start: Math.min(start, end), end: Math.max(start, end), provisional: true };
    }
    return session.selection ? { ...session.selection, provisional: false } : null;
  }

  function renderStretchOverlay(group, session, object) {
    const selection = stretchOverlaySelection(session); if (!selection) return;
    const vertical = session.axis === 'vertical';
    const objectWidth = Math.max(1, num(object.width, 1)); const objectHeight = Math.max(1, num(object.height, 1));
    const scale = vertical ? objectHeight / session.height : objectWidth / session.width;
    const start = selection.start * scale; const end = selection.end * scale;
    const rectangle = vertical
      ? { x: 0, y: start, width: objectWidth, height: Math.max(.01, end - start) }
      : { x: start, y: 0, width: Math.max(.01, end - start), height: objectHeight };
    group.append(E('rect', {
      ...rectangle, fill: 'rgba(255,122,48,.12)', stroke: 'none', 'pointer-events': 'none',
      'data-stretch-band': selection.provisional ? 'preview' : session.axis
    }));
    const linePath = vertical ? `M0 ${start} H${objectWidth} M0 ${end} H${objectWidth}` : `M${start} 0 V${objectHeight} M${end} 0 V${objectHeight}`;
    group.append(
      E('path', { d: linePath, fill: 'none', stroke: '#ffffff', 'stroke-width': 4 / state.zoom, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none' }),
      E('path', { d: linePath, fill: 'none', stroke: '#ef6c2f', 'stroke-width': 1.6 / state.zoom, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none', 'data-stretch-boundaries': session.axis })
    );
    if (!selection.provisional && Math.abs(num(session.amount)) > .001) {
      const geometry = BitmapWarp.axisStretchGeometry(vertical ? session.height : session.width, selection.start, selection.end, session.amount);
      const targetStart = geometry.targetStart * scale; const targetEnd = geometry.targetEnd * scale;
      const targetPath = vertical ? `M0 ${targetStart} H${objectWidth} M0 ${targetEnd} H${objectWidth}` : `M${targetStart} 0 V${objectHeight} M${targetEnd} 0 V${objectHeight}`;
      group.append(E('path', {
        d: targetPath, fill: 'none', stroke: '#3158db', 'stroke-width': 1.35 / state.zoom,
        'stroke-dasharray': `${5 / state.zoom} ${3 / state.zoom}`, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none', 'data-stretch-target': session.amount
      }));
    }
    if (selection.provisional) return;
    const cross = vertical ? objectWidth / 2 : objectHeight / 2; const radius = 8 / state.zoom;
    [['start', start], ['end', end]].forEach(([edge, position]) => {
      const cx = vertical ? cross : position; const cy = vertical ? position : cross;
      group.append(E('circle', {
        cx, cy, r: radius, fill: '#ffffff', stroke: '#ef6c2f', 'stroke-width': 2 / state.zoom,
        'vector-effect': 'non-scaling-stroke', 'pointer-events': 'all', cursor: vertical ? 'ns-resize' : 'ew-resize', 'data-stretch-handle': edge
      }));
      const arrow = vertical
        ? `M${cx} ${cy - radius * .55} V${cy + radius * .55} M${cx - radius * .28} ${cy - radius * .28} L${cx} ${cy - radius * .58} ${cx + radius * .28} ${cy - radius * .28} M${cx - radius * .28} ${cy + radius * .28} L${cx} ${cy + radius * .58} ${cx + radius * .28} ${cy + radius * .28}`
        : `M${cx - radius * .55} ${cy} H${cx + radius * .55} M${cx - radius * .28} ${cy - radius * .28} L${cx - radius * .58} ${cy} ${cx - radius * .28} ${cy + radius * .28} M${cx + radius * .28} ${cy - radius * .28} L${cx + radius * .58} ${cy} ${cx + radius * .28} ${cy + radius * .28}`;
      group.append(E('path', { d: arrow, fill: 'none', stroke: '#ef6c2f', 'stroke-width': 1.4 / state.zoom, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none' }));
    });
  }

  function renderWarpOverlay(root) {
    const session = state.warpSession; if (!session) return;
    const object = objectEntry(session.objectId)?.object; if (!object) return;
    attachDirectBitmapPreview(session, object);
    const group = E('g', { transform: objectTransform(object), 'data-warp-overlay': session.tool });
    if (session.tool === 'stretch-area') {
      renderStretchOverlay(group, session, object); root.append(group); return;
    }
    const showMesh = session.tool !== 'puppet-warp' || Boolean($('puppetShowMesh')?.checked);
    if (showMesh) {
      const pathData = warpGridPath(session, object, session.destinationPoints);
      group.append(
        E('path', { d: pathData, fill: 'none', stroke: 'rgba(255,255,255,.94)', 'stroke-width': 3.6 / state.zoom, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none' }),
        E('path', { d: pathData, fill: 'none', stroke: '#3158db', 'stroke-width': 1.35 / state.zoom, 'stroke-dasharray': `${5 / state.zoom} ${3 / state.zoom}`, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none', 'data-warp-mesh': session.tool })
      );
    }
    if (['mesh-warp', 'shape-grid'].includes(session.tool)) {
      session.destinationPoints.forEach((point, index) => {
        const local = warpOverlayPoint(session, object, point);
        group.append(E('circle', {
          cx: local.x, cy: local.y, r: 5.4 / state.zoom, fill: '#ffffff', stroke: '#3158db', 'stroke-width': 2 / state.zoom,
          'vector-effect': 'non-scaling-stroke', 'pointer-events': 'all', cursor: 'move', 'data-warp-handle': index,
          'data-warp-handle-tool': session.tool
        }));
      });
      const gesture = state.gesture?.kind === 'warp-control' && state.gesture.session === session ? state.gesture : null;
      if (gesture && session.tool === 'shape-grid' && !gesture.precise) {
        const point = warpOverlayPoint(session, object, gesture.originPoints[gesture.index]);
        const radius = clamp(num($('shapeGridRadius')?.value, 140), 1, 2000);
        group.prepend(E('ellipse', {
          cx: point.x, cy: point.y, rx: radius * num(object.width, 1) / session.width, ry: radius * num(object.height, 1) / session.height,
          fill: 'rgba(49,88,219,.05)', stroke: '#3158db', 'stroke-width': 1.2 / state.zoom,
          'stroke-dasharray': `${4 / state.zoom} ${3 / state.zoom}`, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none', 'data-shape-grid-influence': radius
        }));
      }
    } else if (session.tool === 'puppet-warp') {
      session.pins.forEach((pin) => {
        const local = warpOverlayPoint(session, object, pin.target); const size = (pin.locked ? 4.1 : 6.2) / state.zoom;
        if (pin.locked) {
          group.append(E('rect', { x: local.x - size, y: local.y - size, width: size * 2, height: size * 2, fill: '#f2f4ff', stroke: '#5366be', 'stroke-width': 1.4 / state.zoom, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none', 'data-puppet-anchor': pin.id }));
        } else {
          group.append(E('circle', {
            cx: local.x, cy: local.y, r: size, fill: '#ffd84d', stroke: '#172033', 'stroke-width': 2 / state.zoom,
            'vector-effect': 'non-scaling-stroke', 'pointer-events': 'all', cursor: 'move', 'data-puppet-pin': pin.id
          }));
          group.append(E('circle', { cx: local.x, cy: local.y, r: 1.45 / state.zoom, fill: '#172033', 'pointer-events': 'none' }));
        }
      });
    }
    root.append(group);
  }

  function bitmapPixelGridDescriptor() {
    if (!state.pixelGrid) return { mode: 'hidden' };
    const object = selectedObjects().length === 1 && primaryObject()?.type === 'image' ? primaryObject() : null;
    if (object && BitmapModel.needsLocalGrid(object)) {
      const scale = BitmapModel.pixelScale(object); const screenCell = Math.min(Math.abs(scale.x), Math.abs(scale.y)) * state.zoom;
      return screenCell >= 4 ? { mode: 'bitmap', object, scale, screenCell } : { mode: 'hidden', object, scale, screenCell };
    }
    return state.zoom >= 8 ? { mode: 'document' } : { mode: 'hidden' };
  }

  function documentPixelPreviewTextObjects() {
    return renderableObjects().filter((object) => objectTree(symbolRenderObject(object) || object)
      .some((entry) => entry.type === 'text' && entry.visible !== false));
  }

  function documentPixelPreviewAvailable(descriptor = bitmapPixelGridDescriptor()) {
    const width = Math.max(1, Math.round(num(state.doc.width, 1))); const height = Math.max(1, Math.round(num(state.doc.height, 1)));
    return descriptor.mode === 'document' && !state.gesture && !state.inlineTextSession
      && width <= 16384 && height <= 16384 && width * height <= 100_000_000
      && documentPixelPreviewTextObjects().length > 0;
  }

  function setDocumentPixelPreviewActive(active) {
    const preview = refs.artboard.querySelector('[data-document-pixel-preview]');
    const enabled = Boolean(active && preview?.isConnected);
    refs.artboard.dataset.documentPixelPreview = String(enabled);
  }

  function discardDocumentPixelPreview() {
    documentPixelPreviewToken += 1;
    if (documentPixelPreviewTimer) clearTimeout(documentPixelPreviewTimer);
    documentPixelPreviewTimer = null; documentPixelPreviewRenderedRevision = -1;
    refs.artboard.querySelectorAll('[data-document-pixel-preview]').forEach((node) => node.remove());
    if (documentPixelPreviewUrl) URL.revokeObjectURL(documentPixelPreviewUrl);
    documentPixelPreviewUrl = ''; setDocumentPixelPreviewActive(false);
  }

  function invalidateDocumentPixelPreview({ discard = false } = {}) {
    documentPixelPreviewRevision += 1;
    if (discard) discardDocumentPixelPreview();
    else setDocumentPixelPreviewActive(false);
  }

  async function createDocumentPixelPreview(revision, token) {
    const descriptor = bitmapPixelGridDescriptor();
    if (!documentPixelPreviewAvailable(descriptor) || revision !== documentPixelPreviewRevision || token !== documentPixelPreviewToken) return;
    const width = Math.max(1, Math.round(num(state.doc.width, 1))); const height = Math.max(1, Math.round(num(state.doc.height, 1)));
    try {
      const { source } = await exportSvgSource({ includeBackground: true, keepMetadata: true, embedFonts: true });
      if (!documentPixelPreviewAvailable() || revision !== documentPixelPreviewRevision || token !== documentPixelPreviewToken) return;
      const image = await imageFromSvgSource(source);
      if (!documentPixelPreviewAvailable() || revision !== documentPixelPreviewRevision || token !== documentPixelPreviewToken) return;
      const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height;
      const context = canvas.getContext('2d');
      if (!context) return;
      context.imageSmoothingEnabled = false; context.clearRect(0, 0, width, height); context.drawImage(image, 0, 0, width, height);
      const blob = await canvasPngBlob(canvas);
      if (!documentPixelPreviewAvailable() || revision !== documentPixelPreviewRevision || token !== documentPixelPreviewToken) return;
      const url = URL.createObjectURL(blob);
      const preview = E('image', {
        href: url, x: 0, y: 0, width, height, preserveAspectRatio: 'none',
        'image-rendering': 'pixelated', 'pointer-events': 'none',
        'data-ui-overlay': 'document-pixel-preview', 'data-document-pixel-preview': 'true',
        'data-preview-width': width, 'data-preview-height': height
      });
      refs.artboard.querySelectorAll('[data-document-pixel-preview]').forEach((node) => node.remove());
      if (artboardScene.overlayRoot?.isConnected) refs.artboard.insertBefore(preview, artboardScene.overlayRoot);
      else refs.artboard.append(preview);
      if (documentPixelPreviewUrl) URL.revokeObjectURL(documentPixelPreviewUrl);
      documentPixelPreviewUrl = url; documentPixelPreviewRenderedRevision = revision;
      setDocumentPixelPreviewActive(true);
    } catch {
      setDocumentPixelPreviewActive(false);
    }
  }

  function scheduleDocumentPixelPreview(descriptor = bitmapPixelGridDescriptor()) {
    if (!documentPixelPreviewAvailable(descriptor)) {
      if (descriptor.mode !== 'document' || !state.pixelGrid || !documentPixelPreviewTextObjects().length) discardDocumentPixelPreview();
      else setDocumentPixelPreviewActive(false);
      return;
    }
    if (documentPixelPreviewRenderedRevision === documentPixelPreviewRevision
      && refs.artboard.querySelector('[data-document-pixel-preview]')) {
      setDocumentPixelPreviewActive(true); return;
    }
    if (documentPixelPreviewTimer) clearTimeout(documentPixelPreviewTimer);
    const revision = documentPixelPreviewRevision; const token = ++documentPixelPreviewToken;
    documentPixelPreviewTimer = setTimeout(() => {
      documentPixelPreviewTimer = null; void createDocumentPixelPreview(revision, token);
    }, 80);
  }

  function renderBitmapPixelGrid(root, descriptor) {
    const { object, scale } = descriptor; if (!object || scale.x <= 0 || scale.y <= 0) return;
    const safeId = String(object.id || 'bitmap').replace(/[^a-z0-9_-]/gi, '-'); const patternId = `bitmap-pixel-grid-${safeId}`;
    const definitions = E('defs'); const pattern = E('pattern', {
      id: patternId, width: scale.x, height: scale.y, patternUnits: 'userSpaceOnUse',
      'data-bitmap-pixel-grid-pattern': object.id
    });
    pattern.append(E('path', {
      d: `M ${scale.x} 0 H 0 V ${scale.y}`, fill: 'none', stroke: 'rgba(33,72,168,.58)',
      'stroke-width': Math.min(.12, .7 / Math.max(state.zoom, .01)), 'shape-rendering': 'crispEdges'
    })); definitions.append(pattern);
    const group = E('g', {
      transform: objectTransform(object), 'data-bitmap-pixel-grid': object.id,
      'data-grid-pixel-width': scale.x, 'data-grid-pixel-height': scale.y, 'pointer-events': 'none'
    });
    group.append(definitions, E('rect', { x: 0, y: 0, width: object.width, height: object.height, fill: `url(#${patternId})`, 'pointer-events': 'none' })); root.append(group);
  }

  function renderEditorOverlays() {
    const { width, height } = state.doc;
    const root = E('g', { 'data-ui-overlay': 'editor-overlays' });
    const canvasGrid = documentGrid();
    if (canvasGrid.visible) {
      root.append(E('rect', {
        x: 0, y: 0, width, height, fill: 'url(#canvasGridPattern)',
        'data-canvas-grid-overlay': 'true', 'data-grid-size': canvasGrid.size, 'pointer-events': 'none'
      }));
    }
    const pixelGrid = bitmapPixelGridDescriptor();
    if (pixelGrid.mode === 'document') {
      root.append(E('rect', { x: 0, y: 0, width, height, fill: 'url(#pixelGridPattern)', 'data-pixel-grid-overlay': 'true', 'pointer-events': 'none' }));
    } else if (pixelGrid.mode === 'bitmap') renderBitmapPixelGrid(root, pixelGrid);
    refs.artboard.dataset.pixelGrid = String(pixelGrid.mode !== 'hidden'); refs.artboard.dataset.pixelGridMode = pixelGrid.mode;
    renderGuides(root);
    renderCanvasResizeHandles(root);
    renderBitmapMarquee(root);
    renderRetouchSource(root);
    renderLiquifyCursor(root);
    renderWarpOverlay(root);
    if (!state.warpSession) renderSelection(root);
    if (artboardScene.overlayRoot?.isConnected) artboardScene.overlayRoot.replaceWith(root);
    else refs.artboard.append(root);
    artboardScene.overlayRoot = root;
    scheduleDocumentPixelPreview(pixelGrid);
    renderEraserCursor();
    renderPathModifierCursor();
  }

  function renderArtboard() {
    return measurePerformancePhase('render.full', () => {
      invalidateDocumentPixelPreview({ discard: true });
      settleZoomPreview({ refresh: false });
      syncSymbolMasters();
      syncBoundConnectors();
      artboardScene.beginFullRender();
      const { width, height, bg } = state.doc;
      refs.artboard.replaceChildren();
      refs.artboard.setAttribute('width', width);
      refs.artboard.setAttribute('height', height);
      refs.artboard.setAttribute('viewBox', `0 0 ${width} ${height}`);
      refs.artboard.setAttribute('aria-label', `Leinwand, ${width} mal ${height} Pixel`);
      const pixelGrid = bitmapPixelGridDescriptor();
      refs.artboard.dataset.pixelGrid = String(pixelGrid.mode !== 'hidden'); refs.artboard.dataset.pixelGridMode = pixelGrid.mode;
      refs.artboard.dataset.canvasGrid = String(documentGrid().visible);
      refs.artboard.dataset.canvasResizeMode = String(state.canvasResizeMode);
      refs.artboard.classList.toggle('is-transparent-canvas', bg === 'transparent');

      const defs = E('defs');
      const canvasGrid = documentGrid();
      if (canvasGrid.visible) {
        const gridLineWidth = Math.min(1 / Math.max(state.zoom, .06), canvasGrid.size * .12);
        const canvasGridPattern = E('pattern', { id: 'canvasGridPattern', width: canvasGrid.size, height: canvasGrid.size, patternUnits: 'userSpaceOnUse' });
        canvasGridPattern.append(E('path', {
          d: `M ${canvasGrid.size} 0 H 0 V ${canvasGrid.size}`,
          fill: 'none', stroke: 'rgba(55,91,205,.42)', 'stroke-width': gridLineWidth, 'shape-rendering': 'crispEdges'
        }));
        defs.append(canvasGridPattern);
      }
      const pattern = E('pattern', { id: 'pixelGridPattern', width: 1, height: 1, patternUnits: 'userSpaceOnUse' });
      pattern.append(E('path', { d: 'M 1 0 L 0 0 0 1', fill: 'none', stroke: 'rgba(54,65,84,.38)', 'stroke-width': .04 }));
      defs.append(pattern);
      const filterObjects = renderableObjects().flatMap((object) => objectTree(symbolRenderObject(object) || object));
      [...new Map(filterObjects.map((object) => [object.id, object])).values()].forEach((object) => appendObjectFilter(defs, object));
      artboardScene.defs = defs;
      refs.artboard.append(defs, E('rect', { x: 0, y: 0, width, height, fill: bg, stroke: 'none', 'data-canvas-background': 'true' }));

      state.doc.layers.forEach((layer) => {
        const layerGroup = E('g', { 'data-layer-id': layer.id, 'data-blend-mode': normalizeBlendMode(layer.blendMode) });
        layerGroup.style.mixBlendMode = normalizeBlendMode(layer.blendMode);
        if (!layer.visible) layerGroup.style.display = 'none';
        if (layer.locked) layerGroup.style.pointerEvents = 'none';
        layer.objects.forEach((object) => {
          const node = artboardScene.registerObjectNode(object.id, objectGroup(object)); layerGroup.append(node);
        });
        refs.artboard.append(layerGroup);
      });
      renderEditorOverlays();
      artboardScene.completeFullRender();
      refs.artboard.dataset.lastInteractionObjects = '0';
      refs.emptyHint.hidden = allObjects().length > 0;
    });
  }

  function insertArtboardObject(object, layer) {
    const layerGroup = [...refs.artboard.querySelectorAll(':scope > g[data-layer-id]')]
      .find((candidate) => candidate.dataset.layerId === layer?.id);
    if (!object || !layerGroup?.isConnected) return false;
    const node = artboardScene.registerObjectNode(object.id, objectGroup(object));
    layerGroup.append(node);
    refreshObjectFilterDefinitions(object);
    renderEditorOverlays();
    refs.emptyHint.hidden = true;
    return true;
  }

  const layerObjectDragMime = 'application/x-pyro-layer-objects';

  function topLevelObjectEntry(id) {
    const entry = objectEntry(id);
    return entry && !entry.parent && entry.container === entry.layer.objects ? entry : null;
  }

  function layerObjectDragRecords(objectId) {
    const source = topLevelObjectEntry(objectId); if (!source) return [];
    const requested = state.selected.includes(objectId) ? new Set(state.selected) : new Set([objectId]);
    const records = [];
    state.doc.layers.forEach((layer) => layer.objects.forEach((object) => {
      if (requested.has(object.id)) records.push({ object, layer });
    }));
    return records.length ? records : [{ object: source.object, layer: source.layer }];
  }

  function clearLayerObjectDropIndicators() {
    refs.layerList.querySelectorAll('.is-object-drop-before, .is-object-drop-after, .is-object-drop-target, .is-object-drop-blocked').forEach((node) => {
      node.classList.remove('is-object-drop-before', 'is-object-drop-after', 'is-object-drop-target', 'is-object-drop-blocked');
      delete node.dataset.dropPosition;
    });
  }

  function finishLayerObjectDrag() {
    clearLayerObjectDropIndicators();
    refs.layerList.classList.remove('is-object-dragging');
    refs.layerList.querySelectorAll('.layer-object-row.is-dragging').forEach((row) => {
      row.classList.remove('is-dragging'); row.setAttribute('aria-grabbed', 'false'); delete row.dataset.dragBlocked;
    });
    layerObjectDrag = null;
  }

  function beginLayerObjectDrag(event) {
    const row = event.target.closest?.('.layer-object-row'); if (!row || row.dataset.dragBlocked === 'true') { event.preventDefault(); return; }
    const records = layerObjectDragRecords(row.dataset.objectId);
    if (!records.length || state.gesture || state.warpSession || state.warpPreparation) { event.preventDefault(); return; }
    if (records.some(({ object, layer }) => object.locked || layer.locked)) {
      event.preventDefault(); toast('Gesperrte Objekte oder Ebenen können nicht verschoben werden', 'warning'); return;
    }
    const objectIds = records.map(({ object }) => object.id);
    layerObjectDrag = { objectIds, sourceLayerIds: [...new Set(records.map(({ layer }) => layer.id))] };
    refs.layerList.classList.add('is-object-dragging');
    objectIds.forEach((id) => {
      const sourceRow = refs.layerList.querySelector(`.layer-object-row[data-object-id="${CSS.escape(id)}"]`);
      sourceRow?.classList.add('is-dragging'); sourceRow?.setAttribute('aria-grabbed', 'true');
    });
    if (event.dataTransfer) {
      event.dataTransfer.effectAllowed = 'move';
      event.dataTransfer.setData(layerObjectDragMime, JSON.stringify(objectIds));
      event.dataTransfer.setData('text/plain', records.map(({ object }) => object.name || names[object.type] || 'Objekt').join(', '));
    }
  }

  function layerObjectDropTarget(event) {
    const drag = layerObjectDrag; if (!drag) return null;
    const objectRow = event.target.closest?.('.layer-object-row');
    if (objectRow) {
      const targetObjectId = objectRow.dataset.objectId; const entry = topLevelObjectEntry(targetObjectId);
      if (!entry) return null;
      const blocked = drag.objectIds.includes(targetObjectId) || entry.layer.locked;
      const bounds = objectRow.getBoundingClientRect(); const position = event.clientY < bounds.top + bounds.height / 2 ? 'before' : 'after';
      return { layer: entry.layer, targetObjectId, position, node: objectRow, blocked };
    }
    const layerRow = event.target.closest?.('.layer-row');
    const objectList = event.target.closest?.('.layer-object-list');
    const layerEntry = event.target.closest?.('.layer-entry');
    const layerId = layerRow?.dataset.layerId || objectList?.dataset.layerId || layerEntry?.dataset.layerId;
    const layer = state.doc.layers.find((entry) => entry.id === layerId); if (!layer) return null;
    return { layer, targetObjectId: null, position: 'front', node: layerRow || objectList || layerEntry, blocked: layer.locked };
  }

  function showLayerObjectDropTarget(target) {
    clearLayerObjectDropIndicators(); if (!target?.node) return;
    if (target.blocked) { target.node.classList.add('is-object-drop-blocked'); return; }
    if (target.targetObjectId) target.node.classList.add(target.position === 'before' ? 'is-object-drop-before' : 'is-object-drop-after');
    else target.node.classList.add('is-object-drop-target');
    target.node.dataset.dropPosition = target.position;
  }

  function scrollLayerListDuringDrag(event) {
    const bounds = refs.layerList.getBoundingClientRect(); const edge = Math.min(34, bounds.height / 4);
    if (event.clientY < bounds.top + edge) refs.layerList.scrollTop -= Math.ceil((bounds.top + edge - event.clientY) / 4);
    else if (event.clientY > bounds.bottom - edge) refs.layerList.scrollTop += Math.ceil((event.clientY - (bounds.bottom - edge)) / 4);
  }

  function moveLayerObjectsFromDrop(drag, target) {
    const destination = target?.layer; if (!drag?.objectIds?.length || !destination || destination.locked) return false;
    const ids = new Set(drag.objectIds); if (target.targetObjectId && ids.has(target.targetObjectId)) return false;
    const records = [];
    state.doc.layers.forEach((layer) => layer.objects.forEach((object) => {
      if (ids.has(object.id)) records.push({ object, layer });
    }));
    if (records.length !== ids.size || records.some(({ object, layer }) => object.locked || layer.locked)) return false;
    const sourceLayers = new Set(records.map(({ layer }) => layer.id)); const moving = records.map(({ object }) => object);
    state.doc.layers.forEach((layer) => { layer.objects = layer.objects.filter((object) => !ids.has(object.id)); });
    let insertion = destination.objects.length;
    if (target.targetObjectId) {
      const targetIndex = destination.objects.findIndex((object) => object.id === target.targetObjectId);
      if (targetIndex >= 0) insertion = targetIndex + (target.position === 'before' ? 1 : 0);
    }
    destination.objects.splice(clamp(insertion, 0, destination.objects.length), 0, ...moving);
    state.doc.activeLayerId = destination.id; state.expandedLayerIds.add(destination.id); clearVectorSubselection();
    state.selected = destination.visible ? moving.filter((object) => object.visible !== false).map((object) => object.id) : [];
    const crossLayer = sourceLayers.size > 1 || !sourceLayers.has(destination.id);
    const label = crossLayer
      ? I18n.t(moving.length === 1 ? 'status.objectMovedToLayer' : 'status.objectsMovedToLayer', { count: moving.length })
      : I18n.t(moving.length === 1 ? 'ui.objectReorderedWithinLayer' : 'status.reorderedObjects', { count: moving.length });
    const changed = commitHistory(label); renderAll();
    if (changed) toast(crossLayer
      ? I18n.t(moving.length === 1 ? 'status.objectMovedToNamedLayer' : 'status.objectsMovedToNamedLayer', { count: moving.length, name: displayedLayerName(destination) })
      : I18n.t(moving.length === 1 ? 'ui.objectReorderedWithinLayer' : 'status.reorderedObjects', { count: moving.length }));
    return changed;
  }

  function dragLayerObjectOver(event) {
    if (!layerObjectDrag) return;
    const target = layerObjectDropTarget(event); showLayerObjectDropTarget(target); scrollLayerListDuringDrag(event);
    if (!target || target.blocked) { if (event.dataTransfer) event.dataTransfer.dropEffect = 'none'; return; }
    event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
  }

  function dropLayerObject(event) {
    const drag = layerObjectDrag; const target = layerObjectDropTarget(event);
    if (!drag) return;
    event.preventDefault(); event.stopPropagation();
    if (!target || target.blocked) { finishLayerObjectDrag(); return; }
    layerObjectDrag = null; clearLayerObjectDropIndicators(); refs.layerList.classList.remove('is-object-dragging');
    if (!moveLayerObjectsFromDrop(drag, target)) finishLayerObjectDrag();
  }

  function layerObjectThumbnail(object) {
    const thumbnail = document.createElement('span');
    thumbnail.className = 'layer-object-thumbnail';
    thumbnail.dataset.objectType = object.type;
    thumbnail.setAttribute('aria-hidden', 'true');
    try {
      const preview = reidentifyObjectTree(object);
      preview.visible = true;
      preview.locked = false;
      objectTree(preview).forEach((entry) => {
        if (entry.type === 'text') delete entry.pathBinding;
      });

      const bounds = boundsOf([object], { visual: true });
      const width = Math.max(.001, num(bounds.width, 1)); const height = Math.max(.001, num(bounds.height, 1));
      const paddingX = Math.max(.5, width * .08); const paddingY = Math.max(.5, height * .08);
      const svg = E('svg', {
        viewBox: `${round(bounds.x - paddingX, 3)} ${round(bounds.y - paddingY, 3)} ${round(width + paddingX * 2, 3)} ${round(height + paddingY * 2, 3)}`,
        preserveAspectRatio: 'xMidYMid meet', focusable: 'false', 'data-layer-thumbnail-svg': object.type
      });
      const definitions = E('defs');
      objectTree(preview).flatMap((entry) => objectTree(symbolRenderObject(entry) || entry)).forEach((entry) => appendObjectFilter(definitions, entry));
      if (definitions.childElementCount) svg.append(definitions);
      svg.append(objectGroup(preview, false));
      thumbnail.append(svg);
      thumbnail.dataset.rendered = 'true';
    } catch (error) {
      thumbnail.classList.add('is-fallback');
      thumbnail.textContent = object.type === 'group' ? 'Gr.' : object.type === 'mask-group' ? 'Ma.' : object.type === 'clip-group' ? 'Cl.' : (names[object.type] || 'Obj.').slice(0, 2);
      thumbnail.title = `Vorschau konnte nicht gerendert werden: ${error?.message || error}`;
    }
    return thumbnail;
  }

  function createLayerObjectRow(object, layer) {
    const objectRow = document.createElement('div');
    const isKeyObject = object.id === alignmentKeyObjectId();
    objectRow.className = `layer-object-row${state.selected.includes(object.id) ? ' is-selected' : ''}${isKeyObject ? ' is-key-object' : ''}${object.visible === false ? ' is-hidden' : ''}${object.locked ? ' is-locked' : ''}`;
    objectRow.dataset.objectId = object.id; objectRow.draggable = !layer.locked && !object.locked; objectRow.setAttribute('aria-grabbed', 'false');
    const displayedName = displayedObjectName(object);
    const displayedType = I18n.translateSource(names[object.type] || object.type);
    objectRow.dataset.baseTitle = `${displayedName} · ${displayedType} · ${I18n.translateSource('Ziehen zum Anordnen oder Verschieben')}`;
    objectRow.title = `${objectRow.dataset.baseTitle}${isKeyObject ? ` · ${I18n.t('ui.keyObject')}` : ''}`;
    objectRow.setAttribute('aria-label', objectRow.title);
    if (isKeyObject) objectRow.setAttribute('aria-current', 'true');

    const dragHandle = document.createElement('span'); dragHandle.className = 'layer-object-drag-handle'; dragHandle.title = 'Objekt ziehen'; dragHandle.setAttribute('aria-hidden', 'true');
    dragHandle.innerHTML = Tabler.svg('grip-vertical');

    const objectVisibility = document.createElement('button');
    objectVisibility.type = 'button'; objectVisibility.className = 'object-visibility'; objectVisibility.title = object.visible === false ? 'Objekt einblenden' : 'Objekt ausblenden'; objectVisibility.setAttribute('aria-label', objectVisibility.title);
    objectVisibility.innerHTML = Tabler.svg(object.visible === false ? 'eye-off' : 'eye');
    objectVisibility.addEventListener('click', (event) => {
      event.stopPropagation(); object.visible = object.visible === false;
      if (object.visible === false) state.selected = state.selected.filter((id) => id !== object.id);
      commitHistory(object.visible === false ? 'Objekt ausgeblendet' : 'Objekt eingeblendet'); renderAll();
    });

    const objectLock = document.createElement('button');
    objectLock.type = 'button'; objectLock.className = 'object-lock'; objectLock.title = object.locked ? 'Objekt entsperren' : 'Objekt sperren'; objectLock.setAttribute('aria-label', objectLock.title);
    objectLock.innerHTML = Tabler.svg(object.locked ? 'lock' : 'lock-open');
    objectLock.addEventListener('click', (event) => {
      event.stopPropagation(); object.locked = !object.locked;
      if (object.locked) state.selected = state.selected.filter((id) => id !== object.id);
      commitHistory(object.locked ? 'Objekt gesperrt' : 'Objekt entsperrt'); renderAll();
    });

    const thumbnail = layerObjectThumbnail(object);
    const objectName = document.createElement('span'); objectName.className = 'layer-object-name'; objectName.dataset.i18nIgnoreText = ''; objectName.textContent = displayedName;
    objectRow.append(dragHandle, objectVisibility, objectLock, thumbnail, objectName);
    objectRow.addEventListener('pointerdown', (event) => { objectRow.dataset.dragBlocked = String(Boolean(event.target.closest?.('button'))); });
    objectRow.addEventListener('pointerup', () => { delete objectRow.dataset.dragBlocked; });
    objectRow.addEventListener('pointercancel', () => { delete objectRow.dataset.dragBlocked; });
    objectRow.addEventListener('click', (event) => { select([object.id], event.shiftKey); });
    return objectRow;
  }

  function insertLayerObjectRow(object, layer) {
    const entry = [...refs.layerList.querySelectorAll('.layer-entry')].find((candidate) => candidate.dataset.layerId === layer?.id);
    if (!entry) { renderLayers(); return false; }
    const label = entry.querySelector(':scope > .layer-row .layer-name');
    if (label) label.textContent = `${displayedLayerName(layer)} · ${layer.objects.length}`;
    const objectList = entry.querySelector(':scope > .layer-object-list');
    if (objectList && !objectList.hidden && !objectList.querySelector(`.layer-object-row[data-object-id="${CSS.escape(object.id)}"]`)) {
      objectList.prepend(createLayerObjectRow(object, layer));
    }
    syncLayerSelectionState();
    return true;
  }

  function beginLayerRename(layer, row, label) {
    if (!layer || !row || !label || row.querySelector('.layer-name-input')) return false;
    const input = document.createElement('input');
    input.type = 'text'; input.className = 'layer-name-input'; input.value = layer.name || displayedLayerName(layer);
    input.maxLength = 120; input.setAttribute('aria-label', I18n.t('dialog.enterLayerName'));
    row.classList.add('is-renaming'); label.replaceWith(input);
    let finished = false;
    const finish = (apply) => {
      if (finished) return;
      finished = true;
      const next = input.value.trim();
      if (apply && next && next !== layer.name) {
        layer.name = next; layer.nameManuallyEdited = true;
        commitHistory('Ebene umbenannt'); renderLayers();
        return;
      }
      input.replaceWith(label); row.classList.remove('is-renaming');
    };
    input.addEventListener('pointerdown', (event) => event.stopPropagation());
    input.addEventListener('click', (event) => event.stopPropagation());
    input.addEventListener('dblclick', (event) => event.stopPropagation());
    input.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') { event.preventDefault(); finish(true); }
      else if (event.key === 'Escape') { event.preventDefault(); finish(false); }
    });
    input.addEventListener('blur', () => finish(true));
    input.focus(); input.select(); return true;
  }

  function renderLayers() {
    const layerBlendMode = $('layerBlendMode'); const selectedLayer = activeLayer();
    if (layerBlendMode) {
      layerBlendMode.value = normalizeBlendMode(selectedLayer?.blendMode);
      layerBlendMode.disabled = !selectedLayer;
    }
    refs.layerList.replaceChildren();
    [...state.doc.layers].reverse().forEach((layer) => {
      const entry = document.createElement('div'); entry.className = 'layer-entry'; entry.dataset.layerId = layer.id;
      const row = document.createElement('div');
      row.className = `layer-row${layer.id === state.doc.activeLayerId ? ' is-active' : ''}`;
      row.dataset.layerId = layer.id;
      row.setAttribute('role', 'option');
      row.setAttribute('aria-selected', layer.id === state.doc.activeLayerId ? 'true' : 'false');

      const visibility = document.createElement('button');
      visibility.type = 'button'; visibility.className = 'layer-visibility';
      visibility.title = layer.visible ? 'Ebene ausblenden' : 'Ebene einblenden'; visibility.setAttribute('aria-label', visibility.title);
      visibility.innerHTML = Tabler.svg(layer.visible ? 'eye' : 'eye-off');
      visibility.addEventListener('click', (event) => {
        event.stopPropagation(); layer.visible = !layer.visible;
        if (!layer.visible) state.selected = state.selected.filter((id) => layerForObject(id)?.id !== layer.id);
        commitHistory('Ebenensichtbarkeit geändert'); renderAll();
      });

      const lock = document.createElement('button');
      lock.type = 'button'; lock.title = layer.locked ? 'Ebene entsperren' : 'Ebene sperren'; lock.setAttribute('aria-label', lock.title);
      lock.innerHTML = Tabler.svg(layer.locked ? 'lock' : 'lock-open');
      lock.addEventListener('click', (event) => { event.stopPropagation(); layer.locked = !layer.locked; if (layer.locked) state.selected = state.selected.filter((id) => layerForObject(id)?.id !== layer.id); commitHistory(layer.locked ? 'Ebene gesperrt' : 'Ebene entsperrt'); renderAll(); });

      const expanded = state.expandedLayerIds.has(layer.id);
      const expand = document.createElement('button');
      expand.type = 'button'; expand.className = 'layer-expand'; expand.title = expanded ? 'Objekte einklappen' : 'Objekte aufklappen'; expand.setAttribute('aria-label', expand.title); expand.setAttribute('aria-expanded', String(expanded));
      expand.innerHTML = `<svg viewBox="0 0 24 24"><path d="m8 10 4 4 4-4"/></svg>`;
      expand.addEventListener('click', (event) => {
        event.stopPropagation();
        if (state.expandedLayerIds.has(layer.id)) state.expandedLayerIds.delete(layer.id); else state.expandedLayerIds.add(layer.id);
        renderLayers();
      });

      const layerName = displayedLayerName(layer);
      const label = document.createElement('span');
      label.className = 'layer-name'; label.dataset.i18nIgnoreText = ''; label.textContent = `${layerName} · ${layer.objects.length}`; label.title = I18n.translateSource('Doppelklick zum Umbenennen');
      label.addEventListener('dblclick', (event) => {
        event.preventDefault(); event.stopPropagation(); beginLayerRename(layer, row, label);
      });
      const indicator = document.createElement('span');
      indicator.className = 'layer-active-indicator';
      indicator.textContent = layer.id === state.doc.activeLayerId ? '●' : ''; indicator.style.color = 'var(--accent)'; indicator.setAttribute('aria-hidden', 'true');
      row.append(visibility, lock, expand, label, indicator);
      row.addEventListener('keydown', (event) => {
        if (event.target === row && event.key === 'F2') {
          event.preventDefault(); beginLayerRename(layer, row, label);
        }
      });
      row.tabIndex = 0;
      row.addEventListener('click', (event) => {
        if (event.target.closest('.layer-name-input')) return;
        if (state.doc.activeLayerId === layer.id && state.selected.length === 0) return;
        state.doc.activeLayerId = layer.id; state.selected = []; renderSelectionState();
      });

      const objectList = document.createElement('div'); objectList.className = 'layer-object-list'; objectList.dataset.layerId = layer.id; objectList.hidden = !expanded;
      if (expanded) [...layer.objects].reverse().forEach((object) => {
        objectList.append(createLayerObjectRow(object, layer));
      });
      entry.append(row, objectList); refs.layerList.append(entry);
    });
  }

  function setField(id, value) {
    const field = $(id);
    if (field && document.activeElement !== field) field.value = value;
  }

  function setColorField(id, value, fallback = '#ffffff') {
    const field = $(id); if (!field) return;
    const transparent = value === 'none' || value === 'transparent';
    field.dataset.transparent = String(transparent);
    field.closest('.color-field, .inline-color, .dialog-color-field')?.classList.toggle('is-transparent', transparent);
    setField(id, transparent ? fallback : (/^#[0-9a-f]{6}$/i.test(value || '') ? value : fallback));
  }

  function backgroundFromField(id) {
    const field = $(id);
    return field?.dataset.transparent === 'true' ? 'transparent' : field?.value || '#ffffff';
  }

  function updateNewCanvasPreview() {
    const preview = document.querySelector('#newDialog .new-canvas-preview');
    if (!preview) return;
    const width = clamp(Math.round(num($('newWidth')?.value, 960)), 1, 10000);
    const height = clamp(Math.round(num($('newHeight')?.value, 640)), 1, 10000);
    const scale = Math.min(248 / width, 64 / height);
    const previewWidth = clamp(Math.round(width * scale), 28, 248);
    const previewHeight = clamp(Math.round(height * scale), 22, 64);
    const background = backgroundFromField('newBg');
    preview.style.setProperty('--preview-width', `${previewWidth}px`);
    preview.style.setProperty('--preview-height', `${previewHeight}px`);
    preview.style.setProperty('--preview-color', background === 'transparent' ? '#ffffff' : background);
    preview.classList.toggle('is-transparent', background === 'transparent');
    preview.querySelector('span')?.setAttribute('title', `${width} × ${height} px${background === 'transparent' ? ', transparent' : `, ${background}`}`);
  }

  function setNewBackground(value) {
    const field = $('newBg');
    if (!field) return;
    if (value === 'transparent') {
      field.dataset.transparent = 'true'; setColorField('newBg', 'transparent', '#ffffff');
      field.closest('.dialog-color-field')?.querySelector('em')?.replaceChildren('transparent');
    } else {
      const color = normalizeColor(value) || '#ffffff';
      field.dataset.transparent = 'false'; setColorField('newBg', color, '#ffffff');
      field.closest('.dialog-color-field')?.querySelector('em')?.replaceChildren(color);
      field.dispatchEvent(new Event('input', { bubbles: true }));
      field.dispatchEvent(new Event('change', { bubbles: true }));
    }
    updateNewCanvasPreview();
  }

  function inspectorSectionContext(objects = selectedObjects()) {
    const types = [...new Set(objects.map((object) => object.type).filter(Boolean))].sort();
    if (!types.length) return 'none';
    return types.length === 1 ? `tool:${types[0]}` : `multiple:${types.join('+')}`;
  }

  function applyInspectorSectionStates(context = inspectorSectionContext()) {
    const sections = state.inspectorSectionStates[context] || {};
    const accordionSection = state.propertyDockAccordion ? state.inspectorAccordionSections[context] : '';
    document.querySelectorAll('.property-section-toggle').forEach((button) => {
      const fieldset = button.closest('.property-fieldset'); const section = button.dataset.propertySection;
      if (!fieldset || !section) return;
      const expanded = accordionSection ? section === accordionSection : sections[section] !== false;
      fieldset.classList.toggle('is-collapsed', !expanded); button.setAttribute('aria-expanded', String(expanded));
    });
  }

  function persistInspectorAccordionSection(context, section) {
    if (!context || context === 'none' || !section) return;
    state.inspectorAccordionSections = { ...state.inspectorAccordionSections, [context]: section };
    try { localStorage.setItem(inspectorAccordionSectionsStorageKey, JSON.stringify(state.inspectorAccordionSections)); } catch { /* Lokale Einstellung ist optional. */ }
  }

  function enforceInspectorAccordion(context = inspectorSectionContext(), preferredSection = '') {
    if (!state.propertyDockAccordion || context === 'none') return null;
    const allButtons = [...document.querySelectorAll('.property-section-toggle')];
    const visibleButtons = allButtons.filter((button) => {
      const fieldset = button.closest('.property-fieldset');
      return fieldset && !fieldset.hidden;
    });
    if (!visibleButtons.length) return null;
    const requested = preferredSection || state.inspectorAccordionSections[context];
    const active = visibleButtons.find((button) => button.dataset.propertySection === requested)
      || visibleButtons.find((button) => button.getAttribute('aria-expanded') === 'true')
      || visibleButtons[0];
    allButtons.forEach((button) => {
      const expanded = button === active; const fieldset = button.closest('.property-fieldset');
      if (!fieldset) return;
      fieldset.classList.toggle('is-collapsed', !expanded); button.setAttribute('aria-expanded', String(expanded));
    });
    if (state.inspectorAccordionSections[context] !== active.dataset.propertySection) persistInspectorAccordionSection(context, active.dataset.propertySection);
    return active;
  }

  function persistInspectorSectionState(section, expanded, context = inspectorSectionContext()) {
    if (!section || context === 'none') return;
    const sections = state.inspectorSectionStates[context] || {};
    state.inspectorSectionStates[context] = { ...sections, [section]: Boolean(expanded) };
    try { localStorage.setItem(inspectorSectionsStorageKey, JSON.stringify(state.inspectorSectionStates)); } catch { /* Lokale Einstellung ist optional. */ }
  }

  function setupRightSidebarCollapsibles() {
    if (!refs.rightSidebar || refs.rightSidebar.dataset.collapsiblesReady === 'true') return;
    refs.rightSidebar.dataset.collapsiblesReady = 'true';
    let saved = {};
    try {
      const parsed = JSON.parse(localStorage.getItem(rightSidebarSectionsStorageKey) || '{}');
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) saved = Object.fromEntries(Object.entries(parsed).slice(0, 256)
        .filter(([key, expanded]) => !Security.isBlockedObjectKey(key) && key.length <= 160 && typeof expanded === 'boolean'));
    } catch { /* Defekte Einstellungen fallen auf geöffnete Bereiche zurück. */ }
    const persist = (key, expanded) => {
      saved[key] = Boolean(expanded);
      try { localStorage.setItem(rightSidebarSectionsStorageKey, JSON.stringify(saved)); } catch { /* Lokale Einstellung ist optional. */ }
    };
    const chevron = () => {
      const svg = document.createElementNS(svgNS, 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true');
      const path = document.createElementNS(svgNS, 'path'); path.setAttribute('d', 'm7 10 5 5 5-5'); svg.append(path); return svg;
    };
    const connect = (container, button, content, key, defaultExpanded = true) => {
      const apply = (expanded) => {
        button.setAttribute('aria-expanded', String(expanded)); content.hidden = !expanded;
        container.classList.toggle('is-collapsed', !expanded);
        const action = expanded ? 'einklappen' : 'aufklappen'; const label = button.dataset.sidebarCollapseLabel || 'Bereich';
        button.title = `${label} ${action}`; button.setAttribute('aria-label', `${label} ${action}`);
      };
      apply(Object.hasOwn(saved, key) ? saved[key] !== false : defaultExpanded);
      button.addEventListener('click', () => {
        const expanded = button.getAttribute('aria-expanded') !== 'true'; apply(expanded); persist(key, expanded);
      });
    };
    const panelDefinitions = [
      ['#documentOverviewPanel', 'panel:document', true], ['.modifier-panel', 'panel:modifier', true],
      ['#reusableAssetsPanel', 'panel:library', false], ['.layers-panel', 'panel:layers', true]
    ];
    panelDefinitions.forEach(([selector, key, defaultExpanded]) => {
      const panel = refs.rightSidebar.querySelector(selector); const heading = panel?.querySelector(':scope > .panel-heading');
      const labels = heading?.querySelector(':scope > div'); if (!panel || !heading || !labels) return;
      const content = document.createElement('div'); content.className = 'sidebar-panel-content'; content.id = `sidebar-${key.replace(':', '-')}-content`;
      [...panel.children].filter((child) => child !== heading).forEach((child) => content.append(child)); panel.append(content);
      const button = document.createElement('button'); button.type = 'button'; button.className = 'sidebar-panel-toggle';
      button.dataset.sidebarCollapseKey = key; button.dataset.sidebarCollapseLabel = labels.querySelector('h2')?.textContent?.trim() || 'Bereich';
      button.setAttribute('aria-controls', content.id); button.append(chevron()); heading.append(button);
      connect(panel, button, content, key, defaultExpanded);
    });
    const wrapSubsection = (element, key, label, defaultExpanded = false) => {
      if (!element || element.closest('.sidebar-subsection')) return;
      const wrapper = document.createElement('section'); wrapper.className = 'sidebar-subsection'; wrapper.dataset.sidebarSection = key;
      const button = document.createElement('button'); button.type = 'button'; button.className = 'sidebar-subsection__toggle';
      button.dataset.sidebarCollapseKey = key; button.dataset.sidebarCollapseLabel = label; button.setAttribute('aria-controls', `sidebar-${key.replace(':', '-')}-content`);
      const title = document.createElement('span'); title.textContent = label; button.append(title, chevron());
      const content = document.createElement('div'); content.className = 'sidebar-subsection__body'; content.id = button.getAttribute('aria-controls');
      element.before(wrapper); content.append(element); wrapper.append(button, content); connect(wrapper, button, content, key, defaultExpanded);
    };
    [
      ['.align-section', 'modifier:align', 'Ausrichten', false], ['#transformControls', 'modifier:transform', 'Drehen & Spiegeln', true],
      ['#freeTransformControls', 'modifier:free-transform', 'Freie Transformation', false], ['.arrange-actions', 'modifier:arrange', 'Anordnung', false],
      ['.group-actions', 'modifier:group', 'Gruppierung', false], ['#pathOperations', 'modifier:paths', 'Pfade & Masken', false]
    ].forEach(([selector, key, label, defaultExpanded]) => wrapSubsection(refs.modifierPanelContent?.querySelector(selector), key, label, defaultExpanded));
    const libraryLabels = ['Symbole', 'Objektstile', 'Verläufe & Muster', 'Konturvorgaben'];
    refs.rightSidebar.querySelectorAll('#reusableAssetsPanel .library-resource-group').forEach((element, index) => {
      wrapSubsection(element, `library:resource-${index}`, libraryLabels[index] || `Bibliotheksgruppe ${index + 1}`);
    });
    wrapSubsection(refs.rightSidebar.querySelector('#reusableAssetsPanel .library-transfer-actions'), 'library:transfer', 'Übertragen');
  }

  function effectEditableObject(object) { return Boolean(object && !containerObjectTypes.has(object.type)); }

  const effectCategories = Object.freeze([
    Object.freeze(['adjust-color', 'Farbe anpassen']),
    Object.freeze(['bevel-emboss', 'Abschrägung und Relief']),
    Object.freeze(['blur', 'Weichzeichnen']),
    Object.freeze(['noise', 'Rauschen']),
    Object.freeze(['other', 'Sonstige']),
    Object.freeze(['shadow-glow', 'Schatten und Glühen']),
    Object.freeze(['sharpen', 'Schärfen']),
    Object.freeze(['face', 'Gesichter'])
  ]);
  const curveEditorChannels = Object.freeze([
    Object.freeze({ value: 'rgb', label: 'RGB', field: 'rgbCurve' }),
    Object.freeze({ value: 'red', label: 'Rot', field: 'redCurve' }),
    Object.freeze({ value: 'green', label: 'Grün', field: 'greenCurve' }),
    Object.freeze({ value: 'blue', label: 'Blau', field: 'blueCurve' })
  ]);
  let effectEditorTarget = null;
  let effectPickerOpenCategory = '';
  let pendingEffectListMotion = null;

  function closeEffectTypePicker({ restoreFocus = false } = {}) {
    const menu = $('effectTypeMenu'); const trigger = $('effectTypeTrigger');
    if (!menu || menu.hidden) return false;
    menu.hidden = true; trigger?.setAttribute('aria-expanded', 'false');
    if (restoreFocus) trigger?.focus();
    return true;
  }

  function positionEffectTypePicker() {
    const menu = $('effectTypeMenu'); const trigger = $('effectTypeTrigger');
    if (!menu || !trigger || menu.hidden) return;
    const margin = 8; const gap = 4; const viewportWidth = Math.max(1, window.innerWidth); const viewportHeight = Math.max(1, window.innerHeight);
    const triggerRect = trigger.getBoundingClientRect(); const width = Math.min(viewportWidth - margin * 2, Math.max(220, triggerRect.width));
    const below = Math.max(0, viewportHeight - margin - triggerRect.bottom - gap);
    const above = Math.max(0, triggerRect.top - margin - gap); const openAbove = above > below;
    menu.style.width = `${Math.round(width)}px`; menu.style.maxHeight = `${Math.max(80, openAbove ? above : below)}px`;
    menu.style.left = `${Math.round(clamp(triggerRect.left, margin, Math.max(margin, viewportWidth - width - margin)))}px`;
    const menuHeight = menu.getBoundingClientRect().height;
    const top = openAbove ? triggerRect.top - menuHeight - gap : triggerRect.bottom + gap;
    menu.style.top = `${Math.round(clamp(top, margin, Math.max(margin, viewportHeight - menuHeight - margin)))}px`;
  }

  function openEffectTypePicker({ focusFirst = false } = {}) {
    const menu = $('effectTypeMenu'); const trigger = $('effectTypeTrigger');
    if (!menu || !trigger || trigger.disabled) return;
    menu.hidden = false; trigger.setAttribute('aria-expanded', 'true'); positionEffectTypePicker();
    if (focusFirst) requestAnimationFrame(() => menu.querySelector('.effect-picker__category-toggle')?.focus());
  }

  function syncEffectTypePickerSelection() {
    const select = $('effectTypeSelect'); const menu = $('effectTypeMenu'); const triggerLabel = $('effectTypeTriggerLabel');
    if (!select || !menu || !triggerLabel) return;
    const type = select.value; const selected = [...menu.querySelectorAll('[data-effect-type]')].find((button) => button.dataset.effectType === type);
    menu.querySelectorAll('[data-effect-type]').forEach((button) => {
      const active = button === selected; button.classList.toggle('is-selected', active);
      if (active) button.setAttribute('aria-current', 'true'); else button.removeAttribute('aria-current');
    });
    triggerLabel.textContent = selected?.textContent.trim() || select.selectedOptions[0]?.textContent || 'Effekt auswählen';
  }

  function toggleEffectTypeCategory(category) {
    const menu = $('effectTypeMenu'); if (!menu) return;
    effectPickerOpenCategory = effectPickerOpenCategory === category ? '' : category;
    menu.querySelectorAll('[data-effect-category]').forEach((button) => {
      const expanded = button.dataset.effectCategory === effectPickerOpenCategory;
      button.setAttribute('aria-expanded', String(expanded));
      button.querySelector('.effect-picker__category-marker').textContent = expanded ? '⌄' : '›';
      const options = document.getElementById(button.getAttribute('aria-controls')); if (options) options.hidden = !expanded;
    });
    positionEffectTypePicker();
  }

  function renderEffectTypePicker(definitions) {
    const trigger = $('effectTypeTrigger'); const triggerLabel = $('effectTypeTriggerLabel'); const menu = $('effectTypeMenu');
    if (!trigger || !triggerLabel || !menu) return;
    if (!definitions.some((definition) => definition.category === effectPickerOpenCategory)) effectPickerOpenCategory = '';
    menu.replaceChildren();
    effectCategories.forEach(([category, label]) => {
      const members = definitions.filter((definition) => definition.category === category); if (!members.length) return;
      const section = document.createElement('section'); section.className = 'effect-picker__category';
      const toggle = document.createElement('button'); toggle.type = 'button'; toggle.className = 'effect-picker__category-toggle';
      toggle.dataset.effectCategory = category; toggle.setAttribute('aria-expanded', String(category === effectPickerOpenCategory));
      const optionsId = `effect-picker-${category}`; toggle.setAttribute('aria-controls', optionsId);
      const marker = document.createElement('span'); marker.className = 'effect-picker__category-marker'; marker.setAttribute('aria-hidden', 'true');
      marker.textContent = category === effectPickerOpenCategory ? '⌄' : '›';
      const title = document.createElement('span'); title.textContent = label; toggle.append(marker, title);
      const options = document.createElement('div'); options.id = optionsId; options.className = 'effect-picker__category-options';
      options.setAttribute('role', 'group'); options.setAttribute('aria-label', label); options.hidden = category !== effectPickerOpenCategory;
      members.forEach((definition) => {
        const option = document.createElement('button'); option.type = 'button'; option.className = 'effect-picker__option';
        option.dataset.effectType = definition.type; option.textContent = definition.label; options.append(option);
      });
      section.append(toggle, options); menu.append(section);
    });
    trigger.disabled = !definitions.length;
    if (!definitions.length) {
      triggerLabel.textContent = 'Effekt auswählen'; closeEffectTypePicker(); return;
    }
    syncEffectTypePickerSelection();
    if (!menu.hidden) requestAnimationFrame(positionEffectTypePicker);
  }

  function captureEffectListMotion(object) {
    if (!object || !$('effectStackList')) return;
    const positions = new Map();
    $('effectStackList').querySelectorAll('.effect-card[data-effect-id]').forEach((card) => {
      const rect = card.getBoundingClientRect(); positions.set(card.dataset.effectId, { left: rect.left, top: rect.top });
    });
    pendingEffectListMotion = { objectId: object.id, positions };
  }

  function playEffectListMotion(object) {
    const motion = pendingEffectListMotion;
    if (!motion) return;
    pendingEffectListMotion = null;
    if (motion.objectId !== object?.id || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    requestAnimationFrame(() => {
      $('effectStackList')?.querySelectorAll('.effect-card[data-effect-id]').forEach((card) => {
        const previous = motion.positions.get(card.dataset.effectId); const rect = card.getBoundingClientRect();
        const deltaX = previous ? previous.left - rect.left : 0; const deltaY = previous ? previous.top - rect.top : -5;
        if (previous && Math.abs(deltaX) < .5 && Math.abs(deltaY) < .5) return;
        card.dataset.effectListMoving = 'true';
        const animation = card.animate([
          { transform: `translate(${round(deltaX, 2)}px, ${round(deltaY, 2)}px)`, opacity: previous ? 1 : 0 },
          { transform: 'translate(0, 0)', opacity: 1 }
        ], { duration: 190, easing: 'cubic-bezier(.2,.78,.24,1)', fill: 'both' });
        animation.finished.catch(() => {}).finally(() => { delete card.dataset.effectListMoving; });
      });
    });
  }

  function closeEffectEditor() {
    effectEditorTarget = null;
    const card = $('effectStackList')?.querySelector('.effect-card.is-editing');
    if (!card) return;
    card.classList.remove('is-editing');
    const controls = card.querySelector('.effect-card__controls');
    if (controls) {
      controls.removeAttribute('id'); controls.setAttribute('aria-hidden', 'true');
      controls.style.removeProperty('left'); controls.style.removeProperty('top'); controls.style.removeProperty('visibility');
    }
  }

  function positionEffectEditor() {
    const controls = document.getElementById('effectEditorPopover');
    const card = controls?.closest('.effect-card'); const anchor = card?.querySelector('[data-effect-action="edit"]');
    if (!controls || !anchor || !effectEditorTarget) return;
    const margin = 8; controls.style.visibility = 'hidden'; controls.style.left = `${margin}px`; controls.style.top = `${margin}px`;
    const anchorRect = anchor.getBoundingClientRect(); const cardRect = card.getBoundingClientRect(); const popupRect = controls.getBoundingClientRect(); const gap = 7;
    const right = cardRect.right + gap; const leftSide = cardRect.left - popupRect.width - gap;
    const left = right + popupRect.width <= window.innerWidth - margin
      ? right
      : leftSide >= margin
        ? leftSide
        : clamp(cardRect.left, margin, Math.max(margin, window.innerWidth - popupRect.width - margin));
    const top = clamp(anchorRect.top + anchorRect.height / 2 - popupRect.height / 2, margin, Math.max(margin, window.innerHeight - popupRect.height - margin));
    controls.style.left = `${Math.round(left)}px`; controls.style.top = `${Math.round(top)}px`; controls.style.visibility = '';
  }

  function openEffectEditor(object, effectId) {
    if (!effectEditableObject(object) || !effectId) return;
    const existing = effectEditorTarget?.objectId === object.id && effectEditorTarget?.effectId === effectId ? effectEditorTarget : null;
    const effect = effectStack(object).find((entry) => entry.id === effectId);
    effectEditorTarget = effect?.type === 'curves'
      ? { objectId: object.id, effectId, curveChannel: existing?.curveChannel || 'rgb', curvePointIndex: existing?.curvePointIndex ?? 0 }
      : { objectId: object.id, effectId };
    renderEffectStackInspector(object);
  }

  function effectActionButton(action, label, icon, disabled = false) {
    const button = document.createElement('button'); button.type = 'button'; button.dataset.effectAction = action;
    button.setAttribute('aria-label', label); button.title = label; button.innerHTML = Tabler.svg(icon); button.disabled = disabled; return button;
  }

  function effectControlElement(effect, index, control) {
    if (control.kind === 'boolean') {
      const row = document.createElement('label'); row.className = 'toggle-row toggle-row--compact effect-boolean-control';
      const input = document.createElement('input'); input.type = 'checkbox'; input.checked = Boolean(effect[control.key]);
      input.dataset.effectIndex = index; input.dataset.effectField = control.key;
      const toggle = document.createElement('span'); toggle.className = 'switch'; toggle.setAttribute('aria-hidden', 'true');
      const text = document.createElement('span'); text.textContent = control.label; row.append(input, toggle, text); return row;
    }
    if (control.kind === 'color') {
      const row = document.createElement('label'); row.className = 'effect-color-control';
      const label = document.createElement('span'); label.textContent = control.label;
      const input = document.createElement('input'); input.type = 'color'; input.value = effect[control.key]; input.dataset.effectIndex = index; input.dataset.effectField = control.key;
      input.setAttribute('aria-label', `${control.label} für ${Effects.definitionFor(effect.type).label}`); row.append(label, input); return row;
    }
    if (control.kind === 'select') {
      const row = document.createElement('label'); row.className = 'effect-select-control';
      const label = document.createElement('span'); label.textContent = control.label;
      const input = document.createElement('select'); input.dataset.effectIndex = index; input.dataset.effectField = control.key;
      input.setAttribute('aria-label', `${control.label} für ${Effects.definitionFor(effect.type).label}`);
      const optionGroups = new Map();
      control.options.forEach((option) => {
        let parent = input;
        if (option.group) {
          if (!optionGroups.has(option.group)) {
            const group = document.createElement('optgroup'); group.label = option.group;
            optionGroups.set(option.group, group); input.append(group);
          }
          parent = optionGroups.get(option.group);
        }
        const entry = document.createElement('option'); entry.value = option.value; entry.textContent = option.label;
        entry.selected = option.value === effect[control.key]; parent.append(entry);
      });
      row.append(label, input); return row;
    }
    const row = document.createElement('div'); row.className = 'effect-control-row';
    const label = document.createElement('label'); label.textContent = control.label;
    const slider = document.createElement('input'); slider.type = 'range'; slider.min = control.min; slider.max = control.max; slider.step = control.step; slider.value = effect[control.key];
    slider.dataset.effectIndex = index; slider.dataset.effectField = control.key; slider.setAttribute('aria-label', control.label);
    const value = document.createElement('label'); value.className = 'effect-control-value';
    const number = document.createElement('input'); number.type = 'number'; number.min = control.min; number.max = control.max; number.step = control.step; number.value = effect[control.key];
    number.dataset.effectIndex = index; number.dataset.effectField = control.key; number.setAttribute('aria-label', `${control.label} exakt eingeben`);
    const unit = document.createElement('em'); unit.textContent = control.unit; value.append(number, unit); attachNumberScrubber(number, value, control.label); row.append(label, slider, value); return row;
  }

  function appendToneGrid(preview) {
    const namespace = 'http://www.w3.org/2000/svg';
    [64, 128, 192].forEach((position) => {
      const vertical = document.createElementNS(namespace, 'line');
      vertical.setAttribute('x1', position); vertical.setAttribute('x2', position); vertical.setAttribute('y1', 0); vertical.setAttribute('y2', 255);
      const horizontal = document.createElementNS(namespace, 'line');
      horizontal.setAttribute('x1', 0); horizontal.setAttribute('x2', 255); horizontal.setAttribute('y1', position); horizontal.setAttribute('y2', position);
      preview.append(vertical, horizontal);
    });
  }

  function effectTonePreview(effect) {
    if (effect?.type !== 'levels') return null;
    const namespace = 'http://www.w3.org/2000/svg';
    const preview = document.createElementNS(namespace, 'svg');
    preview.classList.add('effect-tone-preview'); preview.setAttribute('viewBox', '0 0 255 255');
    preview.setAttribute('preserveAspectRatio', 'none'); preview.setAttribute('aria-hidden', 'true'); appendToneGrid(preview);
    const table = Effects.levelsTable(effect);
    const curve = document.createElementNS(namespace, 'polyline');
    curve.setAttribute('points', table.map((value, index) => `${round(index / (table.length - 1) * 255, 2)},${round((1 - value) * 255, 2)}`).join(' '));
    preview.append(curve); return preview;
  }

  function curveChannelDefinition(value) {
    return curveEditorChannels.find((entry) => entry.value === value) || curveEditorChannels[0];
  }

  function updateCurveEffect(effectId, channel, points) {
    const object = primaryObject(); if (!effectEditableObject(object)) return null;
    const stack = effectStack(object); const index = stack.findIndex((effect) => effect.id === effectId && effect.type === 'curves');
    if (index < 0) return null;
    const field = curveChannelDefinition(channel).field;
    stack[index][field] = Effects.normalizeCurvePoints(points); delete stack[index].svgFilter;
    const normalized = setObjectEffectStack(object, stack);
    return { object, effect: normalized[index], index };
  }

  function refreshCurveEditorGraph(graph, effect, channel, selectedIndex = 0) {
    if (!graph || effect?.type !== 'curves') return;
    const namespace = 'http://www.w3.org/2000/svg'; const definition = curveChannelDefinition(channel);
    const points = Effects.normalizeCurvePoints(effect[definition.field]);
    const safeIndex = clamp(Math.round(num(selectedIndex, 0)), 0, points.length - 1);
    graph.querySelectorAll('[data-curve-plot]').forEach((node) => node.remove());
    graph.classList.remove('is-rgb', 'is-red', 'is-green', 'is-blue'); graph.classList.add(`is-${definition.value}`);
    const table = Effects.curvePointsTable(points, 257);
    const curve = document.createElementNS(namespace, 'polyline'); curve.dataset.curvePlot = 'curve';
    curve.setAttribute('points', table.map((value, index) => `${index},${round((1 - value) * 255, 2)}`).join(' ')); graph.append(curve);
    points.forEach((point, index) => {
      const marker = document.createElementNS(namespace, 'circle'); marker.dataset.curvePlot = 'point'; marker.dataset.curvePointIndex = index;
      marker.setAttribute('cx', point[0]); marker.setAttribute('cy', 255 - point[1]); marker.setAttribute('r', index === safeIndex ? 5.5 : 4.25);
      marker.classList.toggle('is-selected', index === safeIndex); graph.append(marker);
    });
    const editor = graph.closest('.effect-curve-editor'); const selected = points[safeIndex];
    const input = editor?.querySelector('[data-curve-coordinate="input"]'); const output = editor?.querySelector('[data-curve-coordinate="output"]');
    if (input) input.value = round(selected[0], 2); if (output) output.value = round(selected[1], 2);
    if (effectEditorTarget?.effectId === effect.id) effectEditorTarget.curvePointIndex = safeIndex;
  }

  function curvePointerCoordinates(event, graph) {
    const bounds = graph.getBoundingClientRect();
    return {
      x: round(clamp((event.clientX - bounds.left) / Math.max(1, bounds.width) * 255, 0, 255), 2),
      y: round(clamp((bounds.bottom - event.clientY) / Math.max(1, bounds.height) * 255, 0, 255), 2)
    };
  }

  function effectCurveEditor(effect) {
    const editor = document.createElement('div'); editor.className = 'effect-curve-editor';
    const channel = curveChannelDefinition(effectEditorTarget?.curveChannel).value;
    const channelRow = document.createElement('label'); channelRow.className = 'effect-curve-editor__channel';
    const channelLabel = document.createElement('span'); channelLabel.textContent = 'Kanal';
    const channelSelect = document.createElement('select'); channelSelect.setAttribute('aria-label', 'Kanal');
    curveEditorChannels.forEach((entry) => {
      const option = document.createElement('option'); option.value = entry.value; option.textContent = entry.label; option.selected = entry.value === channel; channelSelect.append(option);
    });
    channelRow.append(channelLabel, channelSelect);

    const namespace = 'http://www.w3.org/2000/svg';
    const graph = document.createElementNS(namespace, 'svg'); graph.classList.add('effect-tone-preview', 'effect-curve-editor__graph');
    graph.setAttribute('viewBox', '0 0 255 255'); graph.setAttribute('preserveAspectRatio', 'none'); graph.setAttribute('tabindex', '0');
    graph.setAttribute('role', 'application'); graph.setAttribute('aria-label', 'Kurve durch Klicken und Ziehen bearbeiten'); appendToneGrid(graph);

    const readout = document.createElement('div'); readout.className = 'effect-curve-editor__readout';
    const coordinateControl = (coordinate, label) => {
      const wrapper = document.createElement('label'); const text = document.createElement('span'); text.textContent = label;
      const input = document.createElement('input'); input.type = 'number'; input.min = 0; input.max = 255; input.step = 1; input.dataset.curveCoordinate = coordinate;
      wrapper.append(text, input); readout.append(wrapper); return input;
    };
    const inputCoordinate = coordinateControl('input', 'Eingabe'); const outputCoordinate = coordinateControl('output', 'Ausgabe');
    editor.append(channelRow, graph, readout);

    const activePoints = () => {
      const object = primaryObject(); const current = effectStack(object).find((entry) => entry.id === effect.id);
      const activeChannel = curveChannelDefinition(effectEditorTarget?.curveChannel || channel);
      return current?.type === 'curves' ? { effect: current, field: activeChannel.field, points: Effects.normalizeCurvePoints(current[activeChannel.field]) } : null;
    };
    const applyPoints = (points, pointIndex, commit = false) => {
      const activeChannel = curveChannelDefinition(effectEditorTarget?.curveChannel || channel);
      const updated = updateCurveEffect(effect.id, activeChannel.value, points); if (!updated) return null;
      effectEditorTarget.curvePointIndex = pointIndex; refreshCurveEditorGraph(graph, updated.effect, activeChannel.value, pointIndex);
      if (commit) { commitHistory('Gradationskurven geändert'); renderAll(); }
      else scheduleInteractionRender([updated.object]);
      return updated;
    };
    const constrainedPoint = (points, pointIndex, next) => {
      const endpoint = pointIndex === 0 || pointIndex === points.length - 1;
      const minimum = pointIndex > 0 ? points[pointIndex - 1][0] + 1 : 0;
      const maximum = pointIndex < points.length - 1 ? points[pointIndex + 1][0] - 1 : 255;
      return [endpoint ? points[pointIndex][0] : round(clamp(next.x, minimum, maximum), 2), round(clamp(next.y, 0, 255), 2)];
    };

    channelSelect.addEventListener('change', () => {
      effectEditorTarget.curveChannel = channelSelect.value; effectEditorTarget.curvePointIndex = 0; renderEffectStackInspector(primaryObject());
    });
    graph.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      event.preventDefault(); graph.focus({ preventScroll: true });
      const current = activePoints(); if (!current) return;
      let points = current.points.map((point) => [...point]);
      let pointIndex = Number(event.target.closest?.('[data-curve-point-index]')?.dataset.curvePointIndex);
      const existingPoint = Number.isInteger(pointIndex);
      if (event.detail >= 2 && existingPoint && pointIndex > 0 && pointIndex < points.length - 1) {
        points.splice(pointIndex, 1); pointIndex = Math.max(0, pointIndex - 1); applyPoints(points, pointIndex, true); return;
      }
      let changed = false;
      if (!existingPoint) {
        if (points.length >= 32) return;
        const next = curvePointerCoordinates(event, graph); points.push([next.x, next.y]); points = Effects.normalizeCurvePoints(points);
        pointIndex = points.reduce((nearest, point, index) => Math.abs(point[0] - next.x) < Math.abs(points[nearest][0] - next.x) ? index : nearest, 0);
        const updated = applyPoints(points, pointIndex, false); points = updated?.effect[curveChannelDefinition(channelSelect.value).field] || points; changed = true;
      } else {
        effectEditorTarget.curvePointIndex = pointIndex; refreshCurveEditorGraph(graph, current.effect, channelSelect.value, pointIndex);
      }
      graph.setPointerCapture?.(event.pointerId);
      const move = (moveEvent) => {
        if (moveEvent.pointerId !== event.pointerId) return;
        const next = curvePointerCoordinates(moveEvent, graph); points[pointIndex] = constrainedPoint(points, pointIndex, next);
        const updated = applyPoints(points, pointIndex, false); points = updated?.effect[curveChannelDefinition(channelSelect.value).field] || points; changed = true;
      };
      const finish = (finishEvent) => {
        if (finishEvent.pointerId !== event.pointerId) return;
        graph.removeEventListener('pointermove', move); graph.removeEventListener('pointerup', finish); graph.removeEventListener('pointercancel', finish);
        if (graph.hasPointerCapture?.(event.pointerId)) graph.releasePointerCapture(event.pointerId);
        if (changed) { commitHistory('Gradationskurven geändert'); renderAll(); }
      };
      graph.addEventListener('pointermove', move); graph.addEventListener('pointerup', finish); graph.addEventListener('pointercancel', finish);
    });
    graph.addEventListener('keydown', (event) => {
      const current = activePoints(); if (!current) return;
      const pointIndex = clamp(Math.round(num(effectEditorTarget?.curvePointIndex, 0)), 0, current.points.length - 1);
      if (['Delete', 'Backspace'].includes(event.key) && pointIndex > 0 && pointIndex < current.points.length - 1) {
        event.preventDefault(); const points = current.points.map((point) => [...point]); points.splice(pointIndex, 1); applyPoints(points, Math.max(0, pointIndex - 1), true); return;
      }
      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
      event.preventDefault(); const points = current.points.map((point) => [...point]); const selected = points[pointIndex];
      const next = { x: selected[0] + (event.key === 'ArrowLeft' ? -1 : event.key === 'ArrowRight' ? 1 : 0), y: selected[1] + (event.key === 'ArrowDown' ? -1 : event.key === 'ArrowUp' ? 1 : 0) };
      points[pointIndex] = constrainedPoint(points, pointIndex, next); applyPoints(points, pointIndex, true);
    });
    const updateCoordinate = (target, coordinate, commit) => {
      if (target.value === '') return;
      const current = activePoints(); if (!current) return;
      const pointIndex = clamp(Math.round(num(effectEditorTarget?.curvePointIndex, 0)), 0, current.points.length - 1);
      const selected = current.points[pointIndex]; const next = { x: selected[0], y: selected[1] };
      next[coordinate === 'input' ? 'x' : 'y'] = num(target.value, next[coordinate === 'input' ? 'x' : 'y']);
      const points = current.points.map((point) => [...point]); points[pointIndex] = constrainedPoint(points, pointIndex, next); applyPoints(points, pointIndex, commit);
    };
    inputCoordinate.addEventListener('input', () => updateCoordinate(inputCoordinate, 'input', false));
    inputCoordinate.addEventListener('change', () => updateCoordinate(inputCoordinate, 'input', true));
    outputCoordinate.addEventListener('input', () => updateCoordinate(outputCoordinate, 'output', false));
    outputCoordinate.addEventListener('change', () => updateCoordinate(outputCoordinate, 'output', true));
    refreshCurveEditorGraph(graph, effect, channel, effectEditorTarget?.curvePointIndex ?? 0);
    return editor;
  }

  function renderEffectStackInspector(object) {
    const fieldset = $('effectsProperties'); const list = $('effectStackList'); const select = $('effectTypeSelect');
    if (!fieldset || !list || !select) return;
    const editable = effectEditableObject(object); fieldset.hidden = !editable; list.replaceChildren();
    select.replaceChildren();
    if (!editable) {
      effectEditorTarget = null; pendingEffectListMotion = null;
      renderEffectTypePicker([]);
      $('addEffectBtn').disabled = true; $('copyEffectsBtn').disabled = true; $('pasteEffectsBtn').disabled = true; return;
    }
    const previousType = select.dataset.lastType || select.value; const definitions = Effects.availableFor(object.type);
    effectCategories.forEach(([category, label]) => {
      const members = definitions.filter((definition) => definition.category === category); if (!members.length) return;
      const group = document.createElement('optgroup'); group.label = label;
      members.forEach((definition) => { const option = document.createElement('option'); option.value = definition.type; option.textContent = definition.label; group.append(option); });
      select.append(group);
    });
    const allowedTypes = definitions.map((definition) => definition.type);
    select.value = allowedTypes.includes(previousType) ? previousType : (allowedTypes[0] || ''); select.dataset.lastType = select.value;
    renderEffectTypePicker(definitions);
    const stack = effectStack(object); const active = stack.filter((effect) => effect.enabled).length;
    if (effectEditorTarget?.objectId !== object.id || !stack.some((effect) => effect.id === effectEditorTarget.effectId)) effectEditorTarget = null;
    $('effectStackHint').textContent = stack.length
      ? `${stack.length} Einträge, davon ${active} aktiv · oben wird zuerst berechnet.`
      : object.type === 'image'
        ? 'Effekte, Gesichter und Farbkorrekturen verändern die Originalpixel nicht.'
        : 'Effekte und Gesichter verändern das Vektor- oder Textobjekt nicht.';
    $('addEffectBtn').disabled = !allowedTypes.length || stack.length >= Effects.MAX_EFFECTS;
    $('copyEffectsBtn').disabled = !stack.length; $('pasteEffectsBtn').disabled = !state.effectClipboard.length;
    if (!stack.length) {
      const empty = document.createElement('p'); empty.className = 'effect-stack-empty'; empty.textContent = 'Noch keine Effekte'; list.append(empty); playEffectListMotion(object); return;
    }
    stack.forEach((effect, index) => {
      const definition = Effects.definitionFor(effect.type); if (!definition) return;
      const visibleControls = definition.controls.filter((control) => !control.hidden);
      const hasEditor = Boolean(definition.editor || definition.description || visibleControls.length);
      const editing = hasEditor && effectEditorTarget?.objectId === object.id && effectEditorTarget.effectId === effect.id;
      const card = document.createElement('section'); card.className = `effect-card${effect.enabled ? '' : ' is-disabled'}${editing ? ' is-editing' : ''}`; card.dataset.effectIndex = index; card.dataset.effectId = effect.id;
      const header = document.createElement('div'); header.className = 'effect-card__header';
      const enabled = document.createElement('label'); enabled.className = 'effect-card__enabled';
      const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = effect.enabled; checkbox.dataset.effectIndex = index; checkbox.dataset.effectField = 'enabled';
      const title = document.createElement('span'); title.textContent = definition.label; enabled.append(checkbox, title);
      const actions = document.createElement('div'); actions.className = 'effect-card__actions';
      const actionButtons = [
        effectActionButton('up', 'Im Stapel nach oben', 'chevron-up', index === 0),
        effectActionButton('down', 'Im Stapel nach unten', 'chevron-down', index === stack.length - 1),
        effectActionButton('duplicate', 'Effekt duplizieren', 'copy', stack.length >= Effects.MAX_EFFECTS),
        effectActionButton('remove', 'Effekt entfernen', 'trash')
      ];
      if (hasEditor) actionButtons.splice(3, 0, effectActionButton('edit', 'Effekt bearbeiten', 'pencil'));
      actions.append(...actionButtons);
      const controls = document.createElement('div'); controls.className = 'effect-card__controls';
      controls.setAttribute('role', 'dialog'); controls.setAttribute('aria-label', `${definition.label} bearbeiten`); controls.setAttribute('aria-hidden', String(!editing));
      if (editing) controls.id = 'effectEditorPopover';
      const editorHeader = document.createElement('div'); editorHeader.className = 'effect-editor__header';
      const editorTitle = document.createElement('strong'); editorTitle.textContent = definition.label;
      const editorClose = effectActionButton('close-editor', 'Effektfenster schließen', 'x'); editorClose.className = 'effect-editor__close';
      editorHeader.append(editorTitle, editorClose); controls.append(editorHeader);
      if (definition.description) { const note = document.createElement('p'); note.className = 'effect-card__note'; note.textContent = definition.description; controls.append(note); }
      const customEditor = definition.editor === 'curves' ? effectCurveEditor(effect) : effectTonePreview(effect); if (customEditor) controls.append(customEditor);
      visibleControls.forEach((control) => controls.append(effectControlElement(effect, index, control)));
      header.append(enabled, actions); card.append(header, controls); list.append(card);
    });
    playEffectListMotion(object);
    if (effectEditorTarget) requestAnimationFrame(positionEffectEditor);
  }

  function setObjectEffectStack(object, value) {
    if (!object) return [];
    const normalized = Effects.normalizeStack(value, { createId: uid });
    if (normalized.length) object.effects = normalized; else delete object.effects;
    if (!normalized.some((effect) => effect.svgFilter)) delete object.svgFilterSettings;
    return normalized;
  }

  async function analyzeBitmapAutoColor(object) {
    const image = await imageFromSource(object.href); const sourceWidth = Math.max(1, image.naturalWidth); const sourceHeight = Math.max(1, image.naturalHeight);
    const scale = Math.min(1, 512 / Math.max(sourceWidth, sourceHeight)); const width = Math.max(1, Math.round(sourceWidth * scale)); const height = Math.max(1, Math.round(sourceHeight * scale));
    const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height;
    const context = canvas.getContext('2d', { willReadFrequently: true }); context.clearRect(0, 0, width, height); context.drawImage(image, 0, 0, width, height);
    return Effects.autoColorAnalysis(context.getImageData(0, 0, width, height).data);
  }

  async function addSelectedEffect() {
    const object = primaryObject(); const type = $('effectTypeSelect').value;
    if (!effectEditableObject(object) || !Effects.isAllowed(type, object.type)) { toast('Dieser Effekt ist für die Auswahl nicht verfügbar', 'warning'); return; }
    const stack = effectStack(object);
    if (stack.length >= Effects.MAX_EFFECTS) { toast(`Maximal ${Effects.MAX_EFFECTS} Effekte pro Objekt`, 'warning'); return; }
    const effect = Effects.createEffect(type, { createId: uid });
    if (type === 'auto-color') {
      const button = $('addEffectBtn'); button.disabled = true; button.setAttribute('aria-busy', 'true'); toast('Bitmapfarben werden analysiert …');
      try { Object.assign(effect, await analyzeBitmapAutoColor(object)); }
      catch { toast('Die Bitmapfarben konnten nicht analysiert werden', 'error'); return; }
      finally { button.removeAttribute('aria-busy'); button.disabled = false; }
      if (primaryObject()?.id !== object.id) { renderInspector(); return; }
    }
    const currentStack = effectStack(object);
    if (currentStack.length >= Effects.MAX_EFFECTS) { renderInspector(); toast(`Maximal ${Effects.MAX_EFFECTS} Effekte pro Objekt`, 'warning'); return; }
    setObjectEffectStack(object, [...currentStack, effect]);
    const definition = Effects.definitionFor(type);
    effectEditorTarget = definition?.editor || definition?.description || definition?.controls.some((control) => !control.hidden)
      ? { objectId: object.id, effectId: effect.id, ...(type === 'curves' ? { curveChannel: 'rgb', curvePointIndex: 0 } : {}) }
      : null;
    commitHistory(`${Effects.definitionFor(type).label} hinzugefügt`); renderAll();
  }

  function copySelectedEffects() {
    const object = primaryObject(); const stack = effectStack(object);
    if (!effectEditableObject(object) || !stack.length) { toast('Die Auswahl besitzt keinen Effektstapel', 'warning'); return; }
    state.effectClipboard = deepClone(stack); renderInspector();
    toast(I18n.t(stack.length === 1 ? 'status.effectCopied' : 'status.copiedEffects', { count: stack.length }));
  }

  function pasteSelectedEffects() {
    const object = primaryObject();
    if (!effectEditableObject(object) || !state.effectClipboard.length) { toast('Keine kopierten Effekte verfügbar', 'warning'); return; }
    const compatible = state.effectClipboard.filter((effect) => Effects.isAllowed(effect.type, object.type));
    if (!compatible.length) { toast('Die kopierten Bitmapkorrekturen passen nicht auf dieses Objekt', 'warning'); return; }
    const existing = effectStack(object); const room = Effects.MAX_EFFECTS - existing.length;
    if (room <= 0) { toast(`Maximal ${Effects.MAX_EFFECTS} Effekte pro Objekt`, 'warning'); return; }
    const pasted = compatible.slice(0, room).map((effect) => ({ ...deepClone(effect), id: uid('effect') }));
    setObjectEffectStack(object, [...existing, ...pasted]); commitHistory('Effektstapel eingefügt'); renderAll();
    const skipped = state.effectClipboard.length - compatible.length + Math.max(0, compatible.length - room);
    toast(I18n.t(
      skipped
        ? pasted.length === 1 ? 'status.effectInsertedSkipped' : 'ui.countEffectsInsertedSkippedSkipped'
        : pasted.length === 1 ? 'status.effectInserted' : 'status.insertedEffects',
      { count: pasted.length, skipped }
    ));
  }

  function updateEffectControl(target, commit = false) {
    const object = primaryObject(); const index = Number(target?.dataset.effectIndex); const field = target?.dataset.effectField;
    if (!effectEditableObject(object) || !Number.isInteger(index) || !field) return;
    const stack = setObjectEffectStack(object, effectStack(object)); const effect = stack[index]; const definition = Effects.definitionFor(effect?.type);
    if (!effect || !definition) return;
    const control = field === 'enabled' ? null : definition.controls.find((entry) => entry.key === field);
    if (field === 'enabled') effect.enabled = Boolean(target.checked);
    else {
      if (!control) return;
      if (control.kind !== 'boolean' && target.value === '' && !commit) return;
      const value = control.kind === 'boolean' ? target.checked : target.value;
      effect[field] = Effects.normalizeControlValue(control, value, definition.defaults[field]);
      delete effect.svgFilter;
    }
    object.effects = stack;
    if (!stack.some((entry) => entry.svgFilter)) delete object.svgFilterSettings;
    [...$('effectStackList').querySelectorAll('[data-effect-index][data-effect-field]')].filter((input) => Number(input.dataset.effectIndex) === index && input.dataset.effectField === field).forEach((input) => {
      if (field === 'enabled' || control?.kind === 'boolean') input.checked = Boolean(effect[field]);
      else if (input !== target || String(input.value) !== String(effect[field])) input.value = effect[field];
    });
    $('effectStackList').querySelector(`.effect-card[data-effect-index="${index}"]`)?.classList.toggle('is-disabled', !effect.enabled);
    if (commit) { commitHistory(`${definition.label} geändert`); renderAll(); }
    else scheduleInteractionRender([object]);
  }

  function runEffectStackAction(button) {
    const object = primaryObject(); const index = Number(button.closest('.effect-card')?.dataset.effectIndex); const action = button.dataset.effectAction;
    if (!effectEditableObject(object) || !Number.isInteger(index)) return;
    const stack = effectStack(object); const effect = stack[index]; if (!effect) return;
    if (action === 'edit') { openEffectEditor(object, effect.id); return; }
    if (action === 'close-editor') { closeEffectEditor(); return; }
    const movable = action === 'remove'
      || (action === 'up' && index > 0)
      || (action === 'down' && index < stack.length - 1)
      || (action === 'duplicate' && stack.length < Effects.MAX_EFFECTS);
    if (!movable) return;
    captureEffectListMotion(object);
    if (action === 'remove') stack.splice(index, 1);
    else if (action === 'up' && index > 0) [stack[index - 1], stack[index]] = [stack[index], stack[index - 1]];
    else if (action === 'down' && index < stack.length - 1) [stack[index], stack[index + 1]] = [stack[index + 1], stack[index]];
    else if (action === 'duplicate' && stack.length < Effects.MAX_EFFECTS) stack.splice(index + 1, 0, { ...deepClone(effect), id: uid('effect') });
    const label = Effects.definitionFor(effect.type)?.label || 'Effekt'; setObjectEffectStack(object, stack);
    commitHistory(action === 'remove' ? `${label} entfernt` : action === 'duplicate' ? `${label} dupliziert` : 'Effektstapel sortiert'); renderAll();
  }

  function autoShapePropertyText(field, value, format = (entry) => String(entry)) {
    if (field.type === 'checkbox') return value ? 'Ein' : 'Aus';
    if (field.type === 'select') return field.options.find(([option]) => option === value)?.[1] || String(value ?? '—');
    return `${format(value)}${field.unit || ''}`;
  }

  function autoShapeReadoutItems(object, format = (entry) => String(entry)) {
    if (!AutoShapes.isAutoShape(object)) return [];
    return AutoShapes.propertyFields(object).map((field) => ({
      label: field.label,
      value: autoShapePropertyText(field, AutoShapes.getProperty(object, field.key), format)
    }));
  }

  function renderAutoShapeInspector(object) {
    const fieldset = $('autoShapeProperties'); const grid = $('autoShapePropertyGrid'); const heading = $('autoShapePropertiesLabel');
    const definition = AutoShapes.definitionFor(object);
    if (!fieldset || !grid || !heading) return;
    fieldset.hidden = !definition;
    if (!definition) { grid.replaceChildren(); heading.textContent = 'Auto-Form'; return; }
    heading.textContent = definition.label;
    const controls = AutoShapes.propertyFields(object).map((field) => {
      if (field.type === 'checkbox') {
        const label = document.createElement('label'); label.className = 'toggle-row toggle-row--compact';
        const input = document.createElement('input'); input.type = 'checkbox'; input.checked = Boolean(AutoShapes.getProperty(object, field.key)); input.dataset.autoShapeField = field.key;
        const toggle = document.createElement('span'); toggle.className = 'switch'; toggle.setAttribute('aria-hidden', 'true');
        const text = document.createElement('span'); text.textContent = field.label; label.append(input, toggle, text); return label;
      }
      if (field.type === 'select') {
        const label = document.createElement('label'); label.className = 'field-label'; label.append(document.createTextNode(field.label));
        const select = document.createElement('select'); select.dataset.autoShapeField = field.key;
        field.options.forEach(([value, text]) => { const option = document.createElement('option'); option.value = value; option.textContent = text; select.append(option); });
        select.value = String(AutoShapes.getProperty(object, field.key) ?? ''); label.append(select); return label;
      }
      const label = document.createElement('label'); label.className = 'number-field number-field--long-label';
      const text = document.createElement('span'); text.textContent = field.label;
      const input = document.createElement('input'); input.type = 'number'; input.dataset.autoShapeField = field.key;
      input.min = String(field.min); input.max = String(field.max); input.step = String(field.step); input.value = String(AutoShapes.getProperty(object, field.key) ?? 0);
      const unit = document.createElement('em'); unit.textContent = field.unit || '';
      label.append(text, input, unit); attachNumberScrubber(input, label, field.label); return label;
    });
    grid.replaceChildren(...controls);
  }

  const strokePatternLabels = Object.freeze({ solid: 'Durchgezogen', dashed: 'Gestrichelt', dotted: 'Gepunktet', zigzag: 'Gezackt', custom: 'Eigene Folge' });
  const strokeAlignmentLabels = Object.freeze({ inside: 'Innen', center: 'Mittig', outside: 'Außen' });

  function setObjectStrokeStack(object, value) {
    if (!supportsStrokeType(object)) return [];
    const fallback = StrokeStyle.legacyStyle(object);
    const normalized = StrokeStyle.normalizeStack(value, { fallback, createId: uid });
    object.strokeStack = normalized.length ? normalized : [StrokeStyle.normalizeStyle({ ...fallback, id: uid('stroke'), enabled: false, strokeType: 'none' }, { fallback })];
    StrokeStyle.syncLegacy(object); return object.strokeStack;
  }

  function activeStrokeContext(object = primaryObject(), ensure = false) {
    if (!supportsStrokeType(object)) return null;
    const stack = ensure ? StrokeStyle.ensureStack(object, { createId: uid }) : strokeStackFor(object);
    const requested = strokeSelection.objectId === object.id ? strokeSelection.strokeId : null;
    let index = requested ? stack.findIndex((style) => style.id === requested) : -1;
    if (index < 0) index = 0;
    const style = stack[index]; strokeSelection = { objectId: object.id, strokeId: style.id };
    return { object, stack, style, index };
  }

  function renderStrokeStackInspector(object, selectionCount = 1) {
    const fieldset = $('strokeProperties'); const list = $('strokeStackList');
    if (!fieldset || !list) return null;
    const editable = selectionCount === 1 && supportsStrokeType(object);
    fieldset.hidden = !editable; list.replaceChildren();
    if (!editable) { strokeSelection = { objectId: null, strokeId: null }; return null; }
    const context = activeStrokeContext(object); const { stack, style, index } = context;
    const patternLabel = (entry) => strokePatternLabels[entry.pattern] || entry.pattern;
    stack.forEach((entry, entryIndex) => {
      const button = document.createElement('button'); button.type = 'button'; button.className = `stroke-stack-item${entry.enabled ? '' : ' is-disabled'}`;
      button.dataset.strokeId = entry.id; button.dataset.strokeIndex = String(entryIndex); button.setAttribute('role', 'option'); button.setAttribute('aria-selected', String(entry.id === style.id));
      const visibility = document.createElement('span'); visibility.className = 'stroke-stack-item__visibility'; visibility.dataset.strokeVisibilityTrigger = 'true'; visibility.title = entry.enabled ? 'Kontur ausblenden' : 'Kontur einblenden'; visibility.innerHTML = Tabler.svg(entry.enabled ? 'eye' : 'eye-off');
      const swatch = document.createElement('span'); swatch.className = 'stroke-stack-item__swatch'; swatch.dataset.strokeColorTrigger = 'true'; swatch.setAttribute('aria-hidden', 'true'); swatch.style.setProperty('--stroke-color', entry.color);
      const name = document.createElement('strong'); name.className = 'stroke-stack-item__name'; name.textContent = `Kontur ${entryIndex + 1}`;
      const summary = document.createElement('small'); summary.className = 'stroke-stack-item__summary'; summary.textContent = `${entry.width}px · ${patternLabel(entry)} · ${strokeAlignmentLabels[entry.alignment]}`;
      button.append(visibility, swatch, name, summary); button.title = `${name.textContent}: ${summary.textContent} · Farbfeld anklicken, um die Farbe zu ändern`; list.append(button);
    });
    $('addStrokeBtn').disabled = stack.length >= StrokeStyle.MAX_STROKES;
    $('duplicateStrokeBtn').disabled = stack.length >= StrokeStyle.MAX_STROKES;
    $('removeStrokeBtn').disabled = stack.length <= 1; $('moveStrokeUpBtn').disabled = index === 0; $('moveStrokeDownBtn').disabled = index === stack.length - 1;
    setField('strokePattern', style.pattern); setField('strokeDashArray', StrokeStyle.dashText(style)); $('strokeDashArrayRow').hidden = style.pattern !== 'custom';
    setField('strokeLineCap', style.lineCap); setField('strokeLineJoin', style.lineJoin); setField('strokeMiterLimit', style.miterLimit); $('strokeMiterLimitRow').hidden = style.lineJoin !== 'miter';
    setField('strokeStartMarker', style.startMarker); setField('strokeEndMarker', style.endMarker); setField('strokeStartMarkerSize', style.startMarkerSize); setField('strokeEndMarkerSize', style.endMarkerSize);
    $('strokeStartMarkerSize').disabled = style.startMarker === 'none'; $('strokeEndMarkerSize').disabled = style.endMarker === 'none';
    setField('strokeAlignment', supportsStrokeAlignment(object) ? style.alignment : 'center'); $('strokeAlignment').disabled = !supportsStrokeAlignment(object);
    $('convertStrokeToPathBtn').disabled = !StrokeStyle.isVisible(style) || !canConvertStrokeToPath(object);
    return context;
  }

  function renderBitmapInspector(object, selectionCount = 1) {
    const fieldset = $('bitmapProperties'); if (!fieldset) return null;
    const editable = selectionCount === 1 && object?.type === 'image'; fieldset.hidden = !editable;
    if (!editable) return null;
    const mode = bitmapModeFor(object); const scale = BitmapModel.pixelScale(object); const interpolation = bitmapInterpolationFor(object);
    const percentX = scale.x * 100; const percentY = scale.y * 100; const sameScale = Math.abs(percentX - percentY) < .05;
    $('bitmapModeLabel').textContent = mode === 'direct' ? 'Direkt bearbeitbare Bitmap' : 'Platziertes Bild';
    $('bitmapScaleInfo').textContent = sameScale ? `${round(percentX, 1)} %` : `${round(percentX, 1)} × ${round(percentY, 1)} %`;
    $('bitmapPixelInfo').textContent = `${scale.pixelWidth} × ${scale.pixelHeight} Quellpixel · ${round(object.width, 2)} × ${round(object.height, 2)} Dokumentpixel`;
    const dpiX = num(object.sourceDpiX, 0); const dpiY = num(object.sourceDpiY, 0);
    const dpiInfo = dpiX > 0 && dpiY > 0
      ? I18n.t(Math.abs(dpiX - dpiY) > .01 ? 'ui.sourceDpiXTimesDpiYDpiDocumentDocumentDpiDpi' : 'ui.sourceDpiDpiDocumentDocumentDpiDpi', {
        dpi: round(dpiX, 2), dpiX: round(dpiX, 2), dpiY: round(dpiY, 2), documentDpi: state.doc.dpi
      })
      : I18n.t('ui.noEmbeddedSourceDpiDocumentDpiDpi', { dpi: state.doc.dpi });
    $('bitmapDpiInfo').textContent = [dpiInfo, ...bitmapMetadataParts(object)].join(' · ');
    setField('bitmapMode', mode); setField('bitmapInterpolation', interpolation);
    $('rasterizeBitmapBtn').disabled = mode === 'direct' && Math.abs(scale.x - 1) < .001 && Math.abs(scale.y - 1) < .001;
    $('rasterizeBitmapBtn').textContent = mode === 'placed' ? 'Auf aktuelle Größe neu berechnen' : 'Pixelmatrix auf Größe aktualisieren';
    $('bitmapModeHint').textContent = mode === 'placed'
      ? 'Quellpixel bleiben unverändert. Direkte Pixelwerkzeuge fragen vor dem ersten Eingriff nach einer einmaligen Neuberechnung.'
      : 'Abgeschlossene Skalierungen werden einmalig mit der gewählten Interpolation neu berechnet; Vorschau und Undo bleiben verlustfrei.';
    return { mode, scale, interpolation };
  }

  function renderCompactPropertyLabels(objects) {
    const container = refs.propertyDockReadOnly;
    if (!container) return;
    const object = objects.at(-1) || null;
    if (!object) { container.replaceChildren(); return; }
    const formatNumber = (value) => new Intl.NumberFormat(I18n.language === 'de' ? 'de-DE' : 'en-GB', { maximumFractionDigits: 2 }).format(num(value, 0));
    const commonValue = (property, fallback = '—') => {
      const values = objects.map((item) => item?.[property]);
      return values.every((value) => value === values[0]) ? (values[0] ?? fallback) : 'Gemischt';
    };
    const colorValue = (property) => {
      const value = commonValue(property, 'none');
      if (value === 'Gemischt') return { value };
      const normalized = String(value || 'none').toLowerCase();
      if (['none', 'transparent', ''].includes(normalized)) return { value: 'Transparent' };
      return { value: normalized.toUpperCase(), color: normalized };
    };
    const makeGroup = (title, items, className = '') => {
      const section = document.createElement('section');
      section.className = `property-readout__group${className ? ` ${className}` : ''}`;
      const heading = document.createElement('h3'); heading.textContent = title; section.append(heading);
      const list = document.createElement('div'); list.className = 'property-readout__items';
      items.forEach((item) => {
        const row = document.createElement('span'); row.className = 'property-readout__item';
        const label = document.createElement('small'); label.textContent = item.label;
        const value = document.createElement('strong'); value.textContent = String(item.value ?? '—'); value.title = String(item.value ?? '—');
        if (item.color) { const swatch = document.createElement('i'); swatch.style.backgroundColor = item.color; value.prepend(swatch); }
        row.append(label, value); list.append(row);
      });
      section.append(list); container.append(section);
    };
    container.replaceChildren();
    const bounds = layoutBoundsOf(objects);
    const multi = objects.length > 1;
    const strokeWidthProperty = !multi && object.type === 'text' ? 'textStrokeWidth' : 'strokeWidth';
    const strokeWidth = commonValue(strokeWidthProperty, 0);
    const strokeType = commonValue('strokeType', DEFAULT_STROKE_TYPE);
    const transformItems = [
      { label: 'X', value: `${formatNumber(bounds.x)} px` }, { label: 'Y', value: `${formatNumber(bounds.y)} px` },
      { label: 'B', value: `${formatNumber(bounds.width)} px` }, { label: 'H', value: `${formatNumber(bounds.height)} px` },
      { label: 'Drehung', value: commonValue('rotation', 0) === 'Gemischt' ? 'Gemischt' : `${formatNumber(commonValue('rotation', 0))}°` },
      { label: 'Skalierung', value: commonValue('scalePercent', 100) === 'Gemischt' ? 'Gemischt' : `${formatNumber(commonValue('scalePercent', 100))} %` }
    ];
    if (!multi && supportsCornerRoundness(object)) transformItems.push({ label: 'Rundung', value: `${formatNumber(objectRoundness(object))} px` });
    makeGroup('Transformieren', transformItems, 'property-readout__group--object-transform');
    if (object.type === 'text') {
      const texture = textTextureSettings(object);
      makeGroup('Text', [
        { label: 'Inhalt', value: object.text || 'Leer' },
        { label: 'Schrift', value: object.fontFamily || 'Arimo' },
        { label: 'Größe', value: `${formatNumber(object.fontSize || 32)} px` },
        { label: 'Textur', value: textTextureSummary(texture, formatNumber) }
      ], 'property-readout__group--context property-readout__group--text');
    }
    const stroke = colorValue(!multi && object.type === 'text' ? 'textStroke' : 'stroke');
    const fillPaint = !multi ? normalizedObjectFillPaint(object) : null;
    const fill = fillPaint
      ? { value: FillPaint.typeLabel(fillPaint.type), color: FillPaint.isGradient(fillPaint.type) ? fillPaint.stops[0]?.color : undefined }
      : colorValue('fill');
    const opacity = commonValue('opacity', 100);
    const appearanceItems = [
      { label: 'Deckkraft', value: opacity === 'Gemischt' ? opacity : `${formatNumber(opacity)} %` },
      { label: 'Kontur', ...stroke }, { label: 'Füllung', ...fill },
      { label: 'Stärke', value: strokeWidth === 'Gemischt' ? strokeWidth : `${formatNumber(strokeWidth)} px` }
    ];
    if (supportsStrokeType(object)) appearanceItems.splice(3, 0, { label: 'Linientyp', value: strokeTypeLabel(strokeType) });
    if (!multi && object.type !== 'text' && supportsObjectTexture(object)) appearanceItems.push({ label: 'Textur', value: textTextureSummary(textTextureSettings(object), formatNumber) });
    makeGroup('Darstellung', appearanceItems, 'property-readout__group--appearance');
    const stack = effectStack(object);
    const effectLabels = stack.map((effect) => `${effect.enabled === false ? 'Aus: ' : ''}${Effects.definitionFor(effect.type)?.label || effect.type}`).join(' · ');
    makeGroup('Effekte & Korrekturen', [
      { label: stack.length === 1 ? '1 Effekt' : `${stack.length} Effekte`, value: effectLabels || 'Keine' }
    ], 'property-readout__group--effects');
    if (AutoShapes.isAutoShape(object)) {
      makeGroup(AutoShapes.definitionFor(object)?.label || 'Auto-Form', autoShapeReadoutItems(object, formatNumber), 'property-readout__group--context');
    } else if (object.type === 'brush') {
      makeGroup('Pinsel', [{ label: 'Glättung', value: `${formatNumber(object.smoothing || 0)} %` }], 'property-readout__group--context');
    } else if (object.type === 'polygon') {
      makeGroup('Polygon', [{ label: 'Punkte', value: String(object.sides || object.points?.length || 5) }], 'property-readout__group--context');
    } else if (object.type === 'image') {
      makeGroup('Bitmap', [{ label: 'Pixelgröße', value: `${object.pixelWidth || Math.round(object.width)} × ${object.pixelHeight || Math.round(object.height)} px` }], 'property-readout__group--context');
    }
  }

  function syncPropertyDockSizeMode(object, objects = selectedObjects(), toolMode = false) {
    const hasObject = Boolean(object);
    const compact = hasObject && !toolMode && state.propertyDockCompact;
    refs.propertyDock?.classList.toggle('is-compact', compact);
    if (refs.propertyDock) refs.propertyDock.dataset.compact = String(compact);
    refs.workColumn?.classList.toggle('has-compact-properties', compact);
    refs.workColumn?.classList.toggle('has-tool-properties', toolMode);
    if (refs.propertyDockReadOnly) {
      refs.propertyDockReadOnly.hidden = !compact;
      if (compact) renderCompactPropertyLabels(objects);
      else refs.propertyDockReadOnly.replaceChildren();
    }
    if (refs.propertyDockSizeBtn) {
      refs.propertyDockSizeBtn.hidden = !hasObject || toolMode;
      refs.propertyDockSizeBtn.classList.toggle('is-compact', compact);
      refs.propertyDockSizeBtn.setAttribute('aria-expanded', String(!compact));
      const label = compact ? 'Größer' : 'Kleiner';
      const description = compact ? 'Eigenschaftenleiste vergrößern und Bearbeitung aktivieren' : 'Eigenschaftenleiste verkleinern und nur anzeigen';
      refs.propertyDockSizeBtn.title = description;
      refs.propertyDockSizeBtn.setAttribute('aria-label', description);
      if (refs.propertyDockSizeLabel) refs.propertyDockSizeLabel.textContent = label;
    }
    [refs.propertyDockTools, refs.propertyDockInspector].forEach((panel) => {
      if (!panel) return;
      panel.inert = compact;
      if (compact) panel.setAttribute('aria-disabled', 'true');
      else panel.removeAttribute('aria-disabled');
    });
  }

  function togglePropertyDockSize() {
    if (!selectedObjects().length || state.tool !== 'select') return;
    state.propertyDockCompact = !state.propertyDockCompact;
    try { localStorage.setItem(propertyDockCompactStorageKey, String(state.propertyDockCompact)); } catch { /* Lokale Einstellung ist optional. */ }
    syncPropertyDockContext();
    scheduleRulerRender();
    setTimeout(scheduleRulerRender, 180);
  }

  function syncPropertyDockNameEditability(editable) {
    const heading = refs.propertyDockHeading;
    if (!heading) return;
    heading.classList.toggle('is-editable', editable);
    if (editable) {
      heading.tabIndex = 0;
      heading.setAttribute('role', 'button');
      heading.setAttribute('aria-label', 'Objektnamen bearbeiten');
      heading.title = 'Objektnamen bearbeiten';
    } else {
      heading.removeAttribute('tabindex');
      heading.removeAttribute('role');
      heading.removeAttribute('aria-label');
      heading.removeAttribute('title');
    }
  }

  function editPropertyDockObjectName() {
    const heading = refs.propertyDockHeading;
    const objects = selectedObjects();
    const object = objects.length === 1 && state.tool === 'select' ? objects[0] : null;
    if (!heading || !object || heading.parentElement?.querySelector('.property-dock__heading-input')) return;
    const currentName = displayedObjectName(object);
    const input = document.createElement('input');
    input.type = 'text'; input.className = 'property-dock__heading-input'; input.value = currentName; input.maxLength = 80; input.spellcheck = false;
    input.setAttribute('aria-label', 'Objektname');
    let finished = false;
    const finish = (save) => {
      if (finished) return;
      finished = true;
      input.remove(); heading.hidden = false;
      const currentObject = objectEntry(object.id)?.object;
      if (!save || !currentObject) { syncPropertyDockContext(); return; }
      const fallback = names[currentObject.type] || 'Objekt';
      const nextName = String(input.value || '').trim().slice(0, 80) || fallback;
      const previousName = currentObject.name || fallback;
      if (nextName === previousName) { syncPropertyDockContext(); return; }
      currentObject.name = nextName;
      currentObject.nameManuallyEdited = true;
      delete currentObject.nameKey;
      commitHistory('Objekt umbenannt');
      renderAll();
    };
    input.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); finish(true); }
      if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); finish(false); }
    });
    input.addEventListener('blur', () => finish(true));
    heading.hidden = true; heading.after(input); input.focus(); input.select();
  }

  function syncPropertyDockContext(objects = selectedObjects()) {
    const object = objects.at(-1) || null;
    const toolMode = state.tool !== 'select';
    const automaticObjectName = object ? isAutomaticObjectName(object) : false;
    const objectName = object ? displayedObjectName(object) : '';
    const setSectionVisible = (element, visible) => {
      if (!element) return;
      element.hidden = !visible;
      element.setAttribute('aria-hidden', String(!visible));
    };
    const drawingStrokeTool = ['pencil', 'brush', 'line', 'polyline', 'pen'].includes(state.tool)
      || Boolean(shapeToolInfo[state.tool] && !['vector-emoji', 'vector-flag'].includes(state.tool));
    const options = $('propertyToolOptions');
    const strokeControls = $('strokeControls');
    if (options && strokeControls && strokeControls.parentElement !== options) options.prepend(strokeControls);
    setSectionVisible($('canvasControls'), !object && !toolMode);
    setSectionVisible($('canvasGridControls'), !object && !toolMode);
    setSectionVisible(strokeControls, drawingStrokeTool);
    setSectionVisible($('fontControls'), state.tool === 'text');
    setSectionVisible($('vectorEmojiControls'), state.tool === 'vector-emoji');
    setSectionVisible($('vectorFlagControls'), state.tool === 'vector-flag');

    if (options) {
      const hasVisibleSection = [...options.querySelectorAll(':scope > .options-section')].some((section) => !section.hidden);
      options.hidden = !hasVisibleSection;
      options.setAttribute('aria-hidden', String(!hasVisibleSection));
      if (refs.propertyDockTools) refs.propertyDockTools.hidden = !hasVisibleSection;
    }

    if (!refs.propertyDock) return;
    refs.propertyDock.dataset.objectName = objectName;
    refs.propertyDock.dataset.mode = toolMode ? 'tool' : object ? 'object' : 'canvas';
    refs.propertyDockHeading?.toggleAttribute('data-i18n-ignore-text', Boolean(object && objects.length === 1 && !toolMode && !automaticObjectName));
    refs.propertyDockSummary?.toggleAttribute('data-i18n-ignore-text', Boolean(objectName && toolMode && !automaticObjectName));
    refs.workColumn?.classList.toggle('has-canvas-properties', !object && !toolMode);
    refs.inspector?.classList.toggle('is-tool-context', toolMode);
    syncPropertyDockSizeMode(object, objects, toolMode);
    if (toolMode) {
      refs.propertyDockEyebrow.textContent = 'Werkzeug';
      refs.propertyDockHeading.textContent = names[state.tool] || 'Werkzeug';
      refs.propertyDockSummary.textContent = objectName || I18n.t('ui.active');
      syncPropertyDockNameEditability(false);
      return;
    }
    if (!object) {
      refs.propertyDockEyebrow.textContent = 'Dokument';
      refs.propertyDockHeading.textContent = 'Leinwand';
      refs.propertyDockSummary.textContent = `${state.doc.width} × ${state.doc.height} px · ${state.doc.dpi} dpi`;
      syncPropertyDockNameEditability(false);
      return;
    }
    refs.propertyDockEyebrow.textContent = objects.length > 1 ? 'Mehrfachauswahl' : 'Auswahl';
    refs.propertyDockHeading.textContent = objects.length > 1 ? `${objects.length} Objekte` : objectName;
    refs.propertyDockSummary.textContent = objects.length > 1 ? `${objects.length} ausgewählt` : (names[object.type] || object.type || 'Objekt');
    syncPropertyDockNameEditability(objects.length === 1);
  }

  function renderCollapsedPropertySummaries(objects, pathContext = textPathContext()) {
    const object = objects.at(-1) || null;
    const format = (value) => new Intl.NumberFormat(I18n.language === 'de' ? 'de-DE' : 'en-GB', { maximumFractionDigits: 2 }).format(num(value, 0));
    const common = (property, fallback = 0) => {
      const values = objects.map((item) => item?.[property] ?? fallback);
      return values.every((value) => value === values[0]) ? values[0] : 'Gemischt';
    };
    const numberUnit = (value, unit) => value === 'Gemischt' ? value : `${format(value)}${unit}`;
    const colorLabel = (value) => ['none', 'transparent', ''].includes(String(value || '').toLowerCase()) ? 'Transparent' : String(value).toUpperCase();
    const summaries = new Map();
    if (object) {
      const bounds = layoutBoundsOf(objects);
      const transformSummary = [
        ['X', `${format(bounds.x)} px`], ['Y', `${format(bounds.y)} px`], ['B', `${format(bounds.width)} px`], ['H', `${format(bounds.height)} px`],
        ['Drehung', numberUnit(common('rotation'), '°')], ['Skalierung', numberUnit(common('scalePercent', 100), ' %')]
      ];
      if (objects.length === 1 && supportsCornerRoundness(object)) transformSummary.push(['Rundung', `${format(objectRoundness(object))} px`]);
      summaries.set('transform', transformSummary);
      const stroke = objects.length === 1 && object.type === 'text' ? object.textStroke || 'none' : common('stroke', 'none');
      const strokeWidth = objects.length === 1 && object.type === 'text' ? num(object.textStrokeWidth, 1) : common('strokeWidth');
      const strokeType = common('strokeType', DEFAULT_STROKE_TYPE);
      const fillPaint = objects.length === 1 ? normalizedObjectFillPaint(object) : null;
      const appearance = [
        ['Deckkraft', numberUnit(common('opacity', 100), ' %')], ['Kontur', stroke === 'Gemischt' ? stroke : colorLabel(stroke)],
        ['Füllung', fillPaint ? FillPaint.typeLabel(fillPaint.type) : colorLabel(common('fill', 'none'))], ['Stärke', numberUnit(strokeWidth, ' px')]
      ];
      if (supportsStrokeType(object)) appearance.splice(3, 0, ['Linientyp', strokeTypeLabel(strokeType)]);
      if (objects.length === 1 && object.type !== 'text' && supportsObjectTexture(object)) appearance.push(['Textur', textTextureSummary(textTextureSettings(object), format)]);
      summaries.set('appearance', appearance);
      if (supportsStrokeType(object) && objects.length === 1) {
        const styles = strokeStackFor(object); const active = styles.filter(StrokeStyle.isVisible).length;
        summaries.set('stroke', [
          [styles.length === 1 ? '1 Kontur' : `${styles.length} Konturen`, `${active} aktiv`],
          ['Muster', styles.map((style) => strokePatternLabels[style.pattern] || style.pattern).join(' · ')],
          ['Ausrichtung', styles.map((style) => strokeAlignmentLabels[style.alignment] || style.alignment).join(' · ')]
        ]);
      }
      if (object.type === 'image' && objects.length === 1) {
        const scale = BitmapModel.pixelScale(object); const mode = bitmapModeFor(object);
        summaries.set('bitmap', [
          ['Modell', mode === 'direct' ? 'Direkt' : 'Platziert'],
          ['Pixel', `${scale.pixelWidth} × ${scale.pixelHeight}`],
          ['Skalierung', `${format(scale.x * 100)} × ${format(scale.y * 100)} %`]
        ]);
      }
      const stack = effectStack(object);
      summaries.set('effects', [[stack.length === 1 ? '1 Effekt' : `${stack.length} Effekte`, stack.map((effect) => Effects.definitionFor(effect.type)?.label || effect.type).join(' · ') || 'Keine']]);
      if (object.type === 'text') {
        const texture = textTextureSettings(object);
        summaries.set('text', [
          ['Inhalt', textObjectName(object.text)], ['Schrift', object.fontFamily || 'Arimo'], ['Größe', `${format(object.fontSize || 32)} px`],
          ['Stil', object.bold ? (object.italic ? 'Fett kursiv' : 'Fett') : (object.italic ? 'Kursiv' : 'Regular')],
          ['Textur', textTextureSummary(texture, format)]
        ]);
      }
      if (pathContext) {
        const settings = pathContext.binding || textPathDefaults;
        summaries.set('text-path', [
          ['Position', settings.placement === 'inside' ? 'Innen' : 'Außen'], ['Abstand', `${format(settings.distance)} px`],
          ['Versatz', `${format(settings.offset)}°`], ['Zeichen', settings.orientation === 'horizontal' ? 'Horizontal' : 'Vertikal'],
          ['Zeichenabstand', `${format(settings.spacing)} px`],
          ['Richtung', settings.reversed ? 'Umgedreht' : 'Normal'], ['Form', settings.targetVisible === false ? 'Ausgeblendet' : 'Sichtbar']
        ]);
      }
      if (object.type === 'brush') summaries.set('brush', [['Glättung', `${format(object.smoothing || 0)} %`]]);
      if (object.type === 'polygon') summaries.set('polygon', [['Punkte', String(object.sides || object.points?.length || 5)]]);
      if (AutoShapes.isAutoShape(object)) summaries.set('auto-shape', autoShapeReadoutItems(object, format).map((item) => [item.label, item.value]));
      summaries.set('paths', [['Auswahl', $('pathOperationHint')?.textContent || 'Keine Pfadaktion verfügbar']]);
    }
    document.querySelectorAll('.property-fieldset').forEach((fieldset) => {
      const section = fieldset.querySelector(':scope > legend [data-property-section]')?.dataset.propertySection;
      let summary = fieldset.querySelector(':scope > .property-collapsed-summary');
      const items = summaries.get(section) || [];
      if (!items.length) { summary?.remove(); return; }
      if (!summary) { summary = document.createElement('div'); summary.className = 'property-collapsed-summary'; fieldset.append(summary); }
      summary.replaceChildren(...items.map(([labelText, valueText]) => {
        const item = document.createElement('span'); item.className = 'property-collapsed-summary__item';
        const label = document.createElement('small'); label.textContent = labelText;
        const value = document.createElement('strong'); value.textContent = valueText; value.title = valueText;
        item.append(label, value); return item;
      }));
    });
  }

  function selectedFillStop(object, paint = normalizedObjectFillPaint(object)) {
    if (!paint || !FillPaint.isGradient(paint.type)) return null;
    const requested = fillStopSelection.objectId === object.id ? fillStopSelection.stopId : null;
    const stop = paint.stops.find((entry) => entry.id === requested) || paint.stops[0];
    fillStopSelection = { objectId: object.id, stopId: stop.id };
    return stop;
  }

  function stopCssColor(stop) {
    const rgb = rgbFromColor(stop.color) || [0, 0, 0];
    return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${round(stop.opacity / 100, 3)})`;
  }

  function setFillStopListGradient(list, paint) {
    if (!list || !paint?.stops) return;
    const gradient = paint.stops.map((stop) => `${stopCssColor(stop)} ${stop.offset}%`).join(', ');
    list.style.setProperty('--fill-stop-gradient', `linear-gradient(90deg, ${gradient})`);
  }

  function snapFillStopOffset(offset, bounds, event) {
    if (event?.altKey) return offset;
    const snapDistance = 7 / Math.max(1, bounds.width) * 100;
    return [25, 50, 75].find((point) => Math.abs(point - offset) <= snapDistance) ?? offset;
  }

  function selectFillStopInInspector(object, paint, stopId) {
    const stop = paint?.stops?.find((entry) => entry.id === stopId);
    if (!object || !stop) return null;
    fillStopSelection = { objectId: object.id, stopId: stop.id };
    $('fillStopList')?.querySelectorAll('[data-fill-stop-id]').forEach((marker) => {
      marker.setAttribute('aria-selected', String(marker.dataset.fillStopId === stop.id));
    });
    setColorField('propFill', stop.color, object.fill || '#4f6df5');
    setField('fillStopPosition', stop.offset); setField('fillStopOpacity', stop.opacity);
    return stop;
  }

  function renderFillStopList(object, paint) {
    const list = $('fillStopList'); if (!list) return;
    const selected = selectedFillStop(object, paint);
    setFillStopListGradient(list, paint);
    list.replaceChildren();
    paint.stops.forEach((stop) => {
      const marker = document.createElement('button'); marker.type = 'button'; marker.className = 'fill-stop-marker'; marker.dataset.fillStopId = stop.id;
      marker.style.left = `${stop.offset}%`; marker.style.setProperty('--stop-color', stop.color); marker.style.setProperty('--stop-opacity', stop.opacity);
      marker.style.setProperty('--stop-alpha', String(1 - stop.opacity / 100));
      marker.setAttribute('role', 'option'); marker.setAttribute('aria-selected', String(stop.id === selected?.id));
      marker.setAttribute('aria-label', `Farbpunkt ${Math.round(stop.offset)} Prozent, Deckkraft ${Math.round(stop.opacity)} Prozent`);
      marker.title = `${stop.color.toUpperCase()} · ${Math.round(stop.offset)} % · ${Math.round(stop.opacity)} %`;
      list.append(marker);
    });
    $('removeFillStopBtn').disabled = paint.stops.length <= 2;
  }

  function renderFillPaintInspector(object, selectionCount = 1) {
    const panel = $('fillPaintPanel'); const advanced = selectionCount === 1 && supportsAdvancedFill(object);
    panel.hidden = !advanced;
    if (!advanced) {
      $('fillGradientControls').hidden = true; $('fillImageControls').hidden = true;
      $('propFillField').hidden = !supportsAppearanceFill(object); $('propFillLabel').textContent = 'Füllung';
      setColorField('propFill', object?.fill, '#4f6df5'); return;
    }
    const paint = normalizedObjectFillPaint(object);
    const type = paint?.type || (['none', 'transparent', ''].includes(String(object.fill || '').toLowerCase()) ? 'none' : 'solid');
    setField('propFillType', type);
    const gradient = Boolean(paint && FillPaint.isGradient(type)); const image = Boolean(paint && FillPaint.isImage(type));
    $('fillGradientControls').hidden = !gradient; $('fillImageControls').hidden = !image;
    $('propFillField').hidden = image || type === 'none';
    $('propFillLabel').textContent = gradient ? 'Stoppfarbe' : 'Füllung';
    if (gradient) {
      const stop = selectedFillStop(object, paint);
      renderFillStopList(object, paint);
      setColorField('propFill', stop?.color, object.fill || '#4f6df5');
      setField('fillStopPosition', stop?.offset ?? 0); setField('fillStopOpacity', stop?.opacity ?? 100);
    } else {
      setColorField('propFill', object.fill, '#4f6df5');
    }
    if (image) {
      setField('fillImageScale', paint.scale); setField('fillImageRotation', paint.rotation);
      setField('fillImageOffsetX', paint.offsetX); setField('fillImageOffsetY', paint.offsetY);
      $('loadFillImageBtn').textContent = paint.href ? 'Bild ersetzen' : 'Bild laden'; $('removeFillImageBtn').disabled = !paint.href;
    }
  }

  function syncFreeTransformControls(object = primaryObject(), selectionCount = selectedObjects().length) {
    const single = selectionCount === 1 && Boolean(object); const transform = single ? activeFreeTransform(object) : null;
    const activeMode = single ? (state.freeTransformMode || transform?.mode || null) : null;
    document.querySelectorAll('[data-free-transform-mode]').forEach((button) => {
      const active = button.dataset.freeTransformMode === activeMode; button.setAttribute('aria-pressed', String(active)); button.classList.toggle('is-active', active); button.disabled = !single;
    });
    setField('freeTransformSkewX', transform?.skewX || 0); setField('freeTransformSkewY', transform?.skewY || 0);
    $('freeTransformSkewX').disabled = !single; $('freeTransformSkewY').disabled = !single;
    $('applyFreeTransformBtn').disabled = !(single && transform && isVectorObject(object));
    $('resetFreeTransformBtn').disabled = !(single && (transform || object?.transformPivot));
    $('freeTransformHint').textContent = !single
      ? 'Für Eck-, Perspektiv- oder Hüllkurvenverformung genau ein Objekt wählen.'
      : transform
        ? `${{ free: 'Vier Ecken', perspective: 'Perspektive', envelope: 'Hüllkurve' }[transform.mode]} aktiv · Drehmittelpunkt am roten Fadenkreuz.`
        : 'Modus wählen und die Anfasser auf der Leinwand ziehen.';
  }

  let inspectorScrollContext = '';

  function renderInspector() {
    const objects = selectedObjects();
    const object = objects.at(-1);
    const scrollContext = `${activeDocument()?.id || ''}:${objects.map((entry) => entry.id).join(',')}:${state.tool}`;
    const panelScrollPositions = inspectorScrollContext === scrollContext
      ? new Map([...refs.inspector.querySelectorAll('.property-fieldset[id]')].map((panel) => [panel.id, panel.scrollTop]))
      : new Map();
    inspectorScrollContext = scrollContext;
    const modifierPanel = refs.rightSidebar?.querySelector('.modifier-panel');
    if (modifierPanel) modifierPanel.hidden = objects.length === 0;
    const freeTransformSection = refs.modifierPanelContent?.querySelector('[data-sidebar-section="modifier:free-transform"]');
    if (freeTransformSection) freeTransformSection.hidden = objects.length !== 1;
    syncFreeTransformControls(object, objects.length);
    const pathContext = textPathContext();
    syncPropertyDockContext(objects);
    refs.summary.textContent = objects.length ? `${objects.length} ${objects.length === 1 ? 'Objekt' : 'Objekte'}` : '0 Objekte';
    refs.inspectorEmpty.hidden = Boolean(object);
    refs.inspector.hidden = !object;
    $('textPathProperties').hidden = !pathContext;
    const pathMembers = selectedTopLevelMembers(); const selectedMask = objects.length === 1 && object?.type === 'mask-group' ? object : null;
    const selectedClip = objects.length === 1 && object?.type === 'clip-group' ? object : null;
    $('pathOperationHint').textContent = selectedMask
      ? `Maske ${selectedMask.maskInverted ? 'umgekehrt' : 'normal'} · ${(selectedMask.children || []).filter((child) => child.maskRole !== 'mask').length} Inhaltobjekt(e)`
      : selectedClip
        ? `Clipping-Pfad · ${(selectedClip.children || []).filter((child) => child.clipRole !== 'clip').length} Inhaltobjekt(e)`
      : pathMembers.length >= 2
        ? `${pathMembers.length} Objekte · vorderstes Objekt dient als Maske oder Clipping-Pfad, hinterstes als Basis der Subtraktion.`
        : 'Für boolesche Pfade mindestens zwei geschlossene Vektoren auswählen.';
    renderEffectStackInspector(object);
    renderAutoShapeInspector(object);
    const strokeContext = renderStrokeStackInspector(object, objects.length);
    renderBitmapInspector(object, objects.length);
    applyInspectorSectionStates(inspectorSectionContext(objects));
    const cornerRoundnessVisible = objects.length === 1 && supportsCornerRoundness(object);
    $('propCornerRadiusField').hidden = !cornerRoundnessVisible;
    $('objectTransformProperties').classList.toggle('has-corner-radius', cornerRoundnessVisible);
    if (pathContext) {
      const { text, target, binding } = pathContext;
      const settings = binding || textPathDefaults;
      setField('propTextPathPlacement', settings.placement); setField('propTextPathDistance', settings.distance); setField('propTextPathOffset', settings.offset); setField('propTextPathOrientation', settings.orientation); setField('propTextPathSpacing', settings.spacing);
      $('propTextPathReversed').checked = Boolean(settings.reversed);
      $('propTextPathTargetVisible').checked = target.visible !== false;
      $('textPathHint').textContent = binding ? `„${text.name || 'Text'}“ folgt „${target.name || names[target.type] || 'Form'}“.` : 'Text und Form sind bereit zum Verbinden.';
      $('bindTextPathBtn').disabled = Boolean(binding); $('unbindTextPathBtn').disabled = !binding;
    }
    if (!object) return;
    setField('propX', round(object.x)); setField('propY', round(object.y));
    setField('propW', round(object.width)); setField('propH', round(object.height));
    setField('propRotation', round(object.rotation)); setField('propScale', objectScalePercent(object)); setField('propOpacity', num(object.opacity, 100)); setField('propBlendMode', normalizeBlendMode(object.blendMode));
    if (cornerRoundnessVisible) {
      $('propCornerRadius').max = String(round(objectRoundnessLimit(object), 4));
      setField('propCornerRadius', objectRoundness(object));
    }
    const selectedStroke = strokeContext?.style || primaryStrokeStyle(object);
    setColorField('propStroke', object.type === 'image' ? object.stroke : selectedStroke.color, '#000000');
    renderFillPaintInspector(object, objects.length);
    $('strokeTypeRow').hidden = !supportsStrokeType(object); setField('propStrokeType', selectedStroke.enabled ? selectedStroke.strokeType : 'none');
    if (!['image', ...containerObjectTypes, 'text'].includes(object.type)) setColorField('strokeColor', object.stroke, '#172033');
    if (supportsObjectFill(object)) {
      const paint = normalizedObjectFillPaint(object); const representative = paint && FillPaint.isGradient(paint.type) ? paint.stops[0]?.color : object.fill;
      setColorField('fillColor', representative, '#4f6df5');
      $('noFill').checked = !paint && (object.fill === 'none' || object.fill === 'transparent');
    }
    setField('propStrokeWidth', object.type === 'image' ? num(object.strokeWidth, 0) : selectedStroke.width);
    if (!['image', ...containerObjectTypes, 'text'].includes(object.type)) {
      const strokeWidth = clamp(Math.round(num(object.strokeWidth, 3)), 1, 64);
      setField('strokeWidth', strokeWidth);
    }
    $('textProperties').hidden = object.type !== 'text';
    $('brushProperties').hidden = object.type !== 'brush';
    $('polygonProperties').hidden = object.type !== 'polygon';
    if (object.type === 'brush') {
      const smoothing = clamp(Math.round(num(object.smoothing, 50)), 0, 100);
      setField('propSmoothing', smoothing); $('propSmoothingValue').textContent = `${smoothing} %`;
    }
    if (object.type === 'polygon') setField('propPolygonSides', polygonSideCount(object.sides || object.points?.length));
    const texturePanel = document.querySelector('.text-texture-panel');
    if (texturePanel) texturePanel.hidden = !supportsObjectTexture(object);
    if (supportsObjectTexture(object)) syncObjectTextureInspector(object);
    if (object.type === 'text') {
      const settings = textSettings(object); const style = object.bold ? (object.italic ? 'bold-italic' : 'bold') : (object.italic ? 'italic' : 'regular');
      setField('propText', object.text || ''); setField('propFontFamily', object.fontFamily || DEFAULT_FONT_FAMILY); setField('fontFamily', object.fontFamily || DEFAULT_FONT_FAMILY); refreshFontPreviews();
      setField('propFontSize', num(object.fontSize, 32)); setField('fontSize', num(object.fontSize, 32)); setField('propFontStyle', style);
      setField('propLetterSpacing', settings.letterSpacing); setField('propLineHeight', settings.lineHeight); setField('propHorizontalScale', settings.horizontalScale); setField('propVerticalScale', settings.verticalScale); setField('propBaselineShift', settings.baselineShift); setField('propAntiAlias', settings.antiAlias);
      $('propAutoKern').checked = settings.autoKern;
      $('propBold').classList.toggle('is-active', Boolean(object.bold)); $('propBold').setAttribute('aria-pressed', Boolean(object.bold));
      $('propItalic').classList.toggle('is-active', Boolean(object.italic)); $('propItalic').setAttribute('aria-pressed', Boolean(object.italic));
      $('propUnderline').classList.toggle('is-active', settings.underline); $('propUnderline').setAttribute('aria-pressed', settings.underline);
      $('propStrike').classList.toggle('is-active', settings.strike); $('propStrike').setAttribute('aria-pressed', settings.strike);
      [['fontBold', object.bold], ['fontItalic', object.italic]].forEach(([id, enabled]) => { $(id).classList.toggle('is-active', Boolean(enabled)); $(id).setAttribute('aria-pressed', String(Boolean(enabled))); });
      [['propAlignLeft', 'left'], ['propAlignCenter', 'center'], ['propAlignRight', 'right'], ['propAlignJustify', 'justify']].forEach(([id, align]) => { $(id).classList.toggle('is-active', settings.align === align); $(id).setAttribute('aria-pressed', String(settings.align === align)); });
    }
    enforceInspectorAccordion(inspectorSectionContext(objects));
    renderCollapsedPropertySummaries(objects, pathContext);
    panelScrollPositions.forEach((scrollTop, id) => {
      const panel = $(id);
      if (panel) panel.scrollTop = Math.min(scrollTop, Math.max(0, panel.scrollHeight - panel.clientHeight));
    });
  }

  function updateCommandState() {
    $('undoBtn').disabled = state.historyIndex <= 0;
    $('redoBtn').disabled = state.historyIndex >= state.history.length - 1;
    const undoCount = Math.max(0, state.historyIndex);
    const undoAction = undoCount > 0
      ? I18n.translateSource(state.history[state.historyIndex]?.label || 'Änderung')
      : I18n.t('status.noUndoSteps');
    const undoSummary = I18n.t('status.undoSummary', { count: undoCount, action: undoAction });
    refs.undoStatusText.textContent = undoSummary;
    refs.undoStatus.title = undoSummary;
    refs.undoStatus.setAttribute('aria-label', undoSummary);
    refs.undoStatus.dataset.undoCount = String(undoCount);
    refs.undoStatus.dataset.undoAction = undoCount > 0 ? state.history[state.historyIndex]?.label || 'Änderung' : '';
    ['cutBtn', 'copyBtn', 'deleteBtn', 'duplicateBtn'].forEach((id) => { $(id).disabled = !state.selected.length; });
    ['rotateLeftBtn', 'rotateRightBtn', 'flipHorizontalBtn', 'flipVerticalBtn'].forEach((id) => { $(id).disabled = !selectedObjects().length; });
    ['distributeHorizontalBtn', 'distributeVerticalBtn'].forEach((id) => { $(id).disabled = selectedObjects().length < 3; });
    syncFreeTransformControls(primaryObject(), selectedObjects().length);
    const selectedGroup = state.selected.length === 1 ? primaryObject() : null;
    const selectedGroups = selectedObjects().filter((object) => object.type === 'group');
    const vectorTool = $('vectorSelectTool'); const editableVectors = vectorEditObjects();
    if (vectorTool) vectorTool.disabled = state.tool !== 'vector-select' && !editableVectors.length;
    $('groupBtn').disabled = !canGroupSelection();
    $('ungroupBtn').disabled = selectedGroups.length === 0;
    $('ungroupAllBtn').disabled = selectedGroups.length === 0;
    $('flattenBtn').disabled = selectedGroup?.type !== 'group';
    const booleanReady = canBooleanSelection();
    ['booleanUnionBtn', 'booleanSubtractBtn', 'booleanIntersectBtn', 'booleanExcludeBtn'].forEach((id) => { $(id).disabled = !booleanReady; });
    $('maskSelectionBtn').disabled = !canCreateVectorMask();
    const maskReady = selectedGroup?.type === 'mask-group' && Boolean(maskChildFor(selectedGroup));
    $('releaseMaskBtn').disabled = !maskReady; $('invertMaskBtn').disabled = !maskReady;
    if (maskReady) $('invertMaskBtn').textContent = selectedGroup.maskInverted ? 'Maske normalisieren' : 'Maske umkehren';
    else $('invertMaskBtn').textContent = 'Maske umkehren';
    $('clipSelectionBtn').disabled = !canCreateClippingPath();
    $('releaseClipBtn').disabled = !(selectedGroup?.type === 'clip-group' && Boolean(clipChildFor(selectedGroup)));
    $('pasteBtn').disabled = false;
    refs.count.textContent = `${allObjects().length} ${allObjects().length === 1 ? 'Objekt' : 'Objekte'}`;
    setStatus(state.dirty ? 'Ungespeichert' : 'Bereit');
  }

  function syncZoomControls() {
    const percent = Math.round(state.zoom * 100);
    $('zoomRange').value = String(clamp(percent, 6, 6400));
    $('zoomLabel').textContent = `${percent} %`;
    const preset = $('zoomPreset');
    const customPreset = $('zoomCustomPreset');
    const isPreset = Array.from(preset.options).some((option) => option !== customPreset && Number(option.value) === percent);
    if (isPreset) {
      customPreset.hidden = true;
      customPreset.disabled = true;
      preset.value = String(percent);
    } else {
      customPreset.value = String(percent);
      customPreset.textContent = `${percent} %`;
      customPreset.hidden = false;
      customPreset.disabled = false;
      preset.value = customPreset.value;
    }
  }

  let pendingZoom = null;
  let pendingZoomFrame = null;
  let zoomSettleTimer = null;
  let renderedZoom = state.zoom;
  const ZOOM_SETTLE_DELAY = 120;

  function cancelZoomScheduling() {
    if (pendingZoomFrame) cancelAnimationFrame(pendingZoomFrame);
    if (zoomSettleTimer) clearTimeout(zoomSettleTimer);
    pendingZoomFrame = null; pendingZoom = null; zoomSettleTimer = null;
  }

  function setZoomDimensions(zoom, { artboard = true } = {}) {
    const width = state.doc.width * zoom; const height = state.doc.height * zoom;
    if (artboard) {
      refs.artboard.style.width = `${width}px`;
      refs.artboard.style.height = `${height}px`;
    }
    refs.shell.style.width = `${width}px`;
    refs.shell.style.height = `${height}px`;
  }

  function refreshZoomDependentArtboard() {
    const canvasGridPath = refs.artboard.querySelector('#canvasGridPattern > path');
    if (canvasGridPath) {
      const canvasGrid = documentGrid();
      canvasGridPath.setAttribute('stroke-width', Math.min(1 / Math.max(state.zoom, .06), canvasGrid.size * .12));
    }
    refs.artboard.querySelectorAll('[data-bitmap-bleed]').forEach((node) => {
      const objectId = node.closest('g[data-object-id]')?.dataset.objectId;
      const object = objectId ? objectEntry(objectId)?.object : null;
      if (!object) return;
      const bleed = 1.1 / Math.max(state.zoom, .01);
      node.setAttribute('x', -bleed); node.setAttribute('y', -bleed);
      node.setAttribute('width', Math.max(1, num(object.width, 1)) + bleed * 2);
      node.setAttribute('height', Math.max(1, num(object.height, 1)) + bleed * 2);
    });
    renderEditorOverlays();
  }

  function settleZoomPreview({ refresh = true } = {}) {
    cancelZoomScheduling();
    setZoomDimensions(state.zoom);
    refs.artboard.style.transform = '';
    refs.artboard.style.willChange = '';
    refs.artboard.dataset.zoomPreview = 'false';
    refs.artboard.dataset.renderedZoom = String(state.zoom);
    renderedZoom = state.zoom;
    if (refresh && artboardScene.overlayRoot?.isConnected) refreshZoomDependentArtboard();
  }

  function applyZoomAnchor(oldZoom, anchor) {
    if (!anchor || oldZoom === state.zoom) return;
    const factor = state.zoom / oldZoom;
    refs.workspace.scrollLeft = (refs.workspace.scrollLeft + anchor.x) * factor - anchor.x;
    refs.workspace.scrollTop = (refs.workspace.scrollTop + anchor.y) * factor - anchor.y;
  }

  function applyZoom(next, anchor = null) {
    cancelZoomScheduling();
    const old = state.zoom;
    state.zoom = clamp(num(next, 1), .06, 64);
    setZoomDimensions(state.zoom);
    refs.artboard.style.transform = '';
    refs.artboard.style.willChange = '';
    refs.artboard.dataset.zoomPreview = 'false';
    refs.artboard.dataset.renderedZoom = String(state.zoom);
    renderedZoom = state.zoom;
    syncZoomControls();
    applyZoomAnchor(old, anchor);
    refreshZoomDependentArtboard(); positionInlineTextEditor(); renderRulers();
  }

  function previewZoom(next, anchor = null) {
    const old = state.zoom;
    state.zoom = clamp(num(next, 1), .06, 64);
    const baseZoom = Math.max(.0001, renderedZoom);
    setZoomDimensions(baseZoom);
    setZoomDimensions(state.zoom, { artboard: false });
    refs.artboard.style.willChange = 'transform';
    refs.artboard.style.transform = `scale(${state.zoom / baseZoom})`;
    refs.artboard.dataset.zoomPreview = 'true';
    syncZoomControls();
    applyZoomAnchor(old, anchor);
    positionInlineTextEditor(); scheduleRulerRender();
    zoomSettleTimer = setTimeout(() => {
      settleZoomPreview();
      positionInlineTextEditor(); renderRulers();
    }, ZOOM_SETTLE_DELAY);
  }

  function scheduleZoom(next, anchor = null) {
    pendingZoom = { next: clamp(num(next, 1), .06, 64), anchor };
    if (zoomSettleTimer) {
      clearTimeout(zoomSettleTimer);
      zoomSettleTimer = null;
    }
    if (pendingZoomFrame) return;
    pendingZoomFrame = requestAnimationFrame(() => {
      pendingZoomFrame = null;
      const request = pendingZoom; pendingZoom = null;
      if (request) previewZoom(request.next, request.anchor);
    });
  }

  function zoomFit() {
    const padding = 92;
    const scale = Math.min((refs.workspace.clientWidth - padding) / state.doc.width, (refs.workspace.clientHeight - padding) / state.doc.height);
    applyZoom(clamp(scale, .06, 64));
    requestAnimationFrame(() => { refs.workspace.scrollLeft = Math.max(0, (refs.workspace.scrollWidth - refs.workspace.clientWidth) / 2); refs.workspace.scrollTop = Math.max(0, (refs.workspace.scrollHeight - refs.workspace.clientHeight) / 2); scheduleRulerRender(); });
  }

  function renderAll() {
    return measurePerformancePhase('render.all', () => {
      $('canvasWidth').value = state.doc.width; $('canvasHeight').value = state.doc.height; $('canvasDpi').value = state.doc.dpi; setColorField('canvasBg', state.doc.bg, '#ffffff');
      syncCanvasGridControls();
      syncSnapControls();
      syncManualCanvasResizeButton();
      renderArtboard();
      measurePerformancePhase('render.layers', renderLayers);
      measurePerformancePhase('render.inspector', renderInspector);
      measurePerformancePhase('render.library', renderReusableAssetsPanel);
      measurePerformancePhase('render.tabs', renderDocumentTabs);
      measurePerformancePhase('render.commands', updateCommandState);
      syncAutoSaveControl(); syncTabSessionControl(); syncGuideLockButton();
      if (state.tool === 'vector-select') syncVectorPathOptions();
      applyZoomDimensionsOnly(); positionInlineTextEditor();
    });
  }

  function syncLayerSelectionState() {
    const selected = new Set(state.selected);
    const keyObjectId = alignmentKeyObjectId();
    const layerBlendMode = $('layerBlendMode'); const selectedLayer = activeLayer();
    if (layerBlendMode) {
      layerBlendMode.value = normalizeBlendMode(selectedLayer?.blendMode);
      layerBlendMode.disabled = !selectedLayer;
    }
    refs.layerList.querySelectorAll('.layer-entry').forEach((entry) => {
      const active = entry.dataset.layerId === state.doc.activeLayerId;
      const row = entry.querySelector(':scope > .layer-row');
      row?.classList.toggle('is-active', active);
      row?.setAttribute('aria-selected', String(active));
      const indicator = row?.querySelector('.layer-active-indicator');
      if (indicator) indicator.textContent = active ? '●' : '';
    });
    refs.layerList.querySelectorAll('.layer-object-row').forEach((row) => {
      row.classList.toggle('is-selected', selected.has(row.dataset.objectId));
      const isKeyObject = row.dataset.objectId === keyObjectId;
      row.classList.toggle('is-key-object', isKeyObject);
      if (isKeyObject) row.setAttribute('aria-current', 'true');
      else row.removeAttribute('aria-current');
      const title = `${row.dataset.baseTitle || row.title || ''}${isKeyObject ? ` · ${I18n.t('ui.keyObject')}` : ''}`;
      row.title = title; row.setAttribute('aria-label', title);
    });
  }

  function renderSelectionState() {
    return measurePerformancePhase('render.selection', () => {
      const startedAt = performance.now();
      measurePerformancePhase('render.overlay', renderEditorOverlays);
      const overlaysAt = performance.now();
      measurePerformancePhase('render.layerSelection', syncLayerSelectionState);
      const layersAt = performance.now();
      measurePerformancePhase('render.inspector', renderInspector);
      const inspectorAt = performance.now();
      measurePerformancePhase('render.library', renderReusableAssetsPanel);
      const libraryAt = performance.now();
      measurePerformancePhase('render.commands', updateCommandState);
      const commandsAt = performance.now();
      if (state.tool === 'vector-select') syncVectorPathOptions();
      refs.artboard.dataset.selectionRenderMs = String(round(performance.now() - startedAt, 2));
      refs.artboard.dataset.selectionOverlayMs = String(round(overlaysAt - startedAt, 2));
      refs.artboard.dataset.selectionLayersMs = String(round(layersAt - overlaysAt, 2));
      refs.artboard.dataset.selectionInspectorMs = String(round(inspectorAt - layersAt, 2));
      refs.artboard.dataset.selectionLibraryMs = String(round(libraryAt - inspectorAt, 2));
      refs.artboard.dataset.selectionCommandsMs = String(round(commandsAt - libraryAt, 2));
      refs.artboard.dataset.selectionRenderCount = String(Number(refs.artboard.dataset.selectionRenderCount || 0) + 1);
    });
  }

  function syncMovedObjectInspector(objects = selectedObjects()) {
    const object = objects.at(-1); if (!object) return;
    setField('propX', round(object.x)); setField('propY', round(object.y));
    setField('propW', round(object.width)); setField('propH', round(object.height));
    setField('propScale', objectScalePercent(object));
    const bounds = layoutBoundsOf(objects);
    const format = (value) => new Intl.NumberFormat(I18n.language === 'de' ? 'de-DE' : 'en-GB', { maximumFractionDigits: 2 }).format(num(value, 0));
    const values = new Map([
      ['X', `${format(bounds.x)} px`], ['Y', `${format(bounds.y)} px`],
      ['B', `${format(bounds.width)} px`], ['H', `${format(bounds.height)} px`]
    ]);
    const patchItems = (root, selector) => root?.querySelectorAll(selector).forEach((item) => {
      const label = item.querySelector('small')?.textContent?.trim(); const value = values.get(label);
      const output = item.querySelector('strong');
      if (!value || !output) return;
      output.textContent = value; output.title = value;
    });
    patchItems(refs.propertyDockReadOnly?.querySelector('.property-readout__group--object-transform'), '.property-readout__item');
    const transformFieldset = document.querySelector('.property-fieldset legend [data-property-section="transform"]')?.closest('.property-fieldset');
    patchItems(transformFieldset?.querySelector('.property-collapsed-summary'), '.property-collapsed-summary__item');
  }

  function renderMovedSelectionState() {
    return measurePerformancePhase('render.selection', () => {
      const startedAt = performance.now();
      const overlaysAt = startedAt;
      const layersAt = overlaysAt;
      measurePerformancePhase('render.transformInspector', syncMovedObjectInspector);
      const inspectorAt = performance.now();
      const libraryAt = inspectorAt;
      measurePerformancePhase('render.commands', updateCommandState);
      const commandsAt = performance.now();
      refs.artboard.dataset.selectionRenderMs = String(round(performance.now() - startedAt, 2));
      refs.artboard.dataset.selectionOverlayMs = String(round(overlaysAt - startedAt, 2));
      refs.artboard.dataset.selectionLayersMs = '0';
      refs.artboard.dataset.selectionInspectorMs = String(round(inspectorAt - layersAt, 2));
      refs.artboard.dataset.selectionLibraryMs = '0';
      refs.artboard.dataset.selectionCommandsMs = String(round(commandsAt - libraryAt, 2));
      refs.artboard.dataset.selectionRenderCount = String(Number(refs.artboard.dataset.selectionRenderCount || 0) + 1);
    });
  }

  function renderDrawCompletionState(object, layer) {
    return measurePerformancePhase('render.drawFinalize', () => {
      measurePerformancePhase('render.layerInsert', () => insertLayerObjectRow(object, layer));
      measurePerformancePhase('render.transformInspector', () => {
        syncMovedObjectInspector([object]);
        if (AutoShapes.isAutoShape(object)) renderAutoShapeInspector(object);
      });
      measurePerformancePhase('render.commands', updateCommandState);
      persistActiveDocument();
      measurePerformancePhase('render.tabState', syncActiveDocumentTabSaveState);
      renderDocumentOverview();
    });
  }

  function applyZoomDimensionsOnly() {
    settleZoomPreview({ refresh: false });
    syncZoomControls(); positionInlineTextEditor(); renderRulers();
  }

  function select(ids, additive = false, { render = true } = {}) {
    const previousPrimary = state.selected.at(-1);
    const allowed = ids.filter((id) => {
      const entry = objectEntry(id); return entry && entry.layer.visible && !entry.layer.locked && entry.object.visible !== false && !entry.object.locked;
    });
    if (allowed.length && state.canvasResizeMode) {
      state.canvasResizeMode = false;
      refs.shell.style.transform = '';
    }
    if (additive) allowed.forEach((id) => { state.selected.includes(id) ? state.selected.splice(state.selected.indexOf(id), 1) : state.selected.push(id); });
    else state.selected = [...new Set(allowed)];
    if (state.selected.at(-1) !== previousPrimary) state.freeTransformMode = null;
    clearVectorSubselection();
    if (state.bitmapMarquee && !state.selected.includes(state.bitmapMarquee.objectId)) state.bitmapMarquee = null;
    if (state.wandSeed && !state.selected.includes(state.wandSeed.objectId)) state.wandSeed = null;
    const layer = layerForObject(state.selected.at(-1)); if (layer) state.doc.activeLayerId = layer.id;
    syncPixelToolOptions();
    if (render) renderSelectionState();
  }

  function filteredPencilPoints(points, strength = 50) {
    const amount = clamp(num(strength, 50), 0, 100) / 100;
    let result = points.map(([x, y]) => [round(x), round(y)]);
    if (result.length < 3 || amount <= 0) return result;
    const passes = Math.ceil(amount * 6); const weight = .72 * amount;
    for (let pass = 0; pass < passes; pass += 1) {
      const next = [result[0]];
      for (let index = 1; index < result.length - 1; index += 1) {
        const [previousX, previousY] = result[index - 1]; const [x, y] = result[index]; const [nextX, nextY] = result[index + 1];
        next.push([round(x * (1 - weight) + (previousX + nextX) * weight / 2), round(y * (1 - weight) + (previousY + nextY) * weight / 2)]);
      }
      next.push(result.at(-1)); result = next;
    }
    return result;
  }

  function pencilPathData(points, strength = 50) {
    const source = filteredPencilPoints(points, strength);
    if (!source.length) return 'M 0 0';
    if (source.length < 3 || num(strength) <= 0) return source.map((point, index) => `${index ? 'L' : 'M'} ${round(point[0])} ${round(point[1])}`).join(' ');
    const tension = .35 + clamp(num(strength, 50), 0, 100) / 100 * .65;
    let d = `M ${round(source[0][0])} ${round(source[0][1])}`;
    for (let index = 0; index < source.length - 1; index += 1) {
      const previous = source[Math.max(0, index - 1)]; const current = source[index];
      const next = source[index + 1]; const after = source[Math.min(source.length - 1, index + 2)];
      const controlOne = [current[0] + (next[0] - previous[0]) * tension / 6, current[1] + (next[1] - previous[1]) * tension / 6];
      const controlTwo = [next[0] - (after[0] - current[0]) * tension / 6, next[1] - (after[1] - current[1]) * tension / 6];
      d += ` C ${round(controlOne[0])} ${round(controlOne[1])} ${round(controlTwo[0])} ${round(controlTwo[1])} ${round(next[0])} ${round(next[1])}`;
    }
    return d;
  }

  function normalizePath(object) {
    const absolute = (object.points || []).map(([x, y]) => [num(object.x) + x, num(object.y) + y]);
    if (!absolute.length) return;
    const minX = Math.min(...absolute.map((p) => p[0])); const minY = Math.min(...absolute.map((p) => p[1]));
    const maxX = Math.max(...absolute.map((p) => p[0])); const maxY = Math.max(...absolute.map((p) => p[1]));
    object.x = minX; object.y = minY; object.width = Math.max(1, maxX - minX); object.height = Math.max(1, maxY - minY);
    const normalized = absolute.map(([x, y]) => [round(x - minX), round(y - minY)]);
    if (object.type === 'pencil') {
      object.rawPoints = normalized;
      object.points = normalized;
    } else object.points = normalized;
  }

  function gridSnappedPoint(point) {
    const grid = documentGrid();
    if (!grid.snap) return point;
    return {
      x: round(Math.round(num(point.x) / grid.size) * grid.size),
      y: round(Math.round(num(point.y) / grid.size) * grid.size)
    };
  }

  function snapDelta(dx, dy, origins) {
    state.snapGuides = [];
    const grid = documentGrid();
    if ((!state.snap && !grid.snap) || !origins.length) return { dx, dy };
    const moving = origins.map((item) => ({ ...item.object, x: item.x + dx, y: item.y + dy }));
    const box = layoutBoundsOf(moving); const threshold = 7 / state.zoom;
    const targetsX = []; const targetsY = [];
    if (state.snap) {
      targetsX.push(0, state.doc.width / 2, state.doc.width, ...(state.doc.guides?.vertical || []));
      targetsY.push(0, state.doc.height / 2, state.doc.height, ...(state.doc.guides?.horizontal || []));
      const selectedSet = new Set(origins.map((item) => item.object.id));
      state.doc.layers.forEach((layer) => {
        if (!layer.visible) return;
        layer.objects.forEach((object) => {
          if (selectedSet.has(object.id)) return;
          const targetBounds = layoutBoundsOf([object]);
          targetsX.push(targetBounds.x, targetBounds.cx, targetBounds.right);
          targetsY.push(targetBounds.y, targetBounds.cy, targetBounds.bottom);
        });
      });
    }
    let adjustX = 0; let bestX = threshold + 1; let guideX = null;
    const sourceX = [box.x, box.cx, box.right]; const sourceY = [box.y, box.cy, box.bottom];
    const considerX = (source, target) => { const delta = target - source; if (Math.abs(delta) < bestX) { bestX = Math.abs(delta); adjustX = delta; guideX = target; } };
    sourceX.forEach((source) => targetsX.forEach((target) => considerX(source, target)));
    let adjustY = 0; let bestY = threshold + 1; let guideY = null;
    const considerY = (source, target) => { const delta = target - source; if (Math.abs(delta) < bestY) { bestY = Math.abs(delta); adjustY = delta; guideY = target; } };
    sourceY.forEach((source) => targetsY.forEach((target) => considerY(source, target)));
    if (grid.snap) {
      sourceX.forEach((source) => considerX(source, Math.round(source / grid.size) * grid.size));
      sourceY.forEach((source) => considerY(source, Math.round(source / grid.size) * grid.size));
    }
    if (bestX <= threshold && guideX !== null) state.snapGuides.push({ axis: 'x', value: guideX });
    if (bestY <= threshold && guideY !== null) state.snapGuides.push({ axis: 'y', value: guideY });
    return { dx: dx + (bestX <= threshold ? adjustX : 0), dy: dy + (bestY <= threshold ? adjustY : 0) };
  }

  const snapSettingFields = Object.freeze({
    anchors: 'snapAnchors', segments: 'snapSegments', intersections: 'snapIntersections', midpoints: 'snapMidpoints',
    centers: 'snapCenters', tangents: 'snapTangents', connections: 'snapConnections'
  });

  function syncSnapControls() {
    $('snapToggle').checked = Boolean(state.snap);
    Object.entries(snapSettingFields).forEach(([setting, id]) => {
      const field = $(id); if (!field) return;
      field.checked = state.snapSettings[setting] !== false; field.disabled = !state.snap;
    });
  }

  function persistSnapSettings() {
    try { localStorage.setItem(snapSettingsStorageKey, JSON.stringify(state.snapSettings)); }
    catch { /* Einrastvorgaben sind eine optionale lokale Einstellung. */ }
  }

  function snapPathDefinition(object) {
    if (pointVectorTypes.has(object?.type)) {
      const points = vectorEditablePoints(object);
      return points.length >= 2 ? [{
        points, closed: isClosedVectorPath(object),
        segments: Array.from({ length: vectorSegmentCount(object, points) }, (_, index) => deepClone(object.bezierSegments?.[index] || null))
      }] : [];
    }
    return closedVectorPaths(object);
  }

  function sampledSnapPath(object, path) {
    const points = path?.points || []; if (points.length < 2) return [];
    const result = [];
    const count = path.closed ? points.length : points.length - 1;
    for (let index = 0; index < count; index += 1) {
      const next = (index + 1) % points.length; const start = points[index]; const end = points[next]; const segment = path.segments?.[index];
      if (!result.length) result.push(objectLocalToDocument(object, { x: start[0], y: start[1] }));
      if (isBezierSegment(segment)) {
        const estimated = distanceBetweenPoints(start, segment.c1) + distanceBetweenPoints(segment.c1, segment.c2) + distanceBetweenPoints(segment.c2, end);
        const steps = clamp(Math.ceil(estimated / 9), 8, 48);
        for (let step = 1; step <= steps; step += 1) {
          const sample = cubicPoint(start, segment.c1, segment.c2, end, step / steps);
          result.push(objectLocalToDocument(object, { x: sample[0], y: sample[1] }));
        }
      } else result.push(objectLocalToDocument(object, { x: end[0], y: end[1] }));
    }
    if (path.closed && result.length > 2 && Math.hypot(result[0].x - result.at(-1).x, result[0].y - result.at(-1).y) < .001) result.pop();
    return result;
  }

  function pathMidpoint(points, closed = false) {
    if (points.length < 2) return null;
    const segments = [];
    for (let index = 0; index < points.length - 1; index += 1) segments.push([points[index], points[index + 1]]);
    if (closed) segments.push([points.at(-1), points[0]]);
    const lengths = segments.map(([a, b]) => Math.hypot(b.x - a.x, b.y - a.y)); const total = lengths.reduce((sum, value) => sum + value, 0);
    if (total < .001) return null;
    let remaining = total / 2;
    for (let index = 0; index < segments.length; index += 1) {
      const length = lengths[index];
      if (remaining <= length || index === segments.length - 1) {
        const [a, b] = segments[index]; const t = length > .0001 ? remaining / length : 0;
        return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
      }
      remaining -= length;
    }
    return null;
  }

  function precisionSnapScene(excludeObjectIds = []) {
    const excluded = new Set(excludeObjectIds); const scene = { points: [], segments: [] };
    state.doc.layers.forEach((layer) => {
      if (!layer.visible) return;
      layer.objects.flatMap(objectTree).forEach((object) => {
      if (!object || excluded.has(object.id) || object.visible === false) return;
      const localBounds = objectLocalGeometryBounds(object);
      const center = objectLocalToDocument(object, { x: localBounds.cx, y: localBounds.cy });
      scene.points.push({ ...center, kind: 'center', objectId: object.id, label: 'Objektzentrum' });
      if (object.type !== 'connector-line') {
        [
          ['north', localBounds.cx, localBounds.y], ['east', localBounds.right, localBounds.cy],
          ['south', localBounds.cx, localBounds.bottom], ['west', localBounds.x, localBounds.cy]
        ].forEach(([port, x, y]) => {
          const target = objectLocalToDocument(object, { x, y });
          scene.points.push({ ...target, kind: 'connection', objectId: object.id, port, label: 'Anschluss' });
        });
      }
      if (isVectorObject(object)) vectorEditablePoints(object).forEach(([x, y], index) => {
        const target = objectLocalToDocument(object, { x, y });
        scene.points.push({ ...target, kind: 'anchor', objectId: object.id, anchorIndex: index, label: 'Anker' });
      });
      snapPathDefinition(object).forEach((path, pathIndex) => {
        const sampled = sampledSnapPath(object, path); if (sampled.length < 2) return;
        const pathId = `${object.id}:${pathIndex}`; const count = path.closed ? sampled.length : sampled.length - 1;
        for (let index = 0; index < count; index += 1) scene.segments.push({
          a: sampled[index], b: sampled[(index + 1) % sampled.length], objectId: object.id, pathId, segmentIndex: index
        });
        const midpoint = pathMidpoint(sampled, path.closed);
        if (midpoint) scene.points.push({ ...midpoint, kind: 'midpoint', objectId: object.id, pathId, label: 'Pfadmitte' });
      });
      });
    });
    return scene;
  }

  function precisionSnappedPoint(point, { scene = null, excludeObjectIds = [], tangentOrigin = null, connections = false } = {}) {
    if (state.snap) {
      const snapped = Snap.snapPoint(point, scene || precisionSnapScene(excludeObjectIds), {
        threshold: 8 / Math.max(state.zoom, .01), settings: { ...state.snapSettings, connections: connections && state.snapSettings.connections }, excludeObjectIds, tangentOrigin
      });
      if (snapped.target) {
        state.snapGuides = [{ ...snapped.target, source: { x: num(point.x), y: num(point.y) } }];
        return { x: round(snapped.x, 4), y: round(snapped.y, 4), snapTarget: snapped.target };
      }
    }
    state.snapGuides = [];
    const gridPoint = gridSnappedPoint(point);
    return { ...gridPoint, snapTarget: null };
  }

  function connectorPortPoint(binding) {
    const target = objectEntry(binding?.objectId)?.object;
    if (!target || target.visible === false || target.type === 'connector-line') return null;
    const bounds = objectLocalGeometryBounds(target);
    const local = {
      north: { x: bounds.cx, y: bounds.y }, east: { x: bounds.right, y: bounds.cy },
      south: { x: bounds.cx, y: bounds.bottom }, west: { x: bounds.x, y: bounds.cy }
    }[binding.port];
    return local ? objectLocalToDocument(target, local) : null;
  }

  function connectorEndpointPoint(object, edge) {
    const params = AutoShapes.ensure(object); const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    return objectLocalToDocument(object, edge === 'start'
      ? { x: 0, y: height * num(params?.startY) / 100 }
      : { x: width, y: height * num(params?.endY, 100) / 100 });
  }

  function syncBoundConnector(object) {
    if (object?.type !== 'connector-line' || !object.connectorBindings) return false;
    let bindings = deepClone(object.connectorBindings); let start = connectorPortPoint(bindings.start) || connectorEndpointPoint(object, 'start');
    let end = connectorPortPoint(bindings.end) || connectorEndpointPoint(object, 'end');
    if (!start || !end) return false;
    if (start.x > end.x) { [start, end] = [end, start]; bindings = { start: bindings.end || null, end: bindings.start || null }; }
    const previous = [object.x, object.y, object.width, object.height, object.rotation, object.flipX, object.flipY, object.autoShape?.startY, object.autoShape?.endY];
    const x = Math.min(start.x, end.x); const y = Math.min(start.y, end.y); const width = Math.max(1, Math.abs(end.x - start.x)); const height = Math.max(1, Math.abs(end.y - start.y));
    object.x = round(x, 4); object.y = round(y, 4); object.width = round(width, 4); object.height = round(height, 4);
    object.rotation = 0; object.flipX = false; object.flipY = false; object.connectorBindings = bindings;
    const params = AutoShapes.ensure(object);
    params.startY = round((start.y - y) / height * 100, 4); params.endY = round((end.y - y) / height * 100, 4);
    const current = [object.x, object.y, object.width, object.height, object.rotation, object.flipX, object.flipY, params.startY, params.endY];
    return current.some((value, index) => value !== previous[index]);
  }

  function syncBoundConnectors(changedTargetIds = null) {
    const changed = changedTargetIds ? new Set(changedTargetIds) : null; const updated = [];
    renderableObjects().forEach((object) => {
      if (object?.type !== 'connector-line' || !object.connectorBindings) return;
      const targets = [object.connectorBindings.start?.objectId, object.connectorBindings.end?.objectId].filter(Boolean);
      if (changed && !targets.some((id) => changed.has(id))) return;
      if (syncBoundConnector(object)) updated.push(object);
    });
    return updated;
  }

  function guideValueFromPoint(orientation, point) {
    return round(orientation === 'vertical' ? point.x : point.y);
  }

  function guideMaximum(orientation) {
    return orientation === 'vertical' ? state.doc.width : state.doc.height;
  }

  function beginGuideGesture(event, orientation, index = null) {
    if (documentGuides().locked) {
      toast('Hilfslinien sind gesperrt – mit dem Schloss am Lineal entsperren', 'warning');
      event.preventDefault();
      return;
    }
    const point = pointFromEvent(event); const value = guideValueFromPoint(orientation, point);
    state.gesture = { kind: 'guide', orientation, index, value, isNew: index === null };
    state.guidePreview = { orientation, value };
    refs.workspace.setPointerCapture?.(event.pointerId);
    renderArtboard();
    scheduleRulerRender();
    event.preventDefault();
  }

  function finishGuideGesture(gesture) {
    const orientation = gesture.orientation;
    const maximum = guideMaximum(orientation);
    const value = round(gesture.value);
    const guides = documentGuides();
    const list = guides[orientation] || (guides[orientation] = []);
    if (value >= 0 && value <= maximum) {
      if (gesture.isNew) {
        if (!list.some((item) => Math.abs(item - value) < .01)) {
          list.push(value); commitHistory('Hilfslinie erstellt');
        }
      } else if (list[gesture.index] !== value) {
        list[gesture.index] = value; commitHistory('Hilfslinie verschoben');
      }
    } else if (!gesture.isNew && gesture.index >= 0 && gesture.index < list.length) {
      list.splice(gesture.index, 1); commitHistory('Hilfslinie entfernt');
    }
    state.guidePreview = null;
  }

  function beginDraw(type, point) {
    const layer = activeLayer();
    if (!layer || layer.locked || !layer.visible) { toast('Die aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return; }
    const snapScene = precisionSnapScene(); const freehand = ['pencil', 'brush', 'eraser'].includes(type);
    if (!freehand) point = precisionSnappedPoint(point, { scene: snapScene, connections: type === 'connector-line' });
    const transaction = beginDocumentTransaction(`${names[type] || 'Objekt'} zeichnen`, { reuseCurrentHistory: true });
    const object = objectBase(type, point.x, point.y);
    if (['pencil', 'brush', 'eraser'].includes(type)) object.points = [[0, 0]];
    if (['line', 'arrow', 'measure'].includes(type)) object.points = [[0, 0], [0, 0]];
    if (type === 'polygon') setPolygonSides(object, $('polygonSides').value);
    layer.objects.push(object); state.selected = [object.id];
    state.gesture = {
      kind: 'draw', object, start: point, moved: false, transaction, snapScene,
      connectorStartTarget: type === 'connector-line' && point.snapTarget?.kind === 'connection' ? point.snapTarget : null,
      connectorEndTarget: null
    };
    if (!measurePerformancePhase('render.objectInsert', () => insertArtboardObject(object, layer))) renderArtboard();
    measurePerformancePhase('render.inspector', renderInspector);
  }

  function activePenGesture() {
    return state.tool === 'pen' && state.gesture?.kind === 'pen' ? state.gesture : null;
  }

  function penLocalPoint(object, point) {
    return [round(point.x - num(object.x), 3), round(point.y - num(object.y), 3)];
  }

  function beginPenGesture(event, point) {
    const layer = activeLayer();
    if (!layer || layer.locked || !layer.visible) { toast('Die aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return; }
    let gesture = activePenGesture();
    if (!gesture) {
      const snapScene = precisionSnapScene(); point = precisionSnappedPoint(point, { scene: snapScene });
      const transaction = beginDocumentTransaction('Bézierpfad zeichnen');
      const object = objectBase('pen', point.x, point.y);
      object.name = 'Bézierpfad'; object.points = [[0, 0]]; object.bezierSegments = []; object.closed = false;
      layer.objects.push(object); state.selected = [object.id];
      gesture = state.gesture = {
        kind: 'pen', object, layerId: layer.id, transaction,
        anchorIndex: 0, anchorLocal: [0, 0], pointerActive: true, down: point, previewPoint: point,
        nextControl: null, firstIncoming: null, dragged: false, snapScene
      };
    } else {
      const object = gesture.object; const points = object.points || [];
      const first = { x: num(object.x) + num(points[0]?.[0]), y: num(object.y) + num(points[0]?.[1]) };
      if (points.length >= 3 && Math.hypot(point.x - first.x, point.y - first.y) <= 9 / state.zoom) {
        finishPenPath(true); event.preventDefault(); return;
      }
      const previousAnchor = points.at(-1); const tangentOrigin = { x: num(object.x) + num(previousAnchor?.[0]), y: num(object.y) + num(previousAnchor?.[1]) };
      point = precisionSnappedPoint(point, { scene: gesture.snapScene, excludeObjectIds: [object.id], tangentOrigin, connections: object.type === 'connector-line' });
      const local = penLocalPoint(object, point); const previousIndex = points.length - 1; const previous = points[previousIndex];
      const segments = Array.isArray(object.bezierSegments) ? object.bezierSegments : (object.bezierSegments = []);
      segments[previousIndex] = gesture.nextControl
        ? { c1: deepClone(gesture.nextControl), c2: deepClone(local) }
        : null;
      points.push(local); gesture.anchorIndex = points.length - 1; gesture.anchorLocal = local;
      gesture.pointerActive = true; gesture.down = point; gesture.previewPoint = point; gesture.nextControl = null; gesture.dragged = false;
      object.pointModes = Array.isArray(object.pointModes) ? object.pointModes : [];
      object.pointModes[gesture.anchorIndex] = 'corner';
    }
    refs.artboard.setPointerCapture?.(event.pointerId); syncPixelToolOptions(); scheduleInteractionRender([gesture.object]); event.preventDefault();
  }

  function updatePenGesture(gesture, point) {
    const object = gesture.object; const anchor = gesture.anchorLocal;
    const tangentOrigin = { x: num(object.x) + num(anchor?.[0]), y: num(object.y) + num(anchor?.[1]) };
    point = precisionSnappedPoint(point, { scene: gesture.snapScene, excludeObjectIds: [object.id], tangentOrigin });
    gesture.previewPoint = point;
    if (!gesture.pointerActive) { scheduleInteractionRender(); return; }
    const local = penLocalPoint(object, point);
    const dx = local[0] - anchor[0]; const dy = local[1] - anchor[1];
    if (Math.hypot(dx, dy) <= .35 / Math.max(state.zoom, .01)) return;
    const incoming = [round(anchor[0] - dx, 3), round(anchor[1] - dy, 3)];
    const outgoing = [round(anchor[0] + dx, 3), round(anchor[1] + dy, 3)];
    if (gesture.anchorIndex === 0) gesture.firstIncoming = incoming;
    else {
      const segmentIndex = gesture.anchorIndex - 1; const previous = object.points[segmentIndex];
      const existing = object.bezierSegments[segmentIndex];
      object.bezierSegments[segmentIndex] = { c1: isBezierSegment(existing) ? existing.c1 : deepClone(previous), c2: incoming };
    }
    gesture.nextControl = outgoing; gesture.dragged = true;
    object.pointModes = Array.isArray(object.pointModes) ? object.pointModes : [];
    object.pointModes[gesture.anchorIndex] = 'symmetric';
    scheduleInteractionRender([object]);
  }

  function finishPenPath(closed = false) {
    const gesture = activePenGesture(); if (!gesture) return false;
    const object = gesture.object; const points = object.points || [];
    if (closed && points.length < 3) { toast('Zum Schließen werden mindestens drei Punkte benötigt', 'warning'); return false; }
    if (!closed && points.length < 2) { cancelActiveGesture(); return false; }
    const segmentCount = closed ? points.length : points.length - 1;
    object.bezierSegments = Array.from({ length: segmentCount }, (_, index) => deepClone(object.bezierSegments?.[index] || null));
    if (closed) {
      const last = points.at(-1); const first = points[0];
      object.bezierSegments[points.length - 1] = gesture.nextControl || gesture.firstIncoming
        ? { c1: deepClone(gesture.nextControl || last), c2: deepClone(gesture.firstIncoming || first) }
        : null;
    }
    object.closed = Boolean(closed); object.name = closed ? 'Geschlossener Bézierpfad' : 'Offener Bézierpfad';
    normalizeVectorPointBounds(object); object.scalePercent = 100;
    state.gesture = null; clearVectorSubselection(); state.snapGuides = [];
    commitHistory(closed ? 'Bézierpfad geschlossen' : 'Bézierpfad gezeichnet');
    syncPixelToolOptions(); renderAll(); toast(closed ? 'Bézierpfad geschlossen' : 'Bézierpfad offen beendet'); return true;
  }

  function removeLastPenAnchor() {
    const gesture = activePenGesture(); if (!gesture) return false;
    const points = gesture.object.points || [];
    if (points.length <= 1) { cancelActiveGesture(); return true; }
    points.pop(); const removedSegment = gesture.object.bezierSegments?.pop();
    gesture.anchorIndex = points.length - 1; gesture.anchorLocal = deepClone(points.at(-1));
    gesture.nextControl = isBezierSegment(removedSegment) ? deepClone(removedSegment.c1) : null;
    gesture.pointerActive = false; gesture.dragged = false;
    syncPixelToolOptions(); scheduleInteractionRender([gesture.object]); return true;
  }

  function activePolylineGesture() {
    return state.tool === 'polyline' && state.gesture?.kind === 'polyline' ? state.gesture : null;
  }

  function constrainedPolylinePoint(gesture, point, constrain = false, scene = gesture?.snapScene || null) {
    const last = gesture?.object?.points?.at(-1);
    const start = last ? { x: num(gesture.object.x) + num(last[0]), y: num(gesture.object.y) + num(last[1]) } : null;
    if (constrain && start) {
      const dx = point.x - start.x; const dy = point.y - start.y; const length = Math.hypot(dx, dy);
      if (length >= .0001) {
        const angle = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
        point = { x: round(start.x + Math.cos(angle) * length, 3), y: round(start.y + Math.sin(angle) * length, 3) };
      }
    }
    return precisionSnappedPoint(point, { scene: scene || precisionSnapScene(), excludeObjectIds: gesture?.object ? [gesture.object.id] : [], tangentOrigin: start });
  }

  function beginPolylineGesture(event, point) {
    const layer = activeLayer();
    if (!layer || layer.locked || !layer.visible) { toast('Die aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return; }
    let gesture = activePolylineGesture();
    const snapScene = gesture?.snapScene || precisionSnapScene();
    point = constrainedPolylinePoint(gesture, point, event.shiftKey, snapScene);
    if (!gesture) {
      const transaction = beginDocumentTransaction('Polylinie zeichnen');
      const object = objectBase('polyline', point.x, point.y);
      object.name = 'Polylinie'; object.points = [[0, 0]];
      layer.objects.push(object); state.selected = [object.id];
      gesture = state.gesture = { kind: 'polyline', object, layerId: layer.id, transaction, previewPoint: point, snapScene };
    } else {
      const points = gesture.object.points || [];
      const first = { x: num(gesture.object.x) + num(points[0]?.[0]), y: num(gesture.object.y) + num(points[0]?.[1]) };
      if (points.length >= 3 && Math.hypot(point.x - first.x, point.y - first.y) <= 9 / state.zoom) {
        finishPolylinePath(true); event.preventDefault(); return;
      }
      const local = penLocalPoint(gesture.object, point); const last = gesture.object.points.at(-1);
      if (!last || Math.hypot(local[0] - last[0], local[1] - last[1]) > .01) gesture.object.points.push(local);
      gesture.previewPoint = point;
      if (event.detail >= 2) { finishPolylinePath(); event.preventDefault(); return; }
    }
    refs.artboard.setPointerCapture?.(event.pointerId); syncPixelToolOptions(); scheduleInteractionRender([gesture.object]); event.preventDefault();
  }

  function updatePolylineGesture(gesture, point, constrain = false) {
    gesture.previewPoint = constrainedPolylinePoint(gesture, point, constrain);
    scheduleInteractionRender([gesture.object]);
  }

  function finishPolylinePath(closed = false) {
    const gesture = activePolylineGesture(); if (!gesture) return false;
    const object = gesture.object; const points = (object.points || []).filter((point, index, list) => {
      if (!index) return true;
      const previous = list[index - 1]; return Math.hypot(num(point[0]) - num(previous[0]), num(point[1]) - num(previous[1])) > .01;
    });
    if (closed && points.length < 3) { toast('Zum Schließen werden mindestens drei Punkte benötigt', 'warning'); return false; }
    if (!closed && points.length < 2) { cancelActiveGesture(); return false; }
    object.points = points; object.closed = Boolean(closed); object.name = closed ? 'Geschlossene Polylinie' : 'Polylinie';
    if (closed && !$('noFill').checked && ['none', 'transparent', ''].includes(String(object.fill || '').toLowerCase())) object.fill = $('fillColor').value;
    normalizeVectorPointBounds(object); object.scalePercent = 100;
    state.gesture = null; clearVectorSubselection(); state.snapGuides = [];
    commitHistory(closed ? 'Polylinie geschlossen' : 'Polylinie gezeichnet'); syncPixelToolOptions(); renderAll(); toast(closed ? 'Polylinie geschlossen' : 'Polylinie beendet'); return true;
  }

  function removeLastPolylineAnchor() {
    const gesture = activePolylineGesture(); if (!gesture) return false;
    const points = gesture.object.points || [];
    if (points.length <= 1) { cancelActiveGesture(); return true; }
    points.pop(); gesture.previewPoint = {
      x: num(gesture.object.x) + num(points.at(-1)?.[0]),
      y: num(gesture.object.y) + num(points.at(-1)?.[1])
    };
    syncPixelToolOptions(); scheduleInteractionRender([gesture.object]); return true;
  }

  function updateDraw(gesture, point, constrain = false) {
    const { object, start } = gesture;
    if (['pencil', 'brush', 'eraser'].includes(object.type)) {
      const last = object.points.at(-1); const next = [round(point.x - object.x), round(point.y - object.y)];
      if (!last || Math.hypot(next[0] - last[0], next[1] - last[1]) >= .7) object.points.push(next);
    } else {
      if (constrain) {
        const dx = point.x - start.x; const dy = point.y - start.y;
        if (['line', 'arrow', 'measure'].includes(object.type)) {
          const length = Math.hypot(dx, dy); const angle = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
          point = { x: start.x + Math.cos(angle) * length, y: start.y + Math.sin(angle) * length };
        } else {
          const side = Math.max(Math.abs(dx), Math.abs(dy));
          point = { x: start.x + Math.sign(dx || 1) * side, y: start.y + Math.sign(dy || 1) * side };
        }
      }
      const tangentOrigin = ['line', 'arrow', 'measure', 'connector-line'].includes(object.type) ? start : null;
      point = precisionSnappedPoint(point, { scene: gesture.snapScene, excludeObjectIds: [object.id], tangentOrigin, connections: object.type === 'connector-line' });
      gesture.connectorEndTarget = object.type === 'connector-line' && point.snapTarget?.kind === 'connection' ? point.snapTarget : null;
      const x = Math.min(start.x, point.x); const y = Math.min(start.y, point.y);
      object.x = round(x); object.y = round(y); object.width = Math.max(1, round(Math.abs(point.x - start.x))); object.height = Math.max(1, round(Math.abs(point.y - start.y)));
      if (['line', 'arrow', 'measure'].includes(object.type)) object.points = [[start.x - x, start.y - y], [point.x - x, point.y - y]];
      if (object.type === 'polygon') setPolygonSides(object, object.sides);
      if (AutoShapes.isAutoShape(object)) {
        if (!gesture.autoShapeSeeded) {
          delete object.autoShape;
          if (object.type === 'l-shape') { delete object.lThickness; delete object.lRoundness; gesture.defaultLShapeThickness = true; }
          AutoShapes.ensure(object); gesture.autoShapeSeeded = true;
        }
        // Auto-Shapes werden schon beim ersten Pointer-Zwischenschritt für die
        // Live-Vorschau initialisiert. Die L-Form darf ihren 40-px-Standard
        // dabei nicht dauerhaft auf die noch winzige Vorschaugröße begrenzen.
        if (object.type === 'l-shape') {
          const maximum = Math.max(1, Math.min(object.width, object.height) - 1);
          const thickness = Math.min(40, maximum);
          object.autoShape.thickness = thickness; object.lThickness = thickness;
          object.autoShape.roundness = clamp(num(object.autoShape.roundness), -thickness, thickness);
          object.lRoundness = object.autoShape.roundness;
        }
        if (object.type === 'connector-line') {
          const startsOnLeft = start.x <= point.x;
          object.autoShape.startY = clamp(((startsOnLeft ? start.y : point.y) - y) / Math.max(1, object.height) * 100, 0, 100);
          object.autoShape.endY = clamp(((startsOnLeft ? point.y : start.y) - y) / Math.max(1, object.height) * 100, 0, 100);
          const startBinding = gesture.connectorStartTarget ? { objectId: gesture.connectorStartTarget.objectId, port: gesture.connectorStartTarget.port } : null;
          const endBinding = gesture.connectorEndTarget ? { objectId: gesture.connectorEndTarget.objectId, port: gesture.connectorEndTarget.port } : null;
          const bindings = startsOnLeft ? { start: startBinding, end: endBinding } : { start: endBinding, end: startBinding };
          if (bindings.start || bindings.end) object.connectorBindings = bindings; else delete object.connectorBindings;
        }
      }
    }
    gesture.moved = true;
  }

  function proportionalResizeBounds(origin, handle, point) {
    const width = Math.max(1, num(origin.width, 1)); const height = Math.max(1, num(origin.height, 1));
    const ratio = width / height;
    const hasWest = handle.includes('w'); const hasEast = handle.includes('e');
    const hasNorth = handle.includes('n'); const hasSouth = handle.includes('s');
    const proportionalSize = (rawWidth, rawHeight) => {
      const scale = Math.max(rawWidth / width, rawHeight / height, 1 / Math.max(width, height));
      return { width: width * scale, height: height * scale };
    };

    if ((hasWest || hasEast) && (hasNorth || hasSouth)) {
      const anchorX = hasWest ? origin.x + width : origin.x;
      const anchorY = hasNorth ? origin.y + height : origin.y;
      const directionX = Math.sign(point.x - anchorX) || (hasWest ? -1 : 1);
      const directionY = Math.sign(point.y - anchorY) || (hasNorth ? -1 : 1);
      const next = proportionalSize(Math.abs(point.x - anchorX), Math.abs(point.y - anchorY));
      return {
        x: directionX < 0 ? anchorX - next.width : anchorX,
        y: directionY < 0 ? anchorY - next.height : anchorY,
        width: next.width, height: next.height
      };
    }

    if (hasWest || hasEast) {
      const anchorX = hasWest ? origin.x + width : origin.x;
      const directionX = Math.sign(point.x - anchorX) || (hasWest ? -1 : 1);
      const nextWidth = Math.max(1, Math.abs(point.x - anchorX)); const nextHeight = nextWidth / ratio;
      return { x: directionX < 0 ? anchorX - nextWidth : anchorX, y: origin.y + (height - nextHeight) / 2, width: nextWidth, height: nextHeight };
    }

    const anchorY = hasNorth ? origin.y + height : origin.y;
    const directionY = Math.sign(point.y - anchorY) || (hasNorth ? -1 : 1);
    const nextHeight = Math.max(1, Math.abs(point.y - anchorY)); const nextWidth = nextHeight * ratio;
    return { x: origin.x + (width - nextWidth) / 2, y: directionY < 0 ? anchorY - nextHeight : anchorY, width: nextWidth, height: nextHeight };
  }

  function scaleObjectEffectAppearance(target, origin, scaleX, scaleY) {
    if (Array.isArray(origin?.effects)) target.effects = Effects.scaleStack(origin.effects, scaleX, scaleY);
    if (origin?.svgFilterSettings?.region) {
      target.svgFilterSettings = deepClone(origin.svgFilterSettings);
      const region = target.svgFilterSettings.region;
      region.x = round(num(region.x) * scaleX);
      region.y = round(num(region.y) * scaleY);
      region.width = Math.max(.0001, round(num(region.width) * Math.abs(scaleX)));
      region.height = Math.max(.0001, round(num(region.height) * Math.abs(scaleY)));
    }
    return target;
  }

  function scaleObjectPaintAppearance(target, origin, scaleX, scaleY) {
    ['fillPaint', 'strokePaint'].forEach((property) => {
      if (origin?.[property]) target[property] = FillPaint.scaleGeometry(origin[property], scaleX, scaleY);
    });
    return target;
  }

  function scaleObjectTree(object, scaleX, scaleY) {
    // Kinder werden rekursiv einzeln kopiert. Ein Deep-Clone des gesamten
    // Teilbaums an dieser Stelle würde große SVG-Gruppen auf jeder Ebene
    // erneut serialisieren.
    const { children, ...properties } = object;
    const copy = deepClone(properties);
    const appearanceScale = Math.sqrt(Math.abs(scaleX * scaleY));
    copy.x = round(num(copy.x) * scaleX); copy.y = round(num(copy.y) * scaleY);
    copy.width = Math.max(1, round(num(copy.width, 1) * scaleX)); copy.height = Math.max(1, round(num(copy.height, 1) * scaleY));
    ['strokeWidth', 'textStrokeWidth', 'strokeDashOffset', 'strokeStartMarkerSize', 'strokeEndMarkerSize'].forEach((property) => {
      if (Number.isFinite(Number(copy[property]))) copy[property] = round(num(copy[property]) * appearanceScale);
    });
    if (Array.isArray(copy.strokeDashArray)) copy.strokeDashArray = copy.strokeDashArray.map((value) => round(num(value) * appearanceScale));
    if (Array.isArray(copy.strokeStack)) {
      copy.strokeStack = copy.strokeStack.map((style) => ({
        ...style,
        width: round(num(style.width) * appearanceScale),
        dashArray: Array.isArray(style.dashArray) ? style.dashArray.map((value) => round(num(value) * appearanceScale)) : style.dashArray,
        dashOffset: round(num(style.dashOffset) * appearanceScale),
        startMarkerSize: round(num(style.startMarkerSize, 12) * appearanceScale),
        endMarkerSize: round(num(style.endMarkerSize, 12) * appearanceScale)
      }));
    }
    clampObjectRoundness(copy);
    copy.scalePercent = Math.abs(scaleX - scaleY) < .015
      ? clamp(round(num(copy.scalePercent, 100) * scaleX, 1), 1, 10000)
      : 100;
    if (AutoShapes.isAutoShape(copy)) AutoShapes.scaleParams(copy, object, scaleX, scaleY);
    if (Array.isArray(copy.points)) copy.points = copy.points.map(([x, y]) => [round(num(x) * scaleX), round(num(y) * scaleY)]);
    if (Array.isArray(copy.rawPoints)) copy.rawPoints = copy.rawPoints.map(([x, y]) => [round(num(x) * scaleX), round(num(y) * scaleY)]);
    if (Array.isArray(copy.bezierSegments)) copy.bezierSegments = copy.bezierSegments.map((segment) => isBezierSegment(segment) ? {
      c1: [round(num(segment.c1[0]) * scaleX), round(num(segment.c1[1]) * scaleY)],
      c2: [round(num(segment.c2[0]) * scaleX), round(num(segment.c2[1]) * scaleY)]
    } : null);
    scaleObjectPaintAppearance(copy, object, scaleX, scaleY);
    if (copy.type === 'text') copy.fontSize = clamp(num(copy.fontSize, 32) * Math.sqrt(Math.abs(scaleX * scaleY)), 6, 400);
    scaleObjectEffectAppearance(copy, object, scaleX, scaleY);
    if (copy.maskRegion) {
      copy.maskRegion.x = round(num(copy.maskRegion.x) * scaleX);
      copy.maskRegion.y = round(num(copy.maskRegion.y) * scaleY);
      copy.maskRegion.width = Math.max(.0001, round(num(copy.maskRegion.width) * Math.abs(scaleX)));
      copy.maskRegion.height = Math.max(.0001, round(num(copy.maskRegion.height) * Math.abs(scaleY)));
    }
    if (Array.isArray(children)) copy.children = children.map((child) => scaleObjectTree(child, scaleX, scaleY));
    return copy;
  }

  function finalizeDeferredResizeGeometry(gesture) {
    if (gesture?.kind !== 'resize' || !gesture.deferredGeometry) return null;
    const object = gesture.object; const origin = gesture.origin;
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    const x = num(object.x); const y = num(object.y);
    const scaleX = width / Math.max(1, num(origin.width, 1)); const scaleY = height / Math.max(1, num(origin.height, 1));
    Object.assign(object, scaleObjectTree(origin, scaleX, scaleY));
    object.x = round(x); object.y = round(y); object.width = width; object.height = height;
    updateObjectScalePercent(object, origin);
    return object;
  }

  function objectScalePercent(object) {
    return clamp(round(num(object?.scalePercent, 100), 1), 1, 10000);
  }

  function updateObjectScalePercent(object, origin) {
    const oldWidth = Math.max(1, num(origin?.width, 1)); const oldHeight = Math.max(1, num(origin?.height, 1));
    const scaleX = Math.max(1 / oldWidth, num(object.width, 1) / oldWidth); const scaleY = Math.max(1 / oldHeight, num(object.height, 1) / oldHeight);
    // Bei einer Verzerrung gibt es keinen einzelnen sinnvollen Prozentwert mehr.
    object.scalePercent = Math.abs(scaleX - scaleY) < .015
      ? clamp(round(objectScalePercent(origin) * scaleX, 1), 1, 10000)
      : 100;
  }

  function setObjectScalePercent(object, value) {
    const next = clamp(round(num(value, 100), 1), 1, 10000); const current = objectScalePercent(object);
    const factor = next / current;
    if (!Number.isFinite(factor) || Math.abs(factor - 1) < .0001) { object.scalePercent = next; return; }
    const x = object.x; const y = object.y;
    Object.assign(object, scaleObjectTree(object, factor, factor));
    // Das Transformationsfeld skaliert von der linken oberen Objektecke aus.
    object.x = x; object.y = y; object.scalePercent = next;
  }

  function objectAtPoint(point) {
    const boxContains = (object, padding) => {
      if (!activeFreeTransform(object)) return Core.boxContainsPoint(object, point, padding);
      const local = pointToObjectLocal(object, point); const width = Math.max(.0001, num(object.width, 1)); const height = Math.max(.0001, num(object.height, 1));
      return local.x >= -padding && local.x <= width + padding && local.y >= -padding && local.y <= height + padding;
    };
    const contains = (object) => {
      const hitPadding = Math.max(3 / state.zoom, objectVisualOutset(object));
      if (outlineVectorTypes.has(object.type)) {
        const local = pointToObjectLocal(object, point); const scale = outlineScale(object);
        const probe = [local.x / scale.x, local.y / scale.y];
        const hasFill = Boolean(normalizedObjectFillPaint(object)) || !['none', 'transparent', ''].includes(String(object.fill || 'none').toLowerCase());
        if (hasFill && PathOperations.pointInContours(probe, outlineContoursFor(object))) return true;
        return Boolean(outlineSegmentHit(object, point));
      }
      if (pointVectorTypes.has(object.type) && object.type !== 'polygon' && !isClosedVectorPath(object)) return Boolean(vectorSegmentHit(object, point));
      if (fillableVectorShapeTypes.has(object.type) || object.type === 'polygon' || isClosedVectorPath(object)) {
        const local = pointToObjectLocal(object, point); const points = sampledClosedShapePoints(object);
        const hasFill = Boolean(normalizedObjectFillPaint(object)) || !['none', 'transparent', ''].includes(String(object.fill || 'none').toLowerCase());
        if (hasFill && points.length >= 3 && Core.pointInPolygon(local, points)) return true;
        return Boolean(closedShapeHit(object, point)) || boxContains(object, Math.min(hitPadding, 8 / state.zoom));
      }
      return boxContains(object, Math.min(hitPadding, 12 / state.zoom));
    };
    for (let layerIndex = state.doc.layers.length - 1; layerIndex >= 0; layerIndex -= 1) {
      const layer = state.doc.layers[layerIndex]; if (!layer.visible || layer.locked) continue;
      for (let index = layer.objects.length - 1; index >= 0; index -= 1) {
        const object = layer.objects[index]; if (object.visible === false || object.locked) continue;
        if (contains(object)) return object;
      }
    }
    return null;
  }

  function updateFillHandleGesture(gesture, point) {
    const object = gesture.object; const origin = gesture.origin; const paint = deepClone(gesture.paint);
    delete paint.gradientCoordinates; delete paint.gradientMatrix;
    const local = pointToObjectLocal(origin, point); const width = Math.max(1, num(origin.width, 1)); const height = Math.max(1, num(origin.height, 1));
    if (gesture.handle === 'linear-start') {
      paint.x1 = local.x / width * 100; paint.y1 = local.y / height * 100;
    } else if (gesture.handle === 'linear-end') {
      paint.x2 = local.x / width * 100; paint.y2 = local.y / height * 100;
    } else if (gesture.handle === 'radial-center' || gesture.handle === 'conic-center') {
      paint.cx = local.x / width * 100; paint.cy = local.y / height * 100;
    } else if (gesture.handle === 'radial-radius') {
      const centerX = width * paint.cx / 100; const centerY = height * paint.cy / 100;
      paint.radius = Math.hypot(local.x - centerX, local.y - centerY) / Math.min(width, height) * 100;
    } else if (gesture.handle === 'conic-angle') {
      const centerX = width * paint.cx / 100; const centerY = height * paint.cy / 100;
      paint.angle = Math.atan2(local.y - centerY, local.x - centerX) * 180 / Math.PI + 90;
    } else if (gesture.handle === 'image-offset') {
      paint.offsetX = local.x - width / 2; paint.offsetY = local.y - height / 2;
    } else if (gesture.handle === 'image-scale') {
      const centerX = width / 2 + paint.offsetX; const centerY = height / 2 + paint.offsetY;
      paint.scale = Math.hypot(local.x - centerX, local.y - centerY) / Math.max(.001, Math.min(width, height) * .35) * 100;
    } else if (gesture.handle === 'image-rotate') {
      const centerX = width / 2 + paint.offsetX; const centerY = height / 2 + paint.offsetY;
      paint.rotation = Math.atan2(local.y - centerY, local.x - centerX) * 180 / Math.PI + 90;
    }
    object.fillPaint = FillPaint.normalizePaint(paint, { fallbackColor: object.fill });
    gesture.moved = gesture.moved || Math.abs(point.x - gesture.start.x) + Math.abs(point.y - gesture.start.y) > .25;
    scheduleInteractionRender([object], { inspector: true });
  }

  function beginSelectGesture(event, point) {
    // Der Browser darf waehrend einer PYRO-Auswahl keinen SVG-Text markieren.
    // Das ist besonders bei Text auf Pfaden sichtbar, weil die native Auswahl
    // sonst unabhaengig vom aufgezogenen Objektrahmen einzelne Glyphen hervorhebt.
    event.preventDefault();
    window.getSelection()?.removeAllRanges();
    const textPathStart = event.target.closest?.('[data-text-path-start]');
    if (textPathStart) {
      const entry = objectEntry(textPathStart.dataset.objectId); const object = entry?.object; const binding = textPathBinding(object);
      const metric = binding && textPathMetric(binding.target)?.metric;
      if (!entry || !binding || !metric || entry.layer.locked || object.locked || object.visible === false) return;
      state.selected = [...state.selected.filter((id) => id !== object.id), object.id];
      state.gesture = {
        kind: 'text-path-start', object, target: binding.target, metric, start: point, origin: deepClone(object), moved: false,
        transaction: beginDocumentTransaction('Textstartpunkt auf Pfad verschieben')
      };
      refs.artboard.setPointerCapture(event.pointerId); return;
    }
    const freeControl = event.target.closest?.('[data-free-transform-control]');
    if (freeControl) {
      const entry = objectEntry(freeControl.dataset.objectId); const object = entry?.object; const mode = freeControl.dataset.freeTransformMode;
      const index = Math.round(num(freeControl.dataset.freeTransformControl, -1));
      if (!entry || !FreeTransform.MODES.has(mode) || entry.layer.locked || object.locked || object.visible === false) return;
      state.selected = [...state.selected.filter((id) => id !== object.id), object.id]; state.freeTransformMode = mode;
      state.gesture = {
        kind: 'free-transform-control', object, mode, index, start: point, origin: deepClone(object),
        transform: activeFreeTransform(object, mode) || FreeTransform.normalize({ mode }), moved: false,
        transaction: beginDocumentTransaction(mode === 'envelope' ? 'Hüllkurve verformen' : mode === 'perspective' ? 'Perspektivisch verzerren' : 'Ecke frei transformieren')
      };
      refs.artboard.setPointerCapture(event.pointerId); return;
    }
    const pivotHandle = event.target.closest?.('[data-transform-pivot]');
    if (pivotHandle) {
      const entry = objectEntry(pivotHandle.dataset.objectId); const object = entry?.object;
      if (!entry || entry.layer.locked || object.locked || object.visible === false) return;
      state.selected = [...state.selected.filter((id) => id !== object.id), object.id];
      state.gesture = { kind: 'transform-pivot', object, start: point, origin: deepClone(object), moved: false, transaction: beginDocumentTransaction('Drehmittelpunkt verschieben') };
      refs.artboard.setPointerCapture(event.pointerId); return;
    }
    const fillHandle = event.target.closest?.('[data-fill-handle]');
    if (fillHandle) {
      const entry = objectEntry(fillHandle.dataset.objectId); const object = entry?.object; const paint = normalizedObjectFillPaint(object);
      if (!entry || !paint || entry.layer.locked || object.locked || object.visible === false) return;
      state.selected = [...state.selected.filter((id) => id !== object.id), object.id];
      state.gesture = {
        kind: 'fill-handle', object, handle: fillHandle.dataset.fillHandle, start: point,
        origin: deepClone(object), paint: deepClone(paint), moved: false,
        transaction: beginDocumentTransaction('Füllung direkt angepasst')
      };
      refs.artboard.setPointerCapture(event.pointerId); return;
    }
    const autoShapeControl = event.target.closest?.('[data-auto-shape-control]');
    if (autoShapeControl) {
      const entry = objectEntry(autoShapeControl.dataset.objectId); const object = entry?.object; const control = autoShapeControl.dataset.autoShapeControl;
      const descriptor = object && AutoShapes.controls(object).find((item) => item.id === control);
      if (!entry || !descriptor || entry.layer.locked || object.locked || object.visible === false) return;
      state.selected = [...state.selected.filter((id) => id !== object.id), object.id];
      state.gesture = {
        kind: 'auto-shape-control', object, control, behavior: descriptor.behavior, label: descriptor.label,
        start: point, origin: deepClone(object), moved: false, altKey: event.altKey, shiftKey: event.shiftKey,
        transaction: beginDocumentTransaction(`${AutoShapes.definitionFor(object)?.label || names[object.type] || 'Auto-Form'} anpassen`)
      };
      refs.artboard.setPointerCapture(event.pointerId); event.preventDefault(); return;
    }
    const handle = event.target.closest?.('[data-handle]');
    if (handle) {
      const entry = objectEntry(handle.dataset.objectId); if (!entry || entry.layer.locked || entry.object.locked || entry.object.visible === false) return;
      const object = entry.object;
      state.selected = [...state.selected.filter((id) => id !== object.id), object.id];
      const rotating = handle.dataset.handle === 'rotate';
      const deferredGeometry = !rotating && containerObjectTypes.has(object.type) && Array.isArray(object.children);
      // Bei großen Gruppen bleiben die unveränderten Kinder während der
      // Vorschau referenziert. Die tiefe Kopie und Skalierung erfolgt einmalig
      // beim Abschluss statt für jeden Pointer-Schritt.
      const origin = deferredGeometry ? { ...object } : deepClone(object); const pivot = origin.transformPivot || { x: .5, y: .5 };
      const pivotWorld = objectLocalToDocument(origin, { x: pivot.x * Math.max(1, num(origin.width, 1)), y: pivot.y * Math.max(1, num(origin.height, 1)) });
      const resizePointerOffset = rotating ? null : {
        x: handle.dataset.handle.includes('w') ? num(origin.x) - point.x
          : handle.dataset.handle.includes('e') ? num(origin.x) + num(origin.width, 1) - point.x : 0,
        y: handle.dataset.handle.includes('n') ? num(origin.y) - point.y
          : handle.dataset.handle.includes('s') ? num(origin.y) + num(origin.height, 1) - point.y : 0
      };
      state.gesture = {
        kind: rotating ? 'rotate' : 'resize', handle: handle.dataset.handle, object, start: point, origin,
        pivotWorld, startAngle: Math.atan2(point.y - pivotWorld.y, point.x - pivotWorld.x), moved: false, deferredGeometry, resizePointerOffset,
        transaction: beginDocumentTransaction(rotating ? 'Objekt drehen' : 'Objekt skalieren', { reuseCurrentHistory: deferredGeometry })
      };
      refs.artboard.setPointerCapture(event.pointerId); return;
    }
    const target = event.target.closest?.('[data-object-id]');
    const id = target?.dataset.objectId || objectAtPoint(point)?.id;
    if (id) {
      const object = objectEntry(id)?.object; const previousTextClick = state.textClick;
      const repeatedTextClick = !event.shiftKey && object?.type === 'text' && previousTextClick?.objectId === object.id
        && performance.now() - previousTextClick.time <= 500
        && Math.hypot(point.x - previousTextClick.x, point.y - previousTextClick.y) <= 7 / state.zoom;
      if (repeatedTextClick) {
        state.textClick = null; event.preventDefault(); openInlineTextEditor(point, object); return;
      }
      state.textClick = object?.type === 'text' && !event.shiftKey
        ? { objectId: object.id, x: point.x, y: point.y, time: performance.now() }
        : null;
      if (event.shiftKey) select([id], true); else if (!state.selected.includes(id)) select([id]);
      const origins = selectedObjects().map((object) => ({ object, x: object.x, y: object.y }));
      state.gesture = { kind: 'move', start: point, origins, moved: false, transaction: null };
      refs.artboard.setPointerCapture(event.pointerId);
    } else {
      state.textClick = null;
      if (!event.shiftKey) select([]);
      state.gesture = { kind: 'marquee', start: point, current: point };
      refs.artboard.setPointerCapture(event.pointerId);
    }
  }

  function beginVectorSelectGesture(event, point) {
    const bezierHandle = event.target.closest?.('[data-vector-bezier-control]');
    if (bezierHandle) {
      const entry = objectEntry(bezierHandle.dataset.vectorObjectId); const object = entry?.object;
      const index = Math.round(num(bezierHandle.dataset.vectorSegmentIndex, -1)); const control = bezierHandle.dataset.vectorBezierControl;
      if (!entry || entry.layer.locked || object.locked || object.visible === false || !bezierControlsFor(object, index) || !['c1', 'c2'].includes(control)) return;
      const anchorIndex = Math.round(num(bezierHandle.dataset.vectorAnchorIndex, index));
      state.selected = [object.id]; setVectorPointSelections([{ objectId: object.id, index: anchorIndex }]); setVectorSegmentSelections([]);
      state.gesture = {
        kind: 'vector-bezier', object, index, control, anchorIndex, start: point, origin: deepClone(object), moved: false,
        snapScene: precisionSnapScene([object.id]), transaction: beginDocumentTransaction('Bézier-Griff verschieben')
      };
      refs.artboard.setPointerCapture(event.pointerId); event.preventDefault(); return;
    }
    const handle = event.target.closest?.('[data-vector-point]');
    if (handle) {
      const entry = objectEntry(handle.dataset.vectorObjectId); const object = entry?.object;
      if (!entry || entry.layer.locked || object.locked || object.visible === false || !isVectorObject(object)) return;
      const index = Math.round(num(handle.dataset.vectorPoint, -1));
      if (index < 0 || !vectorEditablePoints(object)[index]) return;
      const selection = { objectId: object.id, index }; const current = vectorPointSelections();
      const alreadySelected = current.some((item) => item.objectId === object.id && item.index === index);
      const next = event.shiftKey ? toggleVectorSelection(current, selection) : alreadySelected ? current : [selection];
      setVectorPointSelections(next); if (!event.shiftKey) setVectorSegmentSelections([]);
      state.selected = event.shiftKey
        ? [...new Set([...state.selected, ...vectorPointSelections().map((item) => item.objectId)])]
        : [...new Set(vectorPointSelections().map((item) => item.objectId))];
      if (!state.selected.length) { state.selected = [object.id]; renderAll(); event.preventDefault(); return; }
      const transaction = beginDocumentTransaction(vectorPointSelections().length > 1 ? 'Vektorpunkte verschieben' : 'Vektorpunkt verschieben');
      const grouped = new Map();
      vectorPointSelections().forEach((item) => {
        const target = objectEntry(item.objectId)?.object; if (!target) return;
        const group = grouped.get(target.id) || { object: target, indices: [] }; group.indices.push(item.index); grouped.set(target.id, group);
      });
      const items = [...grouped.values()].map((item) => {
        const original = deepClone(item.object); const origin = deepClone(original);
        if (polygonConvertibleTypes.has(origin.type)) convertShapeToPolygon(origin, origin);
        return { ...item, origin, original };
      });
      state.gesture = { kind: 'vector-points', items, start: point, moved: false, snapScene: precisionSnapScene(items.map((item) => item.object.id)), transaction };
      refs.artboard.setPointerCapture(event.pointerId); event.preventDefault(); return;
    }
    const target = event.target.closest?.('[data-object-id]'); const directEntry = objectEntry(target?.dataset.objectId || objectAtPoint(point)?.id); const object = directEntry?.object;
    const previous = state.vectorClick;
    const editablePointObject = isPointEditableVector(object) || polygonConvertibleTypes.has(object?.type);
    const repeatedVectorClick = editablePointObject && previous?.objectId === object.id
      && performance.now() - previous.time <= 460 && Math.hypot(point.x - previous.x, point.y - previous.y) <= 7 / state.zoom;
    if (repeatedVectorClick) {
      state.vectorClick = null;
      if (addVectorAnchorAt(point)) { event.preventDefault(); return; }
    }
    state.vectorClick = editablePointObject ? { objectId: object.id, x: point.x, y: point.y, time: performance.now() } : null;
    const hitEntry = vectorHitAt(point); const hitObject = hitEntry?.object;
    if (hitEntry && pointVectorTypes.has(hitObject.type) && hitObject.type !== 'measure') {
      const selection = { objectId: hitObject.id, index: hitEntry.hit.index };
      setVectorSegmentSelections(event.shiftKey ? toggleVectorSelection(vectorSegmentSelections(), selection) : [selection]);
      if (!event.shiftKey) setVectorPointSelections([]);
      state.selected = [...new Set([...vectorPointSelections(), ...vectorSegmentSelections()].map((item) => item.objectId))];
      if (!state.selected.length) state.selected = [hitObject.id];
      state.doc.activeLayerId = hitEntry.layer.id; renderAll(); event.preventDefault(); return;
    }
    if (isVectorObject(object) && !state.selected.includes(object.id)) {
      state.selected = event.shiftKey ? [...new Set([...state.selected, object.id])] : [object.id];
      if (!event.shiftKey) clearVectorSubselection();
      state.doc.activeLayerId = directEntry.layer.id; renderAll(); event.preventDefault(); return;
    }
    state.gesture = { kind: 'vector-marquee', start: point, current: point, additive: event.shiftKey };
    refs.artboard.setPointerCapture(event.pointerId); event.preventDefault(); renderArtboard();
  }

  function bitmapAtPointerTarget(event, point) {
    const target = event.target.closest?.('[data-object-id]');
    const direct = objectEntry(target?.dataset.objectId)?.object;
    const object = direct || objectAtPoint(point);
    return object?.type === 'image' && bitmapLocalPoint(object, point) ? object : null;
  }

  function onPointerDown(event) {
    if (event.button !== 0 && event.button !== 1) return;
    if (event.target.closest?.('input, textarea, select, button')) return;
    if (state.canvasResizeMode && event.button === 0 && !state.spaceDown) {
      const handle = event.target.closest?.('[data-canvas-resize-handle]');
      if (handle) beginCanvasResizeGesture(event, handle.dataset.canvasResizeHandle);
      else event.preventDefault();
      return;
    }
    const point = pointFromEvent(event);
    // Objekte duerfen ueber die Zeichenflaeche hinaus bis in die Linealzone ragen.
    // Dort hat die Objektauswahl Vorrang; eine freie Linealflaeche zieht weiterhin
    // wie gewohnt eine Hilfslinie auf.
    const ruler = event.target.closest?.('[data-guide-ruler]');
    const guide = event.target.closest?.('[data-guide-axis]');
    const selectableObject = (ruler || guide) && ['select', 'vector-select'].includes(state.tool) ? objectAtPoint(point) : null;
    if (event.button === 0 && ruler && !selectableObject) { beginGuideGesture(event, ruler.dataset.guideRuler); return; }
    if (event.button === 0 && guide && !selectableObject) { beginGuideGesture(event, guide.dataset.guideAxis, num(guide.dataset.guideIndex, -1)); return; }
    const insideArtboard = refs.artboard.contains(event.target);
    if (!insideArtboard && !['select', 'vector-select', 'pan', 'marquee', 'ellipse-marquee'].includes(state.tool) && !state.spaceDown && event.button !== 1) return;
    state.snapGuides = [];
    if (state.tool === 'pan' || state.spaceDown || event.button === 1) {
      state.gesture = { kind: 'pan', clientX: event.clientX, clientY: event.clientY, scrollLeft: refs.workspace.scrollLeft, scrollTop: refs.workspace.scrollTop };
      refs.artboard.style.cursor = 'grabbing'; refs.artboard.setPointerCapture(event.pointerId); event.preventDefault(); return;
    }
    if (['mesh-warp', 'shape-grid', 'puppet-warp', 'stretch-area'].includes(state.tool)) {
      const session = state.warpSession;
      if (!session) {
        if (!state.warpPreparation) void prepareWarpSession(state.tool);
        else toast('Verformung wird vorbereitet …');
        event.preventDefault(); return;
      }
      if (session.tool === 'stretch-area') {
        const stretchHandle = event.target.closest?.('[data-stretch-handle]');
        if (stretchHandle) { beginStretchHandleGesture(event, session, stretchHandle.dataset.stretchHandle, point); return; }
        if (warpContinuousPoint(objectEntry(session.objectId)?.object, point, session)) { beginStretchSelectionGesture(event, session, point); return; }
        event.preventDefault(); return;
      }
      const warpHandle = event.target.closest?.('[data-warp-handle]');
      if (warpHandle && session.tool !== 'puppet-warp') {
        beginWarpControlGesture(event, session, Math.round(num(warpHandle.dataset.warpHandle, -1)), point); return;
      }
      if (session.tool === 'puppet-warp') {
        const pinHandle = event.target.closest?.('[data-puppet-pin]');
        if (pinHandle) {
          const pin = session.pins.find((entry) => entry.id === pinHandle.dataset.puppetPin);
          beginPuppetPinGesture(event, session, pin, point); return;
        }
        if (warpContinuousPoint(objectEntry(session.objectId)?.object, point, session)) { addPuppetPin(event, session, point); return; }
      }
      event.preventDefault(); return;
    }
    if (state.tool === 'select') { beginSelectGesture(event, point); return; }
    if (state.tool === 'vector-select') { beginVectorSelectGesture(event, point); return; }
    if (state.tool === 'knife') { splitVectorAt(point); return; }
    if (pathModifierToolInfo[state.tool]) { beginPathModifierGesture(event, point); return; }
    if (state.tool === 'pen') { beginPenGesture(event, point); return; }
    if (state.tool === 'polyline') { beginPolylineGesture(event, point); return; }
    const activeBitmap = primaryObject()?.type === 'image' ? primaryObject() : null;
    const clickedBitmap = bitmapAtPointerTarget(event, point);
    if (clickedBitmap && (state.selected.length !== 1 || activeBitmap?.id !== clickedBitmap.id)) select([clickedBitmap.id]);
    const selectedBitmap = clickedBitmap || activeBitmap;
    if (state.tool === 'polygon-lasso' && state.gesture?.kind === 'bitmap-polygon-lasso') {
      appendBitmapPolygonLassoPoint(event, point); return;
    }
    if (['marquee', 'ellipse-marquee'].includes(state.tool)) {
      if (selectedBitmap) beginBitmapMarqueeGesture(event, selectedBitmap, point);
      else toast('Wähle ein Bitmap und ziehe die Auswahl über das Bild', 'warning');
      return;
    }
    if (state.tool === 'lasso') {
      if (selectedBitmap && bitmapLocalPoint(selectedBitmap, point)) beginBitmapLassoGesture(event, selectedBitmap, point);
      else toast('Wähle ein Bitmap und ziehe das Lasso auf dem Bild auf', 'warning');
      return;
    }
    if (state.tool === 'polygon-lasso') {
      if (selectedBitmap && bitmapLocalPoint(selectedBitmap, point)) beginBitmapPolygonLasso(event, selectedBitmap, point);
      else toast('Wähle ein Bitmap und setze die Lasso-Punkte auf dem Bild', 'warning');
      return;
    }
    if (state.tool === 'wand') {
      if (selectedBitmap && bitmapLocalPoint(selectedBitmap, point)) void selectBitmapWand(selectedBitmap, point);
      else toast('Wähle ein Bitmap und klicke auf die gewünschte Farbe', 'warning');
      return;
    }
    if (selectedBitmap && ['clone', 'heal'].includes(state.tool) && bitmapLocalPoint(selectedBitmap, point)) {
      const sourcePoint = bitmapPixelPoint(selectedBitmap, point, bitmapPixelDimensions(selectedBitmap));
      if (event.altKey) {
        state.retouchSource = { objectId: selectedBitmap.id, ...sourcePoint }; renderArtboard();
        toast('Retuschequelle gesetzt – jetzt über den Bereich malen'); return;
      }
      if (state.retouchSource?.objectId !== selectedBitmap.id) { toast('Mit Alt-Klick zuerst eine Quelle im selben Bitmap setzen', 'warning'); return; }
      beginDirectBitmapGesture(event, selectedBitmap, point, state.tool); return;
    }
    if (selectedBitmap && state.tool === 'spot-heal' && bitmapLocalPoint(selectedBitmap, point)) {
      beginDirectBitmapGesture(event, selectedBitmap, point, state.tool); return;
    }
    if (selectedBitmap && ['pencil', 'eraser', 'color-eraser', 'fill', 'red-eye', 'liquify'].includes(state.tool) && bitmapLocalPoint(selectedBitmap, point)) {
      beginDirectBitmapGesture(event, selectedBitmap, point, state.tool); return;
    }
    if (state.tool === 'liquify') { toast('Wähle ein Bitmap und ziehe darin zum Verflüssigen', 'warning'); return; }
    if (state.tool === 'spot-heal') { toast('Wähle ein Bitmap und klicke auf den zu entfernenden Fleck', 'warning'); return; }
    if (state.tool === 'color-eraser') { toast('Wähle ein Bitmap und setze den Farbradierer darin an', 'warning'); return; }
    if (state.tool === 'crop') {
      const target = event.target.closest?.('[data-object-id]');
      const object = objectEntry(target?.dataset.objectId)?.object;
      if (object?.type === 'image') { select([object.id]); openCropEditor(object); }
      else toast('Klicke mit Zuschneiden auf ein Bitmap-Bild', 'warning');
      return;
    }
    const target = event.target.closest?.('[data-object-id]');
    if (state.tool === 'eyedropper') {
      const object = objectEntry(target?.dataset.objectId)?.object;
      if (object) { if (/^#[0-9a-f]{6}$/i.test(object.stroke || '')) $('strokeColor').value = object.stroke; if (/^#[0-9a-f]{6}$/i.test(object.fill || '')) $('fillColor').value = object.fill; persistToolColors(); toast('Farben übernommen'); }
      return;
    }
    if (state.tool === 'fill') {
      const object = objectEntry(target?.dataset.objectId)?.object;
      if (object) {
        if (supportsObjectFill(object)) { delete object.fillPaint; object.fill = $('fillColor').value; }
        else object.stroke = $('fillColor').value;
        commitHistory('Objekt gefärbt'); select([object.id]);
      }
      else { state.doc.bg = $('fillColor').value; commitHistory('Leinwand gefüllt'); renderAll(); }
      return;
    }
    if (state.tool === 'vector-emoji') {
      event.preventDefault();
      void insertNotoVectorEmoji(gridSnappedPoint(point));
      return;
    }
    if (state.tool === 'vector-flag') {
      event.preventDefault();
      void insertVectorFlag(gridSnappedPoint(point));
      return;
    }
    if (state.tool === 'text') {
      const object = objectEntry(target?.dataset.objectId || objectAtPoint(point)?.id)?.object;
      openInlineTextEditor(object?.type === 'text' ? point : gridSnappedPoint(point), object?.type === 'text' ? object : null); return;
    }
    if (['pencil', 'brush', 'eraser', 'line'].includes(state.tool) || shapeToolInfo[state.tool]) {
      beginDraw(state.tool, point); refs.artboard.setPointerCapture(event.pointerId); event.preventDefault();
    }
  }

  function onPointerMove(event) {
    const point = pointFromEvent(event);
    refs.cursor.textContent = I18n.t('ui.cursorPosition', { x: Math.round(point.x), y: Math.round(point.y) });
    if (eraserToolInfo[state.tool]) updateEraserCursor(point);
    if (pathModifierToolInfo[state.tool]) updatePathModifierCursor(point);
    if (state.tool === 'liquify') updateWarpCursor(point);
    const gesture = state.gesture; if (!gesture) return;
    if (gesture.kind === 'canvas-resize') { updateCanvasResizeGesture(gesture, event); return; }
    if (gesture.kind === 'pen') { updatePenGesture(gesture, point); return; }
    if (gesture.kind === 'polyline') { updatePolylineGesture(gesture, point, event.shiftKey); return; }
    if (gesture.kind === 'path-reshape' || gesture.kind === 'path-scrub') { updatePathModifierGesture(gesture, point, event); return; }
    if (gesture.kind === 'warp-control') {
      const object = objectEntry(gesture.session.objectId)?.object; const current = object && warpContinuousPoint(object, point, gesture.session);
      if (!current) return;
      const origin = gesture.originPoints[gesture.index]; const target = { x: origin.x + current.x - gesture.start.x, y: origin.y + current.y - gesture.start.y };
      const radius = gesture.session.tool === 'shape-grid' ? clamp(num($('shapeGridRadius')?.value, 140), 1, 2000) : 0;
      gesture.precise = Boolean(event.shiftKey);
      gesture.session.destinationPoints = BitmapWarp.moveGridPoint(gesture.originPoints, gesture.index, target, { radius, precise: gesture.precise });
      gesture.moved = gesture.moved || Math.hypot(current.x - gesture.start.x, current.y - gesture.start.y) > .05;
      if (gesture.moved) gesture.session.dirty = true;
      scheduleWarpPreview(gesture.session); updateWarpActionState(); renderEditorOverlays(); return;
    }
    if (gesture.kind === 'stretch-select') {
      const object = objectEntry(gesture.session.objectId)?.object; const current = object && warpContinuousPoint(object, point, gesture.session);
      if (current) { gesture.current = current; renderEditorOverlays(); } return;
    }
    if (gesture.kind === 'stretch-handle') { updateStretchHandleGesture(gesture, point); return; }
    if (gesture.kind === 'puppet-pin') {
      const object = objectEntry(gesture.session.objectId)?.object; const current = object && warpContinuousPoint(object, point, gesture.session);
      const pin = gesture.session.pins.find((entry) => entry.id === gesture.pinId); if (!current || !pin) return;
      pin.target = { x: gesture.originTarget.x + current.x - gesture.start.x, y: gesture.originTarget.y + current.y - gesture.start.y };
      gesture.moved = gesture.moved || Math.hypot(current.x - gesture.start.x, current.y - gesture.start.y) > .05;
      if (gesture.moved) gesture.session.dirty = true;
      scheduleWarpPreview(gesture.session); updateWarpActionState(); renderEditorOverlays(); return;
    }
    if (gesture.kind === 'pan') {
      refs.workspace.scrollLeft = gesture.scrollLeft - (event.clientX - gesture.clientX);
      refs.workspace.scrollTop = gesture.scrollTop - (event.clientY - gesture.clientY); return;
    }
    if (gesture.kind === 'guide') {
      const value = guideValueFromPoint(gesture.orientation, point);
      gesture.value = value; state.guidePreview = { orientation: gesture.orientation, value };
      scheduleInteractionRender(); scheduleRulerRender(); return;
    }
    if (['bitmap-marquee', 'bitmap-ellipse-marquee'].includes(gesture.kind)) {
      const object = objectEntry(gesture.objectId)?.object; const next = object && BitmapSelection.pixelPointUnclamped(object, point, bitmapPixelDimensions(object));
      if (next) { gesture.current = next; scheduleInteractionRender(); }
      return;
    }
    if (gesture.kind === 'bitmap-lasso') {
      const object = objectEntry(gesture.objectId)?.object; const next = object && bitmapPixelPoint(object, point, bitmapPixelDimensions(object));
      if (next) {
        const last = gesture.points.at(-1);
        if (!last || Math.hypot(next.x - last.x, next.y - last.y) >= .6) gesture.points.push(next);
        gesture.current = next; scheduleInteractionRender();
      }
      return;
    }
    if (gesture.kind === 'bitmap-polygon-lasso') {
      const object = objectEntry(gesture.objectId)?.object; const next = object && bitmapPixelPoint(object, point, bitmapPixelDimensions(object));
      if (next) { gesture.current = next; scheduleInteractionRender(); }
      return;
    }
    if (gesture.kind === 'bitmap') {
      gesture.current = point;
      if (!gesture.ready || ['fill', 'red-eye'].includes(gesture.tool)) return;
      const object = objectEntry(gesture.objectId)?.object; const next = object && bitmapPixelPoint(object, point, gesture.session);
      if (next && gesture.last) {
        if (gesture.tool === 'eraser') eraseBitmapLine(gesture.session, gesture.last, next, gesture.eraser);
        else if (gesture.tool === 'color-eraser') colorEraseBitmapLine(gesture.session, gesture.last, next, gesture.colorEraser, gesture.eraser);
        else if (gesture.tool === 'liquify') {
          gesture.liquify.direction = event.altKey ? -1 : 1;
          if (gesture.liquify.mode === 'push') liquifyBitmapLine(gesture.session, gesture.last, next, gesture.liquify);
          else liquifyEffectLine(gesture.session, gesture.liquifyBase, gesture.last, next, gesture.liquify);
        }
        else if (gesture.tool === 'spot-heal') spotHealBitmapLine(gesture.session, gesture.snapshot, gesture.last, next, gesture.retouch);
        else if (['clone', 'heal'].includes(gesture.tool)) retouchBitmapLine(gesture.session, gesture.snapshot, gesture.last, next, gesture.sourceOffset, gesture.retouch, gesture.tool);
        else paintPixelLine(gesture.session, gesture.last, next, false, bitmapEditColor(gesture.tool), gesture.pencil.size);
        gesture.last = next; previewDirectBitmapEdit(gesture.session);
      }
      return;
    }
    if (gesture.kind === 'draw') { updateDraw(gesture, point, event.shiftKey); scheduleInteractionRender([gesture.object], { inspector: 'transform' }); return; }
    if (gesture.kind === 'text-path-start') {
      const nearest = gesture.metric.nearestOffset([point.x, point.y]);
      const reversedOpen = Boolean(gesture.object.pathBinding?.reversed && !gesture.metric.closed);
      const offset = reversedOpen ? 1 - nearest.offset : nearest.offset;
      gesture.object.pathBinding.offset = round(clamp(offset, 0, 1) * 360, 3);
      syncBoundTextBounds(gesture.object, gesture.target);
      gesture.moved = gesture.moved || Math.hypot(point.x - gesture.start.x, point.y - gesture.start.y) > .25;
      scheduleInteractionRender([gesture.object], { inspector: true }); return;
    }
    if (gesture.kind === 'fill-handle') { updateFillHandleGesture(gesture, point); return; }
    if (gesture.kind === 'free-transform-control') {
      const local = Core.pointToObjectLocal(gesture.origin, point); const width = Math.max(1, num(gesture.origin.width, 1)); const height = Math.max(1, num(gesture.origin.height, 1));
      gesture.object.freeTransform = FreeTransform.moveControl(gesture.transform, gesture.mode, gesture.index, { x: local.x / width, y: local.y / height });
      gesture.moved = gesture.moved || Math.hypot(point.x - gesture.start.x, point.y - gesture.start.y) > .25;
      scheduleInteractionRender([gesture.object], { inspector: true }); return;
    }
    if (gesture.kind === 'transform-pivot') {
      const local = pointToObjectLocal(gesture.origin, point); const width = Math.max(1, num(gesture.origin.width, 1)); const height = Math.max(1, num(gesture.origin.height, 1));
      gesture.object.transformPivot = { x: round(local.x / width, 5), y: round(local.y / height, 5) };
      gesture.moved = gesture.moved || Math.hypot(point.x - gesture.start.x, point.y - gesture.start.y) > .25;
      scheduleInteractionRender([gesture.object], { inspector: true }); return;
    }
    if (gesture.kind === 'vector-points') {
      const snapped = precisionSnappedPoint(point, { scene: gesture.snapScene, excludeObjectIds: gesture.items.map((item) => item.object.id) });
      const changed = updateVectorPointsGesture(gesture, snapped);
      gesture.moved = gesture.moved || Math.abs(point.x - gesture.start.x) + Math.abs(point.y - gesture.start.y) > .3;
      scheduleInteractionRender(changed, { inspector: true }); return;
    }
    if (gesture.kind === 'vector-point') {
      const snapped = precisionSnappedPoint(point, { scene: gesture.snapScene, excludeObjectIds: [gesture.object.id] });
      updateVectorPoint(gesture.object, gesture.index, snapped, gesture.origin);
      gesture.moved = gesture.moved || Math.abs(point.x - gesture.start.x) + Math.abs(point.y - gesture.start.y) > .3;
      scheduleInteractionRender([gesture.object], { inspector: true }); return;
    }
    if (gesture.kind === 'vector-bezier') {
      const anchor = vectorEditablePoints(gesture.origin)[gesture.anchorIndex];
      const tangentOrigin = anchor ? objectLocalToDocument(gesture.origin, { x: anchor[0], y: anchor[1] }) : null;
      const snapped = precisionSnappedPoint(point, { scene: gesture.snapScene, excludeObjectIds: [gesture.object.id], tangentOrigin });
      updateVectorBezierControl(gesture.object, gesture.index, gesture.control, snapped, gesture.origin);
      gesture.moved = gesture.moved || Math.abs(point.x - gesture.start.x) + Math.abs(point.y - gesture.start.y) > .3;
      scheduleInteractionRender([gesture.object], { inspector: true }); return;
    }
    if (gesture.kind === 'auto-shape-control') {
      if (gesture.behavior === 'click') return;
      const local = pointToObjectLocal(gesture.origin, point);
      AutoShapes.dragControl(gesture.object, gesture.control, local, { altKey: event.altKey, shiftKey: event.shiftKey }, gesture.origin);
      gesture.moved = gesture.moved || Math.abs(point.x - gesture.start.x) + Math.abs(point.y - gesture.start.y) > .3;
      scheduleInteractionRender([gesture.object], { inspector: true }); return;
    }
    if (gesture.kind === 'move') {
      const rawDx = point.x - gesture.start.x; const rawDy = point.y - gesture.start.y;
      if (Math.abs(rawDx) + Math.abs(rawDy) > .3) state.textClick = null;
      if (!gesture.transaction && Math.abs(rawDx) + Math.abs(rawDy) > .3) {
        gesture.transaction = beginDocumentTransaction('Objekte verschieben', { reuseCurrentHistory: true });
      }
      const snapped = snapDelta(rawDx, rawDy, gesture.origins);
      gesture.origins.forEach(({ object, x, y }) => { object.x = round(x + snapped.dx); object.y = round(y + snapped.dy); });
      gesture.delta = { x: snapped.dx, y: snapped.dy };
      gesture.moved = gesture.moved || Math.abs(rawDx) + Math.abs(rawDy) > .3;
      scheduleMoveRender(gesture.origins.map((origin) => origin.object), { inspector: 'transform' }); return;
    }
    if (gesture.kind === 'resize') {
      const o = gesture.origin; const object = gesture.object; const handle = gesture.handle;
      const resizePoint = gridSnappedPoint({
        x: point.x + num(gesture.resizePointerOffset?.x),
        y: point.y + num(gesture.resizePointerOffset?.y)
      });
      let left = o.x; let top = o.y; let right = o.x + o.width; let bottom = o.y + o.height;
      if (handle.includes('w')) left = resizePoint.x; if (handle.includes('e')) right = resizePoint.x;
      if (handle.includes('n')) top = resizePoint.y; if (handle.includes('s')) bottom = resizePoint.y;
      const bounds = event.shiftKey
        ? proportionalResizeBounds(o, handle, resizePoint)
        : { x: Math.min(left, right), y: Math.min(top, bottom), width: Math.abs(right - left), height: Math.abs(bottom - top) };
      object.x = round(bounds.x); object.y = round(bounds.y);
      object.width = Math.max(1, round(bounds.width)); object.height = Math.max(1, round(bounds.height));
      if (gesture.deferredGeometry) {
        updateObjectScalePercent(object, o);
        gesture.moved = gesture.moved || Math.hypot(point.x - gesture.start.x, point.y - gesture.start.y) > .25;
        artboardScene.scheduleTransformRender([object], { inspector: 'transform' }); return;
      }
      const scaleX = object.width / Math.max(1, num(o.width, 1)); const scaleY = object.height / Math.max(1, num(o.height, 1));
      scaleObjectEffectAppearance(object, o, scaleX, scaleY);
      scaleObjectPaintAppearance(object, o, scaleX, scaleY);
      clampObjectRoundness(object, o);
      if (AutoShapes.isAutoShape(object)) {
        AutoShapes.scaleParams(object, o, scaleX, scaleY);
      }
      if (containerObjectTypes.has(object.type) && Array.isArray(o.children)) {
        object.children = o.children.map((child) => scaleObjectTree(child, scaleX, scaleY));
      }
      if (['pencil', 'brush', 'eraser', 'line', 'polyline', 'arrow', 'measure', 'polygon', 'pen'].includes(object.type) && o.points) {
        object.points = o.points.map(([x, y]) => [round(x * scaleX), round(y * scaleY)]);
        if (Array.isArray(o.rawPoints)) object.rawPoints = o.rawPoints.map(([x, y]) => [round(x * scaleX), round(y * scaleY)]);
        if (Array.isArray(o.bezierSegments)) object.bezierSegments = o.bezierSegments.map((segment) => isBezierSegment(segment) ? {
          c1: [round(num(segment.c1[0]) * scaleX), round(num(segment.c1[1]) * scaleY)],
          c2: [round(num(segment.c2[0]) * scaleX), round(num(segment.c2[1]) * scaleY)]
        } : null);
      }
      if (object.type === 'text') {
        const ratio = handle.includes('n') || handle.includes('s') ? object.height / Math.max(1, o.height) : object.width / Math.max(1, o.width);
        object.fontSize = clamp(num(o.fontSize, 32) * ratio, 6, 400); sizeTextObject(object);
      }
      updateObjectScalePercent(object, o);
      scheduleInteractionRender([object], { inspector: true }); return;
    }
    if (gesture.kind === 'rotate') {
      const object = gesture.object; const origin = gesture.origin; const pivot = origin.transformPivot || { x: .5, y: .5 };
      let delta = (Math.atan2(point.y - gesture.pivotWorld.y, point.x - gesture.pivotWorld.x) - gesture.startAngle) * 180 / Math.PI;
      let degrees = num(origin.rotation) + delta; if (event.shiftKey) degrees = Math.round(degrees / 15) * 15;
      const radians = degrees * Math.PI / 180; const width = Math.max(1, num(origin.width, 1)); const height = Math.max(1, num(origin.height, 1)); const center = { x: width / 2, y: height / 2 };
      const sourcePivot = { x: pivot.x * width, y: pivot.y * height }; const transform = activeFreeTransform(origin); const mappedPivot = transform ? FreeTransform.mapPoint(transform, sourcePivot.x / width, sourcePivot.y / height) : { x: sourcePivot.x / width, y: sourcePivot.y / height };
      const localPivot = { x: mappedPivot.x * width, y: mappedPivot.y * height }; const dx = localPivot.x - center.x; const dy = localPivot.y - center.y;
      object.rotation = round((degrees % 360 + 360) % 360, 1);
      object.x = round(gesture.pivotWorld.x - center.x - (Math.cos(radians) * dx - Math.sin(radians) * dy), 4);
      object.y = round(gesture.pivotWorld.y - center.y - (Math.sin(radians) * dx + Math.cos(radians) * dy), 4);
      gesture.moved = gesture.moved || Math.hypot(point.x - gesture.start.x, point.y - gesture.start.y) > .25;
      scheduleInteractionRender([object], { inspector: true }); return;
    }
    if (gesture.kind === 'vector-marquee') { gesture.current = point; scheduleInteractionRender(); return; }
    if (gesture.kind === 'marquee') { gesture.current = point; scheduleInteractionRender(); }
  }

  function onPointerUp(event) {
    const gesture = state.gesture; if (!gesture) return;
    if (event.type === 'pointercancel') { cancelActiveGesture(); return; }
    if (gesture.kind === 'canvas-resize') { finishCanvasResizeGesture(gesture, event); return; }
    if (gesture.kind === 'pen') {
      gesture.pointerActive = false;
      const anchor = gesture.object.points?.at(-1); const tangentOrigin = anchor ? { x: num(gesture.object.x) + num(anchor[0]), y: num(gesture.object.y) + num(anchor[1]) } : null;
      gesture.previewPoint = precisionSnappedPoint(pointFromEvent(event), { scene: gesture.snapScene, excludeObjectIds: [gesture.object.id], tangentOrigin });
      refs.artboard.releasePointerCapture?.(event.pointerId); syncPixelToolOptions(); scheduleInteractionRender([gesture.object]); return;
    }
    if (gesture.kind === 'polyline') {
      gesture.previewPoint = constrainedPolylinePoint(gesture, pointFromEvent(event), event.shiftKey, gesture.snapScene);
      refs.artboard.releasePointerCapture?.(event.pointerId); syncPixelToolOptions(); scheduleInteractionRender([gesture.object]); return;
    }
    if (gesture.kind === 'path-reshape' || gesture.kind === 'path-scrub') {
      const changed = gesture.kind === 'path-reshape' ? gesture.moved : gesture.changed;
      if (changed) commitHistory(gesture.kind === 'path-reshape' ? (gesture.mode === 'freeform' ? 'Pfad frei geformt' : 'Pfadbereich geformt') : (gesture.mode === 'path-scrubber-add' ? 'Kontur lokal verstärkt' : 'Kontur lokal ausgedünnt'));
      else rollbackDocumentTransaction(gesture.transaction);
      state.gesture = null; state.snapGuides = []; refs.artboard.releasePointerCapture?.(event.pointerId); renderAll(); return;
    }
    if (gesture.kind === 'stretch-select') {
      finishStretchSelectionGesture(gesture, pointFromEvent(event)); refs.artboard.releasePointerCapture?.(event.pointerId); return;
    }
    if (gesture.kind === 'stretch-handle') {
      updateStretchHandleGesture(gesture, pointFromEvent(event)); state.gesture = null;
      refs.artboard.releasePointerCapture?.(event.pointerId); updateWarpActionState(); renderEditorOverlays(); return;
    }
    if (gesture.kind === 'warp-control' || gesture.kind === 'puppet-pin') {
      state.gesture = null; refs.artboard.releasePointerCapture?.(event.pointerId); updateWarpActionState(); renderEditorOverlays(); return;
    }
    if (['bitmap-marquee', 'bitmap-ellipse-marquee'].includes(gesture.kind)) {
      const object = objectEntry(gesture.objectId)?.object; const dimensions = object && bitmapPixelDimensions(object);
      const outside = object && BitmapSelection.pixelPointUnclamped(object, pointFromEvent(event), dimensions); if (outside) gesture.current = outside;
      const settings = marqueeSettings();
      const ellipse = gesture.kind === 'bitmap-ellipse-marquee';
      const selection = ellipse
        ? BitmapSelection.ellipseSelection(gesture.start, gesture.current, dimensions, settings)
        : BitmapSelection.marqueeIntersection(gesture.start, gesture.current, dimensions, settings);
      state.bitmapMarquee = selection ? {
        objectId: gesture.objectId, ...(ellipse ? { kind: 'ellipse-marquee' } : {}), ...selection,
        edge: settings.edge, feather: settings.feather
      } : null;
      state.gesture = null; state.snapGuides = []; refs.workspace.releasePointerCapture?.(event.pointerId); renderAll();
      if (!selection) toast('Der aufgezogene Rahmen schneidet das Bitmap nicht', 'warning'); return;
    }
    if (gesture.kind === 'bitmap-lasso') {
      const object = objectEntry(gesture.objectId)?.object; const current = object && bitmapPixelPoint(object, pointFromEvent(event), bitmapPixelDimensions(object));
      const last = gesture.points.at(-1);
      if (current && (!last || Math.hypot(current.x - last.x, current.y - last.y) >= .6)) gesture.points.push(current);
      void finishBitmapLassoGesture(gesture);
      state.gesture = null; state.snapGuides = []; renderAll(); return;
    }
    if (gesture.kind === 'bitmap-polygon-lasso') return;
    if (gesture.kind === 'bitmap') {
      gesture.current = pointFromEvent(event);
      gesture.released = true;
      if (gesture.ready) completeDirectBitmapGesture(gesture);
      return;
    }
    if (gesture.kind === 'resize' && gesture.object?.type === 'image' && bitmapModeFor(gesture.object) === 'direct'
      && (Math.abs(num(gesture.object.width) - num(gesture.origin.width)) > .001 || Math.abs(num(gesture.object.height) - num(gesture.origin.height)) > .001)) {
      void finishDirectBitmapResize(gesture, event.pointerId); return;
    }
    const incrementallyResizedObject = finalizeDeferredResizeGeometry(gesture);
    if (gesture.kind === 'draw') {
      if (['pencil', 'brush', 'eraser'].includes(gesture.object.type)) normalizePath(gesture.object);
      commitHistory(`${names[gesture.object.type]} gezeichnet`, { canonicalize: false });
    } else if (gesture.kind === 'text-path-start') {
      if (gesture.moved) commitHistory('Textstartpunkt auf Pfad verschoben'); else rollbackDocumentTransaction(gesture.transaction);
    } else if (gesture.kind === 'fill-handle') {
      if (gesture.moved) commitHistory('Füllung direkt angepasst'); else rollbackDocumentTransaction(gesture.transaction);
    } else if (gesture.kind === 'free-transform-control') {
      if (gesture.moved) commitHistory(gesture.mode === 'envelope' ? 'Hüllkurve verformt' : gesture.mode === 'perspective' ? 'Perspektivisch verzerrt' : 'Ecke frei transformiert');
      else rollbackDocumentTransaction(gesture.transaction);
    } else if (gesture.kind === 'transform-pivot') {
      if (gesture.moved) commitHistory('Drehmittelpunkt verschoben'); else rollbackDocumentTransaction(gesture.transaction);
    } else if (gesture.kind === 'vector-points') {
      if (gesture.moved) commitHistory(gesture.items.reduce((sum, item) => sum + item.indices.length, 0) > 1 ? 'Vektorpunkte verschoben' : 'Vektorpunkt verschoben');
      // Ein reiner Klick verändert das Dokument nicht, behält aber den soeben
      // gewählten Anker. Ein Rollback würde auch die Vektor-Unterauswahl löschen.
    }
    else if (gesture.kind === 'vector-point') {
      if (gesture.moved) commitHistory('Vektorpunkt verschoben'); else rollbackDocumentTransaction(gesture.transaction);
    }
    else if (gesture.kind === 'vector-bezier') {
      if (gesture.moved) commitHistory('Bézier-Griff verschoben'); else rollbackDocumentTransaction(gesture.transaction);
    }
    else if (gesture.kind === 'auto-shape-control') {
      let changed = gesture.moved;
      if (!gesture.moved) changed = AutoShapes.activateControl(gesture.object, gesture.control, { altKey: event.altKey || gesture.altKey, shiftKey: event.shiftKey || gesture.shiftKey });
      if (changed) commitHistory(`${AutoShapes.definitionFor(gesture.object)?.label || 'Auto-Form'} angepasst`);
      else rollbackDocumentTransaction(gesture.transaction);
    }
    else if (gesture.kind === 'move' && gesture.moved) commitHistory('Objekte verschoben', { canonicalize: false });
    else if (gesture.kind === 'resize') commitHistory('Objekt skaliert', { canonicalize: !incrementallyResizedObject });
    else if (gesture.kind === 'rotate') {
      if (gesture.moved) commitHistory('Objekt gedreht'); else rollbackDocumentTransaction(gesture.transaction);
    }
    else if (gesture.kind === 'guide') {
      gesture.value = guideValueFromPoint(gesture.orientation, pointFromEvent(event));
      finishGuideGesture(gesture);
    }
    else if (gesture.kind === 'marquee') {
      const a = gesture.start; const b = gesture.current || a;
      const box = { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), right: Math.max(a.x, b.x), bottom: Math.max(a.y, b.y) };
      const ids = allObjects().filter((object) => {
        const bounds = boundsOf([object]);
        return bounds.x <= box.right && bounds.right >= box.x && bounds.y <= box.bottom && bounds.bottom >= box.y;
      }).map((object) => object.id);
      if (Math.abs(a.x - b.x) + Math.abs(a.y - b.y) > 3) select(ids, event.shiftKey, { render: false });
    }
    else if (gesture.kind === 'vector-marquee') {
      const a = gesture.start; const b = gesture.current || a;
      const box = { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), right: Math.max(a.x, b.x), bottom: Math.max(a.y, b.y) };
      if (Math.abs(a.x - b.x) + Math.abs(a.y - b.y) > 3) {
        const found = [];
        state.doc.layers.forEach((layer) => {
          if (!layer.visible || layer.locked) return;
          layer.objects.forEach((object) => {
            if (!isVectorObject(object) || object.visible === false || object.locked) return;
            vectorEditablePoints(object).forEach(([x, y], index) => {
              const point = objectLocalToDocument(object, { x, y });
              if (point.x >= box.x && point.x <= box.right && point.y >= box.y && point.y <= box.bottom) found.push({ objectId: object.id, index });
            });
          });
        });
        setVectorPointSelections(gesture.additive ? [...vectorPointSelections(), ...found] : found);
        if (!gesture.additive) setVectorSegmentSelections([]);
        state.selected = [...new Set([...vectorPointSelections(), ...vectorSegmentSelections()].map((selection) => selection.objectId))];
        const layer = layerForObject(state.selected.at(-1)); if (layer) state.doc.activeLayerId = layer.id;
      }
    }
    const selectionOnly = gesture.kind === 'marquee' || gesture.kind === 'vector-marquee'
      || gesture.kind === 'move' && !gesture.moved;
    const incrementallyMovedObjects = gesture.kind === 'move' && gesture.moved
      ? gesture.origins.map((origin) => origin.object)
      : [];
    const incrementallyDrawn = gesture.kind === 'draw'
      ? { object: gesture.object, layer: layerForObject(gesture.object.id) }
      : null;
    state.gesture = null; state.snapGuides = [];
    refs.artboard.style.cursor = cursorForTool();
    if (selectionOnly) renderSelectionState();
    else if (incrementallyDrawn?.layer) {
      scheduleInteractionRender([incrementallyDrawn.object]);
      artboardScene.flushInteractionRender();
      renderDrawCompletionState(incrementallyDrawn.object, incrementallyDrawn.layer);
    }
    else if (incrementallyMovedObjects.length) {
      scheduleMoveRender(incrementallyMovedObjects);
      artboardScene.flushInteractionRender();
      renderMovedSelectionState();
      measurePerformancePhase('render.tabState', syncActiveDocumentTabSaveState);
    } else if (incrementallyResizedObject) {
      scheduleInteractionRender([incrementallyResizedObject]);
      artboardScene.flushInteractionRender();
      renderMovedSelectionState();
      measurePerformancePhase('render.tabState', syncActiveDocumentTabSaveState);
    } else renderAll();
  }

  function onDoubleClick(event) {
    if (!activePolylineGesture() || event.button !== 0) return;
    event.preventDefault(); finishPolylinePath();
  }

  function deleteSelection(commit = true) {
    if (state.bitmapMarquee) { void deleteBitmapSelection(); return; }
    if (!state.selected.length) return;
    const ids = new Set(state.selected);
    state.doc.layers.forEach((layer) => { if (!layer.locked) layer.objects = layer.objects.filter((object) => !ids.has(object.id)); });
    state.selected = [];
    if (commit) commitHistory('Auswahl gelöscht');
    renderAll();
  }

  function deleteSelectedVectorSegments() {
    const selections = vectorSegmentSelections();
    if (state.tool !== 'vector-select' || !selections.length) return false;
    const grouped = new Map(); selections.forEach((selection) => {
      const list = grouped.get(selection.objectId) || []; list.push(selection.index); grouped.set(selection.objectId, list);
    });
    const nextSelected = []; let changed = 0;
    grouped.forEach((indices, objectId) => {
      const entry = objectEntry(objectId); const object = entry?.object;
      if (!entry || !pointVectorTypes.has(object?.type) || object.type === 'measure') return;
      // Ein Segment pro Pfad ist eindeutig. Mehrere ausgewählte Segmente
      // werden objektweise vom höchsten Index abwärts entfernt.
      let currentObjects = [{ object, entry }];
      [...new Set(indices)].sort((a, b) => b - a).forEach((segmentIndex) => {
        const target = currentObjects.find((item) => segmentIndex < vectorSegmentCount(item.object)); if (!target) return;
        const parts = PathWorkbench.deleteSegment(workbenchPathFromObject(target.object), segmentIndex); if (!parts.length) return;
        const original = deepClone(target.object); const replacement = parts.map((part, partIndex) => {
          const next = partIndex === 0 ? target.object : { ...deepClone(original), id: uid('object') };
          applyWorkbenchPath(next, part, { forcePathType: true }); next.name = 'Offener Pfad'; return next;
        });
        target.entry.container.splice(target.entry.index, 1, ...replacement); currentObjects = replacement.map((next) => ({ object: next, entry: objectEntry(next.id) }));
        changed += 1;
      });
      currentObjects.forEach(({ object: next }) => nextSelected.push(next.id));
    });
    if (!changed) { toast('Dieser Pfad braucht mindestens ein Segment', 'warning'); return true; }
    state.selected = [...new Set(nextSelected)]; clearVectorSubselection(); commitHistory(changed === 1 ? 'Pfadsegment gelöscht' : 'Pfadsegmente gelöscht'); renderAll(); return true;
  }

  function deleteSelectedVectorPoint() {
    const selections = vectorPointSelections();
    if (state.tool !== 'vector-select' || !selections.length) return false;
    const grouped = new Map(); selections.forEach((selection) => {
      const list = grouped.get(selection.objectId) || []; list.push(selection.index); grouped.set(selection.objectId, list);
    });
    for (const [objectId, indices] of grouped) {
      const object = objectEntry(objectId)?.object; if (!object || !(isPointEditableVector(object) || polygonConvertibleTypes.has(object.type))) continue;
      if (polygonConvertibleTypes.has(object.type)) convertShapeToPolygon(object);
      if (outlineVectorTypes.has(object.type)) {
        const contourCounts = new Map();
        indices.forEach((index) => { const address = outlinePointAddress(object, index); if (address) contourCounts.set(address.contourIndex, (contourCounts.get(address.contourIndex) || 0) + 1); });
        if ([...contourCounts].some(([contourIndex, count]) => outlineContoursFor(object)[contourIndex].length - count < 3)) {
          toast('Jede geschlossene Kontur braucht mindestens 3 Punkte', 'warning'); return true;
        }
      } else {
        const minimum = isClosedVectorPath(object) ? 3 : 2;
        if (vectorEditablePoints(object).length - new Set(indices).size < minimum) {
          toast(`${names[object.type] || 'Pfad'} braucht mindestens ${minimum} Punkte`, 'warning'); return true;
        }
      }
    }
    let removed = 0; const remainingSelections = [];
    grouped.forEach((indices, objectId) => {
      const object = objectEntry(objectId)?.object; if (!object) return;
      if (outlineVectorTypes.has(object.type)) {
        const contours = deepClone(outlineContoursFor(object)); const bezierContours = outlineBezierContoursFor(object);
        [...new Set(indices)].sort((a, b) => b - a).forEach((index) => {
          const address = outlinePointAddress(object, index); if (!address) return;
          const contour = contours[address.contourIndex]; const segments = bezierContours[address.contourIndex]; const previous = (address.pointIndex - 1 + contour.length) % contour.length;
          const incoming = segments[previous]; const outgoing = segments[address.pointIndex];
          segments[previous] = isBezierSegment(incoming) && isBezierSegment(outgoing) ? { c1: incoming.c1, c2: outgoing.c2 } : null;
          contour.splice(address.pointIndex, 1); segments.splice(address.pointIndex, 1); removed += 1;
        });
        object.outlineContours = contours; object.outlineBezierContours = bezierContours; syncOutlinePathData(object);
        const remaining = vectorEditablePoints(object); if (remaining.length) remainingSelections.push({ objectId, index: Math.min(Math.min(...indices), remaining.length - 1) }); return;
      }
      if (polygonConvertibleTypes.has(object.type)) convertShapeToPolygon(object);
      [...new Set(indices)].sort((a, b) => b - a).forEach((index) => {
        const points = object.points || []; if (!points[index]) return;
        const previous = index > 0 ? index - 1 : isClosedVectorPath(object) ? points.length - 1 : -1;
        if (Array.isArray(object.bezierSegments)) {
          const incoming = object.bezierSegments[previous]; const outgoing = object.bezierSegments[index];
          if (previous >= 0) object.bezierSegments[previous] = isBezierSegment(incoming) && isBezierSegment(outgoing) ? { c1: incoming.c1, c2: outgoing.c2 } : null;
          object.bezierSegments.splice(index, 1);
        }
        object.points.splice(index, 1); object.pointModes?.splice(index, 1); object.strokeProfile?.splice(index, 1); removed += 1;
      });
      if (object.type === 'pencil') object.rawPoints = deepClone(object.points);
      normalizeVectorPointBounds(object);
      if (object.points?.length) remainingSelections.push({ objectId, index: Math.min(Math.min(...indices), object.points.length - 1) });
    });
    setVectorPointSelections(remainingSelections); setVectorSegmentSelections([]);
    if (removed) { commitHistory(removed === 1 ? 'Vektorpunkt gelöscht' : `${removed} Vektorpunkte gelöscht`); renderAll(); }
    return true;
  }

  function selectedWorkbenchObjects() {
    return vectorEditObjects().filter((object) => pointVectorTypes.has(object.type) && object.type !== 'measure' && vectorEditablePoints(object).length >= 2);
  }

  function selectedSimplifiableVectorObjects() {
    return vectorEditObjects().filter((object) => {
      if (pointVectorTypes.has(object.type)) return object.type !== 'measure' && vectorEditablePoints(object).length >= 2;
      return outlineVectorTypes.has(object.type) && outlineContoursFor(object).some((contour) => contour.length > 3);
    });
  }

  function closedContourSignedArea(points) {
    return points.reduce((sum, point, index) => {
      const next = points[(index + 1) % points.length];
      return sum + num(point[0]) * num(next[1]) - num(next[0]) * num(point[1]);
    }, 0) / 2;
  }

  function closedContourSelfIntersects(points) {
    const cross = (a, b, c) => (num(b[0]) - num(a[0])) * (num(c[1]) - num(a[1])) - (num(b[1]) - num(a[1])) * (num(c[0]) - num(a[0]));
    const between = (value, left, right) => value >= Math.min(left, right) - 1e-7 && value <= Math.max(left, right) + 1e-7;
    const onSegment = (point, start, end) => Math.abs(cross(start, end, point)) <= 1e-7
      && between(num(point[0]), num(start[0]), num(end[0])) && between(num(point[1]), num(start[1]), num(end[1]));
    const intersects = (a, b, c, d) => {
      const abC = cross(a, b, c); const abD = cross(a, b, d); const cdA = cross(c, d, a); const cdB = cross(c, d, b);
      if ((abC > 1e-7 && abD < -1e-7 || abC < -1e-7 && abD > 1e-7)
        && (cdA > 1e-7 && cdB < -1e-7 || cdA < -1e-7 && cdB > 1e-7)) return true;
      return onSegment(c, a, b) || onSegment(d, a, b) || onSegment(a, c, d) || onSegment(b, c, d);
    };
    for (let left = 0; left < points.length; left += 1) {
      const leftNext = (left + 1) % points.length;
      for (let right = left + 1; right < points.length; right += 1) {
        const rightNext = (right + 1) % points.length;
        if (left === right || leftNext === right || rightNext === left) continue;
        if (intersects(points[left], points[leftNext], points[right], points[rightNext])) return true;
      }
    }
    return false;
  }

  function simplifyClosedOutlineContour(contour, segments, tolerance) {
    const source = (segments || []).some(isBezierSegment)
      ? PathOperations.flattenPathContour({ points: contour, segments, closed: true }, { tolerance: Math.min(.25, Math.max(.05, tolerance / 8)) }).contour
      : contour;
    const simplified = PathWorkbench.simplifyPath({ points: source, segments: source.map(() => null), closed: true }, tolerance).points;
    if (simplified.length < 3 || simplified.length >= contour.length || (simplified.length <= 2048 && closedContourSelfIntersects(simplified))) return null;
    const originalArea = closedContourSignedArea(contour); const simplifiedArea = closedContourSignedArea(simplified);
    if (Math.abs(originalArea) <= 1e-7 || Math.abs(simplifiedArea) <= 1e-7 || Math.sign(originalArea) !== Math.sign(simplifiedArea)) return null;
    return simplified.map(([x, y]) => [round(x, 4), round(y, 4)]);
  }

  function simplifyOutlineObject(object, tolerance) {
    const contours = deepClone(outlineContoursFor(object)); const bezierContours = outlineBezierContoursFor(object, contours);
    let removed = 0;
    const nextContours = contours.map((contour, contourIndex) => {
      const simplified = simplifyClosedOutlineContour(contour, bezierContours[contourIndex], tolerance);
      if (!simplified) return contour;
      removed += contour.length - simplified.length; return simplified;
    });
    if (!removed) return 0;
    const nextBezierContours = nextContours.map((contour, contourIndex) => contour === contours[contourIndex]
      ? bezierContours[contourIndex]
      : contour.map(() => null));
    object.outlineContours = nextContours;
    if (nextBezierContours.some((segments) => segments.some(isBezierSegment))) object.outlineBezierContours = nextBezierContours;
    else delete object.outlineBezierContours;
    syncOutlinePathData(object); object.scalePercent = 100;
    return removed;
  }

  function applySelectedPointMode(mode) {
    const selections = vectorPointSelections(); if (!selections.length) return false;
    const grouped = new Map(); selections.forEach((selection) => {
      const list = grouped.get(selection.objectId) || []; list.push(selection.index); grouped.set(selection.objectId, list);
    });
    let changed = 0;
    grouped.forEach((indices, objectId) => {
      const object = objectEntry(objectId)?.object; if (!object || !pointVectorTypes.has(object.type) || object.type === 'measure') return;
      applyWorkbenchPath(object, PathWorkbench.setPointModes(workbenchPathFromObject(object), indices, mode)); changed += indices.length;
    });
    if (!changed) return false;
    commitHistory(`Punktart: ${{ corner: 'Ecke', smooth: 'Glatt', symmetric: 'Symmetrisch' }[mode]}`); renderAll(); return true;
  }

  function closeSelectedVectorPath() {
    const objects = selectedWorkbenchObjects().filter((object) => !isClosedVectorPath(object) && vectorEditablePoints(object).length >= 3);
    if (objects.length !== 1) { toast('Wähle genau einen offenen Pfad mit mindestens 3 Punkten', 'warning'); return false; }
    applyWorkbenchPath(objects[0], PathWorkbench.closePath(workbenchPathFromObject(objects[0])), { forcePathType: true });
    clearVectorSubselection(); commitHistory('Pfad geschlossen'); renderAll(); return true;
  }

  function openSelectedVectorPath() {
    const selections = vectorPointSelections();
    if (selections.length !== 1) { toast('Wähle den Anker, an dem der Pfad geöffnet werden soll', 'warning'); return false; }
    const object = objectEntry(selections[0].objectId)?.object;
    if (!object || !pointVectorTypes.has(object.type) || !isClosedVectorPath(object)) { toast('Der ausgewählte Anker gehört nicht zu einem geschlossenen Pfad', 'warning'); return false; }
    applyWorkbenchPath(object, PathWorkbench.openPathAt(workbenchPathFromObject(object), selections[0].index), { forcePathType: true });
    setVectorPointSelections([{ objectId: object.id, index: 0 }]); setVectorSegmentSelections([]);
    commitHistory('Pfad geöffnet'); renderAll(); return true;
  }

  function reverseSelectedVectorPaths() {
    const objects = selectedWorkbenchObjects(); if (!objects.length) { toast('Wähle mindestens einen Pfad', 'warning'); return false; }
    objects.forEach((object) => applyWorkbenchPath(object, PathWorkbench.reversePath(workbenchPathFromObject(object)), { forcePathType: false }));
    clearVectorSubselection(); commitHistory(objects.length === 1 ? 'Pfadrichtung umgekehrt' : 'Pfadrichtungen umgekehrt'); renderAll(); return true;
  }

  function joinSelectedVectorEndpoints() {
    const selections = vectorPointSelections();
    if (selections.length !== 2) { toast('Wähle mit Shift genau zwei offene Endpunkte', 'warning'); return false; }
    const left = objectEntry(selections[0].objectId)?.object; const right = objectEntry(selections[1].objectId)?.object;
    if (!left || !right || !pointVectorTypes.has(left.type) || !pointVectorTypes.has(right.type) || isClosedVectorPath(left) || isClosedVectorPath(right)) {
      toast('Verbinden funktioniert mit zwei offenen Vektorpfaden', 'warning'); return false;
    }
    const endpoint = (object, index) => index === 0 || index === vectorEditablePoints(object).length - 1;
    if (!endpoint(left, selections[0].index) || !endpoint(right, selections[1].index)) { toast('Wähle jeweils einen Endpunkt', 'warning'); return false; }
    if (left.id === right.id) return closeSelectedVectorPath();
    const leftEntry = objectEntry(left.id); const rightEntry = objectEntry(right.id);
    const joined = PathWorkbench.joinPaths(workbenchPathFromObject(left, { documentSpace: true }), selections[0].index, workbenchPathFromObject(right, { documentSpace: true }), selections[1].index);
    if (!joined) return false;
    left.x = 0; left.y = 0; left.width = 1; left.height = 1; left.rotation = 0; left.flipX = false; left.flipY = false;
    applyWorkbenchPath(left, joined, { forcePathType: true });
    rightEntry.container.splice(rightEntry.index, 1); state.selected = [left.id]; clearVectorSubselection(); state.doc.activeLayerId = leftEntry.layer.id;
    commitHistory('Pfade verbunden'); renderAll(); return true;
  }

  function simplifySelectedVectorPaths() {
    const objects = selectedSimplifiableVectorObjects(); if (!objects.length) { toast('Wähle mindestens einen Pfad', 'warning'); return false; }
    const tolerance = clamp(num($('vectorSimplifyTolerance').value, 2), .1, 100); $('vectorSimplifyTolerance').value = String(tolerance);
    let changed = 0;
    objects.forEach((object) => {
      if (outlineVectorTypes.has(object.type)) { changed += simplifyOutlineObject(object, tolerance); return; }
      const before = vectorEditablePoints(object).length; const simplified = PathWorkbench.simplifyPath(workbenchPathFromObject(object), tolerance);
      if (simplified.points.length >= before) return;
      applyWorkbenchPath(object, simplified, { forcePathType: true }); changed += before - simplified.points.length;
    });
    clearVectorSubselection();
    if (!changed) { toast('Bei dieser Toleranz gibt es keine überflüssigen Punkte'); renderAll(); return false; }
    commitHistory(`Pfad vereinfacht (${changed} Punkte entfernt)`); renderAll(); return true;
  }

  function smoothSelectedVectorPaths() {
    const objects = selectedWorkbenchObjects(); if (!objects.length) { toast('Wähle mindestens einen Pfad', 'warning'); return false; }
    const strength = clamp(num($('vectorSmoothStrength').value, 50), 0, 100); $('vectorSmoothStrength').value = String(strength);
    const segmentSelections = vectorSegmentSelections();
    objects.forEach((object) => {
      const chosen = segmentSelections.filter((selection) => selection.objectId === object.id).map((selection) => selection.index);
      applyWorkbenchPath(object, PathWorkbench.smoothPath(workbenchPathFromObject(object), strength, chosen.length ? chosen : null));
    });
    commitHistory(segmentSelections.length ? 'Pfadsegmente geglättet' : 'Pfad geglättet'); renderAll(); return true;
  }

  const reusableLibraryClipboardPrefix = 'PYRO_RESOURCES:';

  function nextReusableName(entries, base) {
    const used = new Set((entries || []).map((entry) => entry.name)); let index = 1; let name = base;
    while (used.has(name)) name = `${base} ${++index}`;
    return name;
  }

  function requestedReusableName(entries, base) {
    const fallback = nextReusableName(entries, base); const value = prompt(I18n.t('dialog.enterLibraryName'), fallback);
    return value == null ? '' : String(value).trim().slice(0, 256) || fallback;
  }

  function makeSymbolInstance(definition, source = {}, offset = 0) {
    const base = definition.object;
    return {
      id: source.id || uid('object'), type: 'symbol-instance', symbolId: definition.id,
      name: source.name || `${definition.name} Instanz`, x: round(num(source.x, (state.doc.width - base.width) / 2) + offset), y: round(num(source.y, (state.doc.height - base.height) / 2) + offset),
      width: Math.max(.0001, num(source.width, base.width)), height: Math.max(.0001, num(source.height, base.height)), rotation: num(source.rotation),
      opacity: clamp(num(source.opacity, 100), 0, 100), visible: source.visible !== false, locked: Boolean(source.locked), blendMode: normalizeBlendMode(source.blendMode),
      flipX: Boolean(source.flipX), flipY: Boolean(source.flipY), scalePercent: num(source.scalePercent, 100)
    };
  }

  function createSymbolFromSelection() {
    const object = selectedObjects().length === 1 ? primaryObject() : null; const entry = objectEntry(object?.id);
    if (!object || !entry || object.type === 'symbol-instance' || object.symbolMasterId) { toast('Wähle genau ein normales Grundobjekt aus', 'warning'); return false; }
    const library = reusableLibrary(); const name = requestedReusableName(library.symbols, object.name || names[object.type] || 'Symbol'); if (!name) return false;
    const definition = { id: uid('symbol'), name, object: symbolDefinitionObject(object) }; definition.object.name = name;
    library.symbols.push(definition);
    const instance = makeSymbolInstance(definition, { ...object, name: `${name} Instanz`, opacity: 100, blendMode: 'normal' });
    entry.container.splice(entry.index, 1, instance); state.selected = [instance.id];
    commitHistory('Symbol erstellt'); renderAll(); $('symbolLibrarySelect').value = definition.id; toast(`Symbol „${name}“ erstellt`); return true;
  }

  function insertSelectedSymbol() {
    const library = reusableLibrary(); const definition = library.symbols.find((entry) => entry.id === $('symbolLibrarySelect').value);
    const layer = activeLayer(); if (!definition || !layer || layer.locked || !layer.visible) { toast('Wähle ein Symbol und eine bearbeitbare Ebene', 'warning'); return false; }
    const source = primaryObject(); const position = source ? { x: source.x, y: source.y } : {};
    const instance = makeSymbolInstance(definition, position, source ? 24 : 0); layer.objects.push(instance); state.selected = [instance.id];
    commitHistory('Symbolinstanz eingefügt'); renderAll(); toast(`Instanz von „${definition.name}“ eingefügt`); return true;
  }

  function editSelectedSymbolMaster() {
    const selected = primaryObject(); const symbolId = selected?.type === 'symbol-instance' ? selected.symbolId : selected?.symbolMasterId;
    const definition = symbolDefinition(symbolId); if (!selected || !definition) { toast('Wähle eine Symbolinstanz aus', 'warning'); return false; }
    const existing = renderableObjects().find((object) => object.symbolMasterId === definition.id);
    if (existing) { state.selected = [existing.id]; renderAll(); toast('Das Grundobjekt wird bereits bearbeitet'); return true; }
    const entry = objectEntry(selected.id); if (!entry || selected.type !== 'symbol-instance') return false;
    const scaleX = Math.max(.0001, num(selected.width, definition.object.width) / Math.max(.0001, num(definition.object.width, 1)));
    const scaleY = Math.max(.0001, num(selected.height, definition.object.height) / Math.max(.0001, num(definition.object.height, 1)));
    const master = scaleObjectTree(definition.object, scaleX, scaleY);
    Object.assign(master, {
      id: selected.id, name: definition.name, x: selected.x, y: selected.y, rotation: selected.rotation, flipX: selected.flipX, flipY: selected.flipY,
      visible: selected.visible !== false, locked: false, symbolMasterId: definition.id, symbolMasterScaleX: scaleX, symbolMasterScaleY: scaleY
    });
    entry.container.splice(entry.index, 1, master); state.selected = [master.id]; commitHistory('Symbol-Grundobjekt geöffnet'); renderAll();
    toast('Änderungen am Grundobjekt aktualisieren alle Instanzen'); return true;
  }

  function finishSelectedSymbolMaster() {
    const master = primaryObject(); const entry = objectEntry(master?.id); const definition = symbolDefinition(master?.symbolMasterId);
    if (!master || !entry || !definition) { toast('Kein Symbol-Grundobjekt ausgewählt', 'warning'); return false; }
    syncSymbolMasterObject(master);
    const instance = makeSymbolInstance(definition, {
      id: master.id, name: `${definition.name} Instanz`, x: master.x, y: master.y, width: master.width, height: master.height,
      rotation: master.rotation, flipX: master.flipX, flipY: master.flipY, visible: master.visible, locked: master.locked, opacity: 100, blendMode: 'normal', scalePercent: master.scalePercent
    });
    entry.container.splice(entry.index, 1, instance); state.selected = [instance.id]; commitHistory('Symbol-Grundobjekt geschlossen'); renderAll(); toast('Symbolbearbeitung beendet'); return true;
  }

  function saveSelectedObjectStyle() {
    const object = primaryObject(); const library = reusableLibrary();
    if (!object || object.type === 'symbol-instance' || containerObjectTypes.has(object.type)) { toast('Wähle ein gestaltetes Einzelobjekt aus', 'warning'); return false; }
    const name = requestedReusableName(library.objectStyles, `${object.name || names[object.type] || 'Objekt'} Stil`); if (!name) return false;
    const entry = { id: uid('object-style'), name, style: ReusableAssets.captureObjectStyle(object) }; library.objectStyles.push(entry);
    commitHistory('Objektstil gespeichert'); renderAll(); $('objectStyleLibrarySelect').value = entry.id; toast(`Objektstil „${name}“ gespeichert`); return true;
  }

  function applySelectedObjectStyle() {
    const preset = reusableLibrary().objectStyles.find((entry) => entry.id === $('objectStyleLibrarySelect').value); const objects = selectedObjects().filter((object) => object.type !== 'symbol-instance' && !containerObjectTypes.has(object.type));
    if (!preset || !objects.length) { toast('Wähle einen Objektstil und passende Objekte', 'warning'); return false; }
    objects.forEach((object) => {
      ReusableAssets.applyObjectStyle(object, preset.style, { createId: uid });
      if (object.fillPaint && !supportsAdvancedFill(object)) delete object.fillPaint;
      if (object.effects) { object.effects = effectStack(object).filter((effect) => Effects.isAllowed(effect.type, object.type)); if (!object.effects.length) delete object.effects; }
      if (!supportsStrokeType(object)) delete object.strokeStack;
    });
    commitHistory('Objektstil angewendet'); renderAll(); toast(`„${preset.name}“ auf ${objects.length} Objekt${objects.length === 1 ? '' : 'e'} angewendet`); return true;
  }

  function saveSelectedFillPreset() {
    const object = primaryObject(); const paint = normalizedObjectFillPaint(object); const library = reusableLibrary();
    if (!paint || (!FillPaint.isGradient(paint.type) && !FillPaint.isImage(paint.type))) { toast('Wähle ein Objekt mit Verlauf oder Bildmuster', 'warning'); return false; }
    const name = requestedReusableName(library.fillPresets, FillPaint.typeLabel(paint.type)); if (!name) return false;
    const entry = { id: uid('fill-preset'), name, paint: deepClone(paint) }; library.fillPresets.push(entry);
    commitHistory('Füllungsvorgabe gespeichert'); renderAll(); $('fillPresetLibrarySelect').value = entry.id; toast(`Füllungsvorgabe „${name}“ gespeichert`); return true;
  }

  function applySelectedFillPreset() {
    const preset = reusableLibrary().fillPresets.find((entry) => entry.id === $('fillPresetLibrarySelect').value); const objects = selectedObjects().filter(supportsAdvancedFill);
    if (!preset || !objects.length) { toast('Wähle eine Füllungsvorgabe und geschlossene Vektorobjekte', 'warning'); return false; }
    objects.forEach((object) => { object.fillPaint = deepClone(preset.paint); object.fill = object.fillPaint.stops?.[0]?.color || object.fill || '#4f6df5'; });
    commitHistory('Füllungsvorgabe angewendet'); renderAll(); toast(`„${preset.name}“ angewendet`); return true;
  }

  function saveSelectedStrokePreset() {
    const object = primaryObject(); const stack = strokeStackFor(object); const library = reusableLibrary();
    if (!supportsStrokeType(object) || !stack.length) { toast('Wähle ein Objekt mit Kontur aus', 'warning'); return false; }
    const name = requestedReusableName(library.strokePresets, `${object.name || names[object.type] || 'Objekt'} Kontur`); if (!name) return false;
    const entry = { id: uid('stroke-preset'), name, stack: deepClone(stack) }; library.strokePresets.push(entry);
    commitHistory('Konturvorgabe gespeichert'); renderAll(); $('strokePresetLibrarySelect').value = entry.id; toast(`Konturvorgabe „${name}“ gespeichert`); return true;
  }

  function applySelectedStrokePreset() {
    const preset = reusableLibrary().strokePresets.find((entry) => entry.id === $('strokePresetLibrarySelect').value); const objects = selectedObjects().filter(supportsStrokeType);
    if (!preset || !objects.length) { toast('Wähle eine Konturvorgabe und Vektorobjekte', 'warning'); return false; }
    objects.forEach((object) => setObjectStrokeStack(object, preset.stack.map((stroke) => ({ ...deepClone(stroke), id: uid('stroke') }))));
    commitHistory('Konturvorgabe angewendet'); renderAll(); toast(`„${preset.name}“ angewendet`); return true;
  }

  function removeSelectedReusableResource(key, selectId, label) {
    const library = reusableLibrary(); const id = $(selectId).value; const index = library[key].findIndex((entry) => entry.id === id); if (index < 0) return false;
    if (key === 'symbols' && renderableObjects().some((object) => object.symbolId === id || object.symbolMasterId === id)) { toast('Das Symbol wird noch von Instanzen verwendet', 'warning'); return false; }
    const [removed] = library[key].splice(index, 1); commitHistory(`${label} gelöscht`); renderAll(); toast(`„${removed.name}“ gelöscht`); return true;
  }

  function cleanReusableLibrary(resources) {
    return Core.sanitizeDocument({ width: 1, height: 1, layers: [{ id: 'resource-cleanup', name: 'Ressourcen', objects: [] }], activeLayerId: 'resource-cleanup', resources }, { createId: uid }).document.resources;
  }

  function copyReusableLibrary() {
    const library = cleanReusableLibrary(reusableLibrary()); const total = Object.values(ReusableAssets.libraryCounts({ resources: library })).reduce((sum, value) => sum + value, 0);
    if (!total) { toast('Die Dokumentbibliothek ist leer', 'warning'); return false; }
    state.resourceClipboard = deepClone(library); const payload = `${reusableLibraryClipboardPrefix}${JSON.stringify({ version: 1, resources: library })}`;
    navigator.clipboard?.writeText?.(payload).catch(() => { /* Die tabinterne Übertragung bleibt verfügbar. */ });
    renderReusableAssetsPanel(); toast(`${total} Bibliothekseinträge kopiert`); return true;
  }

  function mergeReusableLibrary(resources, label = 'Bibliothek eingefügt') {
    const cleaned = cleanReusableLibrary(resources); const result = ReusableAssets.mergeLibrary(state.doc, cleaned, { createId: uid });
    if (!result.totalAdded) { toast('Alle Bibliothekseinträge sind bereits vorhanden'); return false; }
    commitHistory(label); renderAll(); toast(`${result.totalAdded} Bibliothekseinträge eingefügt`); return true;
  }

  async function pasteReusableLibrary() {
    if (state.resourceClipboard) return mergeReusableLibrary(state.resourceClipboard);
    try {
      const text = await navigator.clipboard?.readText?.();
      if (!String(text || '').startsWith(reusableLibraryClipboardPrefix)) throw new Error('Keine PYRO-Bibliothek');
      const payload = JSON.parse(text.slice(reusableLibraryClipboardPrefix.length)); return mergeReusableLibrary(payload.resources);
    } catch { toast('Keine kopierte PYRO-Bibliothek verfügbar', 'warning'); return false; }
  }

  function renderReusableSelect(id, entries, emptyLabel) {
    const select = $(id); if (!select) return '';
    const previous = select.value; select.replaceChildren();
    if (!entries.length) { const option = document.createElement('option'); option.value = ''; option.textContent = emptyLabel; select.append(option); select.disabled = true; return ''; }
    entries.forEach((entry) => { const option = document.createElement('option'); option.value = entry.id; option.dataset.i18nIgnoreText = ''; option.textContent = entry.name; select.append(option); });
    select.disabled = false; select.value = entries.some((entry) => entry.id === previous) ? previous : entries[0].id; return select.value;
  }

  function renderReusableAssetsPanel() {
    const library = reusableLibrary(); const counts = ReusableAssets.libraryCounts(state.doc); const total = Object.values(counts).reduce((sum, value) => sum + value, 0); const object = primaryObject();
    const symbolId = renderReusableSelect('symbolLibrarySelect', library.symbols, 'Keine Symbole');
    const styleId = renderReusableSelect('objectStyleLibrarySelect', library.objectStyles, 'Keine Objektstile');
    const fillId = renderReusableSelect('fillPresetLibrarySelect', library.fillPresets, 'Keine Füllungsvorgaben');
    const strokeId = renderReusableSelect('strokePresetLibrarySelect', library.strokePresets, 'Keine Konturvorgaben');
    $('reusableAssetsCount').textContent = `${total} ${total === 1 ? 'Eintrag' : 'Einträge'}`;
    $('reusableAssetsHint').textContent = object?.symbolMasterId ? 'Grundobjekt aktiv: Änderungen aktualisieren alle Instanzen.' : 'Symbole und Vorgaben werden mit dem Projekt gespeichert.';
    $('createSymbolBtn').disabled = selectedObjects().length !== 1 || object?.type === 'symbol-instance' || Boolean(object?.symbolMasterId);
    $('insertSymbolBtn').disabled = !symbolId; $('deleteSymbolBtn').disabled = !symbolId;
    $('editSymbolBtn').disabled = !(object?.type === 'symbol-instance' || object?.symbolMasterId); $('finishSymbolEditBtn').disabled = !object?.symbolMasterId;
    $('saveObjectStyleBtn').disabled = !object || object.type === 'symbol-instance' || containerObjectTypes.has(object.type); $('applyObjectStyleBtn').disabled = !styleId || !object; $('deleteObjectStyleBtn').disabled = !styleId;
    const paint = normalizedObjectFillPaint(object); $('saveFillPresetBtn').disabled = !(paint && (FillPaint.isGradient(paint.type) || FillPaint.isImage(paint.type)));
    $('applyFillPresetBtn').disabled = !fillId || !selectedObjects().some(supportsAdvancedFill); $('deleteFillPresetBtn').disabled = !fillId;
    $('saveStrokePresetBtn').disabled = !supportsStrokeType(object) || !strokeStackFor(object).length; $('applyStrokePresetBtn').disabled = !strokeId || !selectedObjects().some(supportsStrokeType); $('deleteStrokePresetBtn').disabled = !strokeId;
    $('copyLibraryBtn').disabled = !total; $('pasteLibraryBtn').disabled = false;
  }

  const objectClipboardPrefix = 'PYRO_OBJECTS:';

  function writeObjectClipboard(objects, resources = ReusableAssets.referencedLibrary(objects, state.doc)) {
    const payload = `${objectClipboardPrefix}${JSON.stringify({ version: 2, objects, resources })}`;
    navigator.clipboard?.writeText?.(payload).catch(() => { /* Die tabübergreifende Zwischenablage bleibt verfügbar. */ });
  }

  function readObjectClipboard(value) {
    if (!String(value || '').startsWith(objectClipboardPrefix)) return null;
    try {
      const payload = JSON.parse(String(value).slice(objectClipboardPrefix.length));
      return Array.isArray(payload?.objects) ? { objects: payload.objects.map(cleanObject), resources: cleanReusableLibrary(payload.resources || {}) } : null;
    } catch { return null; }
  }

  function reidentifyClipboardObjects(sources) {
    const ids = new Map();
    const cloneTree = (source) => {
      const copy = deepClone(source); const originalId = copy.id; copy.id = uid('object'); ids.set(originalId, copy.id);
      if (Array.isArray(copy.children)) copy.children = copy.children.map(cloneTree);
      return copy;
    };
    const copies = sources.map(cloneTree);
    const fixBindings = (object) => {
      if (object.pathBinding?.targetId) {
        const targetId = ids.get(object.pathBinding.targetId);
        if (targetId) object.pathBinding.targetId = targetId; else delete object.pathBinding;
      }
      if (Array.isArray(object.children)) object.children.forEach(fixBindings);
    };
    copies.forEach(fixBindings); return copies;
  }

  function pasteObjects(sources, label = 'Objekte eingefügt', updateClipboard = true, resources = null) {
    const layer = activeLayer();
    if (!sources?.length) return false;
    if (!layer || layer.locked || !layer.visible) { toast('Aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return false; }
    let prepared = sources.map(deepClone);
    if (resources) {
      const merged = ReusableAssets.mergeLibrary(state.doc, cleanReusableLibrary(resources), { createId: uid });
      ReusableAssets.rewriteSymbolReferences(prepared, merged.idMaps.symbols);
    }
    // Die Zwischenablage bewahrt die Dokumentkoordinaten. Dadurch kann ein
    // ausgeschnittener Pixelbereich ohne Nachjustieren wieder exakt in seine
    // ursprüngliche Lücke eingesetzt werden.
    const pasted = reidentifyClipboardObjects(prepared).map((source) => ({ ...source, name: `${source.name || names[source.type]} Kopie`, x: num(source.x), y: num(source.y) }));
    layer.objects.push(...pasted); state.selected = pasted.map((object) => object.id); state.bitmapMarquee = null;
    if (updateClipboard) {
      state.clipboard = pasted.map((object) => deepClone(object)); state.sharedClipboard = state.clipboard.map(deepClone);
      state.sharedClipboardResources = ReusableAssets.referencedLibrary(pasted, state.doc);
    }
    commitHistory(label); renderAll(); return true;
  }

  function marqueeEdgeSettings(selection) {
    return {
      edge: ['hard', 'antialias', 'feather'].includes(selection?.edge) ? selection.edge : 'hard',
      feather: clamp(Math.round(num(selection?.feather, 4)), 1, 1000)
    };
  }

  function marqueeMaskAlpha(x, y, width, height, selection) {
    const settings = marqueeEdgeSettings(selection);
    if (settings.edge === 'hard') return 1;
    if (settings.edge === 'antialias') return x === 0 || y === 0 || x === width - 1 || y === height - 1 ? .5 : 1;
    const distance = Math.min(x + .5, y + .5, width - x - .5, height - y - .5);
    return clamp(distance / settings.feather, 0, 1);
  }

  function applyMarqueeMask(context, width, height, selection, cut = false, originX = 0, originY = 0) {
    const settings = marqueeEdgeSettings(selection);
    if (settings.edge === 'hard') {
      if (cut) context.clearRect(originX, originY, width, height);
      return;
    }
    const imageData = context.getImageData(originX, originY, width, height); const data = imageData.data;
    for (let y = 0; y < height; y += 1) {
      for (let x = 0; x < width; x += 1) {
        const offset = (y * width + x) * 4; const mask = marqueeMaskAlpha(x, y, width, height, selection);
        data[offset + 3] = Math.round(data[offset + 3] * (cut ? 1 - mask : mask));
      }
    }
    context.putImageData(imageData, originX, originY);
  }

  function applyWandMask(context, selection, cut = false, originX = 0, originY = 0) {
    const mask = wandMaskCanvas(selection);
    context.save(); context.globalCompositeOperation = cut ? 'destination-out' : 'destination-in';
    context.drawImage(mask, originX, originY); context.restore();
  }

  function applyBitmapSelectionMask(context, width, height, selection, cut = false, originX = 0, originY = 0) {
    if (selection?.kind === 'wand') applyWandMask(context, selection, cut, originX, originY);
    else if (['ellipse-marquee', 'lasso', 'polygon-lasso'].includes(selection?.kind)) applyLassoMask(context, selection, cut, originX, originY);
    else applyMarqueeMask(context, width, height, selection, cut, originX, originY);
  }

  function bitmapSelectionObject(object, selection, session) {
    const canvas = document.createElement('canvas'); canvas.width = selection.width; canvas.height = selection.height;
    const context = canvas.getContext('2d'); context.drawImage(session.canvas, selection.x, selection.y, selection.width, selection.height, 0, 0, selection.width, selection.height);
    applyBitmapSelectionMask(context, selection.width, selection.height, selection);
    const scaleX = num(object.width, 1) / session.width; const scaleY = num(object.height, 1) / session.height;
    const width = selection.width * scaleX; const height = selection.height * scaleY;
    let localX = (selection.x + selection.width / 2) * scaleX; let localY = (selection.y + selection.height / 2) * scaleY;
    if (object.flipX) localX = num(object.width, 1) - localX;
    if (object.flipY) localY = num(object.height, 1) - localY;
    const angle = num(object.rotation) * Math.PI / 180; const dx = localX - num(object.width, 1) / 2; const dy = localY - num(object.height, 1) / 2;
    const centerX = num(object.x) + num(object.width, 1) / 2 + Math.cos(angle) * dx - Math.sin(angle) * dy;
    const centerY = num(object.y) + num(object.height, 1) / 2 + Math.sin(angle) * dx + Math.cos(angle) * dy;
    return {
      // Nicht auf Dokumentpixel runden: Ein Bitmap kann skaliert sein. Schon eine
      // Rundung auf zwei Stellen ließ den eingefügten Ausschnitt bei hohen Zooms
      // sichtbar um einen Bildschirm-Pixel kleiner als den Marquee-Rahmen werden.
      id: uid('object'), type: 'image', name: 'Bitmap-Ausschnitt', x: centerX - width / 2, y: centerY - height / 2,
      width, height, rotation: num(object.rotation), opacity: num(object.opacity, 100), visible: true, locked: false,
      stroke: 'none', fill: 'none', strokeWidth: 0, scalePercent: 100, flipX: Boolean(object.flipX), flipY: Boolean(object.flipY),
      pixelWidth: selection.width, pixelHeight: selection.height, href: canvas.toDataURL('image/png'),
      bitmapMode: Math.abs(width - selection.width) < .001 && Math.abs(height - selection.height) < .001 ? 'direct' : 'placed',
      bitmapInterpolation: bitmapInterpolationFor(object), sourceDpiX: object.sourceDpiX, sourceDpiY: object.sourceDpiY,
      marqueeBleed: !selection.kind || selection.kind === 'marquee'
    };
  }

  async function copyBitmapMarquee(cut = false) {
    let selection = state.bitmapMarquee; let entry = objectEntry(selection?.objectId); let object = entry?.object;
    if (!selection || object?.type !== 'image' || entry.layer.locked || object.locked) return false;
    try {
      if (cut && bitmapModeFor(object) === 'placed') {
        if (!await ensureBitmapSelectionDirect(object, 'Ausschneiden in einem platzierten Bild')) return false;
        selection = state.bitmapMarquee; entry = objectEntry(selection?.objectId); object = entry?.object;
        if (!selection || object?.type !== 'image') return false;
      }
      const session = await bitmapEditSessionFor(object);
      const copy = bitmapSelectionObject(object, selection, session);
      state.clipboard = [copy]; state.sharedClipboard = [deepClone(copy)]; state.sharedClipboardResources = ReusableAssets.emptyLibrary(); writeObjectClipboard([copy], state.sharedClipboardResources);
      if (cut) {
        applyBitmapSelectionMask(session.context, selection.width, selection.height, selection, true, selection.x, selection.y);
        markBitmapSessionDirty(session);
        if (syncBitmapSession(session, { trimTransparent: true })) {
          session.dirty = false; commitHistory('Bitmapbereich ausgeschnitten'); renderAll();
          toast(`Bitmapbereich ${selection.width} × ${selection.height} px ausgeschnitten`);
        }
      } else {
        updateCommandState(); toast(`Bitmapbereich ${selection.width} × ${selection.height} px kopiert`);
      }
      return true;
    } catch {
      toast('Bitmapbereich konnte nicht in die Zwischenablage kopiert werden', 'error'); return false;
    }
  }

  async function deleteBitmapSelection() {
    let selection = state.bitmapMarquee; let entry = objectEntry(selection?.objectId); let object = entry?.object;
    if (!selection || object?.type !== 'image' || entry.layer.locked || object.locked) return false;
    try {
      if (bitmapModeFor(object) === 'placed') {
        if (!await ensureBitmapSelectionDirect(object, 'Löschen in einem platzierten Bild')) return false;
        selection = state.bitmapMarquee; entry = objectEntry(selection?.objectId); object = entry?.object;
        if (!selection || object?.type !== 'image') return false;
      }
      const session = await bitmapEditSessionFor(object);
      applyBitmapSelectionMask(session.context, selection.width, selection.height, selection, true, selection.x, selection.y);
      markBitmapSessionDirty(session);
      if (!syncBitmapSession(session, { trimTransparent: true })) return false;
      session.dirty = false; commitHistory(selection.kind === 'wand' ? 'Zauberstab-Auswahl gelöscht' : 'Bitmapbereich gelöscht'); renderAll();
      toast(selection.kind === 'wand' ? 'Zauberstab-Auswahl gelöscht' : 'Bitmapbereich gelöscht'); return true;
    } catch {
      toast('Bitmap-Auswahl konnte nicht gelöscht werden', 'error'); return false;
    }
  }

  async function copySelection(cut = false) {
    if (state.bitmapMarquee) { await copyBitmapMarquee(cut); return; }
    const copied = selectedObjects().map((object) => deepClone(object));
    if (!copied.length) return;
    const resources = ReusableAssets.referencedLibrary(copied, state.doc);
    state.clipboard = copied; state.sharedClipboard = copied.map(deepClone); state.sharedClipboardResources = deepClone(resources); writeObjectClipboard(copied, resources);
    toast(`${copied.length} ${copied.length === 1 ? 'Objekt' : 'Objekte'} ${cut ? 'ausgeschnitten' : 'kopiert'}`);
    if (cut) deleteSelection(); else updateCommandState();
  }

  function pasteText(text) {
    const content = String(text || '').replace(/\r/g, ''); if (!content.trim()) return false;
    const layer = activeLayer(); if (!layer || layer.locked || !layer.visible) { toast('Aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return false; }
    const object = objectBase('text', state.doc.width * .1, state.doc.height * .1);
    applyCurrentTextSettings(object); object.text = content; object.fill = $('fillColor').value; object.stroke = 'none'; object.strokeWidth = 0;
    syncTextObjectName(object);
    sizeTextObject(object); layer.objects.push(object); state.selected = [object.id]; commitHistory('Text aus Zwischenablage eingefügt'); renderAll(); return true;
  }

  async function pasteSelection() {
    const shared = state.sharedClipboard.length ? state.sharedClipboard : state.clipboard;
    if (shared.length) { pasteObjects(shared, 'Objekte eingefügt', true, state.sharedClipboardResources); return; }
    try {
      if (navigator.clipboard?.read) {
        const items = await navigator.clipboard.read();
        for (const item of items) {
          const imageType = item.types.find((type) => type.startsWith('image/'));
          if (imageType) { importImage(await item.getType(imageType)); return; }
          if (item.types.includes('text/plain')) {
            const text = await (await item.getType('text/plain')).text();
            const packageValue = readObjectClipboard(text); if (packageValue?.objects.length) pasteObjects(packageValue.objects, 'Objekte eingefügt', true, packageValue.resources); else pasteText(text);
            return;
          }
        }
      } else if (navigator.clipboard?.readText) {
        const text = await navigator.clipboard.readText(); const packageValue = readObjectClipboard(text); if (packageValue?.objects.length) pasteObjects(packageValue.objects, 'Objekte eingefügt', true, packageValue.resources); else pasteText(text);
        return;
      }
    } catch { toast('Zwischenablage konnte nicht gelesen werden', 'warning'); return; }
    toast('Zwischenablage ist leer oder enthält kein unterstütztes Format', 'warning');
  }

  function handleClipboardPaste(event) {
    const target = event.target;
    if (/INPUT|TEXTAREA|SELECT/.test(target?.tagName || '') || target?.isContentEditable) return;
    const clipboard = event.clipboardData; if (!clipboard) return;
    const imageItem = [...clipboard.items].find((item) => item.type.startsWith('image/'));
    if (imageItem) { event.preventDefault(); const file = imageItem.getAsFile(); if (file) importImage(file); return; }
    const text = clipboard.getData('text/plain'); if (!text) return;
    event.preventDefault(); const packageValue = readObjectClipboard(text);
    if (packageValue?.objects.length) pasteObjects(packageValue.objects, 'Objekte eingefügt', true, packageValue.resources); else pasteText(text);
  }

  function duplicateSelection() {
    const sources = selectedObjects().map(deepClone); if (!sources.length) return;
    pasteObjects(sources, 'Objekte dupliziert', false); updateCommandState();
  }

  function closedVectorContours(object) {
    if (!object) return [];
    if (outlineVectorTypes.has(object.type)) {
      const scale = outlineScale(object);
      return outlineContoursFor(object).map((contour) => contour.map(([x, y]) => [num(x) * scale.x, num(y) * scale.y]));
    }
    if (AutoShapes.isAutoShape(object)) return AutoShapes.contours(object);
    if (object.type === 'polygon' || (['pen', 'polyline'].includes(object.type) && object.closed)) return [sampledClosedShapePoints(object)];
    if (fillableVectorShapeTypes.has(object.type) && object.type !== 'pen' && object.type !== 'compound-path') {
      const contour = sampledClosedShapePoints(object); return contour.length >= 3 ? [contour] : [];
    }
    return [];
  }

  function closedVectorPaths(object) {
    if (!object) return [];
    if (outlineVectorTypes.has(object.type)) {
      const contours = outlineContoursFor(object); const bezierContours = outlineBezierContoursFor(object, contours); const scale = outlineScale(object);
      return contours.map((contour, contourIndex) => ({
        points: contour.map(([x, y]) => [round(x * scale.x, 5), round(y * scale.y, 5)]),
        segments: bezierContours[contourIndex].map((segment) => isBezierSegment(segment) ? {
          c1: [round(segment.c1[0] * scale.x, 5), round(segment.c1[1] * scale.y, 5)],
          c2: [round(segment.c2[0] * scale.x, 5), round(segment.c2[1] * scale.y, 5)]
        } : null), closed: true
      }));
    }
    if (['pen', 'polyline', 'polygon'].includes(object.type) && isClosedVectorPath(object)) {
      const points = vectorEditablePoints(object);
      return [{ points: deepClone(points), segments: Array.from({ length: points.length }, (_, index) => deepClone(object.bezierSegments?.[index] || null)), closed: true }];
    }
    if (object.type === 'rectangle' && object.svgTextPathSource) {
      const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
      const rx = clamp(num(object.svgRx), 0, width / 2); const ry = clamp(num(object.svgRy), 0, height / 2);
      if (!(rx > 0 && ry > 0)) return [{
        points: [[0, 0], [width, 0], [width, height], [0, height]], segments: [null, null, null, null], closed: true
      }];
      const kappa = .5522847498307936;
      return [{
        points: [[rx, 0], [width - rx, 0], [width, ry], [width, height - ry], [width - rx, height], [rx, height], [0, height - ry], [0, ry]],
        segments: [
          null,
          { c1: [width - rx + rx * kappa, 0], c2: [width, ry - ry * kappa] },
          null,
          { c1: [width, height - ry + ry * kappa], c2: [width - rx + rx * kappa, height] },
          null,
          { c1: [rx - rx * kappa, height], c2: [0, height - ry + ry * kappa] },
          null,
          { c1: [0, ry - ry * kappa], c2: [rx - rx * kappa, 0] }
        ],
        closed: true
      }];
    }
    if (object.type === 'ellipse') {
      const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1)); const kappa = .5522847498307936;
      if (object.svgTextPathSource) return [{
        points: [[width, height / 2], [width / 2, height], [0, height / 2], [width / 2, 0]],
        segments: [
          { c1: [width, height / 2 + height / 2 * kappa], c2: [width / 2 + width / 2 * kappa, height] },
          { c1: [width / 2 - width / 2 * kappa, height], c2: [0, height / 2 + height / 2 * kappa] },
          { c1: [0, height / 2 - height / 2 * kappa], c2: [width / 2 - width / 2 * kappa, 0] },
          { c1: [width / 2 + width / 2 * kappa, 0], c2: [width, height / 2 - height / 2 * kappa] }
        ],
        closed: true
      }];
      return [{
        points: [[width / 2, 0], [width, height / 2], [width / 2, height], [0, height / 2]],
        segments: [
          { c1: [width / 2 + width / 2 * kappa, 0], c2: [width, height / 2 - height / 2 * kappa] },
          { c1: [width, height / 2 + height / 2 * kappa], c2: [width / 2 + width / 2 * kappa, height] },
          { c1: [width / 2 - width / 2 * kappa, height], c2: [0, height / 2 + height / 2 * kappa] },
          { c1: [0, height / 2 - height / 2 * kappa], c2: [width / 2 - width / 2 * kappa, 0] }
        ], closed: true
      }];
    }
    return closedVectorContours(object).map((points) => ({ points: deepClone(points), segments: points.map(() => null), closed: true }));
  }

  function globalClosedVectorContours(object) {
    return closedVectorContours(object).map((contour) => contour.map(([x, y]) => {
      const point = objectLocalToDocument(object, { x, y }); return [round(point.x, 5), round(point.y, 5)];
    }));
  }

  function globalClosedVectorPaths(object) {
    const transform = ([x, y]) => {
      const point = objectLocalToDocument(object, { x, y }); return [round(point.x, 5), round(point.y, 5)];
    };
    return closedVectorPaths(object).map((path) => ({
      points: path.points.map(transform), closed: true,
      segments: path.segments.map((segment) => isBezierSegment(segment) ? { c1: transform(segment.c1), c2: transform(segment.c2) } : null)
    }));
  }

  function sampleStrokePointPath(object) {
    const points = vectorEditablePoints(object); if (points.length < 2) return [];
    const sampled = [deepClone(points[0])];
    for (let index = 0; index < vectorSegmentCount(object, points); index += 1) {
      const nextIndex = vectorNextPointIndex(object, index, points); if (nextIndex < 0) continue;
      const a = points[index]; const b = points[nextIndex]; const controls = bezierControlsFor(object, index, points);
      const curved = isBezierSegment(object?.bezierSegments?.[index]) && controls;
      const estimated = curved
        ? distanceBetweenPoints(a, controls.c1) + distanceBetweenPoints(controls.c1, controls.c2) + distanceBetweenPoints(controls.c2, b)
        : distanceBetweenPoints(a, b);
      const steps = curved ? clamp(Math.ceil(estimated / 4), 8, 96) : 1;
      for (let step = 1; step <= steps; step += 1) sampled.push(curved ? cubicPoint(a, controls.c1, controls.c2, b, step / steps) : deepClone(b));
    }
    if (isClosedVectorPath(object) && sampled.length > 2 && distanceBetweenPoints(sampled[0], sampled.at(-1)) < .001) sampled.pop();
    return sampled;
  }

  function sampleSvgStrokeGeometry(object, style) {
    const visual = shapeFor(object, false, style); const geometry = markerTargetFor(visual); if (!geometry?.getTotalLength) return [];
    const host = E('svg', { width: 1, height: 1, viewBox: `0 0 ${Math.max(1, num(object.width, 1))} ${Math.max(1, num(object.height, 1))}` });
    host.style.cssText = 'position:fixed;left:-10000px;top:-10000px;visibility:hidden;pointer-events:none'; host.append(visual); document.body.append(host);
    try {
      const length = geometry.getTotalLength(); if (!Number.isFinite(length) || length <= .001) return [];
      const steps = clamp(Math.ceil(length / 4), 2, 1024);
      return Array.from({ length: steps + 1 }, (_, index) => { const point = geometry.getPointAtLength(length * index / steps); return [round(point.x, 4), round(point.y, 4)]; });
    } catch { return []; }
    finally { host.remove(); }
  }

  function strokeCenterlinesFor(object, style) {
    if (!object) return [];
    if (outlineVectorTypes.has(object.type)) return closedVectorContours(object).map((points) => ({ points, closed: true }));
    if (pointVectorTypes.has(object.type) && object.type !== 'measure') {
      const points = sampleStrokePointPath(object); return points.length >= 2 ? [{ points, closed: isClosedVectorPath(object) }] : [];
    }
    const closed = closedVectorContours(object);
    if (closed.length) return closed.map((points) => ({ points, closed: true }));
    const sampled = sampleSvgStrokeGeometry(object, style);
    return sampled.length >= 2 ? [{ points: sampled, closed: false }] : [];
  }

  function strokeUnitSegment(a, b) {
    const dx = num(b?.[0]) - num(a?.[0]); const dy = num(b?.[1]) - num(a?.[1]); const length = Math.hypot(dx, dy);
    return length > 1e-7 ? { x: dx / length, y: dy / length, length } : null;
  }

  function strokePolylineSampler(rawPoints, closed = false) {
    const points = (rawPoints || []).filter((point, index) => index === 0 || distanceBetweenPoints(point, rawPoints[index - 1]) > 1e-6).map((point) => [num(point[0]), num(point[1])]);
    if (closed && points.length > 2 && distanceBetweenPoints(points[0], points.at(-1)) < 1e-6) points.pop();
    const count = closed ? points.length : points.length - 1; const segments = []; let total = 0;
    for (let index = 0; index < count; index += 1) {
      const start = points[index]; const end = points[(index + 1) % points.length]; const unit = strokeUnitSegment(start, end);
      if (!unit) continue;
      segments.push({ start, unit, from: total, to: total + unit.length }); total += unit.length;
    }
    const at = (rawDistance, offset = 0) => {
      if (!segments.length) return null;
      const distance = closed ? ((num(rawDistance) % total) + total) % total : clamp(num(rawDistance), 0, total);
      const segment = segments.find((entry) => distance <= entry.to + 1e-7) || segments.at(-1);
      const along = clamp(distance - segment.from, 0, segment.unit.length);
      return [
        segment.start[0] + segment.unit.x * along - segment.unit.y * offset,
        segment.start[1] + segment.unit.y * along + segment.unit.x * offset
      ];
    };
    return { points, total, at };
  }

  function zigzagStrokePoints(rawPoints, closed, rawStyle) {
    const sampler = strokePolylineSampler(rawPoints, closed); if (sampler.total <= 1e-6) return [];
    const { amplitude, spacing } = StrokeStyle.zigzagMetrics(rawStyle);
    if (closed) {
      let vertices = clamp(Math.round(sampler.total / spacing), 4, 2048); if (vertices % 2) vertices += 1;
      return Array.from({ length: vertices }, (_, index) => sampler.at(sampler.total * index / vertices, index % 2 ? -amplitude : amplitude));
    }
    const interior = clamp(Math.round(sampler.total / spacing), 1, 2046); const points = [sampler.at(0)];
    for (let index = 1; index <= interior; index += 1) points.push(sampler.at(sampler.total * index / (interior + 1), index % 2 ? amplitude : -amplitude));
    points.push(sampler.at(sampler.total)); return points;
  }

  function strokeLineIntersection(a, directionA, b, directionB) {
    const denominator = directionA.x * directionB.y - directionA.y * directionB.x;
    if (Math.abs(denominator) < 1e-7) return null;
    const dx = b[0] - a[0]; const dy = b[1] - a[1]; const t = (dx * directionB.y - dy * directionB.x) / denominator;
    return [a[0] + directionA.x * t, a[1] + directionA.y * t];
  }

  function strokeArc(center, from, to, steps = 8, forcedDelta = null) {
    const radius = Math.hypot(from[0] - center[0], from[1] - center[1]); if (radius < 1e-7) return [deepClone(center)];
    const start = Math.atan2(from[1] - center[1], from[0] - center[0]); let delta = forcedDelta;
    if (delta == null) {
      delta = Math.atan2(to[1] - center[1], to[0] - center[0]) - start;
      while (delta <= -Math.PI) delta += Math.PI * 2; while (delta > Math.PI) delta -= Math.PI * 2;
    }
    return Array.from({ length: steps + 1 }, (_, index) => [center[0] + Math.cos(start + delta * index / steps) * radius, center[1] + Math.sin(start + delta * index / steps) * radius]);
  }

  function strokeJoinPoints(points, index, side, distance, closed, style) {
    const count = points.length; const point = points[index]; if (distance <= 1e-7) return [deepClone(point)];
    const previous = index > 0 ? points[index - 1] : closed ? points.at(-1) : null;
    const next = index + 1 < count ? points[index + 1] : closed ? points[0] : null;
    if (!previous || !next) {
      const segment = strokeUnitSegment(previous || point, next || point); if (!segment) return [deepClone(point)];
      return [[point[0] - segment.y * side * distance, point[1] + segment.x * side * distance]];
    }
    const incoming = strokeUnitSegment(previous, point); const outgoing = strokeUnitSegment(point, next); if (!incoming || !outgoing) return [deepClone(point)];
    const from = [point[0] - incoming.y * side * distance, point[1] + incoming.x * side * distance];
    const to = [point[0] - outgoing.y * side * distance, point[1] + outgoing.x * side * distance];
    const intersection = strokeLineIntersection(from, incoming, to, outgoing);
    const turn = incoming.x * outgoing.y - incoming.y * outgoing.x; const outside = turn * side < 0;
    if (!outside && intersection) return [intersection];
    if (style.lineJoin === 'round') {
      const angle = Math.acos(clamp(incoming.x * outgoing.x + incoming.y * outgoing.y, -1, 1));
      return strokeArc(point, from, to, clamp(Math.ceil(angle / (Math.PI / 12)), 2, 16));
    }
    if (style.lineJoin === 'miter' && intersection && Math.hypot(intersection[0] - point[0], intersection[1] - point[1]) <= distance * style.miterLimit) return [intersection];
    return [from, to];
  }

  function strokeSignedArea(points) {
    return points.reduce((sum, point, index) => { const next = points[(index + 1) % points.length]; return sum + point[0] * next[1] - next[0] * point[1]; }, 0) / 2;
  }

  function strokeOutlineForPolyline(rawPoints, rawStyle, closed = false) {
    const style = StrokeStyle.normalizeStyle(rawStyle); const points = rawPoints.filter((point, index) => index === 0 || distanceBetweenPoints(point, rawPoints[index - 1]) > 1e-6);
    if (points.length < 2 || style.width <= 0) return [];
    let leftDistance = style.width / 2; let rightDistance = style.width / 2;
    if (closed && style.alignment !== 'center') {
      const clockwise = strokeSignedArea(points) > 0; const insideSide = clockwise ? -1 : 1;
      if (style.alignment === 'inside') { leftDistance = insideSide === 1 ? style.width : 0; rightDistance = insideSide === -1 ? style.width : 0; }
      else { leftDistance = insideSide === 1 ? 0 : style.width; rightDistance = insideSide === -1 ? 0 : style.width; }
    }
    const left = points.flatMap((point, index) => strokeJoinPoints(points, index, 1, leftDistance, closed, style));
    const right = points.flatMap((point, index) => strokeJoinPoints(points, index, -1, rightDistance, closed, style));
    if (closed) return [left, right.reverse()].filter((contour) => contour.length >= 3);
    const firstDirection = strokeUnitSegment(points[0], points[1]); const lastDirection = strokeUnitSegment(points.at(-2), points.at(-1));
    if (!firstDirection || !lastDirection) return [];
    if (style.lineCap === 'square') {
      [left[0], right[0]].forEach((point) => { point[0] -= firstDirection.x * style.width / 2; point[1] -= firstDirection.y * style.width / 2; });
      [left.at(-1), right.at(-1)].forEach((point) => { point[0] += lastDirection.x * style.width / 2; point[1] += lastDirection.y * style.width / 2; });
    }
    const contour = [...left];
    if (style.lineCap === 'round') contour.push(...strokeArc(points.at(-1), left.at(-1), right.at(-1), 12, -Math.PI).slice(1));
    else contour.push(right.at(-1));
    contour.push(...right.slice(0, -1).reverse());
    if (style.lineCap === 'round') contour.push(...strokeArc(points[0], right[0], left[0], 12, -Math.PI).slice(1));
    else contour.push(left[0]);
    return contour.length >= 3 ? [contour] : [];
  }

  function splitStrokeDashes(points, closed, style) {
    const pattern = StrokeStyle.renderedDashArray(style); if (!pattern.length) return [{ points, closed }];
    const values = pattern.length % 2 ? [...pattern, ...pattern] : pattern; const total = values.reduce((sum, value) => sum + value, 0); if (total <= .001) return [{ points, closed }];
    let offset = ((num(style.dashOffset) % total) + total) % total; let patternIndex = 0;
    while (offset >= values[patternIndex] && values[patternIndex] > 0) { offset -= values[patternIndex]; patternIndex = (patternIndex + 1) % values.length; }
    let remaining = values[patternIndex] - offset; let drawing = patternIndex % 2 === 0; const segments = []; let current = [];
    const count = closed ? points.length : points.length - 1;
    for (let index = 0; index < count; index += 1) {
      const a = points[index]; const b = points[(index + 1) % points.length]; const segment = strokeUnitSegment(a, b); if (!segment) continue;
      let position = 0;
      while (position < segment.length - 1e-7) {
        const step = Math.min(remaining, segment.length - position); const from = [a[0] + segment.x * position, a[1] + segment.y * position]; const to = [a[0] + segment.x * (position + step), a[1] + segment.y * (position + step)];
        if (drawing) { if (!current.length) current.push(from); current.push(to); }
        position += step; remaining -= step;
        if (remaining <= 1e-7) {
          if (drawing && current.length >= 2) { segments.push({ points: current, closed: false }); current = []; }
          patternIndex = (patternIndex + 1) % values.length; drawing = patternIndex % 2 === 0; remaining = values[patternIndex];
        }
      }
    }
    if (drawing && current.length >= 2) segments.push({ points: current, closed: false });
    return segments;
  }

  function strokeMarkerContours(points, style, edge) {
    const marker = edge === 'start' ? style.startMarker : style.endMarker; if (marker === 'none' || points.length < 2) return [];
    const tip = edge === 'start' ? points[0] : points.at(-1); const adjacent = edge === 'start' ? points[1] : points.at(-2); const direction = strokeUnitSegment(adjacent, tip); if (!direction) return [];
    const size = edge === 'start' ? style.startMarkerSize : style.endMarkerSize; const normal = { x: -direction.y, y: direction.x };
    const point = (back, side = 0) => [tip[0] - direction.x * back + normal.x * side, tip[1] - direction.y * back + normal.y * side];
    if (marker === 'triangle') return [[tip, point(size, size * .48), point(size, -size * .48)]];
    if (marker === 'diamond') return [[tip, point(size * .5, size * .45), point(size, 0), point(size * .5, -size * .45)]];
    if (marker === 'circle') {
      const center = point(size * .4); const radius = size * .38;
      return [Array.from({ length: 24 }, (_, index) => [center[0] + Math.cos(index * Math.PI * 2 / 24) * radius, center[1] + Math.sin(index * Math.PI * 2 / 24) * radius])];
    }
    const markerStroke = { ...style, width: Math.max(1, Math.min(style.width, size * .16)), lineCap: marker === 'bar' ? 'butt' : 'round', alignment: 'center', pattern: 'solid' };
    if (marker === 'bar') return strokeOutlineForPolyline([point(size * .12, size * .5), point(size * .12, -size * .5)], markerStroke, false);
    return [
      ...strokeOutlineForPolyline([point(size, size * .48), tip], markerStroke, false),
      ...strokeOutlineForPolyline([tip, point(size, -size * .48)], markerStroke, false)
    ];
  }

  function activeStrokeOutlineContours(object, rawStyle) {
    let style = StrokeStyle.normalizeStyle(rawStyle, { fallback: StrokeStyle.legacyStyle(object) });
    if (object.type === 'arrow') {
      const params = AutoShapes.ensure(object); const markerName = (value) => value === 'open' ? 'open-arrow' : StrokeStyle.markers.includes(value) ? value : 'none';
      style = { ...style, startMarker: style.startMarker === 'none' ? markerName(params.startHead) : style.startMarker, endMarker: style.endMarker === 'none' ? markerName(params.endHead) : style.endMarker, startMarkerSize: style.startMarker === 'none' ? params.headSize : style.startMarkerSize, endMarkerSize: style.endMarker === 'none' ? params.headSize : style.endMarkerSize };
    }
    const centerlines = strokeCenterlinesFor(object, style); const result = [];
    centerlines.forEach((centerline) => {
      const segments = style.pattern === 'zigzag'
        ? [{ points: zigzagStrokePoints(centerline.points, centerline.closed, style), closed: centerline.closed }]
        : splitStrokeDashes(centerline.points, centerline.closed, style);
      segments.forEach((segment) => result.push(...strokeOutlineForPolyline(segment.points, style, segment.closed)));
      if (!centerline.closed) { result.push(...strokeMarkerContours(centerline.points, style, 'start')); result.push(...strokeMarkerContours(centerline.points, style, 'end')); }
    });
    return PathOperations.normalizeContours(result);
  }

  function canConvertStrokeToPath(object) {
    return Boolean(object && (isVectorObject(object) || object.type === 'text') && object.type !== 'measure');
  }

  async function convertActiveStrokeToPath() {
    const context = activeStrokeContext(primaryObject(), true); if (!context || !StrokeStyle.isVisible(context.style) || !canConvertStrokeToPath(context.object)) {
      toast('Wähle eine sichtbare Vektorkontur aus', 'warning'); return false;
    }
    const sourceObject = context.object; const entry = objectEntry(sourceObject.id); if (!entry) return false;
    const button = $('convertStrokeToPathBtn'); const previousLabel = button.textContent; button.disabled = true; button.textContent = 'Wandle um …';
    try {
      const geometryObject = sourceObject.type === 'text' ? await textOutlineFromObject(sourceObject) : sourceObject;
      const localContours = activeStrokeOutlineContours(geometryObject, context.style);
      const matrix = Core.objectMatrix(geometryObject); const contours = localContours.map((contour) => contour.map(([x, y]) => {
        const point = Core.applyMatrix(matrix, { x, y }); return [round(point.x, 4), round(point.y, 4)];
      }));
      const bounds = PathOperations.contourBounds(contours);
      if (!contours.length || bounds.width <= .0001 || bounds.height <= .0001) throw new Error('Aus dieser Kontur konnte kein geschlossener Pfad erzeugt werden');
      const outline = objectBase('compound-path', bounds.x, bounds.y); outline.name = `${sourceObject.name || names[sourceObject.type] || 'Objekt'} – Konturpfad`;
      outline.width = Math.max(.1, round(bounds.width, 4)); outline.height = Math.max(.1, round(bounds.height, 4)); outline.pathWidth = outline.width; outline.pathHeight = outline.height;
      outline.outlineContours = contours.map((contour) => contour.map(([x, y]) => [round(x - bounds.x, 4), round(y - bounds.y, 4)])); outline.pathData = outlinePathData(outline.outlineContours);
      outline.fill = context.style.color; outline.stroke = 'none'; outline.strokeWidth = 0; outline.strokeType = 'none'; outline.opacity = num(sourceObject.opacity, 100); delete outline.strokeStack;
      const sourceStack = context.stack; sourceStack[context.index] = { ...sourceStack[context.index], enabled: false };
      setObjectStrokeStack(sourceObject, sourceStack);
      const hasFill = Boolean(normalizedObjectFillPaint(sourceObject)) || !['none', 'transparent', ''].includes(String(sourceObject.fill || 'none').toLowerCase());
      const hasOtherStrokes = strokeStackFor(sourceObject).some(StrokeStyle.isVisible);
      if (!hasFill && !hasOtherStrokes) {
        if (sourceObject.effects) outline.effects = deepClone(effectStack(sourceObject));
        entry.container.splice(entry.index, 1, outline);
      } else entry.container.splice(entry.index + 1, 0, outline);
      state.selected = [outline.id]; strokeSelection = { objectId: null, strokeId: null }; clearVectorSubselection();
      commitHistory('Kontur in Pfad umgewandelt'); renderAll(); toast('Kontur ist jetzt ein vollständig editierbarer Pfad'); return true;
    } catch (error) { toast(error?.message || 'Kontur konnte nicht in einen Pfad umgewandelt werden', 'warning'); return false; }
    finally { button.disabled = false; button.textContent = previousLabel; }
  }

  function isClosedVectorObject(object) {
    return globalClosedVectorContours(object).some((contour) => contour.length >= 3);
  }

  function selectedTopLevelMembers() {
    const objects = selectedObjects();
    if (!objects.length || objects.some((object) => objectEntry(object.id)?.parent)) return [];
    const layer = layerForObject(objects[0].id);
    if (!layer || layer.locked || !layer.visible || objects.some((object) => layerForObject(object.id) !== layer)) return [];
    const ids = new Set(objects.map((object) => object.id));
    return layer.objects.filter((object) => ids.has(object.id));
  }

  function canBooleanSelection() {
    const members = selectedTopLevelMembers();
    return members.length >= 2 && members.every(isClosedVectorObject);
  }

  function pathGeometryBounds(paths) {
    return PathOperations.contourBounds((paths || []).map((path) => PathOperations.flattenPathContour(path, { tolerance: .2 }).contour));
  }

  function applyGlobalPathGeometry(object, paths, name = object.name) {
    const normalized = (paths || []).map(PathOperations.normalizePathContour).filter((path) => path.points.length >= 3); const bounds = pathGeometryBounds(normalized);
    if (!normalized.length || bounds.width <= .0001 || bounds.height <= .0001) return false;
    object.type = 'compound-path'; object.name = name; object.x = round(bounds.x, 4); object.y = round(bounds.y, 4);
    object.width = Math.max(.1, round(bounds.width, 4)); object.height = Math.max(.1, round(bounds.height, 4)); object.pathWidth = object.width; object.pathHeight = object.height;
    object.rotation = 0; object.flipX = false; object.flipY = false; object.scalePercent = 100; object.closed = true;
    object.outlineContours = normalized.map((path) => path.points.map(([x, y]) => [round(x - bounds.x, 4), round(y - bounds.y, 4)]));
    object.outlineBezierContours = normalized.map((path) => path.segments.map((segment) => isBezierSegment(segment) ? {
      c1: [round(segment.c1[0] - bounds.x, 4), round(segment.c1[1] - bounds.y, 4)],
      c2: [round(segment.c2[0] - bounds.x, 4), round(segment.c2[1] - bounds.y, 4)]
    } : null));
    if (!object.outlineBezierContours.some((segments) => segments.some(isBezierSegment))) delete object.outlineBezierContours;
    delete object.points; delete object.rawPoints; delete object.bezierSegments; delete object.pointModes; delete object.sides; delete object.autoShape;
    syncOutlinePathData(object); return true;
  }

  function copyVectorAppearance(target, source) {
    ['fill', 'fillPaint', 'stroke', 'strokePaint', 'strokeWidth', 'strokeType', 'strokeStack', 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeDashArray', 'strokeDashOffset', 'effects', 'svgFilterSettings', 'opacity', 'blendMode']
      .forEach((property) => {
        if (Object.prototype.hasOwnProperty.call(source || {}, property)) target[property] = deepClone(source[property]); else delete target[property];
      });
    return target;
  }

  function offsetSelectedVectorPaths(direction = 1) {
    const members = selectedTopLevelMembers().filter(isClosedVectorObject); const distance = clamp(num($('vectorOffsetDistance')?.value, 10), .1, 1000);
    if ($('vectorOffsetDistance')) $('vectorOffsetDistance').value = String(distance);
    if (!members.length) { toast('Wähle mindestens einen geschlossenen Vektorpfad aus', 'warning'); return false; }
    const selected = []; let changed = 0; let collapsed = 0;
    members.forEach((object) => {
      const contours = globalClosedVectorContours(object); const offset = PathOperations.offsetContours(contours, distance * (direction >= 0 ? 1 : -1), { lineJoin: 'round', miterLimit: 8, minArea: .0025 });
      if (!offset.length) { collapsed += 1; return; }
      const paths = offset.map((points) => ({ points, segments: points.map(() => null), closed: true }));
      if (applyGlobalPathGeometry(object, paths, `${object.name || 'Pfad'} – Offset ${direction >= 0 ? '+' : '−'}${distance}px`)) { selected.push(object.id); changed += 1; }
    });
    if (!changed) { toast('Der Innenoffset lässt diese Pfade vollständig zusammenfallen', 'warning'); return false; }
    state.selected = selected; clearVectorSubselection(); commitHistory(direction >= 0 ? 'Pfade vergrößert' : 'Pfade verkleinert'); renderAll();
    toast(`${changed} ${changed === 1 ? 'Pfad' : 'Pfade'} um ${distance}px ${direction >= 0 ? 'vergrößert' : 'verkleinert'}${collapsed ? ` · ${collapsed} zusammengefallen` : ''}`); return true;
  }

  function booleanSelection(operation) {
    const members = selectedTopLevelMembers();
    if (members.length < 2 || !members.every(isClosedVectorObject)) {
      toast('Wähle mindestens zwei geschlossene Vektorobjekte derselben Ebene aus', 'warning'); return false;
    }
    const labels = { union: 'Vereinen', subtract: 'Subtrahieren', intersection: 'Schnittmenge', exclude: 'Ausschluss' };
    const paths = PathOperations.combinePathSets(members.map(globalClosedVectorPaths), operation, { tolerance: .22, minArea: .0025 });
    const bounds = pathGeometryBounds(paths);
    if (!paths.length || bounds.width <= .0001 || bounds.height <= .0001) {
      toast('Die gewählte Pfadoperation ergibt keine sichtbare Fläche', 'warning'); return false;
    }
    const layer = layerForObject(members[0].id); const ids = new Set(members.map((object) => object.id));
    const firstIndex = layer.objects.findIndex((object) => ids.has(object.id)); const style = members[0];
    const object = objectBase('compound-path', bounds.x, bounds.y);
    applyGlobalPathGeometry(object, paths, `${labels[operation] || 'Boolescher Pfad'} (${members.length})`); copyVectorAppearance(object, style);
    layer.objects = layer.objects.filter((entry) => !ids.has(entry.id)); layer.objects.splice(firstIndex, 0, object);
    state.selected = [object.id]; clearVectorSubselection(); commitHistory(`Pfade: ${labels[operation]}`); renderAll();
    toast(`${members.length} Vektorflächen: ${labels[operation]}`); return true;
  }

  function maskChildFor(group) {
    return group?.type === 'mask-group' ? (group.children || []).find((child) => child.maskRole === 'mask') || null : null;
  }

  function clipChildFor(group) {
    return group?.type === 'clip-group' ? (group.children || []).find((child) => child.clipRole === 'clip') || null : null;
  }

  function canCreateVectorMask() {
    const members = selectedTopLevelMembers();
    return members.length >= 2 && isClosedVectorObject(members.at(-1));
  }

  function createVectorMask() {
    const members = selectedTopLevelMembers();
    if (members.length < 2) { toast('Wähle Inhalt und ein vorderes Maskenobjekt derselben Ebene aus', 'warning'); return false; }
    const maskSource = members.at(-1);
    if (!isClosedVectorObject(maskSource)) { toast('Das vorderste Objekt muss ein geschlossener Vektor sein', 'warning'); return false; }
    const contents = members.slice(0, -1); const maskBounds = boundsOf([maskSource]); const contentBounds = boundsOf(contents);
    const left = Math.max(maskBounds.x, contentBounds.x); const top = Math.max(maskBounds.y, contentBounds.y);
    const right = Math.min(maskBounds.right, contentBounds.right); const bottom = Math.min(maskBounds.bottom, contentBounds.bottom);
    if (right <= left || bottom <= top) { toast('Maske und Inhalt überlappen sich nicht', 'warning'); return false; }
    const layer = layerForObject(members[0].id); const ids = new Set(members.map((object) => object.id));
    const firstIndex = layer.objects.findIndex((object) => ids.has(object.id)); const group = objectBase('mask-group', left, top);
    group.name = `Vektormaske (${contents.length})`; group.width = Math.max(.1, round(right - left)); group.height = Math.max(.1, round(bottom - top));
    group.stroke = 'none'; group.fill = 'none'; group.strokeWidth = 0; group.opacity = 100; group.maskInverted = false;
    group.children = members.map((object) => {
      const copy = deepClone(object); copy.x = round(num(copy.x) - left); copy.y = round(num(copy.y) - top);
      if (copy.id === maskSource.id) copy.maskRole = 'mask';
      else delete copy.maskRole;
      return copy;
    });
    layer.objects = layer.objects.filter((object) => !ids.has(object.id)); layer.objects.splice(firstIndex, 0, group);
    state.selected = [group.id]; clearVectorSubselection(); commitHistory('Vektormaske erstellt'); renderAll();
    toast('Vektormaske erstellt – die Originalobjekte bleiben lösbar'); return true;
  }

  function releaseVectorMask() {
    const entry = objectEntry(state.selected.length === 1 ? state.selected[0] : ''); const group = entry?.object;
    if (!entry || entry.layer.locked || group?.type !== 'mask-group' || !Array.isArray(group.children)) {
      toast('Wähle eine Vektormaske aus', 'warning'); return false;
    }
    const children = group.children.map((child) => {
      const copy = releaseGroupChild(child, group); delete copy.maskRole; return copy;
    });
    entry.layer.objects.splice(entry.index, 1, ...children); state.selected = children.map((child) => child.id);
    commitHistory('Vektormaske gelöst'); renderAll(); toast(`Maske in ${children.length} Objekte aufgelöst`); return true;
  }

  function canCreateClippingPath() {
    const members = selectedTopLevelMembers();
    return members.length >= 2 && isClosedVectorObject(members.at(-1));
  }

  function createClippingPath() {
    const members = selectedTopLevelMembers();
    if (members.length < 2) { toast('Wähle Inhalt und einen vorderen Clipping-Pfad derselben Ebene aus', 'warning'); return false; }
    const clipSource = members.at(-1);
    if (!isClosedVectorObject(clipSource)) { toast('Das vorderste Objekt muss ein geschlossener Vektor sein', 'warning'); return false; }
    const contents = members.slice(0, -1); const clipBounds = boundsOf([clipSource]); const contentBounds = boundsOf(contents);
    const left = Math.max(clipBounds.x, contentBounds.x); const top = Math.max(clipBounds.y, contentBounds.y);
    const right = Math.min(clipBounds.right, contentBounds.right); const bottom = Math.min(clipBounds.bottom, contentBounds.bottom);
    if (right <= left || bottom <= top) { toast('Clipping-Pfad und Inhalt überlappen sich nicht', 'warning'); return false; }
    const layer = layerForObject(members[0].id); const ids = new Set(members.map((object) => object.id));
    const firstIndex = layer.objects.findIndex((object) => ids.has(object.id)); const group = objectBase('clip-group', left, top);
    group.name = `Clipping-Gruppe (${contents.length})`; group.width = Math.max(.1, round(right - left)); group.height = Math.max(.1, round(bottom - top));
    group.stroke = 'none'; group.fill = 'none'; group.strokeWidth = 0; group.opacity = 100;
    group.children = members.map((object) => {
      const copy = deepClone(object); copy.x = round(num(copy.x) - left); copy.y = round(num(copy.y) - top);
      delete copy.maskRole;
      if (copy.id === clipSource.id) copy.clipRole = 'clip';
      else delete copy.clipRole;
      return copy;
    });
    layer.objects = layer.objects.filter((object) => !ids.has(object.id)); layer.objects.splice(firstIndex, 0, group);
    state.selected = [group.id]; clearVectorSubselection(); commitHistory('Clipping-Pfad erstellt'); renderAll();
    toast('Clipping-Pfad erstellt – die Originalobjekte bleiben lösbar'); return true;
  }

  function releaseClippingPath() {
    const entry = objectEntry(state.selected.length === 1 ? state.selected[0] : ''); const group = entry?.object;
    if (!entry || entry.layer.locked || group?.type !== 'clip-group' || !Array.isArray(group.children)) {
      toast('Wähle eine Clipping-Gruppe aus', 'warning'); return false;
    }
    const children = group.children.map((child) => {
      const copy = releaseGroupChild(child, group); delete copy.clipRole; return copy;
    });
    entry.layer.objects.splice(entry.index, 1, ...children); state.selected = children.map((child) => child.id);
    commitHistory('Clipping-Pfad gelöst'); renderAll(); toast(`Clipping-Gruppe in ${children.length} Objekte aufgelöst`); return true;
  }

  function invertVectorMask() {
    const group = state.selected.length === 1 ? primaryObject() : null;
    if (group?.type !== 'mask-group' || !maskChildFor(group)) { toast('Wähle eine Vektormaske aus', 'warning'); return false; }
    group.maskInverted = !Boolean(group.maskInverted); commitHistory('Vektormaske umgekehrt'); renderAll();
    toast(group.maskInverted ? 'Vektormaske umgekehrt' : 'Vektormaske wieder normal'); return true;
  }

  function appendMaskGroupContents(target, group) {
    const maskSource = maskChildFor(group); if (!maskSource) return;
    const children = (group.children || []).filter((child) => child !== maskSource);
    const extent = boundsOf(group.children || []); const padding = Math.max(8, num(group.strokeWidth, 0) * 2);
    const storedRegion = group.maskRegion && num(group.maskRegion.width) > 0 && num(group.maskRegion.height) > 0 ? group.maskRegion : null;
    const region = storedRegion || {
      x: extent.x - padding, y: extent.y - padding,
      width: Math.max(1, extent.width + padding * 2), height: Math.max(1, extent.height + padding * 2)
    };
    const definitionId = `vector-mask-${group.id}`; const definitions = E('defs');
    const maskType = group.maskType === 'alpha' ? 'alpha' : 'luminance';
    const definition = E('mask', {
      id: definitionId, maskUnits: 'userSpaceOnUse', maskContentUnits: 'userSpaceOnUse',
      x: region.x, y: region.y, width: region.width, height: region.height,
      style: `mask-type:${maskType}`, 'data-imported-mask-appearance': group.maskUsesAppearance ? 'true' : undefined
    });
    const inverted = Boolean(group.maskInverted); const background = inverted ? '#ffffff' : '#000000'; const foreground = inverted ? '#000000' : '#ffffff';
    definition.append(E('rect', {
      x: region.x, y: region.y, width: region.width, height: region.height, fill: background,
      'fill-opacity': maskType === 'alpha' ? (inverted ? 1 : 0) : 1
    }));
    const prepare = (source) => {
      const copy = deepClone(source);
      const visit = (object) => {
        object.visible = true; object.locked = false;
        if (!group.maskUsesAppearance) {
          object.opacity = 100; object.fill = foreground; object.stroke = foreground; object.textStroke = foreground;
          delete object.fillPaint; delete object.strokePaint; object.effects = []; delete object.glow;
        }
        (object.children || []).forEach(visit);
      };
      visit(copy); return copy;
    };
    definition.append(objectGroup(prepare(maskSource), false)); definitions.append(definition);
    const content = E('g', { mask: `url(#${definitionId})`, 'data-vector-mask-content': 'true' });
    children.forEach((child) => content.append(objectGroup(child, false)));
    target.append(definitions, content);
  }

  function appendClipGroupContents(target, group) {
    const clipSource = clipChildFor(group); if (!clipSource) return;
    const children = (group.children || []).filter((child) => child !== clipSource);
    const definitionId = `vector-clip-${group.id}`; const definitions = E('defs');
    const definition = E('clipPath', { id: definitionId, clipPathUnits: 'userSpaceOnUse', 'data-vector-clip-definition': 'true' });
    const clipCopy = deepClone(clipSource);
    const prepare = (object) => {
      object.visible = true; object.locked = false; object.opacity = 100;
      object.fill = '#000000'; object.fillPaint = null; object.stroke = 'none'; object.textStroke = 'none'; object.strokeWidth = 0; object.textStrokeWidth = 0;
      delete object.strokePaint; object.effects = []; delete object.glow; delete object.glowOnly; delete object.blendMode;
      (object.children || []).forEach(prepare);
    };
    const matrixText = (matrix) => `matrix(${matrix.map((value) => round(value, 8)).join(' ')})`;
    const appendGeometry = (container, object, parentMatrix = [1, 0, 0, 1, 0, 0], inheritedClip = '') => {
      const matrix = Core.multiplyMatrices(parentMatrix, Core.objectMatrix(object));
      if (object.type === 'group') {
        (object.children || []).forEach((child) => appendGeometry(container, child, matrix, inheritedClip)); return;
      }
      if (object.type === 'clip-group') {
        const nestedSource = clipChildFor(object); if (!nestedSource) return;
        const nestedId = `${definitionId}-nested-${String(object.id || uid('clip')).replace(/[^a-z0-9_-]/gi, '-')}`;
        const nested = E('clipPath', { id: nestedId, clipPathUnits: 'userSpaceOnUse', 'data-vector-clip-definition': 'nested' });
        appendGeometry(nested, nestedSource, matrix); definitions.append(nested);
        (object.children || []).filter((child) => child !== nestedSource).forEach((child) => appendGeometry(container, child, matrix, nestedId));
        return;
      }
      if (object.type === 'mask-group') {
        const maskSource = maskChildFor(object);
        (object.children || []).filter((child) => child !== maskSource).forEach((child) => appendGeometry(container, child, matrix, inheritedClip));
        return;
      }
      const shape = shapeFor(object, false); const intrinsicTransform = shape.getAttribute('transform');
      shape.setAttribute('transform', intrinsicTransform ? `${matrixText(matrix)} ${intrinsicTransform}` : matrixText(matrix));
      if (inheritedClip) shape.setAttribute('clip-path', `url(#${inheritedClip})`);
      container.append(shape);
    };
    prepare(clipCopy); appendGeometry(definition, clipCopy); definitions.append(definition);
    const content = E('g', { 'clip-path': `url(#${definitionId})`, 'data-vector-clip-content': 'true' });
    children.forEach((child) => content.append(objectGroup(child, false)));
    target.append(definitions, content);
  }

  function canGroupSelection() {
    const objects = selectedObjects();
    if (objects.length < 2) return false;
    const layer = layerForObject(objects[0].id);
    return Boolean(layer && !layer.locked && layer.visible && objects.every((object) => layerForObject(object.id) === layer));
  }

  function groupSelection() {
    if (!canGroupSelection()) { toast('Wähle mindestens zwei Objekte derselben Ebene aus', 'warning'); return; }
    const selected = selectedObjects(); const layer = layerForObject(selected[0].id); const ids = new Set(selected.map((object) => object.id));
    const members = layer.objects.filter((object) => ids.has(object.id)); const firstIndex = layer.objects.findIndex((object) => ids.has(object.id));
    const bounds = boundsOf(members); const group = objectBase('group', bounds.x, bounds.y);
    group.name = `Gruppe (${members.length})`; group.width = Math.max(1, round(bounds.width)); group.height = Math.max(1, round(bounds.height));
    group.stroke = 'none'; group.fill = 'none'; group.strokeWidth = 0; group.opacity = 100;
    group.children = members.map((object) => ({ ...deepClone(object), x: round(num(object.x) - bounds.x), y: round(num(object.y) - bounds.y) }));
    layer.objects = layer.objects.filter((object) => !ids.has(object.id)); layer.objects.splice(firstIndex, 0, group);
    state.selected = [group.id]; commitHistory('Objekte gruppiert'); renderAll(); toast(`${members.length} Objekte gruppiert`);
  }

  function releaseGroupChild(child, group) {
    const copy = deepClone(child); const transform = Core.composeBoxTransforms(group, child);
    copy.x = round(transform.x); copy.y = round(transform.y);
    copy.rotation = round(transform.rotation, 1); copy.flipX = transform.flipX; copy.flipY = transform.flipY;
    if (transform.svgTransformMatrix) copy.svgTransformMatrix = transform.svgTransformMatrix.map((value) => round(value, 8));
    else delete copy.svgTransformMatrix;
    copy.opacity = round(clamp(num(copy.opacity, 100) * num(group.opacity, 100) / 100, 0, 100));
    return copy;
  }

  function releaseSelectedGroups(recursive = false) {
    const groupIds = selectedObjects().filter((object) => object.type === 'group').map((object) => object.id);
    if (!groupIds.length) { toast('Wähle mindestens eine Gruppe zum Aufheben aus', 'warning'); return false; }
    const groupIdSet = new Set(groupIds); const preservedSelection = state.selected.filter((id) => !groupIdSet.has(id));
    const queue = [...groupIds]; const processed = new Set(); const releasedSelection = [];
    let releasedGroups = 0;
    while (queue.length) {
      const id = queue.shift();
      if (processed.has(id)) continue;
      const entry = objectEntry(id); const group = entry?.object;
      if (!entry || entry.layer.locked || group?.type !== 'group' || !Array.isArray(group.children)) continue;
      const children = group.children.map((child) => releaseGroupChild(child, group));
      entry.container.splice(entry.index, 1, ...children); processed.add(id); releasedGroups += 1;
      children.forEach((child) => {
        if (recursive && child.type === 'group') queue.push(child.id);
        else releasedSelection.push(child.id);
      });
    }
    if (!releasedGroups) { toast('Die ausgewählten Gruppen konnten nicht aufgehoben werden', 'warning'); return false; }
    state.selected = [...new Set([...preservedSelection, ...releasedSelection])].filter((id) => objectEntry(id));
    commitHistory(recursive ? 'Alle Gruppierungen aufgehoben' : releasedGroups === 1 ? 'Gruppierung aufgehoben' : 'Gruppierungen aufgehoben');
    renderAll();
    const objectCount = releasedSelection.filter((id) => objectEntry(id)).length;
    toast(recursive
      ? `${releasedGroups} Gruppen vollständig in ${objectCount} Objekte aufgelöst`
      : releasedGroups === 1
        ? `Gruppe in ${objectCount} Objekte aufgelöst`
        : `${releasedGroups} Gruppen in ${objectCount} Objekte aufgelöst`);
    return true;
  }

  function ungroupSelection() { return releaseSelectedGroups(false); }
  function ungroupAllSelection() { return releaseSelectedGroups(true); }

  function groupFlatteningBounds(group) {
    const width = Math.max(1, num(group.width, 1)); const height = Math.max(1, num(group.height, 1));
    const visibleChildren = (group.children || []).filter((child) => child.visible !== false);
    const visual = visibleChildren.length ? boundsOf(visibleChildren, { visual: true }) : null;
    const left = Math.floor(Math.min(0, num(visual?.x)));
    const top = Math.floor(Math.min(0, num(visual?.y)));
    const right = Math.ceil(Math.max(width, num(visual?.right, width)));
    const bottom = Math.ceil(Math.max(height, num(visual?.bottom, height)));
    return { x: left, y: top, width: Math.max(1, right - left), height: Math.max(1, bottom - top), right, bottom };
  }

  function groupSvgForFlattening(group, bounds, pixelWidth, pixelHeight) {
    const localGroup = deepClone(group);
    localGroup.x = 0; localGroup.y = 0; localGroup.rotation = 0; localGroup.opacity = 100;
    localGroup.effects = [];
    const svg = E('svg', {
      xmlns: svgNS, width: pixelWidth, height: pixelHeight,
      viewBox: `${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}`
    });
    const defs = E('defs');
    (localGroup.children || []).flatMap(objectTree).forEach((object) => appendObjectFilter(defs, object));
    svg.append(defs, objectGroup(localGroup, false));
    return new XMLSerializer().serializeToString(svg);
  }

  async function flattenGroup() {
    const entry = objectEntry(state.selected.length === 1 ? state.selected[0] : ''); const group = entry?.object;
    if (!entry || entry.layer.locked || group?.type !== 'group' || !Array.isArray(group.children)) { toast('Wähle eine Gruppe zum Flatten aus', 'warning'); return; }
    const bounds = groupFlatteningBounds(group);
    const pixelWidth = bounds.width; const pixelHeight = bounds.height;
    if (pixelWidth * pixelHeight > 32_000_000) { toast('Die Gruppe ist für Flatten zu groß', 'warning'); return; }
    try {
      const source = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(groupSvgForFlattening(group, bounds, pixelWidth, pixelHeight))}`;
      const image = await imageFromSource(source); const canvas = document.createElement('canvas'); canvas.width = pixelWidth; canvas.height = pixelHeight;
      const context = canvas.getContext('2d');
      // Die SVG-Zwischenzeichnung besitzt bereits exakt die Zielpixelgröße.
      // Eine weitere Glättung würde die äußerste deckende Pixelzeile gegen den
      // transparenten Bildrand mischen und beim Vergrößern als Verlauf zeigen.
      context.imageSmoothingEnabled = false;
      context.clearRect(0, 0, pixelWidth, pixelHeight); context.drawImage(image, 0, 0, pixelWidth, pixelHeight);
      const localCenter = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
      const documentCenter = Core.applyMatrix(Core.objectMatrix(group), localCenter);
      const bitmap = objectBase('image', documentCenter.x - bounds.width / 2, documentCenter.y - bounds.height / 2);
      bitmap.name = I18n.t('ui.bitmap'); bitmap.nameKey = 'ui.bitmap';
      bitmap.width = bounds.width; bitmap.height = bounds.height; bitmap.rotation = group.rotation; bitmap.opacity = group.opacity;
      bitmap.flipX = Boolean(group.flipX); bitmap.flipY = Boolean(group.flipY);
      if (Array.isArray(group.svgTransformMatrix) && group.svgTransformMatrix.length >= 6) {
        bitmap.x = 0; bitmap.y = 0; bitmap.rotation = 0; bitmap.flipX = false; bitmap.flipY = false;
        bitmap.svgTransformMatrix = Core.multiplyMatrices(Core.objectMatrix(group), Core.translationMatrix(bounds.x, bounds.y));
      }
      bitmap.stroke = 'none'; bitmap.fill = 'none'; bitmap.strokeWidth = 0; bitmap.pixelWidth = pixelWidth; bitmap.pixelHeight = pixelHeight; bitmap.href = canvas.toDataURL('image/png');
      bitmap.bitmapMode = Math.abs(num(bitmap.width) - pixelWidth) < .001 && Math.abs(num(bitmap.height) - pixelHeight) < .001 ? 'direct' : 'placed';
      bitmap.bitmapInterpolation = 'nearest'; bitmap.sourceDpiX = state.doc.dpi; bitmap.sourceDpiY = state.doc.dpi;
      if (group.effects) bitmap.effects = deepClone(effectStack(group));
      entry.layer.objects.splice(entry.index, 1, bitmap); state.selected = [bitmap.id];
      commitHistory('Gruppe zu Bitmap geflattet'); renderAll(); toast(`Gruppe als Bitmap geflattet: ${pixelWidth} × ${pixelHeight} px`);
    } catch { toast('Die Gruppe konnte nicht geflattet werden', 'error'); }
  }

  function reorderObject(action) {
    const entry = objectEntry(state.selected.at(-1)); if (!entry || entry.layer.locked) return;
    const { object, layer, index } = entry; layer.objects.splice(index, 1);
    if (action === 'front') layer.objects.push(object);
    else if (action === 'back') layer.objects.unshift(object);
    else if (action === 'forward') layer.objects.splice(Math.min(index + 1, layer.objects.length), 0, object);
    else layer.objects.splice(Math.max(0, index - 1), 0, object);
    commitHistory('Anordnung geändert'); renderAll();
  }

  function rotateSelection(degrees) {
    const objects = selectedObjects();
    if (!objects.length) { toast('Wähle mindestens ein Objekt aus', 'warning'); return; }
    objects.forEach((object) => {
      const origin = deepClone(object); const width = Math.max(1, num(origin.width, 1)); const height = Math.max(1, num(origin.height, 1)); const center = { x: width / 2, y: height / 2 };
      const pivot = origin.transformPivot || { x: .5, y: .5 }; const pivotWorld = objectLocalToDocument(origin, { x: pivot.x * width, y: pivot.y * height });
      const transform = activeFreeTransform(origin); const mapped = transform ? FreeTransform.mapPoint(transform, pivot.x, pivot.y) : pivot;
      const dx = mapped.x * width - center.x; const dy = mapped.y * height - center.y; const next = num(origin.rotation) + degrees; const radians = next * Math.PI / 180;
      object.rotation = round((next % 360 + 360) % 360, 1);
      object.x = round(pivotWorld.x - center.x - (Math.cos(radians) * dx - Math.sin(radians) * dy), 4);
      object.y = round(pivotWorld.y - center.y - (Math.sin(radians) * dx + Math.cos(radians) * dy), 4);
    });
    commitHistory(degrees < 0 ? 'Auswahl nach links gedreht' : 'Auswahl nach rechts gedreht'); renderAll();
  }

  function selectFreeTransformMode(mode) {
    if (!FreeTransform.MODES.has(mode)) return false;
    const object = selectedObjects().length === 1 ? primaryObject() : null;
    if (!object) { toast('Wähle genau ein Objekt für die freie Transformation aus', 'warning'); return false; }
    // Ältere oder während einer Drehung bearbeitete Pfade können Punkte außerhalb
    // ihrer gespeicherten Breite/Höhe besitzen. Vor dem Aufbau des Transformations-
    // gitters wird diese Box ohne sichtbare Lageänderung an die Kurve angepasst.
    const normalizedBounds = pointVectorTypes.has(object.type) ? normalizeVectorPointBounds(object) : false;
    const changedMode = object.freeTransform && object.freeTransform.mode !== mode;
    if (changedMode) object.freeTransform = FreeTransform.forMode(object.freeTransform, mode);
    state.freeTransformMode = mode;
    if (changedMode || normalizedBounds) commitHistory(changedMode
      ? mode === 'envelope' ? 'In Hüllkurve umgewandelt' : mode === 'perspective' ? 'In Perspektive umgewandelt' : 'In freie Ecken umgewandelt'
      : 'Pfadbegrenzung aktualisiert');
    renderAll(); return true;
  }

  function shearSelection(axis, degrees) {
    const objects = selectedObjects(); if (!objects.length) { toast('Wähle mindestens ein Objekt zum Scheren aus', 'warning'); return false; }
    const next = clamp(num(degrees), -89, 89); let changed = 0;
    objects.forEach((object) => {
      const mode = object.freeTransform?.mode || state.freeTransformMode || 'free'; const source = activeFreeTransform(object, mode) || FreeTransform.normalize({ mode });
      const property = axis === 'y' ? 'skewY' : 'skewX'; if (Math.abs(num(source[property]) - next) < .0001) return;
      object.freeTransform = FreeTransform.withShear(source, axis, next); changed += 1;
    });
    if (!changed) return false;
    if (objects.length === 1) state.freeTransformMode = objects[0].freeTransform.mode;
    commitHistory(axis === 'y' ? 'Objekte vertikal geschert' : 'Objekte horizontal geschert'); renderAll(); return true;
  }

  function resetFreeTransformSelection() {
    const objects = selectedObjects().filter((object) => object.freeTransform || object.transformPivot);
    if (!objects.length) { toast('Die Auswahl besitzt keine freie Transformation', 'warning'); return false; }
    objects.forEach((object) => { delete object.freeTransform; delete object.transformPivot; }); state.freeTransformMode = null;
    commitHistory('Freie Transformation zurückgesetzt'); renderAll(); return true;
  }

  function warpedDocumentPathPoint(object, transform, value) {
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1)); const mapped = FreeTransform.mapPoint(transform, num(value?.[0]) / width, num(value?.[1]) / height);
    const documentPoint = Core.applyMatrix(Core.objectMatrix(object), { x: mapped.x * width, y: mapped.y * height });
    return [round(documentPoint.x, 5), round(documentPoint.y, 5)];
  }

  function applyFreeTransformToPath() {
    const object = selectedObjects().length === 1 ? primaryObject() : null; const transform = activeFreeTransform(object);
    if (!object || !transform || !isVectorObject(object)) { toast('Wähle einen frei transformierten Vektorpfad aus', 'warning'); return false; }
    const map = (value) => warpedDocumentPathPoint(object, transform, value); let applied = false;
    const paths = closedVectorPaths(object);
    if (paths.length) {
      const warped = paths.map((path) => ({
        points: path.points.map(map), closed: true,
        segments: path.segments.map((segment) => isBezierSegment(segment) ? { c1: map(segment.c1), c2: map(segment.c2) } : null)
      }));
      applied = applyGlobalPathGeometry(object, warped, object.name);
    } else if (pointVectorTypes.has(object.type)) {
      const path = workbenchPathFromObject(object);
      path.points = path.points.map(map); path.segments = path.segments.map((segment) => isBezierSegment(segment) ? { c1: map(segment.c1), c2: map(segment.c2) } : null);
      object.x = 0; object.y = 0; object.width = 1; object.height = 1; object.rotation = 0; object.flipX = false; object.flipY = false;
      applyWorkbenchPath(object, path, { normalize: true, forcePathType: true }); applied = true;
    }
    if (!applied) { toast('Diese Vektorform kann nicht destruktiv angewendet werden', 'warning'); return false; }
    delete object.freeTransform; delete object.transformPivot; state.freeTransformMode = null; clearVectorSubselection();
    commitHistory('Freie Transformation auf Pfad angewendet'); renderAll(); toast('Verformung liegt jetzt in den Pfadkoordinaten'); return true;
  }

  function flipSelection(axis) {
    const objects = selectedObjects();
    if (!objects.length) { toast('Wähle mindestens ein Objekt aus', 'warning'); return; }
    const property = axis === 'horizontal' ? 'flipX' : 'flipY';
    objects.forEach((object) => { object[property] = !Boolean(object[property]); });
    commitHistory(axis === 'horizontal' ? 'Auswahl horizontal gespiegelt' : 'Auswahl vertikal gespiegelt'); renderAll();
  }

  function addLayer() {
    const layer = makeLayer(`${I18n.t('document.layer')} ${state.doc.layers.length + 1}`); state.doc.layers.push(layer); state.doc.activeLayerId = layer.id; state.expandedLayerIds.add(layer.id); state.selected = [];
    commitHistory('Ebene hinzugefügt'); renderAll();
  }

  function duplicateLayer() {
    const layer = activeLayer(); if (!layer) return;
    const copy = deepClone(layer); copy.id = uid('layer'); copy.name = `${layer.name} Kopie`; copy.objects = copy.objects.map(reidentifyObjectTree);
    const index = state.doc.layers.indexOf(layer); state.doc.layers.splice(index + 1, 0, copy); state.doc.activeLayerId = copy.id; state.expandedLayerIds.add(copy.id); state.selected = [];
    commitHistory('Ebene dupliziert'); renderAll();
  }

  function deleteLayer() {
    const layer = activeLayer(); if (!layer) return;
    if (state.doc.layers.length === 1) { layer.objects = []; state.selected = []; commitHistory('Ebene geleert'); renderAll(); return; }
    const index = state.doc.layers.indexOf(layer); state.doc.layers.splice(index, 1); state.expandedLayerIds.delete(layer.id); state.doc.activeLayerId = state.doc.layers[Math.min(index, state.doc.layers.length - 1)].id;
    state.selected = []; commitHistory('Ebene gelöscht'); renderAll();
  }

  function moveLayer(direction) {
    const layer = activeLayer(); const index = state.doc.layers.indexOf(layer); const next = clamp(index + direction, 0, state.doc.layers.length - 1);
    if (index === next) return; state.doc.layers.splice(index, 1); state.doc.layers.splice(next, 0, layer); commitHistory('Ebene verschoben'); renderAll();
  }

  function alignSelection(mode) {
    const objects = selectedObjects(); if (!objects.length) return;
    const targetMode = $('alignTarget').value; let target;
    if (targetMode === 'canvas') target = { x: 0, y: 0, right: state.doc.width, bottom: state.doc.height, cx: state.doc.width / 2, cy: state.doc.height / 2 };
    else if (targetMode === 'key' && objects.length > 1) target = layoutBoundsOf([objects.at(-1)]);
    else target = layoutBoundsOf(objects);
    const movers = targetMode === 'key' && objects.length > 1 ? objects.slice(0, -1) : objects;
    movers.forEach((object) => {
      const current = layoutBoundsOf([object]); let dx = 0; let dy = 0;
      if (mode === 'left') dx = target.x - current.x;
      if (mode === 'hcenter') dx = target.cx - current.cx;
      if (mode === 'right') dx = target.right - current.right;
      if (mode === 'top') dy = target.y - current.y;
      if (mode === 'vcenter') dy = target.cy - current.cy;
      if (mode === 'bottom') dy = target.bottom - current.bottom;
      object.x = round(num(object.x) + dx); object.y = round(num(object.y) + dy);
    });
    commitHistory('Objekte ausgerichtet'); renderAll();
  }

  function distributeSelection(axis) {
    const objects = selectedObjects();
    if (objects.length < 3) { toast('Wähle mindestens drei Objekte zum Verteilen aus', 'warning'); return false; }
    const horizontal = axis === 'horizontal';
    const entries = objects.map((object) => ({ object, bounds: layoutBoundsOf([object]) }))
      .sort((left, right) => horizontal
        ? left.bounds.x - right.bounds.x || left.bounds.y - right.bounds.y
        : left.bounds.y - right.bounds.y || left.bounds.x - right.bounds.x);
    const start = horizontal ? entries[0].bounds.x : entries[0].bounds.y;
    const end = horizontal ? entries.at(-1).bounds.right : entries.at(-1).bounds.bottom;
    const occupied = entries.reduce((sum, entry) => sum + (horizontal ? entry.bounds.width : entry.bounds.height), 0);
    const spacing = (end - start - occupied) / (entries.length - 1);
    let cursor = start;
    entries.forEach((entry, index) => {
      const size = horizontal ? entry.bounds.width : entry.bounds.height;
      if (index > 0 && index < entries.length - 1) {
        const current = horizontal ? entry.bounds.x : entry.bounds.y;
        const delta = cursor - current;
        if (horizontal) entry.object.x = round(num(entry.object.x) + delta, 4);
        else entry.object.y = round(num(entry.object.y) + delta, 4);
      }
      cursor += size + spacing;
    });
    commitHistory(horizontal ? 'Objekte horizontal verteilt' : 'Objekte vertikal verteilt');
    renderAll(); return true;
  }

  function positionInlineTextEditor() {
    const session = state.inlineTextSession; const editor = refs.inlineText;
    if (!session || !editor || editor.hidden) return;
    const object = session.objectId ? objectEntry(session.objectId)?.object : null;
    const settings = object ? textSettings(object) : textDefaults;
    const fontSize = clamp(num(object?.fontSize, $('fontSize').value || 32), 6, 400) * state.zoom;
    editor.style.left = `${round(session.point.x * state.zoom)}px`;
    editor.style.top = `${round(session.point.y * state.zoom)}px`;
    editor.style.fontFamily = object?.fontFamily || $('fontFamily').value || DEFAULT_FONT_FAMILY;
    editor.style.fontSize = `${fontSize}px`;
    editor.style.fontWeight = (object ? object.bold : $('fontBold').classList.contains('is-active')) ? '700' : '400';
    editor.style.fontStyle = (object ? object.italic : $('fontItalic').classList.contains('is-active')) ? 'italic' : 'normal';
    editor.style.textDecoration = object ? [settings.underline && 'underline', settings.strike && 'line-through'].filter(Boolean).join(' ') : 'none';
    editor.style.letterSpacing = object ? `${settings.letterSpacing * state.zoom}px` : 'normal';
    editor.style.lineHeight = object ? String(settings.lineHeight / 100) : '1.2';
    editor.style.color = normalizeColor(object?.fill) || normalizeColor(object?.textStroke) || $('fillColor').value || '#172033';
    const lines = editor.value.split('\n'); const longestLine = Math.max(1, ...lines.map((line) => line.length));
    const availableWidth = Math.max(52, (state.doc.width - session.point.x) * state.zoom);
    const objectWidth = object ? num(object.width, 1) * state.zoom + 12 : 0;
    const objectHeight = object ? num(object.height, 1) * state.zoom + 8 : 0;
    const width = Math.min(availableWidth, Math.max(Math.max(74, fontSize * 3), longestLine * fontSize * .64 + 18, objectWidth));
    editor.style.width = `${round(width)}px`;
    editor.style.height = `${round(Math.max(fontSize * 1.5 + 10, lines.length * fontSize * settings.lineHeight / 100 + 10, objectHeight))}px`;
  }

  function loadRecentEmojis() {
    try {
      const saved = JSON.parse(localStorage.getItem(emojiRecentStorageKey) || '[]');
      if (!Array.isArray(saved)) return [];
      return saved.filter((emoji, index) => typeof emoji === 'string' && knownEmojis.has(emoji) && saved.indexOf(emoji) === index).slice(0, emojiHistoryLimit);
    } catch {
      return [];
    }
  }

  function rememberEmoji(emoji) {
    if (!knownEmojis.has(emoji)) return;
    recentEmojis = [emoji, ...recentEmojis.filter((entry) => entry !== emoji)].slice(0, emojiHistoryLimit);
    try { localStorage.setItem(emojiRecentStorageKey, JSON.stringify(recentEmojis)); } catch { /* Die lokale Zuletzt-Liste ist optional. */ }
  }

  function closeEmojiPicker({ focusEditor = false } = {}) {
    const panel = $('emojiPickerPanel'); const trigger = $('emojiPickerButton');
    if (!panel || !trigger) return;
    panel.hidden = true;
    trigger.setAttribute('aria-expanded', 'false');
    if (focusEditor && state.inlineTextSession) refs.inlineText?.focus({ preventScroll: true });
  }

  function positionEmojiPickerPanel() {
    const panel = $('emojiPickerPanel'); const trigger = $('emojiPickerButton');
    if (!panel || !trigger || panel.hidden) return;
    const triggerRect = trigger.getBoundingClientRect();
    const panelRect = panel.getBoundingClientRect();
    const gap = 8; const edge = 8;
    const left = clamp(triggerRect.right - panelRect.width, edge, Math.max(edge, window.innerWidth - panelRect.width - edge));
    const above = triggerRect.top - panelRect.height - gap;
    const below = triggerRect.bottom + gap;
    const top = above >= edge ? above : clamp(below, edge, Math.max(edge, window.innerHeight - panelRect.height - edge));
    panel.style.left = `${round(left)}px`;
    panel.style.top = `${round(top)}px`;
  }

  function emojiValuesFor(categoryId) {
    if (categoryId === 'recent') return recentEmojis;
    return emojiCategories.find((category) => category.id === categoryId)?.emojis || [];
  }

  function renderEmojiPicker() {
    const categories = $('emojiCategoryList'); const list = $('emojiList'); const empty = $('emojiRecentEmpty');
    if (!categories || !list || !empty) return;
    const selectedCategory = emojiCategories.find((category) => category.id === activeEmojiCategory) || emojiCategories[1];
    activeEmojiCategory = selectedCategory.id;
    if (!categories.childElementCount) {
      const categoryFragment = document.createDocumentFragment();
      emojiCategories.forEach((category) => {
        const button = document.createElement('button');
        button.type = 'button'; button.className = 'emoji-category-button'; button.role = 'tab';
        button.dataset.emojiCategory = category.id; button.textContent = category.icon;
        categoryFragment.append(button);
      });
      categories.append(categoryFragment);
    }
    emojiCategories.forEach((category) => {
      const button = categories.querySelector(`[data-emoji-category="${category.id}"]`); if (!button) return;
      const label = I18n.translateSource(category.label);
      button.title = label; button.setAttribute('aria-label', label);
      button.setAttribute('aria-selected', String(category.id === activeEmojiCategory));
      button.tabIndex = category.id === activeEmojiCategory ? 0 : -1;
    });
    const values = emojiValuesFor(activeEmojiCategory);
    const emojiFragment = document.createDocumentFragment();
    values.forEach((emoji) => {
      const button = document.createElement('button');
      button.type = 'button'; button.className = 'emoji-button'; button.role = 'gridcell';
      button.dataset.emoji = emoji; button.textContent = emoji; button.title = emoji;
      button.setAttribute('aria-label', I18n.t('ui.insertEmoji', { emoji }));
      button.tabIndex = -1;
      emojiFragment.append(button);
    });
    list.replaceChildren(emojiFragment);
    empty.hidden = values.length > 0 || activeEmojiCategory !== 'recent';
    list.hidden = values.length === 0;
    requestAnimationFrame(positionEmojiPickerPanel);
  }

  function openEmojiPicker() {
    if (!state.inlineTextSession || refs.inlineText?.hidden || $('emojiPickerButton')?.disabled) return;
    $('emojiPickerPanel').hidden = false;
    $('emojiPickerButton').setAttribute('aria-expanded', 'true');
    renderEmojiPicker();
    positionEmojiPickerPanel();
  }

  function syncSpecialCharacterButtons() {
    const enabled = Boolean(state.inlineTextSession && !refs.inlineText?.hidden);
    const buttons = [...($('specialCharacterList')?.querySelectorAll('.special-character-button') || [])];
    buttons.forEach((button) => {
      button.disabled = !enabled;
      button.tabIndex = -1;
    });
    const firstVisible = buttons.find((button) => !button.hidden);
    if (enabled && firstVisible) firstVisible.tabIndex = 0;
    if ($('emojiPickerButton')) $('emojiPickerButton').disabled = !enabled;
    if (!enabled) closeEmojiPicker();
  }

  function renderSpecialCharacterPage() {
    const list = $('specialCharacterList'); if (!list) return;
    const buttons = [...list.querySelectorAll('.special-character-button')];
    const pageCount = Math.max(1, Math.ceil(buttons.length / specialCharacterPageSize));
    specialCharacterPage = (specialCharacterPage % pageCount + pageCount) % pageCount;
    buttons.forEach((button, index) => { button.hidden = Math.floor(index / specialCharacterPageSize) !== specialCharacterPage; });
    $('specialCharacterPage').textContent = `${specialCharacterPage + 1}/${pageCount}`;
    const nextPage = specialCharacterPage + 1 === pageCount ? 1 : specialCharacterPage + 2;
    $('specialCharacterNextPage').title = `Sonderzeichenseite ${nextPage} von ${pageCount}`;
    $('specialCharacterNextPage').setAttribute('aria-label', `Sonderzeichenseite ${nextPage} von ${pageCount} anzeigen`);
    syncSpecialCharacterButtons();
  }

  function showNextSpecialCharacterPage() {
    specialCharacterPage += 1;
    renderSpecialCharacterPage();
    if (state.inlineTextSession) refs.inlineText?.focus({ preventScroll: true });
  }

  function insertSpecialCharacter(character) {
    const editor = refs.inlineText;
    if (!state.inlineTextSession || !editor || editor.hidden || !character) return false;
    const start = Number.isInteger(editor.selectionStart) ? editor.selectionStart : editor.value.length;
    const end = Number.isInteger(editor.selectionEnd) ? editor.selectionEnd : start;
    editor.setRangeText(character, start, end, 'end');
    editor.dispatchEvent(new Event('input', { bubbles: true }));
    editor.focus({ preventScroll: true });
    return true;
  }

  function buildSpecialCharacterList() {
    const list = $('specialCharacterList');
    if (!list || list.childElementCount) return;
    const fragment = document.createDocumentFragment();
    specialCharacters.forEach(([character, label]) => {
      const button = document.createElement('button');
      button.type = 'button'; button.className = 'format-button special-character-button';
      button.textContent = character; button.dataset.character = character;
      button.title = `${label} (${character})`; button.setAttribute('aria-label', `${label}: ${character}`);
      fragment.append(button);
    });
    list.append(fragment);
    list.addEventListener('mousedown', (event) => {
      if (event.target.closest?.('.special-character-button:not(:disabled)')) event.preventDefault();
    });
    list.addEventListener('click', (event) => {
      const button = event.target.closest?.('.special-character-button:not(:disabled)');
      if (button) insertSpecialCharacter(button.dataset.character);
    });
    list.addEventListener('keydown', (event) => {
      if (!['ArrowLeft', 'ArrowRight', 'Home', 'End', 'Escape'].includes(event.key)) return;
      event.preventDefault();
      if (event.key === 'Escape') { refs.inlineText?.focus({ preventScroll: true }); return; }
      const buttons = [...list.querySelectorAll('.special-character-button:not(:disabled)')].filter((button) => !button.hidden);
      if (!buttons.length) return;
      const current = Math.max(0, buttons.indexOf(document.activeElement));
      const index = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
        : event.key === 'ArrowLeft' ? (current - 1 + buttons.length) % buttons.length : (current + 1) % buttons.length;
      buttons.forEach((button, buttonIndex) => { button.tabIndex = buttonIndex === index ? 0 : -1; });
      buttons[index].focus(); buttons[index].scrollIntoView({ block: 'nearest', inline: 'nearest' });
    });
    const palette = list.closest('.font-controls');
    palette?.addEventListener('focusout', () => requestAnimationFrame(() => {
      if (state.inlineTextSession && document.activeElement !== refs.inlineText && !palette.contains(document.activeElement)) finishInlineText();
    }));
    $('specialCharacterNextPage').addEventListener('mousedown', (event) => event.preventDefault());
    $('specialCharacterNextPage').addEventListener('click', showNextSpecialCharacterPage);
    renderSpecialCharacterPage();
  }

  function buildEmojiPicker() {
    const picker = $('emojiPicker'); const trigger = $('emojiPickerButton'); const panel = $('emojiPickerPanel');
    const categories = $('emojiCategoryList'); const list = $('emojiList');
    if (!picker || !trigger || !panel || !categories || !list) return;
    recentEmojis = loadRecentEmojis();
    renderEmojiPicker();
    trigger.addEventListener('click', () => {
      if (panel.hidden) openEmojiPicker();
      else closeEmojiPicker({ focusEditor: true });
    });
    categories.addEventListener('click', (event) => {
      const button = event.target.closest?.('[data-emoji-category]');
      if (!button) return;
      activeEmojiCategory = button.dataset.emojiCategory;
      renderEmojiPicker();
    });
    categories.addEventListener('keydown', (event) => {
      if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
      event.preventDefault();
      const buttons = [...categories.querySelectorAll('[data-emoji-category]')];
      const current = Math.max(0, buttons.indexOf(document.activeElement));
      const index = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
        : event.key === 'ArrowLeft' ? (current - 1 + buttons.length) % buttons.length : (current + 1) % buttons.length;
      activeEmojiCategory = buttons[index].dataset.emojiCategory;
      renderEmojiPicker();
      categories.querySelector(`[data-emoji-category="${activeEmojiCategory}"]`)?.focus();
    });
    list.addEventListener('mousedown', (event) => {
      if (event.target.closest?.('[data-emoji]')) event.preventDefault();
    });
    list.addEventListener('click', (event) => {
      const button = event.target.closest?.('[data-emoji]');
      if (!button || !insertSpecialCharacter(button.dataset.emoji)) return;
      rememberEmoji(button.dataset.emoji);
    });
    list.addEventListener('keydown', (event) => {
      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
      const buttons = [...list.querySelectorAll('[data-emoji]')]; if (!buttons.length) return;
      event.preventDefault();
      const columns = 8; const current = Math.max(0, buttons.indexOf(document.activeElement));
      const index = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
        : event.key === 'ArrowLeft' ? (current - 1 + buttons.length) % buttons.length
          : event.key === 'ArrowRight' ? (current + 1) % buttons.length
            : event.key === 'ArrowUp' ? Math.max(0, current - columns) : Math.min(buttons.length - 1, current + columns);
      buttons[index].focus(); buttons[index].scrollIntoView({ block: 'nearest' });
    });
    panel.addEventListener('keydown', (event) => {
      if (event.key !== 'Escape') return;
      event.preventDefault(); closeEmojiPicker({ focusEditor: true });
    });
    document.addEventListener('pointerdown', (event) => {
      if (!panel.hidden && !picker.contains(event.target) && !panel.contains(event.target)) closeEmojiPicker();
    });
    window.addEventListener('resize', positionEmojiPickerPanel);
    window.addEventListener('scroll', positionEmojiPickerPanel, true);
    syncSpecialCharacterButtons();
  }

  function notoEmojiFilename(emoji) {
    const codePoints = [...String(emoji || '')]
      .map((character) => character.codePointAt(0))
      .filter((codePoint) => codePoint !== 0xfe0f);
    if (!codePoints.length) return '';
    const name = `emoji_u${codePoints.map((codePoint) => codePoint.toString(16).padStart(codePoint <= 0xffff ? 4 : 0, '0')).join('_')}.svg`;
    return notoEmojiFileSet.has(name) ? name : '';
  }

  function emojiFromNotoFilename(filename) {
    const match = String(filename || '').match(/^emoji_u([0-9a-f_]+)\.svg$/);
    if (!match) return '';
    try {
      return String.fromCodePoint(...match[1].split('_').map((value) => Number.parseInt(value, 16)));
    } catch {
      return '';
    }
  }

  function notoEmojiAssetUrl(filename) {
    return `assets/noto-emoji/svg/${filename}`;
  }

  function normalizePickerSearch(value) {
    return String(value || '').normalize('NFKD').replace(/\p{Mark}+/gu, '').toLocaleLowerCase('und')
      .replace(/[^\p{Letter}\p{Number}]+/gu, ' ').trim();
  }

  function notoEmojiSearchLocaleIndex() {
    const index = notoEmojiSearchLocales.indexOf(I18n.language);
    return index >= 0 ? index : Math.max(0, notoEmojiSearchLocales.indexOf('en'));
  }

  function notoEmojiLabel(filename, localeIndex = notoEmojiSearchLocaleIndex()) {
    const labels = notoEmojiSearch[filename]?.l;
    return String(labels?.[localeIndex] || labels?.[notoEmojiSearchLocales.indexOf('en')] || emojiFromNotoFilename(filename) || filename);
  }

  function notoEmojiSearchNeedles(token) {
    if (I18n.language === 'de' && token === 'auto') return ['auto', 'automobil', 'wagen', 'fahrzeug', 'taxi'];
    if (I18n.language === 'ru' && token === 'машина') return ['машина', 'автомобиль', 'авто', 'транспорт', 'такси'];
    return [token];
  }

  function notoEmojiWordMatch(word, needle) {
    if (!word || !needle) return false;
    if (needle === 'auto' && word.startsWith('automat')) return false;
    return word === needle || word.startsWith(needle) || word.endsWith(needle);
  }

  function searchNotoEmojiFiles(query) {
    const tokens = normalizePickerSearch(query).split(' ').filter(Boolean);
    if (!tokens.length) return [];
    const localeIndex = notoEmojiSearchLocaleIndex();
    const scored = [];
    notoEmojiFiles.forEach((filename, order) => {
      const entry = notoEmojiSearch[filename]; if (!entry) return;
      const label = normalizePickerSearch(notoEmojiLabel(filename, localeIndex));
      const words = new Set(`${label} ${entry.s?.[localeIndex] || ''}`.split(' ').filter(Boolean));
      let score = 0;
      for (const token of tokens) {
        const needles = notoEmojiSearchNeedles(token);
        const matches = needles.flatMap((needle) => [...words].map((word) => {
          if (!notoEmojiWordMatch(word, needle)) return Infinity;
          return word === needle ? 0 : word.startsWith(needle) ? 1 : 2;
        }));
        const best = Math.min(...matches);
        if (!Number.isFinite(best)) return;
        score += best;
      }
      if (label === normalizePickerSearch(query)) score -= 10;
      else if (label.startsWith(normalizePickerSearch(query))) score -= 4;
      scored.push({ filename, score, label, order });
    });
    return scored.sort((left, right) => left.score - right.score
      || left.label.localeCompare(right.label, I18n.language)
      || left.order - right.order).map((entry) => entry.filename);
  }

  function notoEmojiValuesFor(categoryId) {
    if (normalizePickerSearch(notoEmojiSearchQuery)) return searchNotoEmojiFiles(notoEmojiSearchQuery);
    if (categoryId === 'recent') return recentNotoEmojiFiles;
    if (categoryId === 'all') return notoEmojiFiles;
    const category = emojiCategories.find((entry) => entry.id === categoryId);
    return [...new Set((category?.emojis || []).map(notoEmojiFilename).filter(Boolean))];
  }

  function loadNotoEmojiSettings() {
    let saved = {};
    try { saved = JSON.parse(localStorage.getItem(vectorEmojiSettingsStorageKey) || '{}') || {}; }
    catch { saved = {}; }
    selectedNotoEmojiFile = notoEmojiFileSet.has(saved.file) ? saved.file
      : notoEmojiFileSet.has(selectedNotoEmojiFile) ? selectedNotoEmojiFile
        : notoEmojiFiles[0] || '';
    recentNotoEmojiFiles = Array.isArray(saved.recent)
      ? saved.recent.filter((file, index) => notoEmojiFileSet.has(file) && saved.recent.indexOf(file) === index).slice(0, emojiHistoryLimit)
      : [];
    const size = clamp(num(saved.size, 96), 8, 2000);
    if ($('vectorEmojiSize')) $('vectorEmojiSize').value = String(round(size));
  }

  function persistNotoEmojiSettings() {
    try {
      localStorage.setItem(vectorEmojiSettingsStorageKey, JSON.stringify({
        file: selectedNotoEmojiFile,
        size: clamp(num($('vectorEmojiSize')?.value, 96), 8, 2000),
        recent: recentNotoEmojiFiles
      }));
    } catch { /* Werkzeugwerte bleiben optional lokal gespeichert. */ }
  }

  function rememberNotoEmoji(filename) {
    if (!notoEmojiFileSet.has(filename)) return;
    recentNotoEmojiFiles = [filename, ...recentNotoEmojiFiles.filter((entry) => entry !== filename)].slice(0, emojiHistoryLimit);
    persistNotoEmojiSettings();
  }

  function closeNotoEmojiPicker({ focusCanvas = false } = {}) {
    const panel = $('notoEmojiPickerPanel'); const trigger = $('notoEmojiPickerButton');
    if (!panel || !trigger) return false;
    const wasOpen = !panel.hidden;
    panel.hidden = true;
    trigger.setAttribute('aria-expanded', 'false');
    if (focusCanvas) refs.workspace?.focus({ preventScroll: true });
    return wasOpen;
  }

  function positionNotoEmojiPickerPanel() {
    const panel = $('notoEmojiPickerPanel'); const trigger = $('notoEmojiPickerButton');
    if (!panel || !trigger || panel.hidden) return;
    const triggerRect = trigger.getBoundingClientRect(); const panelRect = panel.getBoundingClientRect();
    const gap = 8; const edge = 8;
    const left = clamp(triggerRect.left, edge, Math.max(edge, window.innerWidth - panelRect.width - edge));
    const above = triggerRect.top - panelRect.height - gap; const below = triggerRect.bottom + gap;
    const top = above >= edge ? above : clamp(below, edge, Math.max(edge, window.innerHeight - panelRect.height - edge));
    panel.style.left = `${round(left)}px`; panel.style.top = `${round(top)}px`;
  }

  function renderNotoEmojiPicker() {
    const categories = $('notoEmojiCategoryList'); const list = $('notoEmojiList'); const empty = $('notoEmojiRecentEmpty');
    const search = $('notoEmojiSearch'); const searchEmpty = $('notoEmojiSearchEmpty');
    const pager = $('notoEmojiPager'); const pageOutput = $('notoEmojiPage'); const preview = $('notoEmojiPickerPreview');
    if (!categories || !list || !empty || !search || !searchEmpty || !pager || !pageOutput || !preview) return;
    const searching = Boolean(normalizePickerSearch(notoEmojiSearchQuery));
    if (search.value !== notoEmojiSearchQuery) search.value = notoEmojiSearchQuery;
    const selectedCategory = notoEmojiCategories.find((category) => category.id === activeNotoEmojiCategory) || notoEmojiCategories[1];
    activeNotoEmojiCategory = selectedCategory.id;
    if (!categories.childElementCount) {
      const fragment = document.createDocumentFragment();
      notoEmojiCategories.forEach((category) => {
        const button = document.createElement('button'); const image = document.createElement('img');
        button.type = 'button'; button.className = 'emoji-category-button'; button.role = 'tab'; button.dataset.notoEmojiCategory = category.id;
        const iconFile = notoEmojiFilename(category.icon);
        image.src = iconFile ? notoEmojiAssetUrl(iconFile) : notoEmojiAssetUrl(selectedNotoEmojiFile);
        image.alt = ''; image.width = 20; image.height = 20; button.append(image); fragment.append(button);
      });
      categories.append(fragment);
    }
    notoEmojiCategories.forEach((category) => {
      const button = categories.querySelector(`[data-noto-emoji-category="${category.id}"]`); if (!button) return;
      const label = I18n.translateSource(category.label);
      button.title = label; button.setAttribute('aria-label', label);
      button.setAttribute('aria-selected', String(category.id === activeNotoEmojiCategory));
      button.tabIndex = category.id === activeNotoEmojiCategory ? 0 : -1;
    });
    const allValues = notoEmojiValuesFor(activeNotoEmojiCategory);
    const pageCount = Math.max(1, Math.ceil(allValues.length / notoEmojiPageSize));
    notoEmojiPage = clamp(Math.floor(num(notoEmojiPage)), 0, pageCount - 1);
    const values = allValues.slice(notoEmojiPage * notoEmojiPageSize, (notoEmojiPage + 1) * notoEmojiPageSize);
    const fragment = document.createDocumentFragment();
    values.forEach((filename) => {
      const label = notoEmojiLabel(filename); const button = document.createElement('button'); const image = document.createElement('img');
      button.type = 'button'; button.className = 'emoji-button'; button.role = 'gridcell'; button.dataset.notoEmojiFile = filename;
      button.title = label; button.setAttribute('aria-label', I18n.t('ui.insertNotoVectorEmoji', { emoji: label }));
      button.setAttribute('aria-pressed', String(filename === selectedNotoEmojiFile)); button.tabIndex = -1;
      image.src = notoEmojiAssetUrl(filename); image.alt = ''; image.loading = 'lazy'; image.width = 31; image.height = 31;
      button.append(image); fragment.append(button);
    });
    list.replaceChildren(fragment);
    empty.hidden = searching || allValues.length > 0 || activeNotoEmojiCategory !== 'recent';
    searchEmpty.hidden = !searching || allValues.length > 0;
    list.hidden = allValues.length === 0;
    pager.hidden = pageCount <= 1;
    pageOutput.textContent = I18n.t('ui.emojiPageStatus', { page: notoEmojiPage + 1, pages: pageCount, count: allValues.length });
    $('notoEmojiPreviousPage').disabled = notoEmojiPage === 0;
    $('notoEmojiNextPage').disabled = notoEmojiPage + 1 >= pageCount;
    if (selectedNotoEmojiFile) preview.src = notoEmojiAssetUrl(selectedNotoEmojiFile);
    requestAnimationFrame(positionNotoEmojiPickerPanel);
  }

  function openNotoEmojiPicker() {
    if (state.tool !== 'vector-emoji' || !notoEmojiFiles.length) return;
    $('notoEmojiPickerPanel').hidden = false;
    $('notoEmojiPickerButton').setAttribute('aria-expanded', 'true');
    renderNotoEmojiPicker(); positionNotoEmojiPickerPanel();
    requestAnimationFrame(() => $('notoEmojiSearch')?.focus({ preventScroll: true }));
  }

  function selectNotoEmoji(filename) {
    if (!notoEmojiFileSet.has(filename)) return;
    selectedNotoEmojiFile = filename; persistNotoEmojiSettings(); renderNotoEmojiPicker();
    closeNotoEmojiPicker({ focusCanvas: true });
  }

  function turnNotoEmojiPage(direction) {
    const values = notoEmojiValuesFor(activeNotoEmojiCategory); const pageCount = Math.max(1, Math.ceil(values.length / notoEmojiPageSize));
    notoEmojiPage = clamp(notoEmojiPage + direction, 0, pageCount - 1); renderNotoEmojiPicker();
  }

  function buildNotoEmojiPicker() {
    const picker = $('notoEmojiPicker'); const trigger = $('notoEmojiPickerButton'); const panel = $('notoEmojiPickerPanel');
    const categories = $('notoEmojiCategoryList'); const list = $('notoEmojiList'); const search = $('notoEmojiSearch');
    if (!picker || !trigger || !panel || !categories || !list || !search) return;
    loadNotoEmojiSettings(); renderNotoEmojiPicker();
    trigger.disabled = !notoEmojiFiles.length;
    trigger.addEventListener('click', () => panel.hidden ? openNotoEmojiPicker() : closeNotoEmojiPicker({ focusCanvas: true }));
    const updateSearch = () => {
      notoEmojiSearchQuery = search.value; notoEmojiPage = 0; renderNotoEmojiPicker();
    };
    search.addEventListener('input', updateSearch);
    search.addEventListener('search', updateSearch);
    categories.addEventListener('click', (event) => {
      const button = event.target.closest?.('[data-noto-emoji-category]'); if (!button) return;
      activeNotoEmojiCategory = button.dataset.notoEmojiCategory; notoEmojiSearchQuery = ''; search.value = '';
      notoEmojiPage = 0; renderNotoEmojiPicker();
    });
    categories.addEventListener('keydown', (event) => {
      if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
      event.preventDefault();
      const buttons = [...categories.querySelectorAll('[data-noto-emoji-category]')]; const current = Math.max(0, buttons.indexOf(document.activeElement));
      const index = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
        : event.key === 'ArrowLeft' ? (current - 1 + buttons.length) % buttons.length : (current + 1) % buttons.length;
      activeNotoEmojiCategory = buttons[index].dataset.notoEmojiCategory; notoEmojiSearchQuery = ''; search.value = '';
      notoEmojiPage = 0; renderNotoEmojiPicker();
      categories.querySelector(`[data-noto-emoji-category="${activeNotoEmojiCategory}"]`)?.focus();
    });
    list.addEventListener('click', (event) => {
      const button = event.target.closest?.('[data-noto-emoji-file]'); if (button) selectNotoEmoji(button.dataset.notoEmojiFile);
    });
    list.addEventListener('keydown', (event) => {
      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
      const buttons = [...list.querySelectorAll('[data-noto-emoji-file]')]; if (!buttons.length) return;
      event.preventDefault();
      const columns = Math.max(1, getComputedStyle(list).gridTemplateColumns.split(' ').length);
      const current = Math.max(0, buttons.indexOf(document.activeElement));
      const index = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
        : event.key === 'ArrowLeft' ? Math.max(0, current - 1) : event.key === 'ArrowRight' ? Math.min(buttons.length - 1, current + 1)
          : event.key === 'ArrowUp' ? Math.max(0, current - columns) : Math.min(buttons.length - 1, current + columns);
      buttons[index].focus(); buttons[index].scrollIntoView({ block: 'nearest' });
    });
    $('notoEmojiPreviousPage').addEventListener('click', () => turnNotoEmojiPage(-1));
    $('notoEmojiNextPage').addEventListener('click', () => turnNotoEmojiPage(1));
    $('vectorEmojiSize').addEventListener('change', () => {
      $('vectorEmojiSize').value = String(round(clamp(num($('vectorEmojiSize').value, 96), 8, 2000)));
      persistNotoEmojiSettings();
    });
    panel.addEventListener('keydown', (event) => {
      if (event.key !== 'Escape') return;
      event.preventDefault();
      if (notoEmojiSearchQuery) {
        notoEmojiSearchQuery = ''; search.value = ''; notoEmojiPage = 0; renderNotoEmojiPicker(); search.focus();
      } else closeNotoEmojiPicker({ focusCanvas: true });
    });
    document.addEventListener('pointerdown', (event) => {
      if (!panel.hidden && !picker.contains(event.target) && !panel.contains(event.target)) closeNotoEmojiPicker();
    });
    window.addEventListener('resize', positionNotoEmojiPickerPanel);
    window.addEventListener('scroll', positionNotoEmojiPickerPanel, true);
  }

  function vectorFlagLocaleIndex() {
    const index = vectorFlagLocales.indexOf(I18n.language);
    return index >= 0 ? index : Math.max(0, vectorFlagLocales.indexOf('en'));
  }

  function vectorFlagName(entry, localeIndex = vectorFlagLocaleIndex()) {
    const englishIndex = vectorFlagLocales.indexOf('en');
    return String(entry?.names?.[localeIndex] || entry?.names?.[englishIndex] || entry?.code || '');
  }

  function vectorFlagAssetUrl(entry) {
    return `assets/flag-icons/svg/${entry.file}`;
  }

  function searchVectorFlags(query) {
    const normalizedQuery = normalizePickerSearch(query);
    const tokens = normalizedQuery.split(' ').filter(Boolean);
    if (!tokens.length) return vectorFlags;
    const localeIndex = vectorFlagLocaleIndex();
    const englishIndex = Math.max(0, vectorFlagLocales.indexOf('en'));
    const scored = [];
    vectorFlags.forEach((entry, order) => {
      const localizedName = normalizePickerSearch(vectorFlagName(entry, localeIndex));
      const englishName = normalizePickerSearch(vectorFlagName(entry, englishIndex));
      const codes = [entry.code, entry.alpha3, entry.numeric].map(normalizePickerSearch).filter(Boolean);
      const words = new Set(`${localizedName} ${englishName} ${codes.join(' ')}`.split(' ').filter(Boolean));
      let score = 0;
      for (const token of tokens) {
        const matches = [...words].map((word) => word === token ? 0 : word.startsWith(token) ? 1 : Infinity);
        const best = Math.min(...matches);
        if (!Number.isFinite(best)) return;
        score += best;
      }
      if (localizedName === normalizedQuery) score -= 20;
      else if (codes.includes(normalizedQuery)) score -= 16;
      else if (localizedName.startsWith(normalizedQuery)) score -= 8;
      scored.push({ entry, score, localizedName, order });
    });
    return scored.sort((left, right) => left.score - right.score
      || left.localizedName.localeCompare(right.localizedName, I18n.language)
      || left.order - right.order).map((result) => result.entry);
  }

  function vectorFlagValues() {
    return normalizePickerSearch(vectorFlagSearchQuery) ? searchVectorFlags(vectorFlagSearchQuery) : vectorFlags;
  }

  function loadVectorFlagSettings() {
    let saved = {};
    try { saved = JSON.parse(localStorage.getItem(vectorFlagSettingsStorageKey) || '{}') || {}; }
    catch { saved = {}; }
    selectedVectorFlagCode = vectorFlagByCode.has(saved.code) ? saved.code
      : vectorFlagByCode.has(selectedVectorFlagCode) ? selectedVectorFlagCode
        : vectorFlagByCode.has('DE') ? 'DE' : vectorFlags[0]?.code || '';
    const size = clamp(num(saved.size, 160), 8, 2000);
    if ($('vectorFlagSize')) $('vectorFlagSize').value = String(round(size));
  }

  function persistVectorFlagSettings() {
    try {
      localStorage.setItem(vectorFlagSettingsStorageKey, JSON.stringify({
        code: selectedVectorFlagCode,
        size: clamp(num($('vectorFlagSize')?.value, 160), 8, 2000)
      }));
    } catch { /* Werkzeugwerte bleiben optional lokal gespeichert. */ }
  }

  function closeVectorFlagPicker({ focusCanvas = false } = {}) {
    const panel = $('vectorFlagPickerPanel'); const trigger = $('vectorFlagPickerButton');
    if (!panel || !trigger) return false;
    const wasOpen = !panel.hidden;
    panel.hidden = true; trigger.setAttribute('aria-expanded', 'false');
    if (focusCanvas) refs.workspace?.focus({ preventScroll: true });
    return wasOpen;
  }

  function positionVectorFlagPickerPanel() {
    const panel = $('vectorFlagPickerPanel'); const trigger = $('vectorFlagPickerButton');
    if (!panel || !trigger || panel.hidden) return;
    const triggerRect = trigger.getBoundingClientRect(); const panelRect = panel.getBoundingClientRect();
    const gap = 8; const edge = 8;
    const left = clamp(triggerRect.left, edge, Math.max(edge, window.innerWidth - panelRect.width - edge));
    const above = triggerRect.top - panelRect.height - gap; const below = triggerRect.bottom + gap;
    const top = above >= edge ? above : clamp(below, edge, Math.max(edge, window.innerHeight - panelRect.height - edge));
    panel.style.left = `${round(left)}px`; panel.style.top = `${round(top)}px`;
  }

  function renderVectorFlagPicker() {
    const search = $('vectorFlagSearch'); const list = $('vectorFlagList'); const empty = $('vectorFlagSearchEmpty');
    const pager = $('vectorFlagPager'); const pageOutput = $('vectorFlagPage'); const preview = $('vectorFlagPickerPreview');
    if (!search || !list || !empty || !pager || !pageOutput || !preview) return;
    if (search.value !== vectorFlagSearchQuery) search.value = vectorFlagSearchQuery;
    const allValues = vectorFlagValues();
    const pageCount = Math.max(1, Math.ceil(allValues.length / vectorFlagPageSize));
    vectorFlagPage = clamp(Math.floor(num(vectorFlagPage)), 0, pageCount - 1);
    const values = allValues.slice(vectorFlagPage * vectorFlagPageSize, (vectorFlagPage + 1) * vectorFlagPageSize);
    const fragment = document.createDocumentFragment();
    values.forEach((entry) => {
      const name = vectorFlagName(entry); const button = document.createElement('button'); const image = document.createElement('img');
      button.type = 'button'; button.className = 'emoji-button'; button.role = 'gridcell'; button.dataset.vectorFlagCode = entry.code;
      button.title = `${name} · ${entry.code}`;
      button.setAttribute('aria-label', I18n.t('ui.insertCountryFlag', { name, code: entry.code }));
      button.setAttribute('aria-pressed', String(entry.code === selectedVectorFlagCode)); button.tabIndex = -1;
      image.src = vectorFlagAssetUrl(entry); image.alt = ''; image.loading = 'lazy'; image.width = 40; image.height = 30;
      button.append(image); fragment.append(button);
    });
    list.replaceChildren(fragment);
    list.hidden = allValues.length === 0;
    empty.hidden = allValues.length > 0;
    pager.hidden = pageCount <= 1;
    pageOutput.textContent = I18n.t('ui.flagPageStatus', { page: vectorFlagPage + 1, pages: pageCount, count: allValues.length });
    $('vectorFlagPreviousPage').disabled = vectorFlagPage === 0;
    $('vectorFlagNextPage').disabled = vectorFlagPage + 1 >= pageCount;
    const selected = vectorFlagByCode.get(selectedVectorFlagCode);
    if (selected) preview.src = vectorFlagAssetUrl(selected);
    requestAnimationFrame(positionVectorFlagPickerPanel);
  }

  function openVectorFlagPicker() {
    if (state.tool !== 'vector-flag' || !vectorFlags.length) return;
    $('vectorFlagPickerPanel').hidden = false;
    $('vectorFlagPickerButton').setAttribute('aria-expanded', 'true');
    renderVectorFlagPicker(); positionVectorFlagPickerPanel();
    requestAnimationFrame(() => $('vectorFlagSearch')?.focus({ preventScroll: true }));
  }

  function selectVectorFlag(code) {
    if (!vectorFlagByCode.has(code)) return;
    selectedVectorFlagCode = code; persistVectorFlagSettings(); renderVectorFlagPicker();
    closeVectorFlagPicker({ focusCanvas: true });
  }

  function turnVectorFlagPage(direction) {
    const pageCount = Math.max(1, Math.ceil(vectorFlagValues().length / vectorFlagPageSize));
    vectorFlagPage = clamp(vectorFlagPage + direction, 0, pageCount - 1); renderVectorFlagPicker();
  }

  function buildVectorFlagPicker() {
    const picker = $('vectorFlagPicker'); const trigger = $('vectorFlagPickerButton'); const panel = $('vectorFlagPickerPanel');
    const search = $('vectorFlagSearch'); const list = $('vectorFlagList');
    if (!picker || !trigger || !panel || !search || !list) return;
    loadVectorFlagSettings(); renderVectorFlagPicker();
    trigger.disabled = !vectorFlags.length;
    trigger.addEventListener('click', () => panel.hidden ? openVectorFlagPicker() : closeVectorFlagPicker({ focusCanvas: true }));
    const updateSearch = () => {
      vectorFlagSearchQuery = search.value; vectorFlagPage = 0; renderVectorFlagPicker();
    };
    search.addEventListener('input', updateSearch);
    search.addEventListener('search', updateSearch);
    list.addEventListener('click', (event) => {
      const button = event.target.closest?.('[data-vector-flag-code]');
      if (button) selectVectorFlag(button.dataset.vectorFlagCode);
    });
    list.addEventListener('keydown', (event) => {
      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
      const buttons = [...list.querySelectorAll('[data-vector-flag-code]')]; if (!buttons.length) return;
      event.preventDefault();
      const columns = Math.max(1, getComputedStyle(list).gridTemplateColumns.split(' ').length);
      const current = Math.max(0, buttons.indexOf(document.activeElement));
      const index = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
        : event.key === 'ArrowLeft' ? Math.max(0, current - 1) : event.key === 'ArrowRight' ? Math.min(buttons.length - 1, current + 1)
          : event.key === 'ArrowUp' ? Math.max(0, current - columns) : Math.min(buttons.length - 1, current + columns);
      buttons[index].focus(); buttons[index].scrollIntoView({ block: 'nearest' });
    });
    $('vectorFlagPreviousPage').addEventListener('click', () => turnVectorFlagPage(-1));
    $('vectorFlagNextPage').addEventListener('click', () => turnVectorFlagPage(1));
    $('vectorFlagSize').addEventListener('change', () => {
      $('vectorFlagSize').value = String(round(clamp(num($('vectorFlagSize').value, 160), 8, 2000)));
      persistVectorFlagSettings();
    });
    panel.addEventListener('keydown', (event) => {
      if (event.key !== 'Escape') return;
      event.preventDefault();
      if (vectorFlagSearchQuery) {
        vectorFlagSearchQuery = ''; search.value = ''; vectorFlagPage = 0; renderVectorFlagPicker(); search.focus();
      } else closeVectorFlagPicker({ focusCanvas: true });
    });
    document.addEventListener('pointerdown', (event) => {
      if (!panel.hidden && !picker.contains(event.target) && !panel.contains(event.target)) closeVectorFlagPicker();
    });
    window.addEventListener('resize', positionVectorFlagPickerPanel);
    window.addEventListener('scroll', positionVectorFlagPickerPanel, true);
  }

  function loadNotoEmojiSourceChunk(filename) {
    const index = notoEmojiFileIndex.get(filename);
    if (!Number.isInteger(index)) return Promise.reject(new Error('Unbekannte Noto-Emoji-Datei'));
    const chunk = Math.floor(index / notoEmojiPageSize);
    if (notoEmojiSourceChunkPromises.has(chunk)) return notoEmojiSourceChunkPromises.get(chunk);
    const promise = new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = `assets/noto-emoji/source/chunk-${String(chunk).padStart(2, '0')}.js`;
      script.async = true;
      script.addEventListener('load', () => { script.remove(); resolve(); }, { once: true });
      script.addEventListener('error', () => {
        script.remove(); notoEmojiSourceChunkPromises.delete(chunk);
        reject(new Error('Die lokale Noto-Emoji-Quelle konnte nicht geladen werden'));
      }, { once: true });
      document.head.append(script);
    });
    notoEmojiSourceChunkPromises.set(chunk, promise); return promise;
  }

  async function notoEmojiMarkup(filename) {
    if (notoEmojiMarkupCache.has(filename)) return notoEmojiMarkupCache.get(filename);
    let source = '';
    if (location.protocol !== 'file:') {
      try {
        const response = await fetch(notoEmojiAssetUrl(filename));
        if (response.ok) source = await response.text();
      } catch { /* Der lokale Script-Fallback funktioniert ohne fetch(). */ }
    }
    if (!source) {
      await loadNotoEmojiSourceChunk(filename);
      source = notoEmojiSourceCache.get(filename) || '';
    }
    if (!source) throw new Error('Noto SVG Emoji konnte nicht geladen werden');
    const markup = Security.prepareSvgSource(source.replace(/^\uFEFF/, '')).source;
    notoEmojiMarkupCache.set(filename, markup);
    return markup;
  }

  function notoVectorObject(markup, filename) {
    const imported = SvgImport.parse(markup, {
      createId: uid, resolveColor: resolveSvgCssColor, resolveFont: resolveSvgFontFamily, measureText: measureSvgImportText
    });
    const objects = imported.layers.flatMap((layer) => Array.isArray(layer.objects) ? layer.objects : []);
    if (!objects.length) throw new Error('Das Noto SVG Emoji enthält keine unterstützten Vektorobjekte');
    const emoji = emojiFromNotoFilename(filename); const group = objectBase('group', 0, 0);
    group.name = `${emoji || 'Emoji'} · Noto SVG`; group.width = Math.max(1, num(imported.width, 128)); group.height = Math.max(1, num(imported.height, 128));
    group.stroke = 'none'; group.fill = 'none'; group.strokeWidth = 0; group.opacity = 100;
    group.children = objects.map(reidentifyObjectTree); group.notoEmoji = emoji; group.notoEmojiAsset = filename;
    return cleanObject(group);
  }

  async function insertNotoVectorEmoji(point) {
    if (notoEmojiInsertionPending || !selectedNotoEmojiFile) return false;
    const layer = activeLayer();
    if (!layer || layer.locked || !layer.visible) { toast('Die aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return false; }
    const documentId = state.activeDocumentId; const layerId = layer.id; const hint = $('vectorEmojiHint');
    notoEmojiInsertionPending = true; if (hint) hint.textContent = 'Emoji wird geladen …';
    try {
      const markup = await notoEmojiMarkup(selectedNotoEmojiFile);
      if (state.activeDocumentId !== documentId) { toast('Emoji-Einfügen abgebrochen, weil der Tab gewechselt wurde', 'warning'); return false; }
      const targetLayer = state.doc.layers.find((entry) => entry.id === layerId);
      if (!targetLayer || targetLayer.locked || !targetLayer.visible) { toast('Die aktive Ebene ist nicht mehr verfügbar', 'warning'); return false; }
      const source = notoVectorObject(markup, selectedNotoEmojiFile);
      const requestedSize = clamp(num($('vectorEmojiSize')?.value, 96), 8, 2000);
      const factor = requestedSize / Math.max(source.width, source.height); const object = scaleObjectTree(source, factor, factor);
      object.x = round(point.x - object.width / 2); object.y = round(point.y - object.height / 2);
      targetLayer.objects.push(object); state.selected = [object.id]; rememberNotoEmoji(selectedNotoEmojiFile);
      commitHistory('Vektor-Emoji eingefügt'); renderAll(); return true;
    } catch (error) {
      toast(error?.message || 'Noto SVG Emoji konnte nicht eingesetzt werden', 'error'); return false;
    } finally {
      notoEmojiInsertionPending = false;
      if (hint) hint.textContent = 'Emoji wählen · auf die Leinwand klicken';
    }
  }

  function loadVectorFlagSourceChunk(entry) {
    const index = vectorFlagFileIndex.get(entry?.file);
    if (!Number.isInteger(index)) return Promise.reject(new Error('Unbekannte flag-icons-Datei'));
    const chunk = Math.floor(index / vectorFlagPageSize);
    if (vectorFlagSourceChunkPromises.has(chunk)) return vectorFlagSourceChunkPromises.get(chunk);
    const promise = new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = `assets/flag-icons/source/chunk-${String(chunk).padStart(2, '0')}.js`;
      script.async = true;
      script.addEventListener('load', () => { script.remove(); resolve(); }, { once: true });
      script.addEventListener('error', () => {
        script.remove(); vectorFlagSourceChunkPromises.delete(chunk);
        reject(new Error('Die lokale flag-icons-Quelle konnte nicht geladen werden'));
      }, { once: true });
      document.head.append(script);
    });
    vectorFlagSourceChunkPromises.set(chunk, promise); return promise;
  }

  async function vectorFlagMarkup(entry) {
    if (vectorFlagMarkupCache.has(entry.file)) return vectorFlagMarkupCache.get(entry.file);
    let source = '';
    if (location.protocol !== 'file:') {
      try {
        const response = await fetch(vectorFlagAssetUrl(entry));
        if (response.ok) source = await response.text();
      } catch { /* Der lokale Script-Fallback funktioniert ohne fetch(). */ }
    }
    if (!source) {
      await loadVectorFlagSourceChunk(entry);
      source = vectorFlagSourceCache.get(entry.file) || '';
    }
    if (!source) throw new Error('flag-icons SVG konnte nicht geladen werden');
    const markup = Security.prepareSvgSource(source.replace(/^\uFEFF/, '')).source;
    vectorFlagMarkupCache.set(entry.file, markup);
    return markup;
  }

  function vectorFlagObject(markup, entry) {
    const imported = SvgImport.parse(markup, {
      createId: uid, resolveColor: resolveSvgCssColor, resolveFont: resolveSvgFontFamily, measureText: measureSvgImportText
    });
    const objects = imported.layers.flatMap((layer) => Array.isArray(layer.objects) ? layer.objects : []);
    if (!objects.length) throw new Error('Die flag-icons-Landesflagge enthält keine unterstützten Vektorobjekte');
    const group = objectBase('group', 0, 0);
    group.name = `${vectorFlagName(entry)} · flag-icons`;
    group.width = Math.max(1, num(imported.width, 32)); group.height = Math.max(1, num(imported.height, 24));
    group.stroke = 'none'; group.fill = 'none'; group.strokeWidth = 0; group.opacity = 100;
    const content = objectBase('group', 0, 0);
    content.name = `${vectorFlagName(entry)} · Vektorinhalt`;
    content.width = group.width; content.height = group.height;
    content.stroke = 'none'; content.fill = 'none'; content.strokeWidth = 0; content.opacity = 100;
    content.children = objects.map(reidentifyObjectTree);
    const viewport = objectBase('rectangle', 0, 0);
    viewport.name = 'Flaggenformat 4:3'; viewport.width = group.width; viewport.height = group.height;
    viewport.fill = '#000000'; viewport.stroke = 'none'; viewport.strokeWidth = 0; viewport.opacity = 100; viewport.clipRole = 'clip';
    const clipped = objectBase('clip-group', 0, 0);
    clipped.name = 'Flagge auf 4:3 begrenzt'; clipped.width = group.width; clipped.height = group.height;
    clipped.stroke = 'none'; clipped.fill = 'none'; clipped.strokeWidth = 0; clipped.opacity = 100;
    clipped.children = [content, viewport];
    group.children = [clipped];
    group.countryFlagCode = entry.code; group.countryFlagAsset = entry.file;
    return cleanObject(group);
  }

  async function insertVectorFlag(point) {
    const entry = vectorFlagByCode.get(selectedVectorFlagCode);
    if (vectorFlagInsertionPending || !entry) return false;
    const layer = activeLayer();
    if (!layer || layer.locked || !layer.visible) { toast('Die aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return false; }
    const documentId = state.activeDocumentId; const layerId = layer.id; const hint = $('vectorFlagHint');
    vectorFlagInsertionPending = true; if (hint) hint.textContent = 'Flagge wird geladen …';
    try {
      const markup = await vectorFlagMarkup(entry);
      if (state.activeDocumentId !== documentId) { toast('Flaggen-Einfügen abgebrochen, weil der Tab gewechselt wurde', 'warning'); return false; }
      const targetLayer = state.doc.layers.find((candidate) => candidate.id === layerId);
      if (!targetLayer || targetLayer.locked || !targetLayer.visible) { toast('Die aktive Ebene ist nicht mehr verfügbar', 'warning'); return false; }
      const source = vectorFlagObject(markup, entry);
      const requestedSize = clamp(num($('vectorFlagSize')?.value, 160), 8, 2000);
      const factor = requestedSize / Math.max(source.width, source.height); const object = scaleObjectTree(source, factor, factor);
      object.x = round(point.x - object.width / 2); object.y = round(point.y - object.height / 2);
      targetLayer.objects.push(object); state.selected = [object.id]; persistVectorFlagSettings();
      commitHistory('Landesflagge eingefügt'); renderAll(); return true;
    } catch (error) {
      toast(error?.message || 'flag-icons-Landesflagge konnte nicht eingesetzt werden', 'error'); return false;
    } finally {
      vectorFlagInsertionPending = false;
      if (hint) hint.textContent = 'Flagge wählen · auf die Leinwand klicken';
    }
  }

  function openInlineTextEditor(point, object = null) {
    if (state.inlineTextSession) finishInlineText();
    const entry = object?.type === 'text' ? objectEntry(object.id) : null;
    const layer = entry?.layer || activeLayer();
    if (!layer || layer.locked || !layer.visible || object?.locked || object?.visible === false) {
      toast(object ? 'Der Text oder seine Ebene ist gesperrt oder ausgeblendet' : 'Die aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return;
    }
    const boundToPath = object && textPathBinding(object);
    const editorPoint = object && !boundToPath ? { x: object.x, y: object.y } : point;
    state.selected = object ? [object.id] : [];
    state.inlineTextSession = {
      point: { x: round(editorPoint.x), y: round(editorPoint.y) }, layerId: layer.id,
      objectId: object?.id || null
    };
    const editor = refs.inlineText; editor.value = object?.text || ''; editor.dataset.mode = object ? 'edit' : 'create'; editor.hidden = false;
    syncSpecialCharacterButtons();
    renderAll();
    requestAnimationFrame(() => {
      editor.focus();
      if (object) editor.setSelectionRange(editor.value.length, editor.value.length);
      positionInlineTextEditor();
    });
  }

  function finishInlineText({ cancel = false, showObjectProperties = false } = {}) {
    const session = state.inlineTextSession; const editor = refs.inlineText;
    if (!session || !editor) return;
    state.inlineTextSession = null;
    syncSpecialCharacterButtons();
    const text = editor.value.replace(/\r/g, ''); editor.value = ''; editor.hidden = true; delete editor.dataset.mode;
    let resultObject = session.objectId ? objectEntry(session.objectId)?.object : null;
    if (resultObject) {
      const layer = layerForObject(resultObject.id);
      if (!cancel && text.trim() && layer?.visible && !layer.locked && !resultObject.locked && resultObject.visible !== false && text !== resultObject.text) {
        const previousText = resultObject.text;
        resultObject.text = text; delete resultObject.textKey; syncTextObjectName(resultObject, previousText); sizeTextObject(resultObject); commitHistory('Text inline bearbeitet');
      }
      state.selected = resultObject ? [resultObject.id] : [];
    } else if (!cancel && text.trim()) {
      const layer = state.doc.layers.find((item) => item.id === session.layerId);
      if (layer && layer.visible && !layer.locked) {
        const object = objectBase('text', session.point.x, session.point.y);
        applyCurrentTextSettings(object); object.text = text;
        object.fill = $('fillColor').value; object.stroke = 'none'; object.strokeWidth = 0;
        syncTextObjectName(object);
        sizeTextObject(object); layer.objects.push(object); state.selected = [object.id]; resultObject = object; commitHistory('Text eingefügt');
      } else toast('Die aktive Ebene ist gesperrt oder ausgeblendet', 'warning');
    }
    if (resultObject && showObjectProperties) chooseTool('select');
    renderAll();
  }

  function openTextDialog(point, object = null) {
    const layer = object ? layerForObject(object.id) : activeLayer();
    if (!layer || layer.locked || !layer.visible) { toast('Die Ebene ist gesperrt oder ausgeblendet', 'warning'); return; }
    state.pendingTextPoint = point; state.editingTextId = object?.id || null;
    $('textDialogInput').value = object?.text || '';
    $('textDialog').showModal(); requestAnimationFrame(() => $('textDialogInput').focus());
  }

  function closeTextDialog() {
    $('textDialog').close(); state.editingTextId = null; state.pendingTextPoint = null;
  }

  function confirmText() {
    const text = $('textDialogInput').value; if (!text.trim()) { closeTextDialog(); return; }
    if (state.editingTextId) {
      const object = objectEntry(state.editingTextId)?.object;
      if (object) { const previousText = object.text; object.text = text; delete object.textKey; syncTextObjectName(object, previousText); sizeTextObject(object); commitHistory('Text bearbeitet'); }
    } else {
      const point = state.pendingTextPoint || { x: 40, y: 40 }; const object = objectBase('text', point.x, point.y);
      applyCurrentTextSettings(object); object.text = text;
      const layer = activeLayer();
      if (!layer || layer.locked || !layer.visible) { $('textDialog').close(); toast('Die aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return; }
      object.fill = $('fillColor').value; syncTextObjectName(object); sizeTextObject(object); layer.objects.push(object); state.selected = [object.id]; commitHistory('Text eingefügt');
    }
    closeTextDialog(); renderAll();
  }

  function sizeTextObject(object) {
    const lines = String(object.text || '').split('\n'); const size = num(object.fontSize, 32); const settings = textSettings(object);
    const naturalWidth = Math.max(...lines.map((line) => Math.max(1, line.length) * size * .62 + Math.max(0, line.length - 1) * settings.letterSpacing), 16);
    const naturalHeight = Math.max(size * 1.2, size + Math.max(0, lines.length - 1) * size * settings.lineHeight / 100 + size * .2);
    object.width = Math.max(16, naturalWidth * settings.horizontalScale / 100);
    object.height = Math.max(size * .25, naturalHeight * settings.verticalScale / 100);
  }

  function textOutlineRegion(object) {
    const binding = textPathBinding(object); const target = binding?.target || object;
    const bounds = transformedObjectBounds(target);
    const padding = Math.max(18, num(object.fontSize, 32) * 1.6 + num(object.textStrokeWidth, 1) * 2) + (binding?.binding.distance || 0);
    const x = Math.floor(bounds.x - padding); const y = Math.floor(bounds.y - padding);
    return { x, y, width: Math.max(2, Math.ceil(bounds.width + padding * 2)), height: Math.max(2, Math.ceil(bounds.height + padding * 2)) };
  }

  function textCanvasFont(object) {
    const fontSize = Math.max(1, num(object.fontSize, 32));
    return `${object.italic ? 'italic ' : ''}${object.bold ? '700 ' : '400 '}${fontSize}px ${object.fontFamily || DEFAULT_FONT_FAMILY}`;
  }

  function primaryCanvasFontFamily(object) {
    const family = String(object.fontFamily || DEFAULT_FONT_FAMILY).split(',')[0].trim();
    return family.replace(/^(['"])(.*)\1$/, '$2');
  }

  async function prepareTextCanvasFont(object) {
    if (!document.fonts?.load) return;
    const family = primaryCanvasFontFamily(object); const wantedStyle = object.italic ? 'italic' : 'normal'; const wantedWeight = object.bold ? 700 : 400;
    let faces = [...document.fonts].filter((face) => String(face.family || '').replace(/^(['"])(.*)\1$/, '$2') === family && face.style === wantedStyle)
      .sort((left, right) => Math.abs(num(left.weight, 400) - wantedWeight) - Math.abs(num(right.weight, 400) - wantedWeight)).slice(0, 1);
    const manifestEntries = Array.isArray(globalThis.PYRO_FONT_MANIFEST?.fonts)
      ? globalThis.PYRO_FONT_MANIFEST.fonts.filter((entry) => entry?.family === family && /^fonts\/.+\.(?:ttf|otf)$/i.test(String(entry.file || '')))
      : [];
    if (!faces.length && manifestEntries.length) {
      const entry = [...manifestEntries].sort((left, right) => {
        const leftStyle = left.style === wantedStyle ? 0 : 1; const rightStyle = right.style === wantedStyle ? 0 : 1;
        return leftStyle - rightStyle || Math.abs(num(left.weight, 400) - wantedWeight) - Math.abs(num(right.weight, 400) - wantedWeight);
      })[0];
      try {
        const face = new FontFace(family, `url(${JSON.stringify(new URL(entry.file, document.baseURI).href)})`, {
          style: entry.style === 'italic' ? 'italic' : 'normal', weight: String(entry.weight || 400), display: 'swap'
        });
        document.fonts.add(face); faces = [face];
      } catch { /* Die nachfolgende FontFaceSet-Prüfung liefert eine verständliche Fehlermeldung. */ }
    }
    if (faces.length) {
      const loaded = await Promise.race([
        Promise.allSettled(faces.map((face) => face.load())).then((results) => results.some((result) => result.status === 'fulfilled' && result.value?.status === 'loaded')),
        new Promise((resolve) => setTimeout(() => resolve(false), 4000))
      ]);
      if (!loaded) throw new Error(`Die Schrift „${family}“ konnte nicht für die Pfadumwandlung geladen werden`);
    }
    const sample = String(object.text || '').replace(/\s+/g, '').slice(0, 48) || 'M';
    // Nur die tatsächlich verwendete Schrift anstoßen. document.fonts.ready würde
    // beim Direktstart auch auf unbenutzte Dateischriften warten und kann hängen.
    await Promise.race([
      document.fonts.load(textCanvasFont(object), sample).catch(() => []),
      new Promise((resolve) => setTimeout(resolve, 2500))
    ]);
  }

  function canvasTransform(context, matrix) {
    context.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
  }

  function drawTextMaskRun(context, object, text, x, y) {
    const fill = String(object.fill || '').toLowerCase();
    const hasFill = !['none', 'transparent', ''].includes(fill) || Boolean(normalizedObjectFillPaint(object)) || hasActiveGlow(object) || hasActiveTextTexture(object);
    const stroke = String(object.textStroke || '').toLowerCase();
    const hasStroke = objectStrokeType(object) !== 'none' && !['none', 'transparent', ''].includes(stroke) && num(object.textStrokeWidth, 0) > 0;
    if (hasStroke) context.strokeText(text, x, y);
    if (hasFill) context.fillText(text, x, y);
  }

  function configureTextMaskContext(context, object) {
    const settings = textSettings(object); const fontSize = Math.max(1, num(object.fontSize, 32));
    context.fillStyle = '#ffffff'; context.strokeStyle = '#ffffff'; context.textBaseline = 'alphabetic'; context.font = textCanvasFont(object);
    context.lineWidth = Math.max(.1, num(object.textStrokeWidth, 1)); context.lineJoin = 'round';
    if ('fontKerning' in context) context.fontKerning = settings.autoKern ? 'normal' : 'none';
    if ('letterSpacing' in context) context.letterSpacing = `${settings.letterSpacing}px`;
    return { settings, fontSize };
  }

  function drawTextMaskDecoration(context, settings, fontSize, text, x, y) {
    if (!settings.underline && !settings.strike) return;
    const width = context.measureText(text).width; let start = x;
    if (context.textAlign === 'center') start -= width / 2; else if (context.textAlign === 'right') start -= width;
    context.lineWidth = Math.max(1, fontSize * .055); context.beginPath();
    if (settings.underline) { context.moveTo(start, y + fontSize * .12); context.lineTo(start + width, y + fontSize * .12); }
    if (settings.strike) { context.moveTo(start, y - fontSize * .3); context.lineTo(start + width, y - fontSize * .3); }
    context.stroke();
  }

  function drawLocalTextMask(context, object) {
    const { settings, fontSize } = configureTextMaskContext(context, object);
    const scaleX = settings.horizontalScale / 100; const scaleY = settings.verticalScale / 100;
    const naturalWidth = Math.max(1, num(object.width, 1) / scaleX);
    const anchor = settings.align === 'center' ? naturalWidth / 2 : settings.align === 'right' ? naturalWidth : 0;
    context.scale(scaleX, scaleY); context.textAlign = settings.align === 'center' ? 'center' : settings.align === 'right' ? 'right' : 'left';
    String(object.text || '').split('\n').forEach((line, index) => {
      const text = line || ' '; const y = fontSize + settings.baselineShift + index * fontSize * settings.lineHeight / 100;
      drawTextMaskRun(context, object, text, anchor, y); drawTextMaskDecoration(context, settings, fontSize, text, anchor, y);
    });
  }

  function drawUnboundTextMask(context, object) {
    const objectMatrix = Core.objectMatrix(object); const transform = activeFreeTransform(object);
    if (!transform || FreeTransform.isIdentity(transform)) {
      context.save(); canvasTransform(context, objectMatrix); drawLocalTextMask(context, object); context.restore(); return;
    }
    const width = Math.max(1, num(object.width, 1)); const height = Math.max(1, num(object.height, 1));
    FreeTransform.triangles(transform, width, height).forEach((triangle) => {
      context.save(); canvasTransform(context, objectMatrix); canvasTransform(context, triangle.matrix);
      context.beginPath(); triangle.source.forEach((point, index) => context[index ? 'lineTo' : 'moveTo'](point.x, point.y)); context.closePath(); context.clip();
      drawLocalTextMask(context, object); context.restore();
    });
  }

  function drawBoundTextMask(context, object, pathBinding) {
    const layout = boundTextLayout(object, pathBinding); if (!layout) return;
    const { settings, fontSize } = configureTextMaskContext(context, object);
    const scaleX = settings.horizontalScale / 100; const scaleY = settings.verticalScale / 100;
    layout.placements.forEach(({ character, point, rotation }) => {
      const text = character || ' ';
      context.save(); context.translate(point.x, point.y); context.rotate(rotation * Math.PI / 180); context.scale(scaleX, scaleY); context.textAlign = 'center';
      drawTextMaskRun(context, object, text, 0, settings.baselineShift); drawTextMaskDecoration(context, settings, fontSize, text, 0, settings.baselineShift); context.restore();
    });
  }

  function contourDirection(edge) {
    if (edge.x2 > edge.x1) return 0;
    if (edge.y2 > edge.y1) return 1;
    if (edge.x2 < edge.x1) return 2;
    return 3;
  }

  function traceAlphaContours(alpha, width, height, threshold = 96) {
    const opaque = new Uint8Array(width * height);
    for (let index = 0; index < opaque.length; index += 1) opaque[index] = alpha[index * 4 + 3] >= threshold ? 1 : 0;
    const filled = (x, y) => x >= 0 && y >= 0 && x < width && y < height && opaque[y * width + x] === 1;
    const edges = [];
    const add = (x1, y1, x2, y2) => edges.push({ x1, y1, x2, y2 });
    for (let y = 0; y < height; y += 1) for (let x = 0; x < width; x += 1) {
      if (!filled(x, y)) continue;
      if (!filled(x, y - 1)) add(x, y, x + 1, y);
      if (!filled(x + 1, y)) add(x + 1, y, x + 1, y + 1);
      if (!filled(x, y + 1)) add(x + 1, y + 1, x, y + 1);
      if (!filled(x - 1, y)) add(x, y + 1, x, y);
    }
    const starts = new Map();
    edges.forEach((edge, index) => {
      edge.direction = contourDirection(edge); const key = `${edge.x1},${edge.y1}`;
      if (!starts.has(key)) starts.set(key, []); starts.get(key).push(index);
    });
    const used = new Uint8Array(edges.length); const contours = [];
    for (let startIndex = 0; startIndex < edges.length; startIndex += 1) {
      if (used[startIndex]) continue;
      const start = edges[startIndex]; const contour = [[start.x1, start.y1]]; let currentIndex = startIndex; let closed = false;
      for (let steps = 0; steps <= edges.length; steps += 1) {
        const current = edges[currentIndex]; used[currentIndex] = 1; contour.push([current.x2, current.y2]);
        if (current.x2 === start.x1 && current.y2 === start.y1) { closed = true; break; }
        const candidates = (starts.get(`${current.x2},${current.y2}`) || []).filter((index) => !used[index]);
        if (!candidates.length) break;
        const priorities = [current.direction, (current.direction + 1) % 4, (current.direction + 3) % 4, (current.direction + 2) % 4];
        candidates.sort((a, b) => priorities.indexOf(edges[a].direction) - priorities.indexOf(edges[b].direction));
        currentIndex = candidates[0];
      }
      if (closed && contour.length >= 4) contours.push(contour.slice(0, -1));
    }
    return contours;
  }

  function pointDistanceToSegment(point, start, end) {
    const dx = end[0] - start[0]; const dy = end[1] - start[1];
    if (dx === 0 && dy === 0) return Math.hypot(point[0] - start[0], point[1] - start[1]);
    const progress = clamp(((point[0] - start[0]) * dx + (point[1] - start[1]) * dy) / (dx * dx + dy * dy), 0, 1);
    return Math.hypot(point[0] - (start[0] + dx * progress), point[1] - (start[1] + dy * progress));
  }

  function simplifyOpenContour(points, tolerance) {
    if (points.length < 3) return points;
    const keep = new Uint8Array(points.length); keep[0] = 1; keep[points.length - 1] = 1; const segments = [[0, points.length - 1]];
    while (segments.length) {
      const [start, end] = segments.pop(); let farthest = -1; let distance = tolerance;
      for (let index = start + 1; index < end; index += 1) {
        const next = pointDistanceToSegment(points[index], points[start], points[end]);
        if (next > distance) { distance = next; farthest = index; }
      }
      if (farthest >= 0) { keep[farthest] = 1; segments.push([start, farthest], [farthest, end]); }
    }
    return points.filter((_, index) => keep[index]);
  }

  function simplifyClosedContour(points, tolerance) {
    if (points.length < 5) return points;
    const first = points[0]; let split = 1; let farthest = -1;
    points.forEach((point, index) => {
      const distance = (point[0] - first[0]) ** 2 + (point[1] - first[1]) ** 2;
      if (distance > farthest) { farthest = distance; split = index; }
    });
    if (split <= 0 || split >= points.length - 1) return points;
    const firstArc = simplifyOpenContour(points.slice(0, split + 1), tolerance);
    const secondArc = simplifyOpenContour([...points.slice(split), first], tolerance);
    return [...firstArc.slice(0, -1), ...secondArc.slice(0, -1)];
  }

  async function textOutlineFromObject(object) {
    const region = textOutlineRegion(object); const desiredScale = 4;
    const maximumPixels = 12_000_000; const scale = Math.min(desiredScale, 4096 / region.width, 4096 / region.height, Math.sqrt(maximumPixels / (region.width * region.height)));
    const rasterWidth = Math.max(1, Math.round(region.width * scale)); const rasterHeight = Math.max(1, Math.round(region.height * scale));
    await prepareTextCanvasFont(object);
    const canvas = document.createElement('canvas'); canvas.width = rasterWidth; canvas.height = rasterHeight;
    const context = canvas.getContext('2d', { willReadFrequently: true });
    if (!context) throw new Error('Kein Canvas-Kontext verfügbar');
    const scaleX = rasterWidth / region.width; const scaleY = rasterHeight / region.height;
    context.setTransform(scaleX, 0, 0, scaleY, -region.x * scaleX, -region.y * scaleY);
    const binding = textPathBinding(object);
    if (binding) drawBoundTextMask(context, object, binding); else drawUnboundTextMask(context, object);
    const contours = traceAlphaContours(context.getImageData(0, 0, rasterWidth, rasterHeight).data, rasterWidth, rasterHeight);
    const tolerance = Math.max(.28, .8 / Math.min(scaleX, scaleY));
    const paths = contours.map((contour) => simplifyClosedContour(contour.map(([x, y]) => [region.x + x / scaleX, region.y + y / scaleY]), tolerance)).filter((contour) => contour.length >= 3);
    const points = paths.flat(); if (!points.length) throw new Error('Der Text enthält keine sichtbaren Pfade');
    const minX = Math.min(...points.map(([x]) => x)); const minY = Math.min(...points.map(([, y]) => y));
    const maxX = Math.max(...points.map(([x]) => x)); const maxY = Math.max(...points.map(([, y]) => y));
    const outlineContours = paths.map((contour) => contour.map(([x, y]) => [round(x - minX, 3), round(y - minY, 3)]));
    const pathData = outlinePathData(outlineContours);
    const invisibleText = ['none', 'transparent'].includes(String(object.fill || '').toLowerCase());
    const glowOnly = invisibleText && hasActiveGlow(object);
    const outline = {
      id: object.id, type: 'text-outline', name: `${object.name || 'Text'} – Pfade`, x: round(minX, 3), y: round(minY, 3),
      width: Math.max(.1, round(maxX - minX, 3)), height: Math.max(.1, round(maxY - minY, 3)),
      pathWidth: Math.max(.1, round(maxX - minX, 3)), pathHeight: Math.max(.1, round(maxY - minY, 3)), pathData, outlineContours,
      rotation: 0, opacity: clamp(num(object.opacity, 100), 0, 100), visible: object.visible !== false, locked: Boolean(object.locked),
      blendMode: normalizeBlendMode(object.blendMode), fill: object.fill, stroke: object.textStroke || 'none', strokeWidth: num(object.textStrokeWidth, 1),
      strokeType: objectStrokeType(object), strokeStack: deepClone(strokeStackFor(object)), effects: deepClone(effectStack(object)), glowOnly,
      sourceText: String(object.text || '')
    };
    if (object.fillPaint) outline.fillPaint = deepClone(object.fillPaint);
    return outline;
  }

  async function convertSelectedTextToPath() {
    const object = primaryObject();
    if (object?.type !== 'text') { toast('Wähle einen Text aus', 'warning'); return; }
    const button = $('convertTextToPathBtn'); const label = button.textContent; button.disabled = true; button.textContent = 'Wandle um…';
    try {
      const outline = await textOutlineFromObject(object); const entry = objectEntry(object.id);
      if (!entry || entry.object !== object) { toast('Die Textauswahl wurde während der Umwandlung geändert', 'warning'); return; }
      entry.layer.objects.splice(entry.index, 1, outline); state.selected = [outline.id];
      commitHistory('Text in Pfade umgewandelt'); renderAll(); toast('Text wurde in editierbare Vektorpfade umgewandelt');
    } catch (error) {
      toast(error?.message || 'Text konnte nicht in Pfade umgewandelt werden', 'warning');
    } finally {
      button.disabled = false; button.textContent = label;
    }
  }

  function saveBlob(blob, name) {
    const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = name; anchor.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  function cleanSvgClone({ includeBackground = true, responsive = false, keepMetadata = true } = {}) {
    const clone = refs.artboard.cloneNode(true);
    clone.querySelectorAll('[data-ui-overlay], [data-bitmap-bleed]').forEach((node) => node.remove());
    clone.removeAttribute('data-document-pixel-preview');
    if (!includeBackground) clone.querySelectorAll('[data-canvas-background]').forEach((node) => node.remove());
    clone.removeAttribute('style');
    if (responsive) {
      clone.removeAttribute('width'); clone.removeAttribute('height');
    } else {
      clone.setAttribute('width', state.doc.width); clone.setAttribute('height', state.doc.height);
    }
    clone.setAttribute('xmlns', svgNS);
    if (!keepMetadata) {
      [clone, ...clone.querySelectorAll('*')].forEach((node) => {
        [...node.attributes].filter((attribute) => attribute.name.startsWith('data-')).forEach((attribute) => node.removeAttribute(attribute.name));
      });
    }
    return clone;
  }

  function cleanSvgString(options = {}) {
    return new XMLSerializer().serializeToString(cleanSvgClone(options));
  }

  const autoSaveIntervalMs = 5 * 60 * 1000;
  let autoSaveInProgress = false;
  let autoSaveTimer = null;

  function projectPayload(document = activeDocument()) {
    return Core.createProjectPayload({
      title: document?.title || 'Unbenannt',
      document: document?.doc || state.doc,
      savedAt: new Date().toISOString()
    }, { createId: uid });
  }

  async function projectContainerBytes(document = activeDocument(), { preview = false } = {}) {
    const previewSvg = preview && document?.id === state.activeDocumentId ? cleanSvgString() : '';
    return (await ProjectContainer.create(projectPayload(document), { previewSvg })).bytes;
  }

  function autoSaveAvailable(document = activeDocument()) {
    return Boolean(document?.hasSavedVersion && document?.fileHandle?.createWritable);
  }

  function syncAutoSaveControl() {
    const toggle = $('autoSaveToggle'); const label = $('autoSaveLabel'); const document = activeDocument();
    if (!toggle || !document) return;
    const available = autoSaveAvailable(document);
    if (!available) document.autoSaveEnabled = false;
    toggle.checked = Boolean(document.autoSaveEnabled && available); toggle.disabled = !available;
    label.title = available
      ? 'Automatisch alle 5 Minuten in die aktuelle Projektdatei speichern'
      : document.hasSavedVersion
        ? 'Für automatisches Speichern einmal Speichern wählen, damit ein fester Speicherort bekannt ist'
        : 'Automatisches Speichern wird nach dem ersten Speichern verfügbar';
  }

  function scheduleAutoSave() {
    if (autoSaveTimer) { window.clearTimeout(autoSaveTimer); autoSaveTimer = null; }
    if (!state.documents.some((document) => document.autoSaveEnabled && autoSaveAvailable(document))) return;
    autoSaveTimer = window.setTimeout(async () => {
      autoSaveTimer = null;
      await autoSaveProjects();
      scheduleAutoSave();
    }, autoSaveIntervalMs);
  }

  function setAutoSaveEnabled(enabled) {
    const document = activeDocument(); if (!document) return;
    if (enabled && !autoSaveAvailable(document)) {
      document.autoSaveEnabled = false; syncAutoSaveControl();
      toast(document.hasSavedVersion ? 'Für automatisches Speichern einmal Speichern wählen' : 'Speichere das Projekt zuerst', 'warning');
      return;
    }
    document.autoSaveEnabled = Boolean(enabled); persistActiveDocument(); syncAutoSaveControl(); scheduleAutoSave();
    toast(document.autoSaveEnabled ? 'Automatisches Speichern aktiviert – Speicherung alle 5 Minuten' : 'Automatisches Speichern deaktiviert');
  }

  function markProjectSaved(message, notify = true) {
    const value = snapshot();
    // Der gespeicherte Stand muss zugleich am aktuellen History-Zeiger liegen.
    // Einige Render-/Importpfade normalisieren Daten, ohne einen eigenen
    // Benutzerschritt anzulegen. Ohne diesen Checkpoint würde Undo nach der
    // nächsten Änderung an dem tatsächlich gespeicherten Stand vorbeispringen.
    if (state.history[state.historyIndex]?.value !== value) {
      state.history.splice(state.historyIndex + 1);
      state.history.push({ id: uid('history'), value, label: 'Gespeicherter Stand', bytes: value.length * 2, createdAt: Date.now() });
      state.historyIndex = state.history.length - 1;
      trimHistory({ pruneAssets: true });
    }
    state.savedSnapshot = value; state.savedTitle = activeDocument()?.title || 'Unbenannt'; state.dirty = false; state.hasSavedVersion = true;
    persistActiveDocument(); renderDocumentTabs(); syncAutoSaveControl(); updateCommandState();
    if (notify && message) toast(message);
  }

  async function writeProjectToHandle(handle, content) {
    const writable = await handle.createWritable();
    await writable.write(content);
    await writable.close();
  }

  async function autoSaveProjects() {
    if (autoSaveInProgress) return;
    finishInlineText(); persistActiveDocument();
    const documents = state.documents.filter((document) => document.autoSaveEnabled && document.dirty && autoSaveAvailable(document));
    if (!documents.length) return;
    autoSaveInProgress = true;
    let savedAny = false;
    try {
      for (const document of documents) {
        const savedSnapshot = snapshot(document.doc, document.historyAssets);
        const savedTitle = document.title;
        try {
          const payload = await projectContainerBytes(document, { preview: document.id === state.activeDocumentId });
          await writeProjectToHandle(document.fileHandle, payload);
          if (document.title === savedTitle && snapshot(document.doc, document.historyAssets) === savedSnapshot) {
            document.savedSnapshot = savedSnapshot;
            document.savedTitle = savedTitle;
            document.dirty = false; document.hasSavedVersion = true; savedAny = true;
            if (document.id === state.activeDocumentId) {
              state.savedSnapshot = savedSnapshot; state.savedTitle = savedTitle;
              state.dirty = false; state.hasSavedVersion = true;
            }
          }
        } catch {
          document.autoSaveEnabled = false;
          if (document.id === state.activeDocumentId) toast('Automatisches Speichern fehlgeschlagen – bitte erneut Speichern wählen', 'warning');
        }
      }
    } finally {
      const document = activeDocument();
      if (document) {
        state.savedSnapshot = document.savedSnapshot ?? null; state.savedTitle = document.savedTitle || document.title;
        state.dirty = document.dirty; state.hasSavedVersion = Boolean(document.hasSavedVersion);
      }
      if (savedAny) { renderDocumentTabs(); updateCommandState(); }
      syncAutoSaveControl(); autoSaveInProgress = false;
    }
  }

  async function saveProject() {
    finishInlineText();
    if (typeof window.showSaveFilePicker !== 'function') {
      try {
        const payload = await projectContainerBytes(activeDocument(), { preview: true });
        saveBlob(new Blob([payload], { type: ProjectContainer.MEDIA_TYPE }), filename('pyro'));
        markProjectSaved('Projekt gespeichert');
      } catch {
        toast('Projekt konnte nicht gespeichert werden', 'error');
      }
      return;
    }
    try {
      const handle = await window.showSaveFilePicker({
        suggestedName: filename('pyro'),
        types: [{ description: 'PYRO-Projekt', accept: { [ProjectContainer.MEDIA_TYPE]: ['.pyro'] } }]
      });
      await writeProjectToHandle(handle, await projectContainerBytes(activeDocument(), { preview: true }));
      activeDocument().fileHandle = handle;
      markProjectSaved(`Projekt gespeichert unter „${handle.name}“`);
    } catch (error) {
      if (error?.name !== 'AbortError') toast('Projekt konnte nicht gespeichert werden', 'error');
    }
  }

  const pngWithDpi = Png.withDpi;

  const exportMaximumCanvasDimension = 16384;
  const exportMaximumCanvasPixels = 100_000_000;
  let exportDialogBusy = false;

  function exportFontFamily(value) {
    return String(value || DEFAULT_FONT_FAMILY).split(',')[0].trim().replace(/^(['"])(.*)\1$/, '$2');
  }

  function usedExportFonts() {
    const requests = new Map();
    const add = (familyValue, styleValue = 'normal', weightValue = 400) => {
      const family = exportFontFamily(familyValue); if (!family) return;
      const style = styleValue === 'italic' ? 'italic' : 'normal';
      const weight = clamp(Math.round(num(weightValue, 400)), 100, 900);
      requests.set(`${family}\u0000${style}\u0000${weight}`, { family, style, weight });
    };
    renderableObjects().filter((object) => object?.type === 'text').forEach((object) => {
      const family = object.fontFamily || DEFAULT_FONT_FAMILY;
      const style = object.italic ? 'italic' : 'normal'; const weight = object.bold ? 700 : 400;
      add(family, style, weight);
      if (Array.isArray(object.svgTextRuns) && object.svgTextRunsSource === object.text) {
        object.svgTextRuns.forEach((run) => add(run?.fontFamily || family, run?.italic ? 'italic' : style, run?.bold ? 700 : weight));
      }
    });
    return [...requests.values()];
  }

  function exportFontMimeType(file = '') {
    if (/\.otf(?:$|[?#])/i.test(file)) return 'font/otf';
    if (/\.woff2(?:$|[?#])/i.test(file)) return 'font/woff2';
    if (/\.woff(?:$|[?#])/i.test(file)) return 'font/woff';
    return 'font/ttf';
  }

  function exportFontFormat(file = '') {
    if (/\.otf(?:$|[?#])/i.test(file)) return 'opentype';
    if (/\.woff2(?:$|[?#])/i.test(file)) return 'woff2';
    if (/\.woff(?:$|[?#])/i.test(file)) return 'woff';
    return 'truetype';
  }

  function dataUrlFromFontData(data, type) {
    return new Promise((resolveDataUrl, rejectDataUrl) => {
      const reader = new FileReader();
      reader.onload = () => resolveDataUrl(String(reader.result || ''));
      reader.onerror = () => rejectDataUrl(reader.error || new Error('Schriftdatei konnte nicht gelesen werden'));
      reader.readAsDataURL(new Blob([data], { type }));
    });
  }

  function cssQuoted(value) {
    return `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
  }

  async function embedUsedFonts(svg) {
    const requests = usedExportFonts();
    if (!requests.length) return { embeddedFiles: 0, unavailableFamilies: [] };
    const manifestEntries = Array.isArray(globalThis.PYRO_FONT_MANIFEST?.fonts)
      ? globalThis.PYRO_FONT_MANIFEST.fonts.filter((entry) => entry?.family && /^fonts\/.+\.(?:ttf|otf)$/i.test(String(entry.file || '')))
      : [];
    let googleRecords = [];
    try { googleRecords = await readInstalledGoogleFontRecords(); }
    catch { /* SVG-Export bleibt ohne lokal installierte Google-Schriften möglich. */ }
    const rules = []; const embeddedSources = new Set(); const unavailableFamilies = new Set();

    const appendRule = async ({ family, style, weight, file, data, unicodeRange = '', stretch = 'normal' }) => {
      const sourceKey = `${family}\u0000${style}\u0000${weight}\u0000${unicodeRange}\u0000${file}`;
      if (embeddedSources.has(sourceKey)) return;
      let bytes = data;
      try {
        if (!(bytes instanceof ArrayBuffer)) {
          const response = await fetch(new URL(file, document.baseURI), { cache: 'force-cache' });
          if (!response.ok) throw new Error(`HTTP ${response.status}`);
          bytes = await response.arrayBuffer();
        }
        if (!bytes.byteLength) throw new Error('Leere Schriftdatei');
        const dataUrl = await dataUrlFromFontData(bytes, exportFontMimeType(file));
        const variableWeight = /\[[^\]]*wght[^\]]*\]/i.test(file) ? '100 900' : String(weight);
        rules.push(`@font-face{font-family:${cssQuoted(family)};font-style:${style};font-weight:${variableWeight};font-stretch:${stretch || 'normal'};src:url(${cssQuoted(dataUrl)}) format(${cssQuoted(exportFontFormat(file))});${unicodeRange ? `unicode-range:${unicodeRange};` : ''}}`);
        embeddedSources.add(sourceKey);
      } catch {
        unavailableFamilies.add(family);
      }
    };

    for (const request of requests) {
      const bundled = manifestEntries.filter((entry) => entry.family === request.family);
      if (bundled.length) {
        const entry = [...bundled].sort((left, right) => {
          const leftStyle = left.style === request.style ? 0 : 1; const rightStyle = right.style === request.style ? 0 : 1;
          return leftStyle - rightStyle || Math.abs(num(left.weight, 400) - request.weight) - Math.abs(num(right.weight, 400) - request.weight);
        })[0];
        await appendRule({
          family: request.family, style: entry.style === 'italic' ? 'italic' : 'normal',
          weight: num(entry.weight, request.weight), file: entry.file
        });
        continue;
      }
      const familyRecords = googleRecords.filter((record) => record?.family === request.family && record?.data instanceof ArrayBuffer);
      if (!familyRecords.length) { unavailableFamilies.add(request.family); continue; }
      const preferredStyle = familyRecords.some((record) => record.style === request.style) ? request.style : familyRecords[0].style || 'normal';
      const styled = familyRecords.filter((record) => (record.style || 'normal') === preferredStyle);
      const preferredWeight = [...styled].sort((left, right) => (
        Math.abs(num(left.weight, 400) - request.weight) - Math.abs(num(right.weight, 400) - request.weight)
      ))[0]?.weight || '400';
      const matching = styled.filter((record) => String(record.weight || '400') === String(preferredWeight));
      for (const record of matching) {
        await appendRule({
          family: request.family, style: preferredStyle, weight: num(preferredWeight, request.weight),
          file: record.sourceUrl || 'font.woff2', data: record.data, unicodeRange: record.unicodeRange || '', stretch: record.stretch || 'normal'
        });
      }
    }
    if (rules.length) {
      let defs = [...svg.children].find((child) => child.tagName?.toLowerCase() === 'defs');
      if (!defs) { defs = document.createElementNS(svgNS, 'defs'); svg.prepend(defs); }
      const style = document.createElementNS(svgNS, 'style');
      style.setAttribute('type', 'text/css');
      style.textContent = rules.join('\n'); defs.prepend(style);
    }
    return { embeddedFiles: embeddedSources.size, unavailableFamilies: [...unavailableFamilies] };
  }

  async function exportSvgSource(options = {}) {
    const svg = cleanSvgClone(options);
    const fontResult = options.embedFonts ? await embedUsedFonts(svg) : { embeddedFiles: 0, unavailableFamilies: [] };
    return { source: new XMLSerializer().serializeToString(svg), ...fontResult };
  }

  function sanitizedExportStem(value) {
    return String(value || '').trim().replace(/\.(?:png|svg)$/i, '')
      .normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
      .replace(/[<>:"/\\|?*\x00-\x1f]/g, ' ').replace(/\s+/g, '-').replace(/^-+|-+$/g, '');
  }

  function exportOutputName(format) {
    const stem = sanitizedExportStem($('exportFilename')?.value);
    return stem ? `${stem}.${format}` : '';
  }

  async function exportSvg({ includeBackground = true, responsive = false, keepMetadata = true, embedFonts = false, outputName = filename('svg') } = {}) {
    const result = await exportSvgSource({ includeBackground, responsive, keepMetadata, embedFonts });
    saveBlob(new Blob([result.source], { type: 'image/svg+xml' }), outputName);
    if (embedFonts && result.unavailableFamilies.length) {
      toast(I18n.t('ui.svgExportedWithUnavailableFonts', { count: result.unavailableFamilies.length }), 'warning');
    } else toast(I18n.t('ui.svgExported'));
    return result;
  }

  function imageFromSvgSource(source) {
    return new Promise((resolveImage, rejectImage) => {
      const url = URL.createObjectURL(new Blob([source], { type: 'image/svg+xml' })); const image = new Image();
      image.onload = () => { URL.revokeObjectURL(url); resolveImage(image); };
      image.onerror = () => { URL.revokeObjectURL(url); rejectImage(new Error(I18n.t('ui.pngCouldNotBeCreated'))); };
      image.src = url;
    });
  }

  function canvasPngBlob(canvas) {
    return new Promise((resolvePng, rejectPng) => canvas.toBlob((png) => {
      if (png) resolvePng(png); else rejectPng(new Error(I18n.t('ui.pngCouldNotBeCreated')));
    }, 'image/png'));
  }

  async function exportPng({ includeBackground = true, scale = 1, smoothing = 'smooth', outputName = filename('png') } = {}) {
    const safeScale = clamp(Math.round(num(scale, 1)), 1, 4);
    const width = Math.max(1, Math.round(state.doc.width * safeScale)); const height = Math.max(1, Math.round(state.doc.height * safeScale));
    if (width > exportMaximumCanvasDimension || height > exportMaximumCanvasDimension || width * height > exportMaximumCanvasPixels) {
      throw new Error(I18n.t('ui.pngOutputTooLarge'));
    }
    const { source } = await exportSvgSource({ includeBackground, keepMetadata: true, embedFonts: true });
    const image = await imageFromSvgSource(source); const canvas = document.createElement('canvas');
    canvas.width = width; canvas.height = height;
    const context = canvas.getContext('2d');
    if (!context) throw new Error(I18n.t('ui.pngCouldNotBeCreated'));
    context.imageSmoothingEnabled = smoothing !== 'pixelated';
    if (context.imageSmoothingEnabled) context.imageSmoothingQuality = 'high';
    context.drawImage(image, 0, 0, width, height);
    const png = await canvasPngBlob(canvas);
    try {
      saveBlob(await pngWithDpi(png, state.doc.dpi * safeScale), outputName);
      toast(I18n.t('ui.pngExported'));
    } catch {
      saveBlob(png, outputName); toast(I18n.t('ui.pngExportedWithoutDpiMetadata'), 'warning');
    }
  }

  function selectedExportFormat() {
    return $('exportFormatSvg')?.checked ? 'svg' : 'png';
  }

  function setExportStatus(message = '', tone = '') {
    const output = $('exportStatus'); if (!output) return;
    output.textContent = message;
    if (tone) output.dataset.tone = tone; else output.removeAttribute('data-tone');
  }

  function setExportDialogBusy(busy) {
    exportDialogBusy = Boolean(busy);
    $('exportForm')?.querySelectorAll('input, select, button').forEach((control) => { control.disabled = exportDialogBusy; });
  }

  function syncExportDialog() {
    const format = selectedExportFormat(); const isPng = format === 'png';
    $('exportPngOptions').hidden = !isPng; $('exportSvgOptions').hidden = isPng;
    const scale = isPng ? clamp(Math.round(num($('exportPngQuality').value, 1)), 1, 4) : 1;
    const width = Math.max(1, Math.round(state.doc.width * scale)); const height = Math.max(1, Math.round(state.doc.height * scale));
    const tooLarge = isPng && (width > exportMaximumCanvasDimension || height > exportMaximumCanvasDimension || width * height > exportMaximumCanvasPixels);
    $('exportSummary').textContent = isPng
      ? I18n.t('ui.pngExportSummary', { width, height, dpi: Math.round(state.doc.dpi * scale), name: exportOutputName(format) || `.${format}` })
      : I18n.t($('exportSvgResponsive').checked ? 'ui.svgResponsiveExportSummary' : 'ui.svgExportSummary', {
        width: Math.round(state.doc.width), height: Math.round(state.doc.height), name: exportOutputName(format) || `.${format}`
      });
    $('exportConfirmBtn').textContent = I18n.t(isPng ? 'ui.exportAsPng' : 'ui.exportAsSvg');
    $('exportConfirmBtn').disabled = exportDialogBusy || tooLarge || !exportOutputName(format);
    if (!exportDialogBusy) setExportStatus(tooLarge ? I18n.t('ui.pngOutputTooLarge') : !exportOutputName(format) ? I18n.t('ui.exportNameRequired') : '', tooLarge || !exportOutputName(format) ? 'error' : '');
  }

  function openExportDialog() {
    finishInlineText();
    $('exportFilename').value = sanitizedExportStem(filename('png')) || 'Unbenannt';
    setExportDialogBusy(false); syncExportDialog();
    if (!$('exportDialog').open) $('exportDialog').showModal();
    requestAnimationFrame(() => $('exportFilename').select());
  }

  function closeExportDialog() {
    if (!exportDialogBusy && $('exportDialog').open) $('exportDialog').close();
  }

  async function submitExportDialog(event) {
    event.preventDefault();
    if (exportDialogBusy) return;
    const format = selectedExportFormat(); const outputName = exportOutputName(format);
    if (!outputName) { syncExportDialog(); return; }
    setExportDialogBusy(true); setExportStatus(I18n.t('ui.creatingExport'));
    try {
      if (format === 'svg') {
        await exportSvg({
          outputName, includeBackground: $('exportBackground').checked, embedFonts: $('exportSvgEmbedFonts').checked,
          responsive: $('exportSvgResponsive').checked, keepMetadata: $('exportSvgKeepMetadata').checked
        });
      } else {
        await exportPng({
          outputName, includeBackground: $('exportBackground').checked, scale: $('exportPngQuality').value,
          smoothing: $('exportPngSmoothing').value
        });
      }
      $('exportDialog').close();
    } catch (error) {
      setExportStatus(error?.message || I18n.t('ui.exportCouldNotBeCreated'), 'error');
      toast(error?.message || I18n.t('ui.exportCouldNotBeCreated'), 'error');
    } finally {
      setExportDialogBusy(false);
      if ($('exportDialog').open) syncExportDialog();
    }
  }

  async function importProject(file, fileHandle = null) {
    if (!file) return;
    try { Security.assertFileSize(file, 'project'); }
    catch (error) { toast(error?.message || 'Die Projektdatei ist zu groß', 'error'); return; }
    try {
      const decoded = await ProjectContainer.readProject(await file.arrayBuffer());
      const payload = Core.parseProject(decoded.payload, { createId: uid, fallbackTitle: titleFromFile(file, 'Projekt') });
      const title = normalizeDocumentTitle(payload.title, titleFromFile(file, 'Projekt'));
      const historyLabel = payload.migrated ? `Projekt v${payload.sourceVersion} migriert` : decoded.isContainer ? 'PYRO-Container geöffnet' : 'Legacy-Projekt geöffnet';
      openDocumentInTab(title, payload.document, { historyLabel, hasSavedVersion: true, fileHandle: decoded.isContainer ? fileHandle : null });
      const detail = payload.migrated
        ? `Projekt v${payload.sourceVersion} geöffnet und auf v${Core.PROJECT_VERSION} migriert`
        : decoded.isContainer ? 'PYRO-Projekt geöffnet' : 'Legacy-Projekt geöffnet – beim Speichern wird .pyro verwendet';
      toast(payload.warnings.length ? `${detail} – ${payload.warnings.length} Datenkorrekturen` : detail, payload.warnings.length ? 'warning' : '');
    } catch (error) {
      const known = error instanceof Core.ProjectValidationError || error instanceof ProjectContainer.ProjectContainerError;
      toast(known ? error.message : 'Die Projektdatei ist ungültig', 'error');
    }
  }

  function openSplash() {
    const dialog = $('splashDialog'); const image = $('splashImage');
    if (!image.getAttribute('src')) image.src = image.dataset.src;
    if (!dialog.open) dialog.showModal();
  }

  function isImageFile(file) {
    if (!file) return false;
    if (file.type.startsWith('image/')) return true;
    return /\.(?:png|jpe?g|webp|gif|svg)$/i.test(file.name || '');
  }

  function isSvgFile(file) {
    if (!file) return false;
    return file.type === 'image/svg+xml' || /\.svg$/i.test(file.name || '');
  }

  const svgDiagnosticStatusLabels = Object.freeze({
    supported: 'Unterstützt', approximated: 'Angenähert', ignored: 'Ignoriert', blocked: 'Blockiert', missing: 'Ressource fehlt'
  });

  function logSvgImportAnalysis(fileName, analysis) {
    const diagnostics = Array.isArray(analysis?.diagnostics) ? analysis.diagnostics : [];
    debugLog('info', 'SVG-Import', 'SVG_IMPORT_ANALYZED', `${fileName || 'SVG'}: ${analysis?.nodeCount || 0} Elemente analysiert`, {
      file: fileName || '', elementSummary: analysis?.elementSummary || {}, diagnosticSummary: analysis?.diagnosticSummary || {}, css: analysis?.css || {}, references: analysis?.references?.length || 0
    });
    diagnostics.forEach((diagnostic) => debugLog(
      diagnostic.severity === 'error' ? 'error' : diagnostic.severity === 'warning' ? 'warning' : 'info',
      'SVG-Import', diagnostic.code, diagnostic.message, diagnostic
    ));
  }

  function logSvgImportFailure(fileName, error) {
    debugLog('error', 'SVG-Import', error?.code || 'SVG_IMPORT_FAILED', 'SVG-Datei konnte nicht sicher importiert werden', {
      file: String(fileName || 'SVG-Datei'),
      code: String(error?.code || 'SVG_IMPORT_FAILED'),
      details: error?.details && typeof error.details === 'object' ? deepClone(error.details) : {}
    });
  }

  function reportListItem(primary, secondary = '') {
    const item = document.createElement('li'); const title = document.createElement('strong'); title.textContent = primary; item.append(title);
    if (secondary) { const meta = document.createElement('span'); meta.textContent = secondary; item.append(meta); }
    return item;
  }

  function reportDetailsGroup(status, label, count, items, open = false) {
    if (!items.length) return null;
    const details = document.createElement('details'); details.dataset.status = status; details.open = open;
    const summary = document.createElement('summary'); const title = document.createElement('span'); title.textContent = label; const amount = document.createElement('strong'); amount.textContent = String(count);
    summary.append(title, amount); const list = document.createElement('ul'); items.forEach((item) => list.append(item)); details.append(summary, list); return details;
  }

  function showSvgImportReport(fileName, analysis) {
    if (!analysis) return;
    lastSvgImportReport = { fileName: String(fileName || 'SVG'), analysis: deepClone(analysis) }; logSvgImportAnalysis(fileName, analysis);
    const diagnostics = Array.isArray(analysis.diagnostics) ? analysis.diagnostics : [];
    if (!diagnostics.length) return;
    if (diagnostics.every((diagnostic) => diagnostic.code === 'SVG_DTD_NEUTRALIZED')) return;
    const file = $('svgImportReportFile'); const summary = $('svgImportReportSummary'); const groups = $('svgImportReportDetails');
    file.textContent = String(fileName || 'SVG-Datei'); summary.replaceChildren(); groups.replaceChildren();
    const cards = [
      ['total', 'Geprüfte Elemente', analysis.nodeCount || 0],
      ['supported', 'Unterstützte Elemente', analysis.elementSummary?.supported || 0],
      ['approximated', 'Angenähert', analysis.diagnosticSummary?.approximated || 0],
      ['ignored', 'Ignoriert', analysis.diagnosticSummary?.ignored || 0],
      ['blocked', 'Blockiert', analysis.diagnosticSummary?.blocked || 0],
      ['missing', 'Ressourcen fehlen', analysis.diagnosticSummary?.missing || 0]
    ];
    cards.forEach(([status, label, count]) => {
      const card = document.createElement('span'); card.dataset.status = status; const amount = document.createElement('strong'); amount.textContent = String(count); const title = document.createElement('span'); title.textContent = label; card.append(amount, title); summary.append(card);
    });
    const supportedElements = (analysis.elements || []).filter((entry) => entry.status === 'supported');
    const supportedDiagnostics = diagnostics.filter((diagnostic) => diagnostic.status === 'supported');
    const supportedItems = supportedElements.map((entry) => reportListItem(`<${entry.name}>`, `${entry.count} Vorkommen`));
    supportedDiagnostics.forEach((diagnostic) => {
      const parts = [diagnostic.code];
      if (diagnostic.element) parts.push(`<${diagnostic.element}>`);
      if (diagnostic.attribute) parts.push(`Attribut ${diagnostic.attribute}`);
      if (diagnostic.ids?.length) parts.push(`ID ${diagnostic.ids.map((id) => `#${id}`).join(', ')}`);
      if (diagnostic.count > 1) parts.push(`${diagnostic.count} Vorkommen`);
      supportedItems.push(reportListItem(diagnostic.message, parts.join(' · ')));
    });
    const supportedGroup = reportDetailsGroup(
      'supported', 'Vollständig unterstützte Elemente und Aktionen',
      supportedElements.reduce((sum, entry) => sum + entry.count, 0) + supportedDiagnostics.reduce((sum, entry) => sum + Number(entry.count || 1), 0),
      supportedItems
    );
    if (supportedGroup) groups.append(supportedGroup);
    ['approximated', 'ignored', 'blocked', 'missing'].forEach((status) => {
      const entries = diagnostics.filter((diagnostic) => diagnostic.status === status); const count = entries.reduce((sum, entry) => sum + Number(entry.count || 1), 0);
      const items = entries.map((diagnostic) => {
        const parts = [diagnostic.code];
        if (diagnostic.element) parts.push(`<${diagnostic.element}>`);
        if (diagnostic.attribute) parts.push(`Attribut ${diagnostic.attribute}`);
        if (diagnostic.ids?.length) parts.push(`ID ${diagnostic.ids.map((id) => `#${id}`).join(', ')}`);
        if (diagnostic.count > 1) parts.push(`${diagnostic.count} Vorkommen`);
        return reportListItem(diagnostic.message, parts.join(' · '));
      });
      const group = reportDetailsGroup(status, svgDiagnosticStatusLabels[status], count, items, true); if (group) groups.append(group);
    });
    const dialog = $('svgImportReportDialog'); if (dialog.open) dialog.close(); dialog.showModal();
  }

  const svgResolvedColorCache = new Map();

  function resolveSvgCssColor(value) {
    const source = String(value || '').trim().toLowerCase();
    if (!source) return '';
    if (svgResolvedColorCache.has(source)) return svgResolvedColorCache.get(source);
    const probe = document.createElement('span'); probe.style.cssText = 'position:fixed;visibility:hidden;pointer-events:none;color-scheme:light';
    probe.style.color = ''; probe.style.color = source;
    if (!probe.style.color) { svgResolvedColorCache.set(source, ''); return ''; }
    document.body.append(probe); const resolved = getComputedStyle(probe).color; probe.remove();
    const result = /^rgba?\(/i.test(resolved) ? resolved : '';
    svgResolvedColorCache.set(source, result); return result;
  }

  function resolveSvgFontFamily(value) {
    const requested = String(value || '').split(',').map((family) => family.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
    const generic = new Set(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui']);
    // Eine SVG-Schriftliste mit generischem Abschluss ist bereits ein sicherer
    // Browser-Fallback. Den ersten Namen beizubehalten erlaubt außerdem lokal
    // installierte Schriften, die nicht in einer auswählbaren Fontliste stehen.
    if (requested.some((family) => generic.has(family.toLowerCase()))) return requested.join(', ');
    const available = new Map();
    [...document.querySelectorAll('#fontFamily option, #propFontFamily option')].forEach((option) => {
      const cssFamily = String(option.value || '').trim(); const label = String(option.textContent || '').trim();
      const primary = cssFamily.split(',')[0].trim().replace(/^['"]|['"]$/g, '');
      if (cssFamily) available.set(cssFamily.toLowerCase(), cssFamily);
      if (primary) available.set(primary.toLowerCase(), cssFamily);
      if (label) available.set(label.toLowerCase(), cssFamily);
    });
    (globalThis.PYRO_FONT_MANIFEST?.fonts || []).forEach((entry) => {
      const family = String(entry?.family || '').trim(); if (family && !available.has(family.toLowerCase())) available.set(family.toLowerCase(), localFontValue(family));
    });
    for (const face of document.fonts || []) {
      const family = String(face?.family || '').trim().replace(/^(['"])(.*)\1$/, '$2');
      if (family && !available.has(family.toLowerCase())) available.set(family.toLowerCase(), localFontValue(family));
    }
    const match = requested.find((family) => generic.has(family.toLowerCase()) || available.has(family.toLowerCase()));
    if (!match) return DEFAULT_FONT_FAMILY;
    return generic.has(match.toLowerCase()) ? match : available.get(match.toLowerCase());
  }

  let svgImportTextMeasurementContext = null;
  function measureSvgImportText(value, style = {}) {
    if (!svgImportTextMeasurementContext) svgImportTextMeasurementContext = document.createElement('canvas').getContext('2d');
    const context = svgImportTextMeasurementContext; if (!context) return NaN;
    const fontSize = Math.max(1, num(style.fontSize, 16)); const family = String(style.fontFamily || 'serif');
    context.font = `${/italic|oblique/i.test(style.fontStyle) ? 'italic ' : ''}${style.fontWeight || '400'} ${fontSize}px ${family}`;
    if ('fontKerning' in context) context.fontKerning = 'normal';
    return context.measureText(String(value || '')).width;
  }

  async function registerEmbeddedSvgFonts(markup) {
    if (typeof FontFace !== 'function' || !document.fonts) return [];
    const loaded = [];
    for (const match of String(markup || '').matchAll(/@font-face\s*\{([\s\S]*?)\}/gi)) {
      if (loaded.length >= 32) break;
      const block = match[1];
      const family = block.match(/(?:^|;)\s*font-family\s*:\s*(?:(["'])(.*?)\1|([^;]+))/i)?.slice(2).find(Boolean)?.trim();
      const source = block.match(/(?:^|;)\s*src\s*:\s*url\(\s*(["']?)(data:font\/(?:woff2?|ttf|otf|opentype|truetype)[^)'"\s]*)\1\s*\)/i)?.[2];
      if (!family || !source || source.length > 12 * 1024 * 1024) continue;
      const style = block.match(/(?:^|;)\s*font-style\s*:\s*([^;]+)/i)?.[1]?.trim() || 'normal';
      const weight = block.match(/(?:^|;)\s*font-weight\s*:\s*([^;]+)/i)?.[1]?.trim() || '400';
      const key = `${family}\u0000${style}\u0000${weight}\u0000${source.slice(-96)}`;
      if (!embeddedSvgFontFaces.has(key)) {
        try {
          const face = new FontFace(family, `url(${JSON.stringify(source)})`, { style, weight, display: 'block' });
          await face.load(); document.fonts.add(face); embeddedSvgFontFaces.set(key, face);
        } catch { continue; }
      }
      loaded.push(family);
    }
    return [...new Set(loaded)];
  }

  async function parseEditableSvg(file) {
    Security.assertFileSize(file, 'svg');
    const markup = await file.text();
    const preparedMarkup = Security.prepareSvgSource(markup).source;
    await registerEmbeddedSvgFonts(preparedMarkup);
    const imported = SvgImport.parse(markup, {
      createId: uid, resolveColor: resolveSvgCssColor, resolveFont: resolveSvgFontFamily, measureText: measureSvgImportText
    });
    const layers = imported.layers.map((layer, index) => ({
      ...layer, name: layer.name || `${I18n.t('ui.svgLayer')} ${index + 1}`, blendMode: normalizeBlendMode(layer.blendMode),
      objects: Array.isArray(layer.objects) ? layer.objects : []
    }));
    const activeLayerId = layers.at(-1)?.id;
    const document = {
      width: Math.max(1, round(imported.width)), height: Math.max(1, round(imported.height)), dpi: 96, bg: 'transparent',
      layers, activeLayerId, guides: { horizontal: [], vertical: [], locked: false }, grid: { visible: false, snap: false, size: 10 }
    };
    return { document: cleanDocument(document), warnings: imported.warnings || [], diagnostics: imported.diagnostics || [], analysis: imported.analysis || null };
  }

  async function openSvgDocument(file) {
    if (!file) return false;
    try {
      const { document, warnings, analysis } = await parseEditableSvg(file); const selected = document.layers.flatMap((layer) => layer.objects).map((object) => object.id);
      openDocumentInTab(titleFromFile(file, 'SVG'), document, { selected, historyLabel: 'SVG editierbar geöffnet', hasSavedVersion: false });
      const dtdNeutralized = Boolean(analysis?.security?.dtdNeutralized);
      const summary = warnings.length
        ? I18n.t('ui.svgOpenedAsEditableCountNoticeS', { count: warnings.length })
        : I18n.t('ui.svgOpenedAsEditableCountObjectS', { count: selected.length });
      toast(`${summary}${dtdNeutralized ? ` · ${I18n.t('status.dtdSafelyIgnored')}` : ''}`, warnings.length ? 'warning' : '');
      showSvgImportReport(file.name, analysis);
      return true;
    } catch (error) {
      logSvgImportFailure(file.name, error);
      toast(error?.message || 'SVG konnte nicht editierbar geöffnet werden', 'error'); return false;
    }
  }

  async function importEditableSvg(file) {
    const targetDocumentId = state.activeDocumentId;
    try {
      const { document, warnings, analysis } = await parseEditableSvg(file);
      if (state.activeDocumentId !== targetDocumentId) {
        toast('SVG-Import abgebrochen, weil während des Imports der Tab gewechselt wurde', 'warning'); return false;
      }
      const dx = round((state.doc.width - document.width) / 2, 4); const dy = round((state.doc.height - document.height) / 2, 4);
      const importedLayers = document.layers.map((layer) => ({
        ...layer, id: uid('layer'), name: layer.name || 'SVG',
        objects: layer.objects.map((object) => { const copy = reidentifyObjectTree(object); copy.x = round(num(copy.x) + dx, 4); copy.y = round(num(copy.y) + dy, 4); return copy; })
      }));
      state.doc.layers.push(...importedLayers); state.doc.activeLayerId = importedLayers.at(-1)?.id || state.doc.activeLayerId;
      importedLayers.forEach((layer) => state.expandedLayerIds.add(layer.id)); state.selected = importedLayers.flatMap((layer) => layer.objects.map((object) => object.id));
      commitHistory('SVG editierbar importiert'); renderAll();
      const dtdNeutralized = Boolean(analysis?.security?.dtdNeutralized);
      const summary = warnings.length
        ? I18n.t('ui.svgImportedAsLayersLayerSWarningsNoticeS', { layers: importedLayers.length, warnings: warnings.length })
        : I18n.t('ui.svgImportedAsLayersEditableLayerS', { layers: importedLayers.length });
      toast(`${summary}${dtdNeutralized ? ` · ${I18n.t('status.dtdSafelyIgnored')}` : ''}`, warnings.length ? 'warning' : '');
      showSvgImportReport(file.name, analysis);
      return true;
    } catch (error) {
      logSvgImportFailure(file?.name, error);
      toast(error?.message || 'SVG konnte nicht editierbar importiert werden', 'error'); return false;
    }
  }

  async function readBitmapFile(file) {
    Security.assertFileSize(file, 'bitmap');
    const [href, buffer] = await Promise.all([fileAsDataUrl(file), file.arrayBuffer()]);
    const image = await imageFromSource(href); Security.assertBitmapDimensions(image.naturalWidth, image.naturalHeight);
    return {
      href, image,
      dpi: BitmapModel.imageDpi(buffer),
      hasTransparency: BitmapModel.imageHasTransparency(buffer),
      metadata: BitmapModel.imageMetadata(buffer)
    };
  }

  function applyBitmapSourceMetadata(object, metadata = {}) {
    const orientation = Math.round(num(metadata.orientation, 0));
    if (orientation >= 1 && orientation <= 8) object.sourceOrientation = orientation;
    if (['icc', 'srgb', 'gamma'].includes(metadata.colorProfile)) object.sourceColorProfile = metadata.colorProfile;
  }

  function bitmapMetadataParts(metadata = {}) {
    const orientation = Math.round(num(metadata.sourceOrientation ?? metadata.orientation, 0));
    const colorProfile = metadata.sourceColorProfile ?? metadata.colorProfile;
    const profileLabel = { icc: 'ICC', srgb: 'sRGB', gamma: 'gAMA' }[colorProfile];
    const parts = [];
    if (orientation >= 2 && orientation <= 8) parts.push(I18n.t('ui.exifOrientationValue', { orientation }));
    if (profileLabel) parts.push(I18n.t('ui.colorProfileValue', { profile: profileLabel }));
    return parts;
  }

  function diagnoseBitmapMetadata(file, metadata, action) {
    const parts = bitmapMetadataParts(metadata); if (!parts.length) return '';
    debugLog('info', 'Bitmap-Import', 'BITMAP_METADATA_DETECTED', `${file?.name || 'Bild'}: ${parts.join(' · ')}`, {
      action,
      orientation: metadata?.orientation || null,
      colorProfile: metadata?.colorProfile || null,
      browserManaged: true
    });
    return parts.join(' · ');
  }

  async function openImageDocument(file) {
    if (!file) return;
    try {
      const { href, image, dpi, hasTransparency, metadata } = await readBitmapFile(file); const width = Math.max(1, image.naturalWidth); const height = Math.max(1, image.naturalHeight);
      const documentDpi = clamp(Math.round(num(dpi?.x, 72)), 1, 2400); const layer = makeLayer('Bild'); const object = objectBase('image', 0, 0);
      object.name = file.name ? `Bild: ${file.name}` : 'Bild';
      object.href = href; object.width = width; object.height = height; object.pixelWidth = width; object.pixelHeight = height;
      object.bitmapMode = 'direct'; object.bitmapInterpolation = 'bicubic'; object.sourceDpiX = num(dpi?.x, documentDpi); object.sourceDpiY = num(dpi?.y, documentDpi); object.stroke = 'none'; object.strokeWidth = 0; object.fill = 'none';
      applyBitmapSourceMetadata(object, metadata);
      layer.objects.push(object);
      const doc = { width, height, dpi: documentDpi, bg: hasTransparency ? 'transparent' : '#ffffff', layers: [layer], activeLayerId: layer.id };
      openDocumentInTab(titleFromFile(file, 'Bild'), doc, { selected: [object.id], historyLabel: 'Bild geöffnet' });
      const diagnostic = diagnoseBitmapMetadata(file, metadata, 'open');
      toast(`Bild geöffnet: ${width} × ${height} px · ${documentDpi} dpi${diagnostic ? ` · ${diagnostic}` : ''}`);
    } catch (error) { toast(importErrorMessage(error, 'Die Bilddatei konnte nicht geöffnet werden'), 'error'); }
  }

  function openFile(file, fileHandle = null) {
    if (!file) return;
    if (isSvgFile(file)) void openSvgDocument(file);
    else if (isImageFile(file)) void openImageDocument(file);
    else void importProject(file, fileHandle);
  }

  const compatibleOpenFileTypes = [
    {
      description: 'Alle kompatiblen Dateien',
      accept: {
        [ProjectContainer.MEDIA_TYPE]: ['.pyro'],
        'application/json': ['.pyro.json', '.json', '.paintstudio'],
        'image/svg+xml': ['.svg'],
        'image/png': ['.png'],
        'image/jpeg': ['.jpg', '.jpeg'],
        'image/webp': ['.webp'],
        'image/gif': ['.gif']
      }
    },
    { description: 'PYRO-Projekte', accept: { [ProjectContainer.MEDIA_TYPE]: ['.pyro'], 'application/json': ['.pyro.json', '.json', '.paintstudio'] } },
    { description: 'SVG-Vektorgrafiken', accept: { 'image/svg+xml': ['.svg'] } },
    { description: 'Bilder', accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg'], 'image/webp': ['.webp'], 'image/gif': ['.gif'] } }
  ];

  async function openProjectPicker() {
    if (typeof window.showOpenFilePicker !== 'function') { $('projectFileInput').click(); return; }
    try {
      const [handle] = await window.showOpenFilePicker({
        id: 'pyro-open-files',
        multiple: false,
        excludeAcceptAllOption: true,
        types: compatibleOpenFileTypes.map((type) => ({ ...type, description: I18n.translateSource(type.description) }))
      });
      const file = await handle.getFile();
      openFile(file, isImageFile(file) ? null : handle);
    } catch (error) {
      if (error?.name !== 'AbortError') toast('Datei konnte nicht geöffnet werden', 'error');
    }
  }

  function installProjectFileLaunchHandler() {
    if (typeof globalThis.launchQueue?.setConsumer !== 'function') return;
    globalThis.launchQueue.setConsumer(async (launchParams) => {
      const handles = Array.from(launchParams?.files || []).slice(0, 16);
      for (const handle of handles) {
        try {
          const file = await handle.getFile();
          await importProject(file, handle);
        } catch {
          toast('Die übergebene Datei konnte nicht geöffnet werden', 'error');
        }
      }
    });
  }

  async function importImage(file) {
    if (!file) return false;
    if (isSvgFile(file)) return importEditableSvg(file);
    const targetDocumentId = state.activeDocumentId; const targetDpi = state.doc.dpi;
    try {
      const { href, image, dpi, metadata } = await readBitmapFile(file); let placement = 'pixels';
      if (state.activeDocumentId !== targetDocumentId) {
        toast('Bildimport abgebrochen, weil während des Imports der Tab gewechselt wurde', 'warning'); return false;
      }
      if (dpi && (Math.abs(dpi.x - targetDpi) > .5 || Math.abs(dpi.y - targetDpi) > .5)) {
        placement = await requestBitmapImportChoice(file, image, dpi, targetDpi); if (!placement) return false;
        if (state.activeDocumentId !== targetDocumentId) {
          toast('Bildimport abgebrochen, weil während der Größenwahl der Tab gewechselt wurde', 'warning'); return false;
        }
      }
      const size = BitmapModel.placementSize(image.naturalWidth, image.naturalHeight, dpi, targetDpi, placement);
      const object = objectBase('image', (state.doc.width - size.width) / 2, (state.doc.height - size.height) / 2);
      object.name = file.name ? `Bild: ${file.name}` : 'Platziertes Bild'; object.href = href; object.width = size.width; object.height = size.height;
      object.pixelWidth = image.naturalWidth; object.pixelHeight = image.naturalHeight; object.bitmapMode = 'placed'; object.bitmapInterpolation = 'bicubic';
      if (dpi) { object.sourceDpiX = dpi.x; object.sourceDpiY = dpi.y; }
      applyBitmapSourceMetadata(object, metadata);
      object.stroke = 'none'; object.strokeWidth = 0; object.fill = 'none';
      const layer = activeLayer(); if (!layer || layer.locked || !layer.visible) { toast('Aktive Ebene ist gesperrt oder ausgeblendet', 'warning'); return false; }
      layer.objects.push(object); state.selected = [object.id]; commitHistory('Bild verlustfrei platziert'); renderAll();
      const diagnostic = diagnoseBitmapMetadata(file, metadata, 'place');
      toast(`Bild platziert: ${image.naturalWidth} × ${image.naturalHeight} Quellpixel${diagnostic ? ` · ${diagnostic}` : ''}`); return true;
    } catch (error) { toast(importErrorMessage(error, 'Bild konnte nicht gelesen werden'), 'error'); return false; }
  }

  function imageFromSource(source) {
    return new Promise((resolveImage, rejectImage) => {
      const image = new Image();
      image.onload = () => resolveImage(image);
      image.onerror = () => rejectImage(new Error('Bild konnte nicht geladen werden'));
      image.src = source;
    });
  }

  function fileAsDataUrl(file) {
    return new Promise((resolve, reject) => {
      try { Security.assertFileSize(file, 'bitmap'); }
      catch (error) { reject(error); return; }
      const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(reader.error || new Error('Bilddatei konnte nicht gelesen werden')); reader.readAsDataURL(file);
    });
  }

  function settleBitmapImportChoice(value) {
    const resolver = bitmapImportChoiceResolver; bitmapImportChoiceResolver = null;
    if ($('bitmapImportDialog')?.open) $('bitmapImportDialog').close();
    resolver?.(value);
  }

  function requestBitmapImportChoice(file, image, dpi, documentDpi = state.doc.dpi) {
    if (bitmapImportChoiceResolver) settleBitmapImportChoice(null);
    const hasDifferentDpi = Math.abs(dpi.x - dpi.y) > .01;
    $('bitmapImportSummary').textContent = I18n.t(
      hasDifferentDpi ? 'ui.nameWidthTimesHeightPxAtDpiXTimesDpiYDpiTheDocumentUsesDocumentDpiDpi' : 'ui.nameWidthTimesHeightPxAtDpiDpiTheDocumentUsesDocumentDpiDpi',
      { name: file.name || I18n.t('ui.image'), width: image.naturalWidth, height: image.naturalHeight, dpi: round(dpi.x, 2), dpiX: round(dpi.x, 2), dpiY: round(dpi.y, 2), documentDpi }
    );
    return new Promise((resolve) => {
      bitmapImportChoiceResolver = resolve;
      $('bitmapImportDialog').showModal();
    });
  }

  function settleBitmapRasterize(value) {
    const resolver = bitmapRasterizeResolver; bitmapRasterizeResolver = null;
    if ($('bitmapRasterizeDialog')?.open) $('bitmapRasterizeDialog').close();
    resolver?.(value);
  }

  function requestBitmapRasterization(object, reason = 'Direkte Pixelbearbeitung') {
    if (bitmapRasterizeResolver) settleBitmapRasterize(null);
    const pixels = BitmapModel.pixelScale(object); const target = BitmapModel.targetPixelSize(object);
    $('bitmapRasterizeSummary').textContent = `${reason} benötigt eine feste Pixelmatrix. ${pixels.pixelWidth} × ${pixels.pixelHeight} Quellpixel werden einmalig auf ${target.width} × ${target.height} Pixel neu berechnet.`;
    setField('bitmapRasterizeInterpolation', bitmapInterpolationFor(object));
    return new Promise((resolve) => {
      bitmapRasterizeResolver = resolve;
      $('bitmapRasterizeDialog').showModal();
    });
  }

  async function resampleBitmapObject(object, options = {}) {
    if (object?.type !== 'image' || !object.href) throw new Error('Bitmap fehlt');
    const target = options.target || BitmapModel.targetPixelSize(object);
    if (target.width * target.height > 32_000_000) throw new Error('Die neu berechnete Bitmap wäre größer als 32 Megapixel');
    const image = await imageFromSource(options.sourceHref || object.href); const canvas = document.createElement('canvas'); canvas.width = target.width; canvas.height = target.height;
    const context = canvas.getContext('2d'); if (!context) throw new Error('Bitmap konnte nicht neu berechnet werden');
    const interpolation = options.interpolation || bitmapInterpolationFor(object); const smoothing = BitmapModel.canvasSmoothing(interpolation);
    context.imageSmoothingEnabled = smoothing.enabled; if ('imageSmoothingQuality' in context) context.imageSmoothingQuality = smoothing.quality;
    context.clearRect(0, 0, target.width, target.height); context.drawImage(image, 0, 0, target.width, target.height);
    const centerX = num(object.x) + num(object.width, 1) / 2; const centerY = num(object.y) + num(object.height, 1) / 2;
    object.href = canvas.toDataURL('image/png'); object.pixelWidth = target.width; object.pixelHeight = target.height;
    object.width = target.width; object.height = target.height; object.bitmapMode = 'direct'; object.bitmapInterpolation = interpolation; object.scalePercent = 100;
    object.sourceDpiX = state.doc.dpi; object.sourceDpiY = state.doc.dpi;
    if (options.position) { object.x = options.position.x; object.y = options.position.y; }
    else if (options.anchor === 'top-left') { object.x = Math.round(num(object.x)); object.y = Math.round(num(object.y)); }
    else { object.x = Math.round(centerX - target.width / 2); object.y = Math.round(centerY - target.height / 2); }
    delete object.marqueeBleed;
    if (state.bitmapEditSession?.objectId === object.id) state.bitmapEditSession = null;
    return object;
  }

  async function makeBitmapDirect(object, options = {}) {
    if (object?.type !== 'image') return false;
    const scale = BitmapModel.pixelScale(object); const alreadyDirect = bitmapModeFor(object) === 'direct' && Math.abs(scale.x - 1) < .001 && Math.abs(scale.y - 1) < .001;
    if (alreadyDirect) return true;
    let interpolation = options.interpolation || bitmapInterpolationFor(object);
    if (options.confirm) {
      const choice = await requestBitmapRasterization(object, options.reason); if (!choice) return false;
      interpolation = choice;
    }
    const current = objectEntry(object.id)?.object; if (current !== object) return false;
    await resampleBitmapObject(object, { interpolation, anchor: options.anchor || 'center' });
    if (options.commit !== false) commitHistory(options.label || 'Bitmap neu berechnet');
    if (options.render !== false) renderAll();
    if (options.notify !== false) toast(`Bitmap direkt bearbeitbar: ${object.pixelWidth} × ${object.pixelHeight} px`);
    return true;
  }

  function restoreObjectSnapshot(object, origin) {
    if (!object || !origin) return false;
    Object.keys(object).forEach((key) => { delete object[key]; });
    Object.assign(object, deepClone(origin)); return true;
  }

  async function finishDirectBitmapPropertyResize(object, origin, label = 'Bitmap skaliert und neu berechnet') {
    if (object?.type !== 'image' || bitmapModeFor(object) !== 'direct') return false;
    const source = origin || deepClone(object);
    try {
      await resampleBitmapObject(object, {
        sourceHref: source.href,
        target: BitmapModel.targetPixelSize(object),
        interpolation: bitmapInterpolationFor(object),
        anchor: 'top-left'
      });
      if (objectEntry(object.id)?.object !== object) return false;
      commitHistory(label); renderAll();
      toast(`Bitmap neu berechnet: ${object.pixelWidth} × ${object.pixelHeight} px`); return true;
    } catch (error) {
      restoreObjectSnapshot(object, source); renderAll();
      toast(error?.message || 'Bitmap konnte nicht neu berechnet werden', 'error'); return false;
    }
  }

  function directBitmapResizePosition(gesture, target) {
    const origin = gesture.origin; const object = gesture.object; const handle = gesture.handle || '';
    return {
      x: Math.round(handle.includes('w') ? num(origin.x) + num(origin.width, 1) - target.width : num(object.x)),
      y: Math.round(handle.includes('n') ? num(origin.y) + num(origin.height, 1) - target.height : num(object.y))
    };
  }

  async function finishDirectBitmapResize(gesture, pointerId) {
    const object = gesture.object; const target = BitmapModel.targetPixelSize(object);
    state.gesture = null; state.snapGuides = [];
    try { refs.artboard.releasePointerCapture?.(pointerId); } catch { /* Zeiger kann bereits freigegeben sein. */ }
    try {
      await resampleBitmapObject(object, {
        sourceHref: gesture.origin.href,
        target,
        position: directBitmapResizePosition(gesture, target),
        interpolation: bitmapInterpolationFor(object)
      });
      if (objectEntry(object.id)?.object !== object) return;
      commitHistory('Bitmap skaliert und neu berechnet');
      toast(`Bitmap neu berechnet: ${target.width} × ${target.height} px`);
    } catch (error) {
      rollbackDocumentTransaction(gesture.transaction);
      toast(error?.message || 'Bitmap konnte nicht neu berechnet werden', 'error');
    }
    refs.artboard.style.cursor = cursorForTool(); renderAll();
  }

  function scaleBitmapSelection(selection, oldDimensions, newDimensions) {
    if (!selection) return null;
    const oldWidth = Math.max(1, num(oldDimensions?.width, 1)); const oldHeight = Math.max(1, num(oldDimensions?.height, 1));
    const width = Math.max(1, Math.round(num(newDimensions?.width, 1))); const height = Math.max(1, Math.round(num(newDimensions?.height, 1)));
    const scaleX = width / oldWidth; const scaleY = height / oldHeight;
    const edge = selection.edge; const feather = selection.feather ? Math.max(1, Math.round(num(selection.feather, 1) * Math.sqrt(scaleX * scaleY))) : selection.feather;
    if (selection.kind === 'ellipse-marquee') {
      const ellipse = selection.ellipse || selection;
      const scaled = BitmapSelection.ellipseSelectionFromRect({
        x: Math.round(num(ellipse.x) * scaleX), y: Math.round(num(ellipse.y) * scaleY),
        width: Math.max(1, Math.round(num(ellipse.width, 1) * scaleX)), height: Math.max(1, Math.round(num(ellipse.height, 1) * scaleY))
      }, { width, height });
      return scaled ? { objectId: selection.objectId, kind: 'ellipse-marquee', ...scaled, edge, feather } : null;
    }
    if (['lasso', 'polygon-lasso'].includes(selection.kind) && Array.isArray(selection.points)) {
      const points = selection.points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));
      const scaled = BitmapSelection.lassoSelection(points, { width, height });
      return scaled ? { objectId: selection.objectId, kind: selection.kind, ...scaled, edge, feather, closed: selection.closed } : null;
    }
    if (selection.kind === 'wand' && selection.mask?.length) {
      const sourceWidth = Math.max(1, Math.round(num(selection.sourceWidth, oldWidth))); const sourceHeight = Math.max(1, Math.round(num(selection.sourceHeight, oldHeight)));
      const mask = new Uint8Array(width * height); let count = 0; let minX = width; let minY = height; let maxX = -1; let maxY = -1;
      for (let y = 0; y < height; y += 1) {
        const sourceY = clamp(Math.floor((y + .5) * sourceHeight / height), 0, sourceHeight - 1);
        for (let x = 0; x < width; x += 1) {
          const sourceX = clamp(Math.floor((x + .5) * sourceWidth / width), 0, sourceWidth - 1);
          if (!selection.mask[sourceY * sourceWidth + sourceX]) continue;
          mask[y * width + x] = 1; count += 1; minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y);
        }
      }
      return count ? {
        objectId: selection.objectId, kind: 'wand', x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1,
        sourceWidth: width, sourceHeight: height, mask, count, tolerance: selection.tolerance, edge, feather
      } : null;
    }
    const left = clamp(Math.round(num(selection.x) * scaleX), 0, width - 1); const top = clamp(Math.round(num(selection.y) * scaleY), 0, height - 1);
    const right = clamp(Math.round((num(selection.x) + num(selection.width, 1)) * scaleX), left + 1, width);
    const bottom = clamp(Math.round((num(selection.y) + num(selection.height, 1)) * scaleY), top + 1, height);
    return { ...selection, x: left, y: top, width: right - left, height: bottom - top, edge, feather };
  }

  async function ensureBitmapSelectionDirect(object, reason) {
    if (bitmapModeFor(object) === 'direct') return true;
    const selection = state.bitmapMarquee?.objectId === object.id ? state.bitmapMarquee : null;
    const oldDimensions = bitmapPixelDimensions(object);
    const converted = await makeBitmapDirect(object, {
      confirm: true, reason, render: false, label: 'Platziertes Bild für Pixelbearbeitung neu berechnet'
    });
    if (!converted) return false;
    if (selection) state.bitmapMarquee = scaleBitmapSelection(selection, oldDimensions, bitmapPixelDimensions(object));
    state.wandSeed = null; state.wandRequest = null; renderAll();
    if (selection && !state.bitmapMarquee) toast('Die Auswahl war nach der Neuberechnung leer', 'warning');
    return !selection || Boolean(state.bitmapMarquee);
  }

  function pixelPointFromEvent(event, session) {
    const rect = $('pixelCanvas').getBoundingClientRect();
    return {
      x: clamp(Math.floor((event.clientX - rect.left) * session.width / rect.width), 0, session.width - 1),
      y: clamp(Math.floor((event.clientY - rect.top) * session.height / rect.height), 0, session.height - 1)
    };
  }

  function pixelColorBytes(color = $('pixelColor').value) {
    const value = normalizeColor(color) || '#000000';
    return [parseInt(value.slice(1, 3), 16), parseInt(value.slice(3, 5), 16), parseInt(value.slice(5, 7), 16), 255];
  }

  function bitmapPencilSettings() {
    return { size: clamp(Math.round(num($('strokeWidth')?.value, 1)), 1, 64) };
  }

  function paintPixel(session, x, y, erase = false, color = $('pixelColor').value, markDirty = true, size = 1) {
    const brushSize = clamp(Math.round(num(size, 1)), 1, 64);
    const left = Math.round(x + .5 - brushSize / 2); const top = Math.round(y + .5 - brushSize / 2);
    const right = Math.min(session.width, left + brushSize); const bottom = Math.min(session.height, top + brushSize);
    const clippedLeft = Math.max(0, left); const clippedTop = Math.max(0, top);
    const width = right - clippedLeft; const height = bottom - clippedTop;
    if (width <= 0 || height <= 0) return false;
    if (erase) session.context.clearRect(clippedLeft, clippedTop, width, height);
    else {
      session.context.fillStyle = normalizeColor(color) || '#000000';
      session.context.fillRect(clippedLeft, clippedTop, width, height);
    }
    if (markDirty) markBitmapSessionDirty(session);
    return true;
  }

  function paintPixelLine(session, from, to, erase = false, color = $('pixelColor').value, size = 1) {
    let x0 = from.x; let y0 = from.y; const x1 = to.x; const y1 = to.y;
    const dx = Math.abs(x1 - x0); const sx = x0 < x1 ? 1 : -1;
    const dy = -Math.abs(y1 - y0); const sy = y0 < y1 ? 1 : -1;
    let error = dx + dy;
    while (true) {
      paintPixel(session, x0, y0, erase, color, false, size);
      if (x0 === x1 && y0 === y1) break;
      const twice = 2 * error;
      if (twice >= dy) { error += dy; x0 += sx; }
      if (twice <= dx) { error += dx; y0 += sy; }
    }
    markBitmapSessionDirty(session);
  }

  function eraserSettings() {
    return {
      size: clamp(Math.round(num($('eraserSize')?.value, 1)), 1, 128),
      shape: $('eraserShape')?.value === 'circle' ? 'circle' : 'square',
      softness: clamp(Math.round(num($('eraserSoftness')?.value, 0)), 0, 100),
      tolerance: clamp(Math.round(num($('colorEraserTolerance')?.value, 18)), 0, 100)
    };
  }

  function eraserBrush(session, settings) {
    const key = `${settings.size}:${settings.shape}:${settings.softness}`;
    session.eraserBrushes ||= new Map();
    if (session.eraserBrushes.has(key)) return session.eraserBrushes.get(key);
    const size = Math.max(1, settings.size); const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size;
    const context = canvas.getContext('2d', { willReadFrequently: true }); const imageData = context.createImageData(size, size); const data = imageData.data;
    const radius = size / 2; const hardRadius = radius * (1 - settings.softness / 100);
    for (let y = 0; y < size; y += 1) {
      for (let x = 0; x < size; x += 1) {
        const dx = x + .5 - radius; const dy = y + .5 - radius;
        const distance = settings.shape === 'circle' ? Math.hypot(dx, dy) : Math.max(Math.abs(dx), Math.abs(dy));
        if (distance > radius) continue;
        let strength = 1;
        if (settings.softness > 0 && distance > hardRadius) {
          const linear = clamp((radius - distance) / Math.max(.0001, radius - hardRadius), 0, 1);
          strength = linear * linear * (3 - 2 * linear);
        }
        data[(y * size + x) * 4 + 3] = Math.round(strength * 255);
      }
    }
    context.putImageData(imageData, 0, 0);
    session.eraserBrushes.set(key, canvas);
    return canvas;
  }

  function eraseBitmapStamp(session, point, settings = eraserSettings(), markDirty = true) {
    const brush = eraserBrush(session, settings); const offset = settings.size / 2;
    session.context.save(); session.context.globalCompositeOperation = 'destination-out'; session.context.imageSmoothingEnabled = false;
    session.context.drawImage(brush, point.x + .5 - offset, point.y + .5 - offset);
    session.context.restore();
    if (markDirty) markBitmapSessionDirty(session);
  }

  function eraseBitmapLine(session, from, to, settings = eraserSettings()) {
    const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.hypot(dx, dy);
    const steps = Math.max(1, Math.ceil(distance / Math.max(1, settings.size / 4)));
    for (let index = 0; index <= steps; index += 1) {
      const progress = index / steps;
      eraseBitmapStamp(session, { x: Math.round(from.x + dx * progress), y: Math.round(from.y + dy * progress) }, settings, false);
    }
    markBitmapSessionDirty(session);
  }

  function colorEraseBitmapStamp(session, point, colorState, settings = eraserSettings()) {
    const size = Math.max(1, settings.size); const radius = size / 2; const centerX = point.x + .5; const centerY = point.y + .5;
    const left = Math.max(0, Math.floor(centerX - radius)); const top = Math.max(0, Math.floor(centerY - radius));
    const right = Math.min(session.width, Math.ceil(centerX + radius)); const bottom = Math.min(session.height, Math.ceil(centerY + radius));
    if (right <= left || bottom <= top) return false;
    const image = session.context.getImageData(left, top, right - left, bottom - top); const data = image.data;
    const sampleX = clamp(Math.floor(point.x), left, right - 1); const sampleY = clamp(Math.floor(point.y), top, bottom - 1);
    const sampleOffset = ((sampleY - top) * (right - left) + sampleX - left) * 4;
    const target = colorState?.target || [...data.slice(sampleOffset, sampleOffset + 4)];
    if (colorState && !colorState.target) colorState.target = target;
    if (target[3] === 0) return false;
    const hardRadius = radius * (1 - settings.softness / 100); const threshold = settings.tolerance / 100 * Math.sqrt(3 * 255 * 255);
    let changed = false;
    for (let y = top; y < bottom; y += 1) {
      for (let x = left; x < right; x += 1) {
        const dx = x + .5 - centerX; const dy = y + .5 - centerY;
        const distance = settings.shape === 'circle' ? Math.hypot(dx, dy) : Math.max(Math.abs(dx), Math.abs(dy));
        if (distance > radius) continue;
        const offset = ((y - top) * (right - left) + x - left) * 4; const alpha = data[offset + 3];
        if (!alpha) continue;
        const colorDistance = Math.hypot(data[offset] - target[0], data[offset + 1] - target[1], data[offset + 2] - target[2]);
        if (colorDistance > threshold + .0001) continue;
        let strength = 1;
        if (settings.softness > 0 && distance > hardRadius) {
          const linear = clamp((radius - distance) / Math.max(.0001, radius - hardRadius), 0, 1);
          strength = linear * linear * (3 - 2 * linear);
        }
        const nextAlpha = Math.round(alpha * (1 - strength));
        if (nextAlpha === alpha) continue;
        data[offset + 3] = nextAlpha; changed = true;
      }
    }
    if (changed) session.context.putImageData(image, left, top);
    return changed;
  }

  function colorEraseBitmapLine(session, from, to, colorState, settings = eraserSettings()) {
    const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.hypot(dx, dy);
    const steps = Math.max(1, Math.ceil(distance / Math.max(1, settings.size / 4))); let changed = false;
    for (let index = 0; index <= steps; index += 1) {
      const progress = index / steps;
      changed = colorEraseBitmapStamp(session, { x: Math.round(from.x + dx * progress), y: Math.round(from.y + dy * progress) }, colorState, settings) || changed;
    }
    if (changed) markBitmapSessionDirty(session);
  }

  function redEyeSettings() {
    return { size: clamp(Math.round(num($('redEyeSize')?.value, 12)), 1, 128) };
  }

  function retouchSettings() {
    return {
      size: clamp(Math.round(num($('retouchSize')?.value, 18)), 1, 128),
      shape: $('retouchShape')?.value === 'square' ? 'square' : 'circle',
      softness: clamp(Math.round(num($('retouchSoftness')?.value, 45)), 0, 100)
    };
  }

  function brushBounds(session, point, size) {
    const radius = Math.max(.5, size / 2); const centerX = point.x + .5; const centerY = point.y + .5;
    const left = clamp(Math.floor(centerX - radius), 0, session.width - 1); const top = clamp(Math.floor(centerY - radius), 0, session.height - 1);
    const right = clamp(Math.ceil(centerX + radius), 0, session.width); const bottom = clamp(Math.ceil(centerY + radius), 0, session.height);
    return { radius, centerX, centerY, left, top, width: right - left, height: bottom - top };
  }

  function brushMaskStrength(x, y, bounds, settings) {
    const dx = x + .5 - bounds.centerX; const dy = y + .5 - bounds.centerY;
    const distance = settings.shape === 'square' ? Math.max(Math.abs(dx), Math.abs(dy)) : Math.hypot(dx, dy);
    if (distance > bounds.radius) return 0;
    const hardRadius = bounds.radius * (1 - settings.softness / 100);
    if (settings.softness <= 0 || distance <= hardRadius) return 1;
    const linear = clamp((bounds.radius - distance) / Math.max(.0001, bounds.radius - hardRadius), 0, 1);
    return linear * linear * (3 - 2 * linear);
  }

  function removeRedEye(session, point, settings = redEyeSettings()) {
    const bounds = brushBounds(session, point, settings.size); if (bounds.width <= 0 || bounds.height <= 0) return;
    const imageData = session.context.getImageData(bounds.left, bounds.top, bounds.width, bounds.height); const data = imageData.data; let changed = false;
    const brush = { shape: 'circle', softness: 70 };
    for (let y = 0; y < bounds.height; y += 1) {
      for (let x = 0; x < bounds.width; x += 1) {
        const offset = (y * bounds.width + x) * 4; const red = data[offset]; const green = data[offset + 1]; const blue = data[offset + 2];
        if (data[offset + 3] === 0 || red < 80 || red <= green * 1.35 || red <= blue * 1.2) continue;
        const strength = brushMaskStrength(bounds.left + x, bounds.top + y, bounds, brush) * clamp((red - (green + blue) / 2 - 25) / 170, .2, 1);
        if (strength <= 0) continue;
        const neutral = clamp(Math.round((green + blue) / 2 + red * .16), 8, 150);
        const corrected = [neutral, Math.round(neutral * .9), Math.round(neutral * .82)];
        [0, 1, 2].forEach((channel) => { data[offset + channel] = Math.round(data[offset + channel] * (1 - strength) + corrected[channel] * strength); });
        changed = true;
      }
    }
    if (changed) { session.context.putImageData(imageData, bounds.left, bounds.top); markBitmapSessionDirty(session); }
  }

  function retouchSnapshot(session) {
    const canvas = document.createElement('canvas'); canvas.width = session.width; canvas.height = session.height;
    const context = canvas.getContext('2d', { willReadFrequently: true }); context.drawImage(session.canvas, 0, 0);
    return { canvas, context };
  }

  function averageBitmapColor(context, center, radius, width, height) {
    const span = Math.max(1, Math.round(radius)); const left = clamp(Math.floor(center.x - span), 0, width - 1); const top = clamp(Math.floor(center.y - span), 0, height - 1);
    const right = clamp(Math.ceil(center.x + span + 1), 0, width); const bottom = clamp(Math.ceil(center.y + span + 1), 0, height);
    const data = context.getImageData(left, top, Math.max(1, right - left), Math.max(1, bottom - top)).data; let red = 0; let green = 0; let blue = 0; let count = 0;
    for (let index = 0; index < data.length; index += 4) {
      if (!data[index + 3]) continue;
      red += data[index]; green += data[index + 1]; blue += data[index + 2]; count += 1;
    }
    return count ? { red: red / count, green: green / count, blue: blue / count } : { red: 0, green: 0, blue: 0 };
  }

  function retouchBitmapStamp(session, snapshot, target, sourceOffset, settings, mode) {
    const bounds = brushBounds(session, target, settings.size); if (bounds.width <= 0 || bounds.height <= 0) return false;
    const destination = session.context.getImageData(bounds.left, bounds.top, bounds.width, bounds.height); const data = destination.data;
    const sourceLeft = clamp(bounds.left + sourceOffset.x, 0, session.width - 1); const sourceTop = clamp(bounds.top + sourceOffset.y, 0, session.height - 1);
    const sourceRight = clamp(bounds.left + bounds.width + sourceOffset.x, 0, session.width); const sourceBottom = clamp(bounds.top + bounds.height + sourceOffset.y, 0, session.height);
    const sourceWidth = Math.max(1, sourceRight - sourceLeft); const sourceHeight = Math.max(1, sourceBottom - sourceTop);
    const sourceData = snapshot.context.getImageData(sourceLeft, sourceTop, sourceWidth, sourceHeight).data;
    const sourceCenter = { x: target.x + sourceOffset.x, y: target.y + sourceOffset.y };
    const correction = mode === 'heal' ? (() => {
      const targetAverage = averageBitmapColor(session.context, target, bounds.radius, session.width, session.height);
      const sourceAverage = averageBitmapColor(snapshot.context, sourceCenter, bounds.radius, session.width, session.height);
      return { red: targetAverage.red - sourceAverage.red, green: targetAverage.green - sourceAverage.green, blue: targetAverage.blue - sourceAverage.blue };
    })() : { red: 0, green: 0, blue: 0 };
    let changed = false;
    for (let y = 0; y < bounds.height; y += 1) {
      for (let x = 0; x < bounds.width; x += 1) {
        const targetX = bounds.left + x; const targetY = bounds.top + y; const sourceX = targetX + sourceOffset.x; const sourceY = targetY + sourceOffset.y;
        if (sourceX < 0 || sourceY < 0 || sourceX >= session.width || sourceY >= session.height) continue;
        const strength = brushMaskStrength(targetX, targetY, bounds, settings); if (strength <= 0) continue;
        const targetOffset = (y * bounds.width + x) * 4; const sourceOffsetIndex = ((sourceY - sourceTop) * sourceWidth + (sourceX - sourceLeft)) * 4;
        const sourceAlpha = sourceData[sourceOffsetIndex + 3] / 255; const mix = strength * sourceAlpha;
        const sourceColor = [
          clamp(Math.round(sourceData[sourceOffsetIndex] + correction.red), 0, 255),
          clamp(Math.round(sourceData[sourceOffsetIndex + 1] + correction.green), 0, 255),
          clamp(Math.round(sourceData[sourceOffsetIndex + 2] + correction.blue), 0, 255)
        ];
        [0, 1, 2].forEach((channel) => { data[targetOffset + channel] = Math.round(data[targetOffset + channel] * (1 - mix) + sourceColor[channel] * mix); });
        data[targetOffset + 3] = Math.round(data[targetOffset + 3] * (1 - strength) + sourceData[sourceOffsetIndex + 3] * strength);
        changed = true;
      }
    }
    if (changed) session.context.putImageData(destination, bounds.left, bounds.top);
    return changed;
  }

  function retouchBitmapLine(session, snapshot, from, to, sourceOffset, settings, mode) {
    const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.hypot(dx, dy);
    const steps = Math.max(1, Math.ceil(distance / Math.max(1, settings.size / 4))); let changed = false;
    for (let index = 0; index <= steps; index += 1) {
      const progress = distance < .001 ? 0 : index / steps;
      changed = retouchBitmapStamp(session, snapshot, {
        x: Math.round(from.x + dx * progress), y: Math.round(from.y + dy * progress)
      }, sourceOffset, settings, mode) || changed;
    }
    if (changed) markBitmapSessionDirty(session); return changed;
  }

  function spotHealBitmapStamp(session, snapshot, target, settings = retouchSettings()) {
    snapshot.pixels ||= new Uint8ClampedArray(snapshot.context.getImageData(0, 0, session.width, session.height).data);
    const patch = BitmapRetouch.spotHealPatch(snapshot.pixels, session.width, session.height, target, {
      radius: Math.max(.5, settings.size / 2), shape: settings.shape, softness: settings.softness
    });
    if (!patch) return false;
    const current = session.context.getImageData(patch.left, patch.top, patch.width, patch.height); let changed = false;
    for (let index = 0; index < patch.data.length; index += 1) {
      if (patch.data[index] !== current.data[index]) { changed = true; break; }
    }
    if (!changed) return false;
    current.data.set(patch.data); session.context.putImageData(current, patch.left, patch.top); return true;
  }

  function spotHealBitmapLine(session, snapshot, from, to, settings = retouchSettings()) {
    const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.hypot(dx, dy);
    const steps = Math.max(1, Math.ceil(distance / Math.max(1, settings.size / 4))); let changed = false;
    for (let index = 0; index <= steps; index += 1) {
      const progress = distance < .001 ? 0 : index / steps;
      changed = spotHealBitmapStamp(session, snapshot, { x: Math.round(from.x + dx * progress), y: Math.round(from.y + dy * progress) }, settings) || changed;
    }
    if (changed) markBitmapSessionDirty(session); return changed;
  }

  function liquifySettings() {
    const mode = $('liquifyMode')?.value;
    return {
      mode: ['push', 'bloat', 'pucker', 'twirl', 'smooth', 'restore'].includes(mode) ? mode : 'push',
      size: clamp(Math.round(num($('liquifySize')?.value, 80)), 4, 500),
      strength: clamp(num($('liquifyStrength')?.value, 55), 1, 100) / 100,
      direction: 1
    };
  }

  function liquifyBaseFor(object, session) {
    const existing = state.liquifyBase;
    if (existing?.objectId === object.id && existing.width === session.width && existing.height === session.height) return existing;
    const canvas = copyCanvas(session.canvas); const context = canvas.getContext('2d', { willReadFrequently: true });
    const base = { objectId: object.id, width: session.width, height: session.height, canvas, context };
    state.liquifyBase = base; return base;
  }

  function putWarpedBitmapRegion(session, imageData, pixels, left, top) {
    let changed = false;
    for (let index = 0; index < pixels.length; index += 1) {
      if (pixels[index] !== imageData.data[index]) { changed = true; break; }
    }
    if (!changed) return false;
    imageData.data.set(pixels); session.context.putImageData(imageData, left, top); return true;
  }

  function liquifyBitmapStamp(session, point, delta, settings = liquifySettings()) {
    const radius = Math.max(2, settings.size / 2); const displacement = Math.hypot(delta.x, delta.y);
    if (displacement < .001) return false;
    const padding = Math.ceil(Math.min(radius * .7, displacement * settings.strength) + 2);
    const left = clamp(Math.floor(point.x - radius) - padding, 0, session.width - 1);
    const top = clamp(Math.floor(point.y - radius) - padding, 0, session.height - 1);
    const right = clamp(Math.ceil(point.x + radius) + padding, 0, session.width);
    const bottom = clamp(Math.ceil(point.y + radius) + padding, 0, session.height);
    const width = right - left; const height = bottom - top; if (width <= 0 || height <= 0) return false;
    const imageData = session.context.getImageData(left, top, width, height);
    const warped = BitmapWarp.liquifyPixels(imageData.data, width, height, { x: point.x - left, y: point.y - top }, delta, { radius, strength: settings.strength });
    return putWarpedBitmapRegion(session, imageData, warped, left, top);
  }

  function liquifyBitmapLine(session, from, to, settings = liquifySettings()) {
    const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.hypot(dx, dy);
    if (distance < .001) return false;
    const steps = Math.max(1, Math.ceil(distance / Math.max(1, settings.size / 8))); let changed = false;
    for (let index = 1; index <= steps; index += 1) {
      const progress = index / steps; const previous = (index - 1) / steps;
      changed = liquifyBitmapStamp(session, {
        x: from.x + dx * progress, y: from.y + dy * progress
      }, {
        x: dx * (progress - previous), y: dy * (progress - previous)
      }, settings) || changed;
    }
    if (changed) markBitmapSessionDirty(session); return changed;
  }

  function radialLiquifyBitmapStamp(session, point, settings = liquifySettings()) {
    const radius = Math.max(2, settings.size / 2);
    const padding = Math.ceil(radius * settings.strength * .7 + 3);
    const left = clamp(Math.floor(point.x - radius) - padding, 0, session.width - 1);
    const top = clamp(Math.floor(point.y - radius) - padding, 0, session.height - 1);
    const right = clamp(Math.ceil(point.x + radius) + padding, 0, session.width);
    const bottom = clamp(Math.ceil(point.y + radius) + padding, 0, session.height);
    const width = right - left; const height = bottom - top; if (width <= 0 || height <= 0) return false;
    const imageData = session.context.getImageData(left, top, width, height);
    const warped = BitmapWarp.radialLiquifyPixels(imageData.data, width, height, { x: point.x - left, y: point.y - top }, {
      radius, strength: settings.strength, mode: settings.mode, direction: settings.direction
    });
    return putWarpedBitmapRegion(session, imageData, warped, left, top);
  }

  function restoreLiquifyBitmapStamp(session, base, point, settings = liquifySettings()) {
    if (!base?.context || base.width !== session.width || base.height !== session.height) return false;
    const radius = Math.max(2, settings.size / 2);
    const left = clamp(Math.floor(point.x - radius) - 1, 0, session.width - 1);
    const top = clamp(Math.floor(point.y - radius) - 1, 0, session.height - 1);
    const right = clamp(Math.ceil(point.x + radius) + 1, 0, session.width);
    const bottom = clamp(Math.ceil(point.y + radius) + 1, 0, session.height);
    const width = right - left; const height = bottom - top; if (width <= 0 || height <= 0) return false;
    const imageData = session.context.getImageData(left, top, width, height);
    const original = base.context.getImageData(left, top, width, height);
    const restored = BitmapWarp.restoreLiquifyPixels(imageData.data, original.data, width, height, { x: point.x - left, y: point.y - top }, {
      radius, strength: settings.strength, mode: settings.mode
    });
    return putWarpedBitmapRegion(session, imageData, restored, left, top);
  }

  function liquifyEffectLine(session, base, from, to, settings = liquifySettings(), { includeStart = false } = {}) {
    const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.hypot(dx, dy);
    if (distance < .001 && !includeStart) return false;
    const steps = Math.max(1, Math.ceil(distance / Math.max(1, settings.size / 6))); let changed = false;
    const first = includeStart ? 0 : 1;
    for (let index = first; index <= steps; index += 1) {
      const progress = distance < .001 ? 0 : index / steps;
      const point = { x: from.x + dx * progress, y: from.y + dy * progress };
      changed = (['smooth', 'restore'].includes(settings.mode)
        ? restoreLiquifyBitmapStamp(session, base, point, settings)
        : radialLiquifyBitmapStamp(session, point, settings)) || changed;
    }
    if (changed) markBitmapSessionDirty(session); return changed;
  }

  function samePixel(data, offset, color) {
    return data[offset] === color[0] && data[offset + 1] === color[1] && data[offset + 2] === color[2] && data[offset + 3] === color[3];
  }

  function fillPixels(session, start, erase = false, color = $('pixelColor').value) {
    let imageData;
    try { imageData = session.context.getImageData(0, 0, session.width, session.height); }
    catch { toast('Dieses Bild kann aus Sicherheitsgründen nicht pixelweise gelesen werden', 'error'); return; }
    const data = imageData.data;
    const startOffset = (start.y * session.width + start.x) * 4;
    const source = [data[startOffset], data[startOffset + 1], data[startOffset + 2], data[startOffset + 3]];
    const replacement = erase ? [0, 0, 0, 0] : pixelColorBytes(color);
    if (source.every((value, index) => value === replacement[index])) return;
    const matchesSource = (x, y) => x >= 0 && y >= 0 && x < session.width && y < session.height && samePixel(data, (y * session.width + x) * 4, source);
    const stack = [start.x, start.y];
    while (stack.length) {
      const y = stack.pop(); let x = stack.pop();
      if (!matchesSource(x, y)) continue;
      while (matchesSource(x - 1, y)) x -= 1;
      let sourceAbove = false; let sourceBelow = false;
      while (matchesSource(x, y)) {
        const offset = (y * session.width + x) * 4;
        data[offset] = replacement[0]; data[offset + 1] = replacement[1]; data[offset + 2] = replacement[2]; data[offset + 3] = replacement[3];
        const above = matchesSource(x, y - 1); const below = matchesSource(x, y + 1);
        if (above && !sourceAbove) stack.push(x, y - 1);
        if (below && !sourceBelow) stack.push(x, y + 1);
        sourceAbove = above; sourceBelow = below; x += 1;
      }
    }
    session.context.putImageData(imageData, 0, 0);
    markBitmapSessionDirty(session);
  }

  const bitmapLocalPoint = BitmapSelection.localPoint;
  const bitmapPixelPoint = BitmapSelection.pixelPoint;
  const bitmapPixelDimensions = BitmapSelection.pixelDimensions;

  function marqueeSettings() {
    const style = ['normal', 'aspect', 'size'].includes($('marqueeStyle')?.value) ? $('marqueeStyle').value : 'normal';
    const edge = ['hard', 'antialias', 'feather'].includes($('marqueeEdge')?.value) ? $('marqueeEdge').value : 'hard';
    return {
      style, edge,
      aspect: clamp(num($('marqueeAspect')?.value, 1), .01, 100),
      width: clamp(Math.round(num($('marqueeWidth')?.value, 32)), 1, 10000),
      height: clamp(Math.round(num($('marqueeHeight')?.value, 32)), 1, 10000),
      feather: clamp(Math.round(num($('marqueeFeather')?.value, 4)), 1, 1000)
    };
  }

  function wandSettings() {
    const edge = ['hard', 'antialias', 'feather'].includes($('wandEdge')?.value) ? $('wandEdge').value : 'hard';
    return {
      tolerance: clamp(Math.round(num($('wandTolerance')?.value, 32)), 0, 100), edge,
      feather: clamp(Math.round(num($('wandFeather')?.value, 4)), 1, 1000)
    };
  }

  function setWandTolerance(value) {
    const tolerance = clamp(Math.round(num(value, 32)), 0, 100);
    $('wandTolerance').value = String(tolerance);
    $('wandTolerance').setAttribute('aria-valuetext', `${tolerance} von 100`);
    return tolerance;
  }

  const bitmapWorkerClient = WorkerClient.createWorkerClient('src/workers/bitmap-worker.js', {
    idPrefix: 'bitmap-task',
    onResult: (type) => {
      refs.artboard.dataset.bitmapWorker = 'active'; refs.artboard.dataset.bitmapWorkerTask = type;
    },
    onFallback: () => { refs.artboard.dataset.bitmapWorker = 'fallback'; }
  });
  const ensureBitmapWorker = () => bitmapWorkerClient.ensure();
  const runBitmapWorker = (type, payload = {}, transfers = []) => bitmapWorkerClient.run(type, payload, transfers);

  function invertWandSelection() {
    const selection = state.bitmapMarquee;
    if (selection?.kind !== 'wand' || !selection.mask?.length) { toast('Erstelle zuerst eine Zauberstab-Auswahl', 'warning'); return; }
    const mask = new Uint8Array(selection.mask.length); let count = 0; let minX = selection.sourceWidth; let minY = selection.sourceHeight; let maxX = -1; let maxY = -1;
    selection.mask.forEach((selected, index) => {
      if (selected) return;
      mask[index] = 1; count += 1;
      const x = index % selection.sourceWidth; const y = Math.floor(index / selection.sourceWidth);
      minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y);
    });
    if (!count) { state.bitmapMarquee = null; syncPixelToolOptions(); renderAll(); toast('Die umgekehrte Auswahl enthält keine Pixel'); return; }
    Object.assign(selection, {
      mask, count, x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1,
      overlayHref: undefined, marqueePath: undefined
    });
    syncPixelToolOptions(); renderAll(); toast(`${count} Bitmap-Pixel ausgewählt`);
  }

  function bitmapWandSelection(session, seed, settings = wandSettings()) {
    const { width, height, context } = session; const pixelCount = width * height;
    if (pixelCount > 12_000_000) throw new Error('Das Bitmap ist für den Zauberstab zu groß');
    const pixels = context.getImageData(0, 0, width, height).data;
    return { kind: 'wand', ...BitmapSelection.wandSelection(pixels, width, height, seed, settings.tolerance), edge: settings.edge, feather: settings.feather };
  }

  async function bitmapWandSelectionComputed(session, seed, settings = wandSettings()) {
    if (session.width * session.height > 12_000_000) throw new Error('Das Bitmap ist für den Zauberstab zu groß');
    if (!ensureBitmapWorker()) return bitmapWandSelection(session, seed, settings);
    try {
      const revision = num(session.pixelRevision, 0);
      const bitmapKey = `${session.objectId}:${revision}`;
      const payload = { bitmapKey, width: session.width, height: session.height, seed, tolerance: settings.tolerance };
      const transfers = [];
      if (session.wandWorkerKey !== bitmapKey || session.wandWorkerGeneration !== bitmapWorkerClient.generation) {
        const pixels = bitmapPixelsForWorker(session).slice(); payload.bitmapBuffer = pixels.buffer; transfers.push(pixels.buffer);
        session.wandWorkerKey = bitmapKey; session.wandWorkerGeneration = bitmapWorkerClient.generation;
      }
      const result = await runBitmapWorker('wand', payload, transfers);
      return {
        kind: 'wand', x: result.x, y: result.y, width: result.width, height: result.height,
        sourceWidth: result.sourceWidth, sourceHeight: result.sourceHeight, mask: new Uint8Array(result.mask), count: result.count,
        tolerance: settings.tolerance, edge: settings.edge, feather: settings.feather
      };
    } catch (error) {
      if (error?.name === 'AbortError') throw error;
      return bitmapWandSelection(session, seed, settings);
    }
  }

  const bitmapMarqueeRect = (start, current, dimensions, settings = marqueeSettings()) => BitmapSelection.marqueeRect(start, current, dimensions, settings);
  const bitmapLassoBounds = BitmapSelection.lassoBounds;

  function bitmapLassoSelection(points, dimensions, kind, settings = marqueeSettings()) {
    const selection = BitmapSelection.lassoSelection(points, dimensions);
    return selection ? { kind, ...selection, edge: settings.edge, feather: settings.feather } : null;
  }

  async function bitmapLassoSelectionComputed(points, dimensions, kind, settings = marqueeSettings()) {
    const contour = (points || []).map((point) => ({ x: Math.round(num(point.x)), y: Math.round(num(point.y)) }));
    if (contour.length < 3) return null;
    if (!ensureBitmapWorker()) return bitmapLassoSelection(contour, dimensions, kind, settings);
    try {
      const result = await runBitmapWorker('lasso', { points: contour, dimensions });
      if (!result?.count) return null;
      return { kind, x: result.x, y: result.y, width: result.width, height: result.height, points: contour, mask: new Uint8Array(result.mask), count: result.count, edge: settings.edge, feather: settings.feather };
    } catch (error) {
      if (error?.name === 'AbortError') return null;
      return bitmapLassoSelection(contour, dimensions, kind, settings);
    }
  }

  function lassoMaskCanvas(selection, overlay = false) {
    const width = Math.max(1, num(selection.width, 1)); const height = Math.max(1, num(selection.height, 1));
    const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height;
    const context = canvas.getContext('2d'); const image = context.createImageData(width, height); const data = image.data;
    const edge = marqueeEdgeSettings(selection); const contains = (x, y) => x >= 0 && y >= 0 && x < width && y < height && Boolean(selection.mask?.[y * width + x]);
    for (let y = 0; y < height; y += 1) {
      for (let x = 0; x < width; x += 1) {
        if (!contains(x, y)) continue;
        const boundary = !contains(x - 1, y) || !contains(x + 1, y) || !contains(x, y - 1) || !contains(x, y + 1);
        const offset = (y * width + x) * 4;
        if (overlay) { data[offset] = 49; data[offset + 1] = 88; data[offset + 2] = 219; }
        data[offset + 3] = Math.round((overlay ? 78 : 255) * (edge.edge === 'antialias' && boundary ? .5 : 1));
      }
    }
    context.putImageData(image, 0, 0);
    if (edge.edge !== 'feather') return canvas;
    const blurred = document.createElement('canvas'); blurred.width = width; blurred.height = height;
    const blurredContext = blurred.getContext('2d');
    try { blurredContext.filter = `blur(${edge.feather}px)`; blurredContext.drawImage(canvas, 0, 0); } catch { blurredContext.drawImage(canvas, 0, 0); }
    return blurred;
  }

  function applyLassoMask(context, selection, cut = false, originX = 0, originY = 0) {
    const mask = lassoMaskCanvas(selection);
    context.save(); context.globalCompositeOperation = cut ? 'destination-out' : 'destination-in';
    context.drawImage(mask, originX, originY); context.restore();
  }

  function beginBitmapMarqueeGesture(event, object, point) {
    const start = BitmapSelection.pixelPointUnclamped(object, point, bitmapPixelDimensions(object));
    const kind = state.tool === 'ellipse-marquee' ? 'bitmap-ellipse-marquee' : 'bitmap-marquee';
    state.selected = [object.id]; state.bitmapMarquee = null; state.gesture = { kind, objectId: object.id, start, current: start };
    refs.workspace.setPointerCapture?.(event.pointerId); event.preventDefault(); renderEditorOverlays();
  }

  function beginBitmapLassoGesture(event, object, point) {
    const start = bitmapPixelPoint(object, point, bitmapPixelDimensions(object));
    if (!start) { toast('Ziehe das Lasso innerhalb des Bitmaps auf', 'warning'); return; }
    state.selected = [object.id]; state.bitmapMarquee = null;
    state.gesture = { kind: 'bitmap-lasso', objectId: object.id, points: [start], current: start };
    refs.artboard.setPointerCapture(event.pointerId); event.preventDefault(); renderArtboard();
  }

  async function finishBitmapLassoGesture(gesture) {
    const object = objectEntry(gesture.objectId)?.object; if (!object) return false;
    const selection = await bitmapLassoSelectionComputed(gesture.points, bitmapPixelDimensions(object), 'lasso');
    if (!selection) { toast('Das Lasso braucht eine geschlossene Fläche', 'warning'); return false; }
    state.bitmapMarquee = { objectId: object.id, ...selection }; syncPixelToolOptions(); renderAll(); toast(`${selection.count} Bitmap-Pixel ausgewählt`); return true;
  }

  function beginBitmapPolygonLasso(event, object, point) {
    const start = bitmapPixelPoint(object, point, bitmapPixelDimensions(object));
    if (!start) { toast('Setze die Punkte innerhalb des Bitmaps', 'warning'); return; }
    state.selected = [object.id]; state.bitmapMarquee = null;
    state.gesture = { kind: 'bitmap-polygon-lasso', objectId: object.id, points: [start], current: start };
    event.preventDefault(); renderArtboard();
  }

  function appendBitmapPolygonLassoPoint(event, point) {
    const gesture = state.gesture; const object = objectEntry(gesture?.objectId)?.object;
    if (gesture?.kind !== 'bitmap-polygon-lasso' || object?.type !== 'image') return false;
    const next = bitmapPixelPoint(object, point, bitmapPixelDimensions(object)); if (!next) return false;
    const first = gesture.points[0];
    if (gesture.points.length >= 3 && Math.hypot(next.x - first.x, next.y - first.y) <= 2) {
      finishBitmapPolygonLassoGesture(); event.preventDefault(); return true;
    }
    const last = gesture.points.at(-1);
    if (!last || Math.hypot(next.x - last.x, next.y - last.y) >= 1) gesture.points.push(next);
    gesture.current = next; event.preventDefault(); scheduleInteractionRender(); return true;
  }

  function finishBitmapPolygonLassoGesture() {
    const gesture = state.gesture; const object = objectEntry(gesture?.objectId)?.object;
    if (gesture?.kind !== 'bitmap-polygon-lasso' || object?.type !== 'image') return false;
    state.gesture = null; state.snapGuides = []; renderAll();
    void bitmapLassoSelectionComputed(gesture.points, bitmapPixelDimensions(object), 'polygon-lasso').then((selection) => {
      if (!selection) { toast('Polygon-Lasso braucht mindestens drei Punkte', 'warning'); return; }
      state.bitmapMarquee = { objectId: object.id, ...selection }; syncPixelToolOptions(); renderAll(); toast(`${selection.count} Bitmap-Pixel ausgewählt`);
    });
    return true;
  }

  async function refreshBitmapWandSelection({ notify = false } = {}) {
    const seed = state.wandSeed; const object = seed && objectEntry(seed.objectId)?.object;
    if (!seed || object?.type !== 'image' || state.tool !== 'wand' || primaryObject()?.id !== object.id) return false;
    const request = uid('wand'); state.wandRequest = request;
    try {
      const session = await bitmapEditSessionFor(object);
      if (state.wandRequest !== request || state.tool !== 'wand' || primaryObject()?.id !== object.id || state.wandSeed !== seed) return false;
      const selection = await bitmapWandSelectionComputed(session, seed);
      if (state.wandRequest !== request || state.tool !== 'wand' || primaryObject()?.id !== object.id || state.wandSeed !== seed) return false;
      state.bitmapMarquee = { objectId: object.id, ...selection }; state.wandRequest = null;
      syncPixelToolOptions(); renderAll();
      if (notify) toast(`${selection.count} zusammenhängende Pixel ausgewählt`);
      return true;
    } catch (error) {
      if (state.wandRequest === request) { state.wandRequest = null; toast(error?.message || 'Zauberstab-Auswahl konnte nicht erstellt werden', 'warning'); }
      return false;
    }
  }

  async function selectBitmapWand(object, point) {
    const seed = bitmapPixelPoint(object, point, bitmapPixelDimensions(object));
    if (!seed) { toast('Klicke innerhalb des Bitmaps auf eine Farbe', 'warning'); return; }
    state.selected = [object.id]; state.bitmapMarquee = null; state.wandSeed = { objectId: object.id, x: seed.x, y: seed.y };
    return refreshBitmapWandSelection({ notify: true });
  }

  async function bitmapEditSessionFor(object) {
    const existing = state.bitmapEditSession;
    if (existing?.objectId === object.id && existing.href === object.href) return existing;
    const image = await imageFromSource(object.href);
    const width = Math.max(1, Math.round(num(object.pixelWidth, image.naturalWidth)) || image.naturalWidth);
    const height = Math.max(1, Math.round(num(object.pixelHeight, image.naturalHeight)) || image.naturalHeight);
    if (width * height > 32_000_000) throw new Error('Bitmap ist für die direkte Pixelbearbeitung zu groß');
    const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height;
    const context = canvas.getContext('2d', { willReadFrequently: true }); context.imageSmoothingEnabled = false;
    context.clearRect(0, 0, width, height); context.drawImage(image, 0, 0, width, height);
    context.getImageData(0, 0, 1, 1);
    const session = {
      objectId: object.id, href: object.href, width, height, canvas, context, dirty: false, previewFrame: 0,
      eraserBrushes: new Map(), preview: null, pixelRevision: 0, wandPixelCache: null, wandWorkerKey: null, wandWorkerGeneration: 0
    };
    state.bitmapEditSession = session;
    return session;
  }

  function markBitmapSessionDirty(session) {
    if (!session) return;
    session.dirty = true; session.pixelRevision = num(session.pixelRevision, 0) + 1;
    session.wandPixelCache = null; session.wandWorkerKey = null; session.visibleBounds = undefined;
  }

  function bitmapSessionVisibleBounds(session) {
    if (!session) return null;
    if (session.visibleBounds !== undefined) return session.visibleBounds;
    session.visibleBounds = BitmapModel.alphaBounds(session.context.getImageData(0, 0, session.width, session.height).data, session.width, session.height);
    return session.visibleBounds;
  }

  function shrinkBitmapSessionVisibleBounds(session) {
    if (!session) return null;
    const source = session.liveVisibleBounds;
    if (!source) return null;
    let left = clamp(Math.floor(source.x), 0, session.width); let top = clamp(Math.floor(source.y), 0, session.height);
    let right = clamp(Math.ceil(source.x + source.width), left, session.width); let bottom = clamp(Math.ceil(source.y + source.height), top, session.height);
    const lineHasAlpha = (x, y, width, height) => {
      if (width <= 0 || height <= 0) return false;
      const data = session.context.getImageData(x, y, width, height).data;
      for (let index = 3; index < data.length; index += 4) if (data[index] > 0) return true;
      return false;
    };
    while (top < bottom && !lineHasAlpha(left, top, right - left, 1)) top += 1;
    while (bottom > top && !lineHasAlpha(left, bottom - 1, right - left, 1)) bottom -= 1;
    if (top >= bottom) { session.liveVisibleBounds = null; return null; }
    while (left < right && !lineHasAlpha(left, top, 1, bottom - top)) left += 1;
    while (right > left && !lineHasAlpha(right - 1, top, 1, bottom - top)) right -= 1;
    session.liveVisibleBounds = left < right ? { x: left, y: top, width: right - left, height: bottom - top } : null;
    return session.liveVisibleBounds;
  }

  function liveBitmapSelectionBounds(object) {
    const gesture = state.gesture;
    if (gesture?.kind !== 'bitmap' || !['eraser', 'color-eraser'].includes(gesture.tool) || gesture.objectId !== object?.id || !gesture.ready) return undefined;
    const session = gesture.session; const bounds = session?.liveVisibleBounds;
    if (!bounds) return null;
    const scaleX = num(object.width, 1) / Math.max(1, session.width); const scaleY = num(object.height, 1) / Math.max(1, session.height);
    return {
      x: bounds.x * scaleX, y: bounds.y * scaleY,
      width: bounds.width * scaleX, height: bounds.height * scaleY
    };
  }

  function sameBitmapBounds(left, right) {
    if (!left || !right) return left === right;
    return ['x', 'y', 'width', 'height'].every((property) => left[property] === right[property]);
  }

  function bitmapPixelsForWorker(session) {
    const revision = num(session.pixelRevision, 0);
    if (session.wandPixelCache?.revision === revision) return session.wandPixelCache.pixels;
    const pixels = new Uint8ClampedArray(session.context.getImageData(0, 0, session.width, session.height).data);
    session.wandPixelCache = { revision, pixels }; return pixels;
  }

  function bitmapEditColor(tool) {
    return tool === 'fill' ? $('fillColor').value : $('strokeColor').value;
  }

  function trimBitmapCanvasToVisiblePixels(canvas, object, options = {}) {
    const width = Math.max(1, Math.round(num(canvas?.width, 1))); const height = Math.max(1, Math.round(num(canvas?.height, 1)));
    if (!canvas || object?.type !== 'image' || object.freeTransform) return { canvas, width, height, trimmed: false };
    const sourceContext = canvas.getContext('2d', { willReadFrequently: true });
    const bounds = BitmapModel.alphaBounds(sourceContext.getImageData(0, 0, width, height).data, width, height);
    if (options.onlyIfBoundsChanged && sameBitmapBounds(bounds, options.previousBounds)) return { canvas, width, height, trimmed: false, bounds };
    if (!bounds || (bounds.x === 0 && bounds.y === 0 && bounds.width === width && bounds.height === height)) return { canvas, width, height, trimmed: false, bounds };
    const output = document.createElement('canvas'); output.width = bounds.width; output.height = bounds.height;
    const outputContext = output.getContext('2d', { willReadFrequently: true }); outputContext.imageSmoothingEnabled = false;
    outputContext.clearRect(0, 0, bounds.width, bounds.height);
    outputContext.drawImage(canvas, bounds.x, bounds.y, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height);
    const placement = BitmapModel.trimmedPlacement(object, width, height, bounds);
    object.x = round(placement.x, 4); object.y = round(placement.y, 4); object.width = round(placement.width, 4); object.height = round(placement.height, 4);
    if (placement.transformPivot) object.transformPivot = { x: round(placement.transformPivot.x, 6), y: round(placement.transformPivot.y, 6) };
    if (state.bitmapMarquee?.objectId === object.id) state.bitmapMarquee = null;
    if (state.wandSeed?.objectId === object.id) { state.wandSeed = null; state.wandRequest = null; }
    return { canvas: output, context: outputContext, width: bounds.width, height: bounds.height, trimmed: true, bounds };
  }

  function syncBitmapSession(session, options = {}) {
    const object = objectEntry(session.objectId)?.object;
    if (!object) return null;
    if (options.trimTransparent) {
      const result = trimBitmapCanvasToVisiblePixels(session.canvas, object, options);
      if (result.trimmed) {
        session.canvas = result.canvas; session.context = result.context; session.width = result.width; session.height = result.height;
        session.pixelRevision = num(session.pixelRevision, 0) + 1; session.wandPixelCache = null; session.wandWorkerKey = null;
      }
      session.visibleBounds = result.trimmed ? { x: 0, y: 0, width: result.width, height: result.height } : result.bounds;
    }
    object.href = session.canvas.toDataURL('image/png'); object.pixelWidth = session.width; object.pixelHeight = session.height;
    object.bitmapMode = 'direct'; object.sourceDpiX = state.doc.dpi; object.sourceDpiY = state.doc.dpi;
    session.href = object.href;
    return object;
  }

  function attachDirectBitmapPreview(session, object) {
    if (session.preview?.node?.isConnected || !object) return;
    const group = [...refs.artboard.querySelectorAll('g[data-object-id]')].find((candidate) => candidate.dataset.objectId === object.id);
    const source = group && [...group.children].find((child) => !child.hasAttribute('data-direct-bitmap-preview'));
    if (!group || !source) return;
    const cornerDefinition = source.querySelector?.('clipPath[data-bitmap-corner-definition]');
    let previewCornerDefinition = null; let previewCornerId = '';
    if (cornerDefinition && artboardScene.defs) {
      previewCornerDefinition = cornerDefinition.cloneNode(true); previewCornerId = uid('bitmap-preview-corners');
      previewCornerDefinition.id = previewCornerId;
      previewCornerDefinition.setAttribute('data-bitmap-preview-corner-definition', 'true');
      artboardScene.defs.append(previewCornerDefinition);
    }
    const useSvgImage = state.gesture?.kind === 'bitmap' && ['eraser', 'color-eraser'].includes(state.gesture.tool);
    if (useSvgImage) {
      const preview = E('image', {
        x: 0, y: 0, width: Math.max(1, num(object.width, 1)), height: Math.max(1, num(object.height, 1)),
        href: session.href || object.href || '', preserveAspectRatio: 'none',
        'data-direct-bitmap-preview': 'true', 'data-bitmap-preview-revision': '0', 'pointer-events': 'none'
      });
      if (previewCornerId) preview.setAttribute('clip-path', `url(#${previewCornerId})`);
      preview.style.imageRendering = bitmapInterpolationFor(object) === 'nearest' ? 'pixelated' : 'auto';
      source.style.visibility = 'hidden'; group.append(preview);
      session.preview = { node: preview, source, cornerDefinition: previewCornerDefinition, svgImage: true, revision: 0 };
      return;
    }
    const preview = E('foreignObject', {
      x: 0, y: 0, width: Math.max(1, num(object.width, 1)), height: Math.max(1, num(object.height, 1)),
      'data-direct-bitmap-preview': 'true', 'pointer-events': 'none'
    });
    if (previewCornerId) preview.setAttribute('clip-path', `url(#${previewCornerId})`);
    const previewContent = document.createElement('div');
    previewContent.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
    Object.assign(preview.style, { overflow: 'hidden' });
    Object.assign(previewContent.style, {
      position: 'relative', display: 'block', width: '100%', height: '100%',
      margin: '0', padding: '0', overflow: 'hidden', lineHeight: '0', contain: 'strict'
    });
    Object.assign(session.canvas.style, {
      position: 'absolute', inset: '0', display: 'block', width: '100%', height: '100%',
      margin: '0', padding: '0', border: '0', boxSizing: 'border-box', pointerEvents: 'none',
      imageRendering: bitmapInterpolationFor(object) === 'nearest' ? 'pixelated' : 'auto'
    });
    source.style.visibility = 'hidden'; previewContent.append(session.canvas); preview.append(previewContent); group.append(preview);
    session.preview = { node: preview, source, cornerDefinition: previewCornerDefinition };
  }

  function detachDirectBitmapPreview(session) {
    const preview = session?.preview;
    if (!preview) return;
    if (session.previewFrame) { cancelAnimationFrame(session.previewFrame); session.previewFrame = 0; }
    preview.source?.style.removeProperty('visibility'); preview.node?.remove(); preview.cornerDefinition?.remove(); session.preview = null;
  }

  function previewDirectBitmapEdit(session) {
    if (!session?.dirty) return;
    // Die meisten Werkzeuge verwenden das Canvas direkt. Der Radierer nutzt ein
    // SVG-Bild, weil Browser Canvas-Inhalte in foreignObject bei starkem Zoom
    // trotz korrekter Elementmaße verzerrt darstellen können.
    if (!session.preview?.node?.isConnected) attachDirectBitmapPreview(session, objectEntry(session.objectId)?.object);
    if (session.preview?.svgImage && !session.previewFrame) {
      session.previewFrame = requestAnimationFrame(() => {
        session.previewFrame = 0;
        const preview = session.preview;
        if (!preview?.svgImage || !preview.node?.isConnected || !session.dirty) return;
        preview.revision += 1;
        preview.node.setAttribute('href', session.canvas.toDataURL('image/png'));
        preview.node.dataset.bitmapPreviewRevision = String(preview.revision);
        shrinkBitmapSessionVisibleBounds(session);
        renderEditorOverlays();
      });
    }
  }

  function completeDirectBitmapGesture(gesture) {
    if (gesture.completed) return;
    gesture.completed = true;
    const session = gesture.session;
    if (session?.previewFrame) { cancelAnimationFrame(session.previewFrame); session.previewFrame = 0; }
    const trimAfterErase = ['eraser', 'color-eraser'].includes(gesture.tool);
    if (session?.dirty && syncBitmapSession(session, {
      trimTransparent: trimAfterErase,
      onlyIfBoundsChanged: trimAfterErase,
      previousBounds: gesture.visibleBoundsBefore
    })) {
      session.dirty = false; state.selected = [session.objectId];
      commitHistory(gesture.tool === 'liquify' ? 'Bitmap verflüssigt' : gesture.tool === 'spot-heal' ? 'Fleck entfernt' : gesture.tool === 'color-eraser' ? 'Farbe radiert' : 'Bitmap direkt bearbeitet');
    } else if (gesture.rasterized) commitHistory('Platziertes Bild für Pixelbearbeitung neu berechnet');
    detachDirectBitmapPreview(session);
    state.gesture = null; state.snapGuides = [];
    refs.artboard.style.cursor = cursorForTool();
    renderAll();
  }

  async function startDirectBitmapGesture(gesture) {
    try {
      let object = objectEntry(gesture.objectId)?.object;
      if (!object?.href) throw new Error('Bitmap fehlt');
      if (bitmapModeFor(object) === 'placed') {
        const oldDimensions = bitmapPixelDimensions(object);
        const interpolation = await requestBitmapRasterization(object, `${names[gesture.tool] || 'Das Werkzeug'} in einem platzierten Bild`);
        if (!interpolation) {
          if (state.gesture === gesture) cancelActiveGesture();
          return;
        }
        if (state.gesture !== gesture || gesture.cancelled) return;
        await resampleBitmapObject(object, { interpolation, anchor: 'center' });
        gesture.rasterized = true;
        if (state.gesture !== gesture || gesture.cancelled) return;
        object = objectEntry(gesture.objectId)?.object;
        if (!object) throw new Error('Bitmap fehlt');
        if (gesture.source?.objectId === object.id) {
          const nextDimensions = bitmapPixelDimensions(object);
          gesture.source.x = clamp(Math.round(gesture.source.x * nextDimensions.width / oldDimensions.width), 0, nextDimensions.width - 1);
          gesture.source.y = clamp(Math.round(gesture.source.y * nextDimensions.height / oldDimensions.height), 0, nextDimensions.height - 1);
          state.retouchSource = deepClone(gesture.source);
        }
      }
      const session = await bitmapEditSessionFor(object);
      if (state.gesture !== gesture || gesture.cancelled) return;
      gesture.session = session; gesture.ready = true;
      if (['eraser', 'color-eraser'].includes(gesture.tool)) {
        gesture.visibleBoundsBefore = deepClone(bitmapSessionVisibleBounds(session));
        session.liveVisibleBounds = deepClone(gesture.visibleBoundsBefore);
      }
      attachDirectBitmapPreview(session, object);
      const start = bitmapPixelPoint(object, gesture.start, session); const current = bitmapPixelPoint(object, gesture.current, session) || start;
      if (!start) { completeDirectBitmapGesture(gesture); return; }
      if (gesture.tool === 'fill') {
        fillPixels(session, current, false, bitmapEditColor('fill'));
        previewDirectBitmapEdit(session); completeDirectBitmapGesture(gesture); return;
      }
      if (gesture.tool === 'red-eye') {
        removeRedEye(session, current, gesture.redEye); previewDirectBitmapEdit(session); completeDirectBitmapGesture(gesture); return;
      }
      if (gesture.tool === 'liquify') {
        gesture.liquifyBase = liquifyBaseFor(object, session);
        if (gesture.liquify.mode === 'push') liquifyBitmapLine(session, start, current, gesture.liquify);
        else liquifyEffectLine(session, gesture.liquifyBase, start, current, gesture.liquify, { includeStart: true });
        gesture.last = current; previewDirectBitmapEdit(session);
        if (gesture.released) completeDirectBitmapGesture(gesture);
        return;
      }
      if (gesture.tool === 'spot-heal') {
        gesture.snapshot = retouchSnapshot(session); spotHealBitmapLine(session, gesture.snapshot, start, current, gesture.retouch);
        gesture.last = current; previewDirectBitmapEdit(session);
        if (gesture.released) completeDirectBitmapGesture(gesture);
        return;
      }
      if (['clone', 'heal'].includes(gesture.tool)) {
        gesture.snapshot = retouchSnapshot(session); gesture.sourceOffset = { x: gesture.source.x - start.x, y: gesture.source.y - start.y };
        retouchBitmapLine(session, gesture.snapshot, start, current, gesture.sourceOffset, gesture.retouch, gesture.tool);
        gesture.last = current; previewDirectBitmapEdit(session);
        if (gesture.released) completeDirectBitmapGesture(gesture);
        return;
      }
      if (gesture.tool === 'eraser') eraseBitmapLine(session, start, current, gesture.eraser);
      else if (gesture.tool === 'color-eraser') {
        colorEraseBitmapLine(session, start, current, gesture.colorEraser, gesture.eraser);
      }
      else paintPixelLine(session, start, current, false, bitmapEditColor(gesture.tool), gesture.pencil.size);
      gesture.last = current; previewDirectBitmapEdit(session);
      if (gesture.released) completeDirectBitmapGesture(gesture);
    } catch (error) {
      if (state.gesture === gesture) {
        rollbackDocumentTransaction(gesture.transaction); state.gesture = null; renderAll();
        toast(error?.message || 'Bitmap konnte nicht direkt bearbeitet werden', 'error');
      }
    }
  }

  function beginDirectBitmapGesture(event, object, point, tool) {
    state.selected = [object.id];
    const gesture = {
      kind: 'bitmap', objectId: object.id, tool, start: point, current: point, last: null, ready: false, released: false, cancelled: false, completed: false, rasterized: false,
      pencil: tool === 'pencil' ? bitmapPencilSettings() : null,
      eraser: ['eraser', 'color-eraser'].includes(tool) ? eraserSettings() : null, colorEraser: tool === 'color-eraser' ? { target: null } : null, redEye: tool === 'red-eye' ? redEyeSettings() : null,
      liquify: tool === 'liquify' ? { ...liquifySettings(), direction: event.altKey ? -1 : 1 } : null,
      liquifyBase: null,
      retouch: ['clone', 'heal', 'spot-heal'].includes(tool) ? retouchSettings() : null, source: ['clone', 'heal'].includes(tool) ? deepClone(state.retouchSource) : null,
      snapshot: null, sourceOffset: null, pointerId: event.pointerId, transaction: beginDocumentTransaction('Bitmap direkt bearbeiten')
    };
    state.gesture = gesture;
    refs.artboard.setPointerCapture(event.pointerId); startDirectBitmapGesture(gesture); event.preventDefault();
  }

  function copyCanvas(source) {
    const canvas = document.createElement('canvas'); canvas.width = source.width; canvas.height = source.height;
    const context = canvas.getContext('2d', { willReadFrequently: true }); context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(source, 0, 0);
    return canvas;
  }

  function warpContinuousPoint(object, point, dimensions = bitmapPixelDimensions(object)) {
    const local = bitmapLocalPoint(object, point); if (!local) return null;
    return {
      x: clamp(local.x * dimensions.width / Math.max(1, num(object.width, 1)), 0, dimensions.width),
      y: clamp(local.y * dimensions.height / Math.max(1, num(object.height, 1)), 0, dimensions.height)
    };
  }

  function warpGridDensity(tool = state.tool) {
    return clamp(Math.round(num(warpGridDensities[tool], tool === 'shape-grid' ? 7 : 4)), 2, 16);
  }

  function warpGridDimensions(tool, width, height) {
    if (tool === 'stretch-area') return { columns: 2, rows: 2 };
    if (tool !== 'puppet-warp') {
      const density = warpGridDensity(tool); return { columns: density, rows: density };
    }
    const longest = 13; const maximum = Math.max(width, height, 1);
    return {
      columns: clamp(Math.round((longest - 1) * width / maximum) + 1, 4, longest),
      rows: clamp(Math.round((longest - 1) * height / maximum) + 1, 4, longest)
    };
  }

  function layerSvgForWarp(layer, bounds, pixelWidth, pixelHeight) {
    const svg = E('svg', {
      xmlns: svgNS, 'xmlns:xlink': 'http://www.w3.org/1999/xlink', width: pixelWidth, height: pixelHeight,
      viewBox: `${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}`
    });
    const defs = E('defs');
    layer.objects.filter((object) => object.visible !== false).flatMap(objectTree).forEach((object) => appendObjectFilter(defs, object));
    svg.append(defs);
    layer.objects.filter((object) => object.visible !== false).forEach((object) => svg.append(objectGroup(object, false)));
    return new XMLSerializer().serializeToString(svg);
  }

  async function rasterizedLayerForWarp(layer) {
    const visible = layer.objects.filter((object) => object.visible !== false);
    if (!visible.length) throw new Error('Die aktive Ebene enthält keine sichtbaren Objekte');
    const visual = boundsOf(visible, { visual: true });
    const left = Math.floor(visual.x); const top = Math.floor(visual.y); const right = Math.ceil(visual.right); const bottom = Math.ceil(visual.bottom);
    const width = Math.max(1, right - left); const height = Math.max(1, bottom - top);
    if (width * height > 32_000_000) throw new Error('Die aktive Ebene ist für die Ebenenverformung zu groß');
    const bounds = { x: left, y: top, width, height };
    const source = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(layerSvgForWarp(layer, bounds, width, height))}`;
    const image = await imageFromSource(source); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height;
    const context = canvas.getContext('2d', { willReadFrequently: true }); context.clearRect(0, 0, width, height); context.drawImage(image, 0, 0, width, height);
    const bitmap = objectBase('image', left, top);
    bitmap.name = `${layer.name || 'Ebene'} (verformt)`; bitmap.width = width; bitmap.height = height; bitmap.pixelWidth = width; bitmap.pixelHeight = height;
    bitmap.stroke = 'none'; bitmap.fill = 'none'; bitmap.strokeWidth = 0; bitmap.href = canvas.toDataURL('image/png'); bitmap.bitmapMode = 'direct'; bitmap.bitmapInterpolation = 'bicubic'; bitmap.sourceDpiX = state.doc.dpi; bitmap.sourceDpiY = state.doc.dpi;
    return { bitmap, canvas };
  }

  async function bitmapCanvasForWarp(object) {
    const image = await imageFromSource(object.href); const dimensions = bitmapPixelDimensions(object);
    if (dimensions.width * dimensions.height > 32_000_000) throw new Error('Das Bitmap ist für die Verformung zu groß');
    const canvas = document.createElement('canvas'); canvas.width = dimensions.width; canvas.height = dimensions.height;
    const context = canvas.getContext('2d', { willReadFrequently: true }); context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(image, 0, 0, canvas.width, canvas.height);
    context.getImageData(0, 0, 1, 1); return canvas;
  }

  function createWarpSession(tool, object, sourceCanvas, transaction, options = {}) {
    const { columns, rows } = warpGridDimensions(tool, sourceCanvas.width, sourceCanvas.height);
    const sourcePoints = BitmapWarp.createGrid(sourceCanvas.width, sourceCanvas.height, columns, rows);
    const canvas = copyCanvas(sourceCanvas);
    const session = {
      tool, objectId: object.id, transaction, sourceCanvas, canvas, context: canvas.getContext('2d', { willReadFrequently: true }),
      width: sourceCanvas.width, height: sourceCanvas.height, columns, rows, sourcePoints,
      destinationPoints: sourcePoints.map((point) => ({ ...point })),
      pins: tool === 'puppet-warp' ? BitmapWarp.defaultPuppetPins(sourceCanvas.width, sourceCanvas.height) : [],
      axis: tool === 'stretch-area' && $('stretchAxis')?.value === 'horizontal' ? 'horizontal' : 'vertical',
      selection: null, amount: tool === 'stretch-area' ? clamp(num($('stretchAmount')?.value, 25), -90, 200) : 0,
      dirty: false, frame: 0, preview: null, rasterizedLayer: Boolean(options.rasterizedLayer), layerId: options.layerId || null
    };
    state.warpSession = session; state.bitmapEditSession = null; state.selected = [object.id];
    updateWarpActionState(); renderAll();
    return session;
  }

  async function prepareWarpSession(tool = state.tool) {
    if (!['mesh-warp', 'shape-grid', 'puppet-warp', 'stretch-area'].includes(tool)) return null;
    if (state.warpSession?.tool === tool && objectEntry(state.warpSession.objectId)) return state.warpSession;
    const preparation = uid('warp-prepare'); state.warpPreparation = preparation;
    updateWarpActionState('Vorbereitung …');
    const transaction = beginDocumentTransaction(`${names[tool] || 'Verformung'} vorbereiten`); let mutated = false;
    try {
      if (tool === 'mesh-warp') {
        const layer = activeLayer();
        if (!layer || layer.locked || !layer.visible) throw new Error('Die aktive Ebene ist gesperrt oder ausgeblendet');
        const rasterized = await rasterizedLayerForWarp(layer);
        if (state.warpPreparation !== preparation || state.tool !== tool || activeLayer()?.id !== layer.id) {
          if (state.warpPreparation === preparation) { state.warpPreparation = null; syncPixelToolOptions(); }
          return null;
        }
        layer.objects = [rasterized.bitmap]; mutated = true; state.doc.activeLayerId = layer.id;
        state.warpPreparation = null;
        const session = createWarpSession(tool, rasterized.bitmap, rasterized.canvas, transaction, { rasterizedLayer: true, layerId: layer.id });
        toast('Aktive Ebene gerastert – Gitterpunkte ziehen und anschließend übernehmen'); return session;
      }
      const object = primaryObject(); const entry = objectEntry(object?.id);
      if (object?.type !== 'image' || !object.href) throw new Error(tool === 'shape-grid'
        ? 'Wähle zuerst ein Bitmap für das Formgitter aus'
        : tool === 'stretch-area' ? 'Wähle zuerst ein Bitmap zum Strecken oder Stauchen aus' : 'Wähle zuerst ein Bitmap für die Gelenkverformung aus');
      if (!entry || entry.layer.locked || !entry.layer.visible || object.locked || object.visible === false) throw new Error('Das ausgewählte Bitmap ist gesperrt oder ausgeblendet');
      if (bitmapModeFor(object) === 'placed') {
        const interpolation = await requestBitmapRasterization(object, `${names[tool] || 'Die Verformung'} in einem platzierten Bild`);
        if (!interpolation) {
          if (state.warpPreparation === preparation) {
            state.warpPreparation = null; rollbackDocumentTransaction(transaction); updateWarpActionState(); renderAll();
          }
          return null;
        }
        if (state.warpPreparation !== preparation || state.tool !== tool || primaryObject()?.id !== object.id) return null;
        await resampleBitmapObject(object, { interpolation, anchor: 'center' }); mutated = true;
      }
      const canvas = await bitmapCanvasForWarp(object);
      if (state.warpPreparation !== preparation || state.tool !== tool || primaryObject()?.id !== object.id) {
        if (mutated) rollbackDocumentTransaction(transaction);
        if (state.warpPreparation === preparation) { state.warpPreparation = null; syncPixelToolOptions(); }
        return null;
      }
      state.warpPreparation = null;
      const session = createWarpSession(tool, object, canvas, transaction);
      if (tool === 'puppet-warp') toast('Klicke zum Setzen eines Gelenkpunkts; ziehe Gelenkpunkte zum Verformen, Alt-Klick löscht sie');
      if (tool === 'stretch-area') toast('Ziehe im Bitmap den Streifen auf, den du strecken oder stauchen möchtest');
      return session;
    } catch (error) {
      if (mutated) rollbackDocumentTransaction(transaction);
      if (state.warpPreparation === preparation) {
        state.warpPreparation = null; updateWarpActionState(error?.message || 'Verformung konnte nicht vorbereitet werden');
        toast(error?.message || 'Verformung konnte nicht vorbereitet werden', 'warning'); renderAll();
      }
      return null;
    }
  }

  function renderWarpPreviewNow(session = state.warpSession) {
    if (!session) return false;
    if (session.tool === 'stretch-area') {
      if (!session.selection) {
        session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0); return true;
      }
      session.amount = clamp(num(session.amount, $('stretchAmount')?.value), -90, 200);
      return Boolean(BitmapWarp.drawAxisStretch(session.sourceCanvas, session.canvas, session.axis, session.selection.start, session.selection.end, session.amount, { smoothing: true }));
    }
    if (session.tool === 'puppet-warp') {
      session.destinationPoints = BitmapWarp.puppetGrid(session.sourcePoints, session.pins, {
        radius: clamp(num($('puppetRadius')?.value, 220), 8, 4000), power: 2.25, rigidity: .48
      });
    }
    return BitmapWarp.drawMesh(session.sourceCanvas, session.canvas, session.sourcePoints, session.destinationPoints, session.columns, session.rows, { smoothing: true, overlap: .72 });
  }

  function scheduleWarpPreview(session = state.warpSession) {
    if (!session || session.frame) return;
    session.frame = requestAnimationFrame(() => {
      session.frame = 0; if (state.warpSession !== session) return;
      renderWarpPreviewNow(session); attachDirectBitmapPreview(session, objectEntry(session.objectId)?.object);
      if (session.tool === 'puppet-warp') renderEditorOverlays();
    });
  }

  function updateWarpActionState(message = '') {
    const session = state.warpSession; const preparing = Boolean(state.warpPreparation);
    if ($('warpApplyBtn')) $('warpApplyBtn').disabled = !session?.dirty || preparing;
    if ($('warpResetBtn')) $('warpResetBtn').disabled = !session || !session.dirty || preparing;
    if ($('warpCancelBtn')) $('warpCancelBtn').disabled = (!session && !preparing);
    if (message && $('warpOptionsHint')) $('warpOptionsHint').textContent = message;
  }

  function resetWarpSession() {
    const session = state.warpSession; if (!session) return false;
    if (session.frame) { cancelAnimationFrame(session.frame); session.frame = 0; }
    if (session.tool === 'stretch-area') {
      session.amount = 0; $('stretchAmount').value = '0'; $('stretchAmountRange').value = '0'; session.dirty = false;
      session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0);
      updateWarpActionState(); syncPixelToolOptions(); renderEditorOverlays(); return true;
    }
    session.destinationPoints = session.sourcePoints.map((point) => ({ ...point }));
    if (session.tool === 'puppet-warp') session.pins = BitmapWarp.defaultPuppetPins(session.width, session.height);
    session.dirty = false; session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0);
    updateWarpActionState(); renderEditorOverlays(); return true;
  }

  function rebuildWarpGrid() {
    const session = state.warpSession; if (!session || !['mesh-warp', 'shape-grid'].includes(session.tool)) return false;
    const density = clamp(Math.round(num($('warpGridDensity')?.value, warpGridDensity(session.tool))), 2, 16);
    warpGridDensities[session.tool] = density; session.columns = density; session.rows = density;
    session.sourcePoints = BitmapWarp.createGrid(session.width, session.height, density, density);
    session.destinationPoints = session.sourcePoints.map((point) => ({ ...point })); session.dirty = false;
    session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0);
    updateWarpActionState(); renderEditorOverlays(); toast(`Gitter auf ${density} × ${density} Punkte gesetzt`); return true;
  }

  function cancelWarpSession({ render = true } = {}) {
    const wasPreparing = Boolean(state.warpPreparation); state.warpPreparation = null;
    const session = state.warpSession;
    if (!session) { updateWarpActionState(); if (render && wasPreparing) renderAll(); return wasPreparing; }
    if (session.frame) cancelAnimationFrame(session.frame);
    detachDirectBitmapPreview(session); rollbackDocumentTransaction(session.transaction);
    state.warpSession = null; state.warpCursorPoint = null; state.gesture = null; state.bitmapEditSession = null;
    updateWarpActionState(); if (render) renderAll(); return true;
  }

  function applyWarpSession() {
    const session = state.warpSession; if (!session?.dirty) return false;
    if (session.frame) { cancelAnimationFrame(session.frame); session.frame = 0; }
    renderWarpPreviewNow(session);
    const object = objectEntry(session.objectId)?.object;
    if (!object) { cancelWarpSession(); return false; }
    object.href = session.canvas.toDataURL('image/png'); object.pixelWidth = session.width; object.pixelHeight = session.height;
    object.bitmapMode = 'direct'; object.sourceDpiX = state.doc.dpi; object.sourceDpiY = state.doc.dpi;
    detachDirectBitmapPreview(session); state.warpSession = null; state.warpPreparation = null; state.bitmapEditSession = null; state.gesture = null;
    state.selected = [object.id]; commitHistory(`${names[session.tool] || 'Verformung'} angewendet`); updateWarpActionState(); renderAll();
    toast(`${names[session.tool] || 'Verformung'} übernommen`); return true;
  }

  function beginWarpControlGesture(event, session, index, point) {
    const target = warpContinuousPoint(objectEntry(session.objectId)?.object, point, session); if (!target || !session.destinationPoints[index]) return false;
    state.gesture = {
      kind: 'warp-control', session, index, start: target, originPoints: session.destinationPoints.map((entry) => ({ ...entry })), moved: false
    };
    refs.artboard.setPointerCapture?.(event.pointerId); event.preventDefault(); return true;
  }

  function beginPuppetPinGesture(event, session, pin, point) {
    if (!pin || pin.locked) return false;
    const target = warpContinuousPoint(objectEntry(session.objectId)?.object, point, session); if (!target) return false;
    if (event.altKey) {
      session.pins = session.pins.filter((entry) => entry.id !== pin.id); session.dirty = session.pins.some((entry) => !entry.locked && (Math.abs(entry.target.x - entry.source.x) > .01 || Math.abs(entry.target.y - entry.source.y) > .01));
      scheduleWarpPreview(session); updateWarpActionState(); renderEditorOverlays(); event.preventDefault(); return true;
    }
    state.gesture = { kind: 'puppet-pin', session, pinId: pin.id, start: target, originTarget: { ...pin.target }, moved: false };
    refs.artboard.setPointerCapture?.(event.pointerId); event.preventDefault(); return true;
  }

  function addPuppetPin(event, session, point) {
    const target = warpContinuousPoint(objectEntry(session.objectId)?.object, point, session); if (!target) return false;
    const pin = { id: uid('puppet-pin'), source: { ...target }, target: { ...target }, locked: false };
    session.pins.push(pin); state.gesture = { kind: 'puppet-pin', session, pinId: pin.id, start: target, originTarget: { ...pin.target }, moved: false, added: true };
    refs.artboard.setPointerCapture?.(event.pointerId); renderEditorOverlays(); event.preventDefault(); return true;
  }

  function beginStretchSelectionGesture(event, session, point) {
    const object = objectEntry(session.objectId)?.object; const target = object && warpContinuousPoint(object, point, session); if (!target) return false;
    if (session.frame) { cancelAnimationFrame(session.frame); session.frame = 0; }
    const originSelection = session.selection ? { ...session.selection } : null; const originDirty = session.dirty;
    session.selection = null; session.dirty = false;
    session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0);
    state.gesture = { kind: 'stretch-select', session, axis: session.axis, start: target, current: target, originSelection, originDirty };
    updateWarpActionState(); refs.artboard.setPointerCapture?.(event.pointerId); renderEditorOverlays(); event.preventDefault(); return true;
  }

  function beginStretchHandleGesture(event, session, edge, point) {
    const object = objectEntry(session.objectId)?.object; const target = object && warpContinuousPoint(object, point, session);
    if (!target || !session.selection || !['start', 'end'].includes(edge)) return false;
    state.gesture = {
      kind: 'stretch-handle', session, edge, start: target, originSelection: { ...session.selection }, originDirty: session.dirty
    };
    refs.artboard.setPointerCapture?.(event.pointerId); event.preventDefault(); return true;
  }

  function updateStretchHandleGesture(gesture, point) {
    const session = gesture.session; const object = objectEntry(session.objectId)?.object; const target = object && warpContinuousPoint(object, point, session); if (!target) return false;
    const length = session.axis === 'vertical' ? session.height : session.width; const value = session.axis === 'vertical' ? target.y : target.x;
    const selection = { ...gesture.originSelection };
    if (gesture.edge === 'start') selection.start = clamp(value, 0, selection.end - Math.min(2, length));
    else selection.end = clamp(value, selection.start + Math.min(2, length), length);
    session.selection = selection; session.dirty = Math.abs(num(session.amount)) > .001;
    if (session.dirty) scheduleWarpPreview(session);
    else { session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0); }
    updateWarpActionState(); renderEditorOverlays(); return true;
  }

  function finishStretchSelectionGesture(gesture, point) {
    const session = gesture.session; const object = objectEntry(session.objectId)?.object; const target = object && warpContinuousPoint(object, point, session);
    const current = target || gesture.current; const startValue = session.axis === 'vertical' ? gesture.start.y : gesture.start.x;
    const endValue = session.axis === 'vertical' ? current.y : current.x; const length = session.axis === 'vertical' ? session.height : session.width;
    let start = clamp(Math.min(startValue, endValue), 0, length); let end = clamp(Math.max(startValue, endValue), 0, length);
    if (end - start < Math.min(2, length)) {
      const center = (start + end) / 2; start = clamp(center - 1, 0, Math.max(0, length - 2)); end = Math.min(length, start + Math.min(2, length));
    }
    session.selection = { start, end }; session.amount = clamp(num($('stretchAmount')?.value, 25), -90, 200);
    session.dirty = Math.abs(session.amount) > .001; state.gesture = null;
    if (session.dirty) scheduleWarpPreview(session);
    updateWarpActionState(); syncPixelToolOptions(); renderEditorOverlays();
    toast('Bereich gewählt – positive Werte strecken, negative Werte stauchen'); return true;
  }

  function clearStretchSelection({ resetAmount = false } = {}) {
    const session = state.warpSession; if (session?.tool !== 'stretch-area') return false;
    if (session.frame) { cancelAnimationFrame(session.frame); session.frame = 0; }
    session.selection = null; session.dirty = false;
    if (resetAmount) { session.amount = 25; $('stretchAmount').value = '25'; $('stretchAmountRange').value = '25'; }
    session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0);
    updateWarpActionState(); syncPixelToolOptions(); renderEditorOverlays(); return true;
  }

  function setPixelTool(tool) {
    const session = state.pixelSession;
    if (!session) return;
    session.tool = tool;
    document.querySelectorAll('[data-pixel-tool]').forEach((button) => {
      const active = button.dataset.pixelTool === tool;
      button.setAttribute('aria-pressed', String(active));
    });
    $('pixelCanvas').style.cursor = tool === 'fill' ? 'cell' : 'crosshair';
    renderPixelEditor();
  }

  function renderPixelEditor(position = null) {
    const session = state.pixelSession;
    if (!session) return;
    const scale = num($('pixelZoom').value, 12);
    const stage = $('pixelStage');
    stage.style.width = `${session.width * scale}px`;
    stage.style.height = `${session.height * scale}px`;
    stage.style.setProperty('--pixel-cell', `${scale}px`);
    $('pixelGridOverlay').classList.toggle('is-visible', $('pixelGrid').checked);
    $('pixelZoomValue').textContent = `${scale * 100} %`;
    const toolLabel = { pencil: 'Stift', fill: 'Füllen', eraser: 'Radierer' }[session.tool] || 'Stift';
    $('pixelStatus').textContent = position ? `x: ${position.x}, y: ${position.y} · ${toolLabel}` : `${session.width} × ${session.height} px · ${toolLabel}`;
  }

  function overlayDialogKeydown(event, dialog, close) {
    if (event.key === 'Escape') {
      event.preventDefault(); event.stopPropagation(); close(); return;
    }
    if (event.key !== 'Tab') return;
    const focusable = [...dialog.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')]
      .filter((element) => !element.hidden && element.getClientRects().length);
    if (!focusable.length) { event.preventDefault(); dialog.querySelector('[tabindex="-1"]')?.focus(); return; }
    const first = focusable[0]; const last = focusable.at(-1);
    if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
    else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
  }

  function restoreOverlayFocus(target) {
    if (!(target instanceof HTMLElement)) return;
    requestAnimationFrame(() => { if (target.isConnected && !target.hidden) target.focus({ preventScroll: true }); });
  }

  async function openPixelEditor(object) {
    if (!object || object.type !== 'image' || !object.href) { toast('Wähle zuerst ein Bitmap-Bild aus', 'warning'); return; }
    const returnFocus = document.activeElement;
    try {
      if (bitmapModeFor(object) === 'placed') {
        const converted = await makeBitmapDirect(object, { confirm: true, reason: 'Der Pixelmodus', label: 'Platziertes Bild für den Pixelmodus neu berechnet' });
        if (!converted) return;
        object = objectEntry(object.id)?.object; if (!object) return;
      }
      const image = await imageFromSource(object.href);
      const width = Math.max(1, Math.round(num(object.pixelWidth, image.naturalWidth)) || image.naturalWidth);
      const height = Math.max(1, Math.round(num(object.pixelHeight, image.naturalHeight)) || image.naturalHeight);
      if (width * height > 32_000_000) { toast('Das Bild ist für den Pixelmodus zu groß', 'warning'); return; }
      const canvas = $('pixelCanvas');
      canvas.width = width; canvas.height = height;
      const context = canvas.getContext('2d', { willReadFrequently: true });
      context.clearRect(0, 0, width, height);
      context.imageSmoothingEnabled = false;
      context.drawImage(image, 0, 0, width, height);
      try { context.getImageData(0, 0, 1, 1); }
      catch { toast('Dieses Bild kann im Pixelmodus nicht gelesen werden', 'error'); return; }
      state.pixelSession = { objectId: object.id, width, height, context, tool: 'pencil', last: null, drawing: false, dirty: false, trimTransparent: false };
      $('pixelZoom').value = String(clamp(Math.round(900 / Math.max(width, height)), 4, 24));
      $('pixelGrid').checked = true;
      $('pixelDialog').hidden = false;
      pixelDialogReturnFocus = returnFocus;
      setPixelTool('pencil');
      renderPixelEditor();
      requestAnimationFrame(() => $('pixelDialog').querySelector('.pixel-dialog__surface').focus({ preventScroll: true }));
    } catch { toast('Das Bild konnte nicht für den Pixelmodus geöffnet werden', 'error'); }
  }

  function closePixelEditor() {
    const returnFocus = pixelDialogReturnFocus; pixelDialogReturnFocus = null;
    state.pixelSession = null;
    $('pixelDialog').hidden = true;
    restoreOverlayFocus(returnFocus);
  }

  function applyPixelEditor() {
    const session = state.pixelSession;
    const object = objectEntry(session?.objectId)?.object;
    if (!session || !object) { closePixelEditor(); return; }
    if (session.dirty) {
      const result = session.trimTransparent ? trimBitmapCanvasToVisiblePixels($('pixelCanvas'), object) : { canvas: $('pixelCanvas'), width: session.width, height: session.height };
      object.href = result.canvas.toDataURL('image/png');
      object.pixelWidth = result.width; object.pixelHeight = result.height;
      object.bitmapMode = 'direct'; object.sourceDpiX = state.doc.dpi; object.sourceDpiY = state.doc.dpi;
      state.bitmapEditSession = null;
      state.selected = [object.id];
      commitHistory('Bitmap im Pixelmodus bearbeitet');
      renderAll();
      toast('Bitmap-Pixel übernommen');
    }
    closePixelEditor();
  }

  function onPixelPointerDown(event) {
    const session = state.pixelSession;
    if (!session || event.button !== 0) return;
    const point = pixelPointFromEvent(event, session);
    if (session.tool === 'fill') fillPixels(session, point);
    else if (session.tool === 'eraser') { session.trimTransparent = true; eraseBitmapLine(session, point, point, eraserSettings()); }
    else paintPixel(session, point.x, point.y, false, $('pixelColor').value, true, bitmapPencilSettings().size);
    session.last = point; session.drawing = session.tool !== 'fill';
    $('pixelCanvas').setPointerCapture(event.pointerId);
    renderPixelEditor(point);
    event.preventDefault();
  }

  function onPixelPointerMove(event) {
    const session = state.pixelSession;
    if (!session) return;
    const point = pixelPointFromEvent(event, session);
    if (session.drawing && session.last) {
      if (session.tool === 'eraser') { session.trimTransparent = true; eraseBitmapLine(session, session.last, point, eraserSettings()); }
      else paintPixelLine(session, session.last, point, false, $('pixelColor').value, bitmapPencilSettings().size);
      session.last = point;
    }
    renderPixelEditor(point);
  }

  function onPixelPointerUp() {
    const session = state.pixelSession;
    if (session) { session.drawing = false; session.last = null; renderPixelEditor(); }
  }

  function cropPointFromEvent(event, session) {
    const rect = $('cropStage').getBoundingClientRect();
    return {
      x: clamp(Math.round((event.clientX - rect.left) * session.width / rect.width), 0, session.width),
      y: clamp(Math.round((event.clientY - rect.top) * session.height / rect.height), 0, session.height)
    };
  }

  function cropRectFromEdges(left, top, right, bottom, session) {
    let x = clamp(Math.min(left, right), 0, session.width - 1);
    let y = clamp(Math.min(top, bottom), 0, session.height - 1);
    let maxX = clamp(Math.max(left, right), 1, session.width);
    let maxY = clamp(Math.max(top, bottom), 1, session.height);
    if (maxX <= x) { x = clamp(x - 1, 0, session.width - 1); maxX = Math.min(session.width, x + 1); }
    if (maxY <= y) { y = clamp(y - 1, 0, session.height - 1); maxY = Math.min(session.height, y + 1); }
    return { x: Math.round(x), y: Math.round(y), width: Math.max(1, Math.round(maxX - x)), height: Math.max(1, Math.round(maxY - y)) };
  }

  function renderCropEditor() {
    const session = state.cropSession;
    if (!session) return;
    const scale = num($('cropZoom').value, 100) / 100;
    const stage = $('cropStage'); const selection = $('cropSelection');
    stage.style.width = `${session.width * scale}px`; stage.style.height = `${session.height * scale}px`;
    selection.style.left = `${session.rect.x * scale}px`; selection.style.top = `${session.rect.y * scale}px`;
    selection.style.width = `${session.rect.width * scale}px`; selection.style.height = `${session.rect.height * scale}px`;
    $('cropZoomValue').textContent = `${Math.round(scale * 100)} %`;
    $('cropStatus').textContent = `${session.rect.width} × ${session.rect.height} px`;
  }

  async function openCropEditor(object) {
    if (!object || object.type !== 'image' || !object.href) { toast('Wähle zuerst ein Bitmap-Bild aus', 'warning'); return; }
    const returnFocus = document.activeElement;
    try {
      if (bitmapModeFor(object) === 'placed') {
        const converted = await makeBitmapDirect(object, { confirm: true, reason: 'Das Zuschneiden', label: 'Platziertes Bild zum Zuschneiden neu berechnet' });
        if (!converted) return;
        object = objectEntry(object.id)?.object; if (!object) return;
      }
      const image = await imageFromSource(object.href);
      const width = Math.max(1, Math.round(num(object.pixelWidth, image.naturalWidth)) || image.naturalWidth);
      const height = Math.max(1, Math.round(num(object.pixelHeight, image.naturalHeight)) || image.naturalHeight);
      if (width * height > 32_000_000) { toast('Das Bild ist für das Zuschneiden zu groß', 'warning'); return; }
      const canvas = $('cropCanvas'); canvas.width = width; canvas.height = height;
      const context = canvas.getContext('2d'); context.imageSmoothingEnabled = false; context.clearRect(0, 0, width, height); context.drawImage(image, 0, 0, width, height);
      state.cropSession = { objectId: object.id, width, height, rect: { x: 0, y: 0, width, height }, gesture: null };
      $('cropZoom').value = String(clamp(Math.round(760 / Math.max(width, height) * 100), 20, 200));
      $('cropDialog').hidden = false;
      cropDialogReturnFocus = returnFocus;
      renderCropEditor();
      requestAnimationFrame(() => $('cropDialog').querySelector('.crop-dialog__surface').focus({ preventScroll: true }));
    } catch { toast('Das Bild konnte nicht zum Zuschneiden geöffnet werden', 'error'); }
  }

  function closeCropEditor() {
    const returnFocus = cropDialogReturnFocus; cropDialogReturnFocus = null;
    state.cropSession = null;
    $('cropDialog').hidden = true;
    restoreOverlayFocus(returnFocus);
  }

  function applyCropEditor() {
    const session = state.cropSession; const object = objectEntry(session?.objectId)?.object;
    if (!session || !object) { closeCropEditor(); return; }
    const { x, y, width, height } = session.rect;
    if (x === 0 && y === 0 && width === session.width && height === session.height) { closeCropEditor(); return; }
    try {
      const output = document.createElement('canvas'); output.width = width; output.height = height;
      const context = output.getContext('2d'); context.imageSmoothingEnabled = false;
      context.drawImage($('cropCanvas'), x, y, width, height, 0, 0, width, height);
      const oldX = num(object.x); const oldY = num(object.y); const oldWidth = Math.max(1, num(object.width, 1)); const oldHeight = Math.max(1, num(object.height, 1));
      const scaleX = oldWidth / session.width; const scaleY = oldHeight / session.height;
      const newWidth = width * scaleX; const newHeight = height * scaleY;
      const localCenterX = x * scaleX + newWidth / 2; const localCenterY = y * scaleY + newHeight / 2;
      const angle = num(object.rotation) * Math.PI / 180; const dx = localCenterX - oldWidth / 2; const dy = localCenterY - oldHeight / 2;
      const centerX = oldX + oldWidth / 2 + Math.cos(angle) * dx - Math.sin(angle) * dy;
      const centerY = oldY + oldHeight / 2 + Math.sin(angle) * dx + Math.cos(angle) * dy;
      object.x = round(centerX - newWidth / 2); object.y = round(centerY - newHeight / 2);
      object.width = round(newWidth); object.height = round(newHeight); object.pixelWidth = width; object.pixelHeight = height; object.href = output.toDataURL('image/png'); object.bitmapMode = 'direct'; object.sourceDpiX = state.doc.dpi; object.sourceDpiY = state.doc.dpi; state.bitmapEditSession = null;
      state.selected = [object.id]; commitHistory('Bitmap zugeschnitten'); renderAll(); toast(`Bitmap zugeschnitten: ${width} × ${height} px`);
    } catch { toast('Das Bild konnte nicht zugeschnitten werden', 'error'); }
    closeCropEditor();
  }

  function onCropPointerDown(event) {
    const session = state.cropSession;
    if (!session || event.button !== 0) return;
    const point = cropPointFromEvent(event, session); const handle = event.target.closest?.('[data-crop-handle]');
    if (handle) state.cropSession.gesture = { kind: 'resize', handle: handle.dataset.cropHandle, start: point, origin: { ...session.rect } };
    else if (event.target.closest?.('[data-crop-region]')) state.cropSession.gesture = { kind: 'move', start: point, origin: { ...session.rect } };
    else { state.cropSession.rect = cropRectFromEdges(point.x, point.y, point.x + 1, point.y + 1, session); state.cropSession.gesture = { kind: 'new', start: point }; }
    $('cropStage').setPointerCapture(event.pointerId); renderCropEditor(); event.preventDefault();
  }

  function onCropPointerMove(event) {
    const session = state.cropSession; const gesture = session?.gesture;
    if (!session || !gesture) return;
    const point = cropPointFromEvent(event, session);
    if (gesture.kind === 'new') session.rect = cropRectFromEdges(gesture.start.x, gesture.start.y, point.x, point.y, session);
    else if (gesture.kind === 'move') {
      session.rect.x = clamp(gesture.origin.x + point.x - gesture.start.x, 0, session.width - gesture.origin.width);
      session.rect.y = clamp(gesture.origin.y + point.y - gesture.start.y, 0, session.height - gesture.origin.height);
    } else {
      const origin = gesture.origin; let left = origin.x; let top = origin.y; let right = origin.x + origin.width; let bottom = origin.y + origin.height;
      if (gesture.handle.includes('w')) left = point.x; if (gesture.handle.includes('e')) right = point.x;
      if (gesture.handle.includes('n')) top = point.y; if (gesture.handle.includes('s')) bottom = point.y;
      session.rect = cropRectFromEdges(left, top, right, bottom, session);
    }
    renderCropEditor();
  }

  function onCropPointerUp() {
    if (state.cropSession) state.cropSession.gesture = null;
  }

  function onCropKeyboard(event) {
    const session = state.cropSession;
    if (!session || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
    const step = event.shiftKey ? 10 : 1;
    const dx = event.key === 'ArrowLeft' ? -step : event.key === 'ArrowRight' ? step : 0;
    const dy = event.key === 'ArrowUp' ? -step : event.key === 'ArrowDown' ? step : 0;
    const handle = event.target.closest?.('[data-crop-handle]')?.dataset.cropHandle;
    if (handle) {
      if ((dx && !/[we]/.test(handle)) || (dy && !/[ns]/.test(handle))) return;
      let left = session.rect.x; let top = session.rect.y;
      let right = left + session.rect.width; let bottom = top + session.rect.height;
      if (handle.includes('w')) left += dx; if (handle.includes('e')) right += dx;
      if (handle.includes('n')) top += dy; if (handle.includes('s')) bottom += dy;
      session.rect = cropRectFromEdges(left, top, right, bottom, session);
    } else if (event.target.closest?.('[data-crop-region]')) {
      session.rect.x = clamp(session.rect.x + dx, 0, session.width - session.rect.width);
      session.rect.y = clamp(session.rect.y + dy, 0, session.height - session.rect.height);
    } else return;
    event.preventDefault(); event.stopPropagation(); renderCropEditor();
  }

  function createNewDocument() {
    const width = clamp(Math.round(num($('newWidth').value, 960)), 1, 10000); const height = clamp(Math.round(num($('newHeight').value, 640)), 1, 10000);
    const dpi = clamp(Math.round(num($('newDpi').value, 72)), 1, 2400);
    const layer = makeLayer(); const doc = { width, height, dpi, bg: backgroundFromField('newBg'), layers: [layer], activeLayerId: layer.id };
    openDocumentInTab(untitledDocumentName(), doc, { historyLabel: 'Neues Dokument', automaticTitle: true }); $('newDialog').close();
  }

  function openExampleDocument() {
    const layer = makeLayer(I18n.t('example.layerName'));
    const makeExampleObject = (type, x, y, properties) => Object.assign(objectBase(type, x, y), properties);
    layer.objects.push(
      makeExampleObject('rectangle', 70, 62, {
        name: I18n.t('example.backgroundCardName'), nameKey: 'example.backgroundCardName', width: 660, height: 396, fill: '#eef1ff', stroke: '#4f63e9', strokeWidth: 3, strokeType: DEFAULT_STROKE_TYPE
      }),
      makeExampleObject('ellipse', 112, 118, {
        name: I18n.t('example.sunName'), nameKey: 'example.sunName', width: 172, height: 172, fill: '#ffb84d', stroke: '#c86d22', strokeWidth: 4, strokeType: DEFAULT_STROKE_TYPE
      }),
      makeExampleObject('block-arrow', 338, 226, {
        name: I18n.t('ui.arrow'), nameKey: 'ui.arrow', width: 270, height: 112, fill: '#ef5d86', stroke: '#9f2449', strokeWidth: 4, strokeType: DEFAULT_STROKE_TYPE
      }),
      makeExampleObject('text', 112, 356, {
        name: I18n.t('example.textObjectName'), nameKey: 'example.textObjectName', nameManuallyEdited: true, width: 520, height: 54,
        text: I18n.t('example.editableText'), textKey: 'example.editableText', fontFamily: DEFAULT_FONT_FAMILY,
        fontSize: 38, bold: true, italic: false, fill: '#172033', stroke: 'none', strokeWidth: 0, strokeType: 'none'
      })
    );
    const doc = {
      width: 800, height: 520, dpi: 72, bg: '#fffaf2', layers: [layer], activeLayerId: layer.id,
      guides: { horizontal: [], vertical: [], locked: false }, grid: { visible: false, snap: false, size: 10 }
    };
    openDocumentInTab(I18n.t('example.documentTitle'), doc, { historyLabel: I18n.t('ui.exampleOpened'), exampleDocument: true });
    toast(I18n.t('ui.exampleWithFourEditableObjectsOpened'));
  }

  function syncLocalizedExampleDocuments() {
    const translationsFor = (key) => new Set(['de', 'en'].map((code) => I18n.t(key, {}, code)));
    const visit = (object) => {
      if (!object || typeof object !== 'object') return;
      if (localizedObjectNameKeys.has(object.nameKey) && translationsFor(object.nameKey).has(String(object.name || ''))) object.name = I18n.t(object.nameKey);
      if (object.textKey === 'example.editableText' && translationsFor(object.textKey).has(String(object.text || ''))) object.text = I18n.t(object.textKey);
      (Array.isArray(object.children) ? object.children : []).forEach(visit);
    };
    state.documents.filter((tabDocument) => tabDocument.exampleDocument).forEach((tabDocument) => {
      const layer = tabDocument.doc.layers[0];
      if (layer && translationsFor('example.layerName').has(String(layer.name || ''))) layer.name = I18n.t('example.layerName');
      tabDocument.doc.layers.forEach((entry) => entry.objects.forEach(visit));
    });
  }

  function resizeCanvas(width = $('canvasWidth').value, height = $('canvasHeight').value, bg = backgroundFromField('canvasBg')) {
    state.doc.width = clamp(Math.round(num(width, state.doc.width)), 1, 10000); state.doc.height = clamp(Math.round(num(height, state.doc.height)), 1, 10000); state.doc.bg = bg;
    const guides = state.doc.guides || (state.doc.guides = { horizontal: [], vertical: [] });
    guides.horizontal = (guides.horizontal || []).filter((value) => value >= 0 && value <= state.doc.height);
    guides.vertical = (guides.vertical || []).filter((value) => value >= 0 && value <= state.doc.width);
    if (state.canvasAspectLocked) state.canvasAspectRatio = state.doc.width / state.doc.height;
    commitHistory('Leinwand angepasst'); renderAll();
  }

  function updateCanvasAspectLock() {
    const button = $('canvasAspectLockBtn'); const locked = state.canvasAspectLocked;
    button.classList.toggle('is-active', locked); button.setAttribute('aria-pressed', String(locked));
    const label = locked ? 'Seitenverhältnis gesperrt – für freie Eingabe klicken' : 'Seitenverhältnis frei – zum Sperren klicken';
    button.title = label; button.setAttribute('aria-label', label);
    button.innerHTML = locked
      ? '<svg aria-hidden="true" viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg>'
      : '<svg aria-hidden="true" viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M16 10V7a4 4 0 0 0-7.2-2.4"/></svg>';
  }

  function toggleCanvasAspectLock() {
    state.canvasAspectLocked = !state.canvasAspectLocked;
    const width = num($('canvasWidth').value, state.doc.width); const height = num($('canvasHeight').value, state.doc.height);
    if (state.canvasAspectLocked && width > 0 && height > 0) state.canvasAspectRatio = width / height;
    updateCanvasAspectLock(); toast(state.canvasAspectLocked ? 'Seitenverhältnis gesperrt' : 'Seitenverhältnis frei');
  }

  function syncCanvasAspect(source) {
    if (!state.canvasAspectLocked || !Number.isFinite(state.canvasAspectRatio) || state.canvasAspectRatio <= 0) return;
    if (source === 'width') {
      const width = num($('canvasWidth').value, 0); if (width <= 0) return;
      $('canvasHeight').value = clamp(Math.round(width / state.canvasAspectRatio), 1, 10000);
    } else {
      const height = num($('canvasHeight').value, 0); if (height <= 0) return;
      $('canvasWidth').value = clamp(Math.round(height * state.canvasAspectRatio), 1, 10000);
    }
  }

  function fitCanvasToObjects() {
    const objects = allObjects();
    if (!objects.length) { toast('Keine Objekte zum Anpassen vorhanden', 'warning'); return; }
    state.canvasResizeMode = false; refs.shell.style.transform = '';
    const bounds = boundsOf(objects); const shiftX = -bounds.x; const shiftY = -bounds.y;
    const guides = deepClone(documentGuides());
    state.doc.width = clamp(Math.ceil(bounds.width), 1, 10000); state.doc.height = clamp(Math.ceil(bounds.height), 1, 10000);
    shiftDocumentContent(shiftX, shiftY, state.doc.width, state.doc.height, guides);
    if (state.canvasAspectLocked) state.canvasAspectRatio = state.doc.width / state.doc.height;
    commitHistory('Leinwand an Inhalt angepasst'); renderAll(); toast(`An Inhalt angepasst: ${state.doc.width} × ${state.doc.height}`);
  }

  function shiftDocumentContent(dx, dy, width = state.doc.width, height = state.doc.height, guideSource = documentGuides()) {
    state.doc.layers.forEach((layer) => layer.objects.forEach((object) => {
      object.x = round(num(object.x) + dx); object.y = round(num(object.y) + dy);
    }));
    const source = guideSource && typeof guideSource === 'object' ? guideSource : {};
    state.doc.guides = {
      ...deepClone(source),
      horizontal: (source.horizontal || []).map((value) => round(num(value) + dy)).filter((value) => value >= 0 && value <= height),
      vertical: (source.vertical || []).map((value) => round(num(value) + dx)).filter((value) => value >= 0 && value <= width),
      locked: Boolean(source.locked)
    };
  }

  function trimCanvasToContent() {
    const objects = state.doc.layers.flatMap((layer) => layer.visible
      ? layer.objects.filter((object) => object.visible !== false)
      : []);
    if (!objects.length) { toast('Kein sichtbarer Inhalt zum Zuschneiden vorhanden', 'warning'); return; }
    const bounds = boundsOf(objects, { visual: true });
    const left = clamp(Math.floor(bounds.x), 0, state.doc.width);
    const top = clamp(Math.floor(bounds.y), 0, state.doc.height);
    const right = clamp(Math.ceil(bounds.right), 0, state.doc.width);
    const bottom = clamp(Math.ceil(bounds.bottom), 0, state.doc.height);
    if (right <= left || bottom <= top) { toast('Innerhalb der Leinwand liegt kein sichtbarer Inhalt', 'warning'); return; }
    if (left === 0 && top === 0 && right === state.doc.width && bottom === state.doc.height) {
      toast('Keine leeren Leinwandränder zum Zuschneiden'); return;
    }
    state.canvasResizeMode = false; refs.shell.style.transform = '';
    const guides = deepClone(documentGuides());
    state.doc.width = clamp(right - left, 1, 10000); state.doc.height = clamp(bottom - top, 1, 10000);
    shiftDocumentContent(-left, -top, state.doc.width, state.doc.height, guides);
    if (state.canvasAspectLocked) state.canvasAspectRatio = state.doc.width / state.doc.height;
    commitHistory('Leinwand zugeschnitten'); renderAll();
    toast(`Leinwand zugeschnitten: ${state.doc.width} × ${state.doc.height}`);
  }

  function canvasResizeOrigin() {
    return {
      width: state.doc.width,
      height: state.doc.height,
      objects: state.doc.layers.flatMap((layer) => layer.objects.map((object) => ({ id: object.id, x: num(object.x), y: num(object.y) }))),
      guides: deepClone(documentGuides())
    };
  }

  function beginCanvasResizeGesture(event, handle) {
    const cursor = ({
      nw: 'nwse-resize', n: 'ns-resize', ne: 'nesw-resize', e: 'ew-resize',
      se: 'nwse-resize', s: 'ns-resize', sw: 'nesw-resize', w: 'ew-resize'
    })[handle] || 'default';
    const rect = refs.artboard.getBoundingClientRect();
    state.gesture = {
      kind: 'canvas-resize', handle,
      startClientX: event.clientX, startClientY: event.clientY,
      startRect: { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom },
      zoom: state.zoom, origin: canvasResizeOrigin(), moved: false, shellTransform: refs.shell.style.transform,
      transaction: beginDocumentTransaction('Leinwand manuell anpassen')
    };
    refs.workspace.style.cursor = cursor;
    refs.workspace.setPointerCapture?.(event.pointerId);
    event.preventDefault();
  }

  function updateCanvasResizeGesture(gesture, event) {
    const horizontalDelta = (event.clientX - gesture.startClientX) / gesture.zoom;
    const verticalDelta = (event.clientY - gesture.startClientY) / gesture.zoom;
    const west = gesture.handle.includes('w'); const east = gesture.handle.includes('e');
    const north = gesture.handle.includes('n'); const south = gesture.handle.includes('s');
    const width = west
      ? clamp(Math.round(gesture.origin.width - horizontalDelta), 1, 10000)
      : east ? clamp(Math.round(gesture.origin.width + horizontalDelta), 1, 10000) : gesture.origin.width;
    const height = north
      ? clamp(Math.round(gesture.origin.height - verticalDelta), 1, 10000)
      : south ? clamp(Math.round(gesture.origin.height + verticalDelta), 1, 10000) : gesture.origin.height;
    if (width === state.doc.width && height === state.doc.height) return;
    const shiftX = west ? width - gesture.origin.width : 0;
    const shiftY = north ? height - gesture.origin.height : 0;
    state.doc.width = width; state.doc.height = height;
    gesture.origin.objects.forEach((origin) => {
      const object = objectEntry(origin.id)?.object; if (!object) return;
      object.x = round(origin.x + shiftX); object.y = round(origin.y + shiftY);
    });
    const sourceGuides = gesture.origin.guides;
    state.doc.guides = {
      ...deepClone(sourceGuides),
      horizontal: (sourceGuides.horizontal || []).map((value) => round(num(value) + shiftY)).filter((value) => value >= 0 && value <= height),
      vertical: (sourceGuides.vertical || []).map((value) => round(num(value) + shiftX)).filter((value) => value >= 0 && value <= width),
      locked: Boolean(sourceGuides.locked)
    };
    gesture.moved = gesture.moved || width !== gesture.origin.width || height !== gesture.origin.height;
    refs.shell.style.transform = '';
    renderArtboard(); applyZoomDimensionsOnly();
    const base = refs.artboard.getBoundingClientRect();
    const desiredLeft = west ? gesture.startRect.left + (gesture.origin.width - width) * gesture.zoom : gesture.startRect.left;
    const desiredTop = north ? gesture.startRect.top + (gesture.origin.height - height) * gesture.zoom : gesture.startRect.top;
    refs.shell.style.transform = `translate(${round(desiredLeft - base.left)}px, ${round(desiredTop - base.top)}px)`;
    $('canvasWidth').value = width; $('canvasHeight').value = height;
    refs.propertyDockSummary.textContent = `${width} × ${height} px · ${state.doc.dpi} dpi`;
    scheduleRulerRender();
  }

  function finishCanvasResizeGesture(gesture, event) {
    const visualRect = refs.artboard.getBoundingClientRect();
    refs.workspace.releasePointerCapture?.(event.pointerId);
    refs.workspace.style.cursor = '';
    refs.shell.style.transform = '';
    state.gesture = null; state.snapGuides = [];
    if (gesture.moved) {
      if (state.canvasAspectLocked) state.canvasAspectRatio = state.doc.width / state.doc.height;
      commitHistory('Leinwand manuell angepasst');
    }
    renderAll();
    const resetRect = refs.artboard.getBoundingClientRect();
    refs.workspace.scrollLeft += resetRect.left - visualRect.left;
    refs.workspace.scrollTop += resetRect.top - visualRect.top;
    const scrolledRect = refs.artboard.getBoundingClientRect();
    const residualX = visualRect.left - scrolledRect.left; const residualY = visualRect.top - scrolledRect.top;
    refs.shell.style.transform = Math.abs(residualX) > .1 || Math.abs(residualY) > .1
      ? `translate(${round(residualX)}px, ${round(residualY)}px)`
      : '';
    scheduleRulerRender();
    if (gesture.moved) toast(`Leinwand: ${state.doc.width} × ${state.doc.height}`);
  }

  function setTextTextureMode(mode) {
    const object = primaryObject(); if (!supportsObjectTexture(object) || !Object.prototype.hasOwnProperty.call(textTextureModeLabels, mode)) return;
    const current = textTextureSettings(object);
    if (current.mode === mode) return;
    const next = { ...current, mode };
    if (current.mode === 'none') {
      const presets = {
        grain: { amount: 35, distortion: 5 }, waves: { amount: 30, distortion: 8 },
        paper: { amount: 32, distortion: 2, scale: 110 }, canvas: { amount: 42, distortion: 1, scale: 90 },
        wood: { amount: 45, distortion: 3, scale: 140 },
        grunge: { amount: 55, distortion: 0 }, distressed: { amount: 40, distortion: 0 },
        cracks: { amount: 42, distortion: 0 }, washed: { amount: 52, distribution: 0 },
        outline: { amount: 35, distortion: 0 }, pixel: { pixelSize: 6, gap: 1 }
      };
      if (presets[mode]) Object.assign(next, presets[mode]);
    }
    if (mode === 'none') delete object.textTexture;
    else object.textTexture = next;
    commitHistory(mode === 'none' ? 'Textur entfernt' : `${textTextureModeLabels[mode]} aktiviert`); renderAll();
  }

  function setTextTextureNumber(property, value, commit = false) {
    const object = primaryObject(); if (!supportsObjectTexture(object)) return;
    const texture = textTextureSettings(object); if (texture.mode === 'none') return;
    if (property === 'scale') {
      if (texture.mode === 'pixel') texture.pixelSize = clamp(num(value, texture.pixelSize), 1, 128);
      else texture.scale = clamp(num(value, texture.scale), 20, 400);
    }
    else if (property === 'distortion') {
      if (texture.mode === 'pixel') texture.gap = clamp(num(value, texture.gap), 0, 64);
      else if (texture.mode === 'washed') texture.distribution = clamp(num(value, texture.distribution), 0, 100);
      else texture.distortion = clamp(num(value, texture.distortion), 0, 40);
    }
    else if (property === 'amount') texture.amount = clamp(num(value, texture.amount), 0, 100);
    else return;
    object.textTexture = texture;
    if (property === 'scale') $('propTextTextureScaleValue').textContent = texture.mode === 'pixel' ? `${Math.round(texture.pixelSize)} px` : `${Math.round(texture.scale)} %`;
    else if (property === 'distortion') $('propTextTextureDistortionValue').textContent = texture.mode === 'pixel' ? `${Math.round(texture.gap)} px` : texture.mode === 'washed' ? `${Math.round(texture.distribution)} %` : `${Math.round(texture.distortion)} px`;
    else $('propTextTextureAmountValue').textContent = `${Math.round(texture.amount)} %`;
    if (commit) {
      const labels = {
        scale: texture.mode === 'pixel' ? 'Pixelgröße geändert' : 'Texturgröße geändert',
        distortion: texture.mode === 'pixel' ? 'Pixelabstand geändert' : texture.mode === 'washed' ? 'Texturverteilung geändert' : 'Textverformung geändert',
        amount: 'Texturintensität geändert'
      };
      commitHistory(labels[property]); renderAll();
    }
    else scheduleInteractionRender([object]);
  }

  function readFileAsDataUrl(file) {
    return new Promise((resolveValue, rejectValue) => {
      try { Security.assertFileSize(file, isSvgFile(file) ? 'svg' : 'bitmap'); }
      catch (error) { rejectValue(error); return; }
      const reader = new FileReader(); reader.onload = () => resolveValue(String(reader.result || ''));
      reader.onerror = () => rejectValue(new Error('Datei konnte nicht gelesen werden')); reader.readAsDataURL(file);
    });
  }

  function setObjectFillPaintType(object, type) {
    if (!supportsAdvancedFill(object) || !FillPaint.PAINT_TYPES.includes(type)) return false;
    const baseColor = normalizeColor(object.fill) || normalizeColor($('fillColor')?.value) || '#4f6df5';
    if (type === 'none') {
      delete object.fillPaint; object.fill = 'none'; fillStopSelection = { objectId: null, stopId: null };
    } else if (type === 'solid') {
      delete object.fillPaint; object.fill = baseColor; fillStopSelection = { objectId: null, stopId: null };
    } else {
      object.fillPaint = FillPaint.convertPaint(object.fillPaint, type, baseColor);
      object.fill = baseColor;
      const first = object.fillPaint?.stops?.[0]; fillStopSelection = { objectId: object.id, stopId: first?.id || null };
    }
    return true;
  }

  function updateSelectedFillStopField(property, value, commit = false) {
    const object = primaryObject(); const paint = normalizedObjectFillPaint(object); const stop = selectedFillStop(object, paint);
    if (!object || !paint || !stop || !FillPaint.isGradient(paint.type)) return;
    const patch = property === 'color' ? { color: FillPaint.normalizeColor(value, stop.color) }
      : property === 'offset' ? { offset: clamp(num(value, stop.offset), 0, 100) }
        : { opacity: clamp(num(value, stop.opacity), 0, 100) };
    object.fillPaint = FillPaint.updateStop(paint, stop.id, patch);
    object.fill = object.fillPaint.stops[0]?.color || object.fill;
    const selected = selectedFillStop(object, object.fillPaint); renderFillStopList(object, object.fillPaint);
    setField('fillStopPosition', selected.offset); setField('fillStopOpacity', selected.opacity);
    if (commit) { commitHistory('Verlaufsstopp geändert'); renderAll(); }
    else scheduleInteractionRender([object]);
  }

  function addFillStop() {
    const object = primaryObject(); const paint = normalizedObjectFillPaint(object);
    if (!object || !paint || !FillPaint.isGradient(paint.type)) return;
    const offset = FillPaint.largestStopGap(paint.stops); const id = uid('fill-stop');
    object.fillPaint = FillPaint.addStop(paint, offset, { id }); fillStopSelection = { objectId: object.id, stopId: id };
    commitHistory('Verlaufsstopp hinzugefügt'); renderAll();
  }

  function removeFillStop() {
    const object = primaryObject(); const paint = normalizedObjectFillPaint(object); const stop = selectedFillStop(object, paint);
    if (!object || !paint || !stop || paint.stops.length <= 2) return;
    object.fillPaint = FillPaint.removeStop(paint, stop.id);
    fillStopSelection = { objectId: object.id, stopId: object.fillPaint.stops[0].id };
    object.fill = object.fillPaint.stops[0].color; commitHistory('Verlaufsstopp entfernt'); renderAll();
  }

  function updateImageFillField(property, value, commit = false) {
    const object = primaryObject(); const paint = normalizedObjectFillPaint(object);
    if (!object || !paint || !FillPaint.isImage(paint.type)) return;
    object.fillPaint = FillPaint.normalizePaint({ ...paint, [property]: value }, { fallbackColor: object.fill });
    if (commit) { commitHistory('Bildfüllung geändert'); renderAll(); }
    else scheduleInteractionRender([object]);
  }

  async function loadObjectFillImage(file) {
    const object = primaryObject(); const paint = normalizedObjectFillPaint(object);
    if (!object || !paint || !FillPaint.isImage(paint.type)) return;
    if (!isImageFile(file)) { toast('Wähle PNG, JPEG, WebP, GIF oder SVG als Füllung', 'warning'); return; }
    const objectId = object.id; const button = $('loadFillImageBtn'); const previous = button.textContent;
    button.disabled = true; button.textContent = 'Lade …';
    try {
      const href = await readFileAsDataUrl(file); const image = await imageFromSource(href); Security.assertBitmapDimensions(image.naturalWidth, image.naturalHeight); const target = objectEntry(objectId)?.object;
      if (!target || !supportsAdvancedFill(target)) return;
      const current = normalizedObjectFillPaint(target);
      if (!current || !FillPaint.isImage(current.type)) return;
      target.fillPaint = FillPaint.normalizePaint({
        ...current, href, imageWidth: Math.max(1, image.naturalWidth), imageHeight: Math.max(1, image.naturalHeight)
      }, { fallbackColor: target.fill });
      state.selected = [target.id]; commitHistory(current.type === 'pattern' ? 'Bildmuster geladen' : 'Bildfüllung geladen'); renderAll();
    } catch (error) { toast(importErrorMessage(error, 'Das Bild konnte nicht als Füllung geladen werden'), 'error'); }
    finally { button.disabled = false; button.textContent = previous; $('fillImageFileInput').value = ''; }
  }

  function removeObjectFillImage() {
    const object = primaryObject(); const paint = normalizedObjectFillPaint(object);
    if (!object || !paint || !FillPaint.isImage(paint.type) || !paint.href) return;
    object.fillPaint = FillPaint.normalizePaint({ ...paint, href: '', imageWidth: 1, imageHeight: 1 }, { fallbackColor: object.fill });
    commitHistory('Bildfüllung gelöst'); renderAll();
  }

  function updateActiveStroke(property, value, commit = false, label = 'Kontur geändert') {
    const object = primaryObject();
    if (object?.type === 'image') {
      if (property === 'color') {
        object.stroke = normalizeColor(value) || 'none';
        if (object.stroke !== 'none' && num(object.strokeWidth, 0) <= 0) object.strokeWidth = 1;
      } else if (property === 'width') object.strokeWidth = clamp(num(value), 0, 10000);
      else return;
      if (commit) { commitHistory(label); renderAll(); }
      else scheduleInteractionRender([object], { inspector: true });
      return;
    }
    const context = activeStrokeContext(object, true); if (!context) return;
    const next = { ...context.style, [property]: value };
    if (property === 'strokeType') {
      next.enabled = value !== 'none';
      if (value !== 'none') { next.lineCap = value.endsWith('rounded') ? 'round' : 'butt'; next.lineJoin = value.endsWith('rounded') ? 'round' : 'miter'; }
    }
    if (property === 'enabled' && value && next.strokeType === 'none') next.strokeType = DEFAULT_STROKE_TYPE;
    if (property === 'pattern' && value === 'dotted' && next.lineCap === 'butt') next.lineCap = 'round';
    context.stack[context.index] = StrokeStyle.normalizeStyle(next, { fallback: context.style, index: context.index, createId: uid });
    setObjectStrokeStack(context.object, context.stack); strokeSelection = { objectId: context.object.id, strokeId: context.stack[context.index].id };
    if (commit) { commitHistory(label); renderAll(); }
    else scheduleInteractionRender([context.object], { inspector: true });
  }

  function addStrokeToSelection(duplicate = false) {
    const context = activeStrokeContext(primaryObject(), true); if (!context || context.stack.length >= StrokeStyle.MAX_STROKES) return;
    const source = context.style; const entry = StrokeStyle.normalizeStyle({
      ...source, id: uid('stroke'), enabled: true, strokeType: source.strokeType === 'none' ? DEFAULT_STROKE_TYPE : source.strokeType,
      width: duplicate ? source.width : Math.max(1, source.width + 4), alignment: duplicate ? source.alignment : 'outside'
    }, { fallback: source, index: context.index + 1, createId: uid });
    const insertAt = duplicate ? context.index + 1 : context.stack.length;
    context.stack.splice(insertAt, 0, entry); setObjectStrokeStack(context.object, context.stack);
    strokeSelection = { objectId: context.object.id, strokeId: entry.id };
    commitHistory(duplicate ? 'Kontur dupliziert' : 'Kontur hinzugefügt'); renderAll();
  }

  function removeActiveStroke() {
    const context = activeStrokeContext(primaryObject(), true); if (!context || context.stack.length <= 1) return;
    context.stack.splice(context.index, 1); const next = context.stack[Math.min(context.index, context.stack.length - 1)];
    setObjectStrokeStack(context.object, context.stack); strokeSelection = { objectId: context.object.id, strokeId: next.id };
    commitHistory('Kontur entfernt'); renderAll();
  }

  function moveActiveStroke(direction) {
    const context = activeStrokeContext(primaryObject(), true); if (!context) return;
    const target = context.index + direction; if (target < 0 || target >= context.stack.length) return;
    [context.stack[context.index], context.stack[target]] = [context.stack[target], context.stack[context.index]];
    setObjectStrokeStack(context.object, context.stack); strokeSelection = { objectId: context.object.id, strokeId: context.style.id };
    commitHistory(direction < 0 ? 'Kontur nach oben verschoben' : 'Kontur nach unten verschoben'); renderAll();
  }

  function bindInspector() {
    document.querySelectorAll('.property-section-toggle').forEach((button) => button.addEventListener('click', () => {
      const fieldset = button.closest('.property-fieldset'); if (!fieldset) return;
      if (state.propertyDockAccordion) {
        if (button.getAttribute('aria-expanded') !== 'true') enforceInspectorAccordion(inspectorSectionContext(), button.dataset.propertySection);
        return;
      }
      const collapsed = fieldset.classList.toggle('is-collapsed'); const expanded = !collapsed;
      button.setAttribute('aria-expanded', String(expanded)); persistInspectorSectionState(button.dataset.propertySection, expanded);
    }));
    $('effectTypeSelect').addEventListener('change', () => {
      $('effectTypeSelect').dataset.lastType = $('effectTypeSelect').value; syncEffectTypePickerSelection();
    });
    $('effectTypeTrigger').addEventListener('click', () => {
      if ($('effectTypeMenu').hidden) openEffectTypePicker(); else closeEffectTypePicker();
    });
    $('effectTypeTrigger').addEventListener('keydown', (event) => {
      if (!['ArrowDown', 'ArrowUp'].includes(event.key)) return;
      event.preventDefault(); event.stopPropagation(); openEffectTypePicker({ focusFirst: true });
    });
    $('effectTypeMenu').addEventListener('click', (event) => {
      const category = event.target.closest?.('[data-effect-category]');
      if (category) { toggleEffectTypeCategory(category.dataset.effectCategory); return; }
      const option = event.target.closest?.('[data-effect-type]'); if (!option) return;
      $('effectTypeSelect').value = option.dataset.effectType;
      $('effectTypeSelect').dispatchEvent(new Event('change', { bubbles: true }));
      closeEffectTypePicker({ restoreFocus: true });
    });
    $('effectTypeMenu').addEventListener('keydown', (event) => {
      if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); closeEffectTypePicker({ restoreFocus: true }); return; }
      const category = event.target.closest?.('[data-effect-category]');
      if (category && ['ArrowLeft', 'ArrowRight'].includes(event.key)) {
        const expanded = category.getAttribute('aria-expanded') === 'true';
        if ((event.key === 'ArrowRight' && !expanded) || (event.key === 'ArrowLeft' && expanded)) {
          event.preventDefault(); event.stopPropagation(); toggleEffectTypeCategory(category.dataset.effectCategory);
        }
        return;
      }
      if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
      const buttons = [...$('effectTypeMenu').querySelectorAll('button')].filter((button) => !button.closest('[hidden]'));
      if (!buttons.length) return;
      event.preventDefault(); event.stopPropagation();
      const current = buttons.indexOf(document.activeElement); const last = buttons.length - 1;
      const next = event.key === 'Home' ? 0 : event.key === 'End' ? last : event.key === 'ArrowDown' ? Math.min(last, current + 1) : Math.max(0, current - 1);
      buttons[next].focus();
    });
    $('addEffectBtn').addEventListener('click', addSelectedEffect);
    $('copyEffectsBtn').addEventListener('click', copySelectedEffects);
    $('pasteEffectsBtn').addEventListener('click', pasteSelectedEffects);
    $('effectStackList').addEventListener('input', (event) => {
      const control = event.target.closest?.('[data-effect-index][data-effect-field]'); if (control) updateEffectControl(control, false);
    });
    $('effectStackList').addEventListener('change', (event) => {
      const control = event.target.closest?.('[data-effect-index][data-effect-field]'); if (control) updateEffectControl(control, true);
    });
    $('effectStackList').addEventListener('click', (event) => {
      const button = event.target.closest?.('[data-effect-action]'); if (button) runEffectStackAction(button);
    });
    document.addEventListener('pointerdown', (event) => {
      if (!$('effectTypePicker')?.contains(event.target)) closeEffectTypePicker();
      const editor = document.getElementById('effectEditorPopover');
      if (!editor || editor.contains(event.target) || $('numberScrubber')?.contains(event.target) || $('colorPalette')?.contains(event.target)) return;
      closeEffectEditor();
    });
    document.addEventListener('keydown', (event) => {
      if (event.key === 'Escape' && document.getElementById('effectEditorPopover')) { closeEffectEditor(); event.preventDefault(); }
    });
    window.addEventListener('resize', () => {
      if (effectEditorTarget) positionEffectEditor();
      if (!$('effectTypeMenu')?.hidden) positionEffectTypePicker();
    });
    $('strokeStackList').addEventListener('click', (event) => {
      const item = event.target.closest?.('[data-stroke-id]'); const object = primaryObject(); if (!item || !supportsStrokeType(object)) return;
      const visibilityTrigger = event.target.closest?.('[data-stroke-visibility-trigger]');
      const colorTrigger = event.target.closest?.('[data-stroke-color-trigger]');
      const selectionChanged = item.getAttribute('aria-selected') !== 'true';
      strokeSelection = { objectId: object.id, strokeId: item.dataset.strokeId };
      if (visibilityTrigger) {
        const context = activeStrokeContext(object, true); const enabled = !context.style.enabled;
        updateActiveStroke('enabled', enabled, true, enabled ? 'Kontur eingeblendet' : 'Kontur ausgeblendet');
        return;
      }
      if (colorTrigger) {
        if (selectionChanged) renderInspector();
        const anchor = selectionChanged
          ? $('strokeStackList').querySelector('[aria-selected="true"] [data-stroke-color-trigger]')
          : colorTrigger;
        openColorPalette($('propStroke'), anchor);
        return;
      }
      renderInspector();
    });
    $('addStrokeBtn').addEventListener('click', () => addStrokeToSelection(false));
    $('duplicateStrokeBtn').addEventListener('click', () => addStrokeToSelection(true));
    $('removeStrokeBtn').addEventListener('click', removeActiveStroke);
    $('moveStrokeUpBtn').addEventListener('click', () => moveActiveStroke(-1)); $('moveStrokeDownBtn').addEventListener('click', () => moveActiveStroke(1));
    $('convertStrokeToPathBtn').addEventListener('click', () => { void convertActiveStrokeToPath(); });
    $('strokePattern').addEventListener('change', () => updateActiveStroke('pattern', $('strokePattern').value, true, 'Konturmuster geändert'));
    $('strokeDashArray').addEventListener('input', () => updateActiveStroke('dashArray', StrokeStyle.dashArray($('strokeDashArray').value), false));
    $('strokeDashArray').addEventListener('change', () => updateActiveStroke('dashArray', StrokeStyle.dashArray($('strokeDashArray').value), true, 'Strich- und Lückenfolge geändert'));
    [['strokeLineCap', 'lineCap', 'Endkappe geändert'], ['strokeLineJoin', 'lineJoin', 'Konturecke geändert'], ['strokeAlignment', 'alignment', 'Konturausrichtung geändert'], ['strokeStartMarker', 'startMarker', 'Anfangsmarker geändert'], ['strokeEndMarker', 'endMarker', 'Endmarker geändert']]
      .forEach(([id, property, label]) => $(id).addEventListener('change', () => updateActiveStroke(property, $(id).value, true, label)));
    [['strokeMiterLimit', 'miterLimit', 1, 1000, 'Gehrungsgrenze geändert'], ['strokeStartMarkerSize', 'startMarkerSize', 1, 1000, 'Anfangsmarkergröße geändert'], ['strokeEndMarkerSize', 'endMarkerSize', 1, 1000, 'Endmarkergröße geändert']]
      .forEach(([id, property, minimum, maximum, label]) => {
        $(id).addEventListener('input', () => { if ($(id).value !== '') updateActiveStroke(property, clamp(num($(id).value), minimum, maximum), false); });
        $(id).addEventListener('change', () => updateActiveStroke(property, clamp(num($(id).value), minimum, maximum), true, label));
      });
    $('bitmapInterpolation').addEventListener('change', () => {
      const object = primaryObject(); if (object?.type !== 'image') return;
      const interpolation = BitmapModel.interpolations.includes($('bitmapInterpolation').value) ? $('bitmapInterpolation').value : 'bicubic';
      if (bitmapInterpolationFor(object) === interpolation && object.bitmapInterpolation === interpolation) return;
      object.bitmapInterpolation = interpolation; commitHistory('Bitmap-Interpolation geändert'); renderAll();
    });
    $('bitmapMode').addEventListener('change', () => {
      const object = primaryObject(); if (object?.type !== 'image') return;
      if ($('bitmapMode').value === 'placed') {
        if (bitmapModeFor(object) === 'placed' && object.bitmapMode === 'placed') return;
        object.bitmapMode = 'placed'; commitHistory('Bitmap verlustfrei platziert'); renderAll();
        toast('Quellpixel bleiben bei weiteren Transformationen unverändert'); return;
      }
      void makeBitmapDirect(object, { label: 'Bitmap direkt bearbeitbar gemacht' }).catch((error) => {
        renderAll(); toast(error?.message || 'Bitmap konnte nicht neu berechnet werden', 'error');
      });
    });
    $('rasterizeBitmapBtn').addEventListener('click', () => {
      const object = primaryObject(); if (object?.type !== 'image') return;
      void makeBitmapDirect(object, { label: 'Bitmap auf aktuelle Größe neu berechnet' }).catch((error) => {
        renderAll(); toast(error?.message || 'Bitmap konnte nicht neu berechnet werden', 'error');
      });
    });
    const fields = {
      propX: ['x', Number], propY: ['y', Number], propW: ['width', Number], propH: ['height', Number],
      propRotation: ['rotation', Number], propOpacity: ['opacity', Number],
      propText: ['text', String], propFontFamily: ['fontFamily', String], propFontSize: ['fontSize', Number],
      propLetterSpacing: ['letterSpacing', Number], propLineHeight: ['lineHeight', Number], propHorizontalScale: ['horizontalScale', Number], propVerticalScale: ['verticalScale', Number], propBaselineShift: ['baselineShift', Number]
    };
    Object.entries(fields).forEach(([id, [property, cast]]) => {
      const field = $(id); if (!field) return;
      field.addEventListener('input', () => {
        const object = primaryObject(); if (!object || (cast === Number && field.value === '')) return; let value = cast(field.value);
        if (object.type === 'image' && ['width', 'height'].includes(property) && bitmapModeFor(object) === 'direct'
          && bitmapPropertyResizeOrigin?.objectId !== object.id) {
          bitmapPropertyResizeOrigin = { objectId: object.id, object: deepClone(object) };
        }
        const previousText = object.text;
        const targetProperty = object.type === 'text' && property === 'stroke' ? 'textStroke'
          : object.type === 'text' && property === 'strokeWidth' ? 'textStrokeWidth'
            : property;
        const origin = {
          type: object.type, width: object.width, height: object.height, scalePercent: object.scalePercent,
          autoShape: deepClone(object.autoShape), lThickness: object.lThickness, lRoundness: object.lRoundness
        };
        if (['width', 'height'].includes(property)) value = Math.max(1, value); if (property === 'opacity') value = clamp(value, 0, 100);
        if (object.type === 'text') {
          if (property === 'letterSpacing') value = clamp(value, -50, 200);
          if (property === 'lineHeight') value = clamp(value, 40, 400);
          if (['horizontalScale', 'verticalScale'].includes(property)) value = clamp(value, 25, 400);
          if (property === 'baselineShift') value = clamp(value, -1000, 1000);
        }
        if (['width', 'height'].includes(property) && containerObjectTypes.has(object.type) && Array.isArray(object.children)) {
          const previous = Math.max(1, num(object[property], 1));
          const scaleX = property === 'width' ? value / previous : 1; const scaleY = property === 'height' ? value / previous : 1;
          object.children = object.children.map((child) => scaleObjectTree(child, scaleX, scaleY)); object[property] = value;
        } else if (['width', 'height'].includes(property) && ['pencil', 'brush', 'eraser', 'line', 'polyline', 'arrow', 'measure', 'polygon', 'pen'].includes(object.type) && object.points) {
          const previous = Math.max(1, num(object[property], 1));
          const coordinate = property === 'width' ? 0 : 1;
          object.points = object.points.map((point) => point.map((entry, index) => index === coordinate ? round(entry * value / previous) : entry));
          if (Array.isArray(object.rawPoints)) object.rawPoints = object.rawPoints.map((point) => point.map((entry, index) => index === coordinate ? round(entry * value / previous) : entry));
          if (Array.isArray(object.bezierSegments)) object.bezierSegments = object.bezierSegments.map((segment) => isBezierSegment(segment) ? {
            c1: segment.c1.map((entry, index) => index === coordinate ? round(entry * value / previous) : entry),
            c2: segment.c2.map((entry, index) => index === coordinate ? round(entry * value / previous) : entry)
          } : null);
          object[property] = value;
        } else if (['width', 'height'].includes(property) && object.type === 'text') {
          const previous = Math.max(1, num(object[property], 1));
          object.fontSize = clamp(num(object.fontSize, 32) * value / previous, 6, 400);
          sizeTextObject(object);
        } else object[targetProperty] = value;
        if (object.type === 'image' && property === 'stroke' && normalizeColor(value) && num(object.strokeWidth, 0) <= 0) object.strokeWidth = 1;
        const textNameChanged = object.type === 'text' && property === 'text' ? syncTextObjectName(object, previousText) : false;
        if (object.type === 'text' && ['text', 'fontFamily', 'fontSize', 'letterSpacing', 'lineHeight', 'horizontalScale', 'verticalScale'].includes(property)) sizeTextObject(object);
        if (['width', 'height'].includes(property)) {
          clampObjectRoundness(object);
          if (AutoShapes.isAutoShape(object)) AutoShapes.scaleParams(object, origin, num(object.width, 1) / Math.max(1, num(origin.width, 1)), num(object.height, 1) / Math.max(1, num(origin.height, 1)));
          updateObjectScalePercent(object, origin);
        }
        if (textNameChanged) refreshObjectNamePresentation(object);
        scheduleInteractionRender([object]);
      });
      field.addEventListener('change', () => {
        const object = primaryObject(); if (!object) return;
        if (object.type === 'image' && ['width', 'height'].includes(property) && bitmapModeFor(object) === 'direct') {
          const origin = bitmapPropertyResizeOrigin?.objectId === object.id ? bitmapPropertyResizeOrigin.object : null;
          bitmapPropertyResizeOrigin = null; void finishDirectBitmapPropertyResize(object, origin); return;
        }
        bitmapPropertyResizeOrigin = null; commitHistory('Eigenschaft geändert'); renderAll();
      });
    });
    $('propCornerRadius').addEventListener('input', () => {
      const object = primaryObject(); if (!supportsCornerRoundness(object)) return;
      const radius = setObjectRoundness(object, $('propCornerRadius').value);
      $('propCornerRadius').value = String(radius);
      scheduleInteractionRender([object], { inspector: true });
    });
    $('propCornerRadius').addEventListener('change', () => {
      const object = primaryObject(); if (!supportsCornerRoundness(object)) return;
      commitHistory('Eckenrundung geändert'); renderAll();
    });
    $('propBlendMode').addEventListener('change', () => {
      const object = primaryObject(); if (!object) return;
      const mode = normalizeBlendMode($('propBlendMode').value); $('propBlendMode').value = mode;
      if (normalizeBlendMode(object.blendMode) === mode) return;
      object.blendMode = mode; commitHistory('Objekt-Mischmodus geändert'); renderAll();
    });
    $('propFill').addEventListener('input', () => {
      const object = primaryObject(); if (!object || !supportsAppearanceFill(object)) return;
      const paint = normalizedObjectFillPaint(object);
      if (paint && FillPaint.isGradient(paint.type)) updateSelectedFillStopField('color', $('propFill').value, false);
      else { delete object.fillPaint; object.fill = $('propFill').value; scheduleInteractionRender([object]); }
    });
    $('propFill').addEventListener('change', () => {
      const object = primaryObject(); if (!object || !supportsAppearanceFill(object)) return;
      const paint = normalizedObjectFillPaint(object);
      if (paint && FillPaint.isGradient(paint.type)) updateSelectedFillStopField('color', $('propFill').value, true);
      else { delete object.fillPaint; object.fill = $('propFill').value; commitHistory('Füllung geändert'); renderAll(); }
    });
    $('propFillType').addEventListener('change', () => {
      const object = primaryObject(); const type = $('propFillType').value;
      if (!setObjectFillPaintType(object, type)) return;
      commitHistory('Füllart geändert'); renderAll();
      if (FillPaint.isImage(type) && !normalizedObjectFillPaint(object)?.href) $('fillImageFileInput').click();
    });
    const fillStopList = $('fillStopList');
    let fillStopDrag = null;
    const finishFillStopDrag = (event, cancelled = false) => {
      const drag = fillStopDrag;
      if (!drag || (event?.pointerId != null && event.pointerId !== drag.pointerId)) return;
      fillStopDrag = null;
      try { drag.captureTarget?.releasePointerCapture?.(drag.pointerId); } catch { /* Der Zeiger kann bereits freigegeben sein. */ }
      if (cancelled) {
        rollbackDocumentTransaction(drag.transaction); renderAll(); return;
      }
      const paint = normalizedObjectFillPaint(drag.object);
      const canRemove = Boolean(paint && paint.stops.length > 2);
      if (drag.outside) {
        if (!canRemove) {
          rollbackDocumentTransaction(drag.transaction); renderAll(); toast('Ein Verlauf braucht mindestens zwei Farbpunkte', 'warning'); return;
        }
        drag.object.fillPaint = FillPaint.removeStop(paint, drag.stopId);
        drag.object.fill = drag.object.fillPaint.stops[0]?.color || drag.object.fill;
        const nearest = drag.object.fillPaint.stops.reduce((best, stop) => Math.abs(stop.offset - drag.offset) < Math.abs(best.offset - drag.offset) ? stop : best);
        fillStopSelection = { objectId: drag.object.id, stopId: nearest.id };
        commitHistory('Verlaufsstopp entfernt'); renderAll(); return;
      }
      if (drag.moved) {
        commitHistory('Verlaufsstopp verschoben'); renderAll(); return;
      }
      const stop = selectFillStopInInspector(drag.object, paint, drag.stopId);
      if (stop) openColorPalette($('propFill'));
    };
    fillStopList.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      const marker = event.target.closest?.('[data-fill-stop-id]'); const object = primaryObject(); const paint = normalizedObjectFillPaint(object);
      if (!marker || !object || !paint || !FillPaint.isGradient(paint.type)) return;
      const stop = selectFillStopInInspector(object, paint, marker.dataset.fillStopId); if (!stop) return;
      const bounds = fillStopList.getBoundingClientRect();
      fillStopDrag = {
        pointerId: event.pointerId, object, stopId: stop.id, marker, bounds,
        pointerType: event.pointerType || 'mouse',
        startX: event.clientX, startY: event.clientY, offset: stop.offset,
        moved: false, outside: false, captureTarget: marker, transaction: beginDocumentTransaction('Verlaufsstopp verschoben')
      };
      if (event.pointerType !== 'mouse') marker.setPointerCapture?.(event.pointerId);
      event.preventDefault(); event.stopPropagation();
    });
    const moveFillStopDrag = (event) => {
      const drag = fillStopDrag; if (!drag) return;
      if (event.pointerId !== drag.pointerId) return;
      const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
      drag.moved ||= distance > 3;
      const rawOffset = clamp((event.clientX - drag.bounds.left) / Math.max(1, drag.bounds.width) * 100, 0, 100);
      drag.offset = snapFillStopOffset(rawOffset, drag.bounds, event);
      drag.outside = event.clientY < drag.bounds.top - 18 || event.clientY > drag.bounds.bottom + 18;
      const paint = normalizedObjectFillPaint(drag.object); if (!paint || !FillPaint.isGradient(paint.type)) return;
      drag.object.fillPaint = FillPaint.updateStop(paint, drag.stopId, { offset: drag.offset });
      drag.object.fill = drag.object.fillPaint.stops[0]?.color || drag.object.fill;
      const marker = fillStopList.querySelector(`[data-fill-stop-id="${drag.stopId}"]`);
      if (marker) {
        marker.style.left = `${drag.offset}%`;
        marker.style.setProperty('--stop-drag-y', `${clamp(event.clientY - drag.startY, -48, 48)}px`);
        marker.classList.toggle('is-removing', drag.outside && drag.object.fillPaint.stops.length > 2);
      }
      setFillStopListGradient(fillStopList, drag.object.fillPaint);
      setField('fillStopPosition', round(drag.offset, 1));
      scheduleInteractionRender([drag.object]); event.preventDefault();
    };
    window.addEventListener('pointermove', moveFillStopDrag);
    window.addEventListener('pointerup', (event) => { if (!fillStopDrag) return; finishFillStopDrag(event); event.preventDefault(); });
    window.addEventListener('mouseup', (event) => { if (fillStopDrag?.pointerType !== 'mouse') return; finishFillStopDrag(null); event.preventDefault(); });
    window.addEventListener('pointercancel', (event) => finishFillStopDrag(event, true));
    fillStopList.addEventListener('click', (event) => {
      // Tastatur-Clicks besitzen detail 0; Zeiger-Clicks werden bereits über pointerup behandelt.
      if (event.detail !== 0) return;
      const marker = event.target.closest?.('[data-fill-stop-id]'); const object = primaryObject(); const paint = normalizedObjectFillPaint(object);
      if (!marker || !object || !paint) return;
      if (selectFillStopInInspector(object, paint, marker.dataset.fillStopId)) openColorPalette($('propFill'));
    });
    fillStopList.addEventListener('dblclick', (event) => {
      if (event.target.closest?.('[data-fill-stop-id]')) return;
      const object = primaryObject(); const paint = normalizedObjectFillPaint(object); if (!object || !paint || !FillPaint.isGradient(paint.type)) return;
      const bounds = fillStopList.getBoundingClientRect(); const offset = clamp((event.clientX - bounds.left) / Math.max(1, bounds.width) * 100, 0, 100); const id = uid('fill-stop');
      object.fillPaint = FillPaint.addStop(paint, offset, { id }); fillStopSelection = { objectId: object.id, stopId: id };
      commitHistory('Verlaufsstopp hinzugefügt'); renderAll();
    });
    $('addFillStopBtn').addEventListener('click', addFillStop); $('removeFillStopBtn').addEventListener('click', removeFillStop);
    $('fillStopPosition').addEventListener('input', () => { if ($('fillStopPosition').value !== '') updateSelectedFillStopField('offset', $('fillStopPosition').value, false); });
    $('fillStopPosition').addEventListener('change', () => updateSelectedFillStopField('offset', $('fillStopPosition').value, true));
    $('fillStopOpacity').addEventListener('input', () => { if ($('fillStopOpacity').value !== '') updateSelectedFillStopField('opacity', $('fillStopOpacity').value, false); });
    $('fillStopOpacity').addEventListener('change', () => updateSelectedFillStopField('opacity', $('fillStopOpacity').value, true));
    [['fillImageScale', 'scale'], ['fillImageRotation', 'rotation'], ['fillImageOffsetX', 'offsetX'], ['fillImageOffsetY', 'offsetY']].forEach(([id, property]) => {
      $(id).addEventListener('input', () => { if ($(id).value !== '') updateImageFillField(property, Number($(id).value), false); });
      $(id).addEventListener('change', () => updateImageFillField(property, Number($(id).value), true));
    });
    $('loadFillImageBtn').addEventListener('click', () => $('fillImageFileInput').click());
    $('removeFillImageBtn').addEventListener('click', removeObjectFillImage);
    $('fillImageFileInput').addEventListener('change', (event) => { void loadObjectFillImage(event.target.files[0]); });
    $('propStroke').addEventListener('input', () => updateActiveStroke('color', $('propStroke').value, false));
    $('propStroke').addEventListener('change', () => updateActiveStroke('color', $('propStroke').value, true, 'Konturfarbe geändert'));
    $('propStrokeWidth').addEventListener('input', () => { if ($('propStrokeWidth').value !== '') updateActiveStroke('width', clamp(num($('propStrokeWidth').value), 0, 10000), false); });
    $('propStrokeWidth').addEventListener('change', () => updateActiveStroke('width', clamp(num($('propStrokeWidth').value), 0, 10000), true, 'Konturstärke geändert'));
    $('propStrokeType').addEventListener('change', () => {
      const value = normalizeStrokeType($('propStrokeType').value);
      updateActiveStroke('strokeType', value, true, value === 'none' ? 'Konturlinie entfernt' : `Linientyp: ${strokeTypeDefinition(value).label}`);
    });
    $('propScale').addEventListener('input', () => {
      const object = primaryObject(); const field = $('propScale'); if (!object || field.value === '') return;
      if (object.type === 'image' && bitmapModeFor(object) === 'direct' && bitmapPropertyResizeOrigin?.objectId !== object.id) {
        bitmapPropertyResizeOrigin = { objectId: object.id, object: deepClone(object) };
      }
      setObjectScalePercent(object, field.value); field.value = objectScalePercent(object); scheduleInteractionRender([object], { inspector: true });
    });
    $('propScale').addEventListener('change', () => {
      const object = primaryObject(); if (!object) return;
      if (object.type === 'image' && bitmapModeFor(object) === 'direct') {
        const origin = bitmapPropertyResizeOrigin?.objectId === object.id ? bitmapPropertyResizeOrigin.object : null;
        bitmapPropertyResizeOrigin = null; void finishDirectBitmapPropertyResize(object, origin); return;
      }
      bitmapPropertyResizeOrigin = null; commitHistory('Objekt skaliert'); renderAll();
    });
    $('propSmoothing').addEventListener('input', () => {
      const object = primaryObject(); if (!object || object.type !== 'brush') return;
      const smoothing = clamp(Math.round(num($('propSmoothing').value, 50)), 0, 100);
      object.smoothing = smoothing;
      $('propSmoothingValue').textContent = `${smoothing} %`; scheduleInteractionRender([object]);
    });
    $('propSmoothing').addEventListener('change', () => {
      const object = primaryObject(); if (object?.type === 'brush') { commitHistory('Pinselglättung geändert'); renderAll(); }
    });
    $('propPolygonSides').addEventListener('input', () => {
      const object = primaryObject(); const field = $('propPolygonSides');
      if (object?.type !== 'polygon' || field.value === '') return;
      const sides = polygonSideCount(field.value); field.value = sides; setPolygonSides(object, sides); scheduleInteractionRender([object]);
    });
    $('propPolygonSides').addEventListener('change', () => {
      const object = primaryObject(); if (object?.type !== 'polygon') return;
      setPolygonSides(object, $('propPolygonSides').value); commitHistory('Polygonpunkte geändert'); renderAll();
    });
    const updateAutoShapeField = (target, commit) => {
      const object = primaryObject(); const field = target?.dataset?.autoShapeField;
      if (!AutoShapes.isAutoShape(object) || !field || (target.type === 'number' && target.value === '')) return;
      const value = target.type === 'checkbox' ? target.checked : target.type === 'number' ? Number(target.value) : target.value;
      AutoShapes.setProperty(object, field, value);
      if (commit) {
        commitHistory(`${AutoShapes.definitionFor(object)?.label || 'Auto-Form'} geändert`); renderAll();
      } else scheduleInteractionRender([object]);
    };
    $('autoShapePropertyGrid').addEventListener('input', (event) => {
      const target = event.target.closest?.('[data-auto-shape-field]'); if (target) updateAutoShapeField(target, false);
    });
    $('autoShapePropertyGrid').addEventListener('change', (event) => {
      const target = event.target.closest?.('[data-auto-shape-field]'); if (target) updateAutoShapeField(target, true);
    });
    [['propBold', 'bold'], ['propItalic', 'italic'], ['propUnderline', 'underline'], ['propStrike', 'strike']].forEach(([id, property]) => $(id).addEventListener('click', () => {
      const object = primaryObject(); if (!object) return; object[property] = !object[property]; commitHistory('Textstil geändert'); renderAll();
    }));
    $('propFontStyle').addEventListener('change', () => {
      const object = primaryObject(); if (object?.type !== 'text') return;
      const style = $('propFontStyle').value; object.bold = style === 'bold' || style === 'bold-italic'; object.italic = style === 'italic' || style === 'bold-italic';
      commitHistory('Textstil geändert'); renderAll();
    });
    $('propAntiAlias').addEventListener('change', () => {
      const object = primaryObject(); if (object?.type !== 'text') return;
      object.antiAlias = $('propAntiAlias').value; commitHistory('Textdarstellung geändert'); renderAll();
    });
    $('propAutoKern').addEventListener('change', () => {
      const object = primaryObject(); if (object?.type !== 'text') return;
      object.autoKern = $('propAutoKern').checked;
      commitHistory('Kerning geändert');
      scheduleInteractionRender([object]);
      renderDocumentTabs();
    });
    [['propAlignLeft', 'left'], ['propAlignCenter', 'center'], ['propAlignRight', 'right'], ['propAlignJustify', 'justify']].forEach(([id, align]) => $(id).addEventListener('click', () => {
      const object = primaryObject(); if (object?.type !== 'text') return;
      object.align = align; commitHistory('Text ausgerichtet'); renderAll();
    }));
  }

  function cursorForTool(tool = state.tool) {
    if (state.spaceDown || tool === 'pan') return 'grab';
    if (tool === 'select') return 'default';
    if (tool === 'vector-select') return vectorCursor;
    if (tool === 'knife') return knifeCursor;
    if (tool === 'pen') return penCursor;
    if (tool === 'text') return 'text';
    return 'crosshair';
  }

  function renderEraserCursor(point = state.eraserCursorPoint) {
    const previous = refs.artboard.querySelector('[data-eraser-cursor]');
    const object = primaryObject();
    const dimensions = object?.type === 'image' && point ? bitmapPixelDimensions(object) : null;
    const pixel = dimensions && bitmapPixelPoint(object, point, dimensions);
    if (!eraserToolInfo[state.tool] || !object || object.type !== 'image' || !pixel) {
      previous?.remove();
      if (!state.spaceDown) refs.artboard.style.cursor = cursorForTool();
      return;
    }
    const settings = eraserSettings(); const widthScale = num(object.width, 1) / dimensions.width; const heightScale = num(object.height, 1) / dimensions.height;
    const centerX = (pixel.x + .5) * widthScale; const centerY = (pixel.y + .5) * heightScale;
    const radiusX = settings.size * widthScale / 2; const radiusY = settings.size * heightScale / 2;
    const group = previous || E('g', { 'data-eraser-cursor': 'true', 'pointer-events': 'none' });
    group.setAttribute('transform', objectTransform(object)); group.dataset.eraserTool = state.tool; group.dataset.eraserShape = settings.shape; group.dataset.eraserSize = String(settings.size);
    const attributes = settings.shape === 'circle'
      ? { cx: centerX, cy: centerY, rx: radiusX, ry: radiusY }
      : { x: centerX - radiusX, y: centerY - radiusY, width: radiusX * 2, height: radiusY * 2 };
    const makeOutline = (stroke, width) => E(settings.shape === 'circle' ? 'ellipse' : 'rect', {
      ...attributes, fill: 'none', stroke, 'stroke-width': width, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none'
    });
    group.replaceChildren(makeOutline('rgba(23,32,51,.92)', 3), makeOutline('rgba(255,255,255,.95)', 1));
    if (!previous) (artboardScene.overlayRoot || refs.artboard).append(group);
    refs.artboard.style.cursor = 'none';
  }

  function updateEraserCursor(point) {
    state.eraserCursorPoint = point ? { x: point.x, y: point.y } : null;
    renderEraserCursor();
  }

  function renderPathModifierCursor(point = state.pathModifierCursorPoint) {
    const previous = refs.artboard.querySelector('[data-path-modifier-cursor]');
    if (!pathModifierToolInfo[state.tool] || !point) {
      previous?.remove();
      if (!state.spaceDown && !eraserToolInfo[state.tool]) refs.artboard.style.cursor = cursorForTool();
      return;
    }
    const settings = pathModifierSettings(); const radius = settings.size / 2;
    const group = previous || E('g', { 'data-path-modifier-cursor': 'true', 'pointer-events': 'none' });
    group.dataset.pathModifierTool = state.tool; group.dataset.pathModifierSize = String(settings.size);
    const circle = (r, stroke, width, dash) => E('circle', {
      cx: point.x, cy: point.y, r, fill: 'none', stroke, 'stroke-width': width / state.zoom,
      'stroke-dasharray': dash, 'vector-effect': 'non-scaling-stroke', 'pointer-events': 'none'
    });
    const children = [circle(radius, 'rgba(23,32,51,.94)', 3), circle(radius, 'rgba(255,255,255,.96)', 1)];
    if (state.tool === 'reshape-area') {
      children.push(circle(radius * .34, 'rgba(23,32,51,.9)', 2, `${3 / state.zoom} ${3 / state.zoom}`));
      children.push(circle(radius * .34, 'rgba(255,255,255,.95)', 1, `${3 / state.zoom} ${3 / state.zoom}`));
    }
    if (state.tool.startsWith('path-scrubber-')) {
      const sign = state.tool === 'path-scrubber-add' ? '+' : '−';
      const label = E('text', {
        x: point.x, y: point.y, 'text-anchor': 'middle', 'dominant-baseline': 'central', fill: '#ffffff', stroke: '#172033',
        'stroke-width': 2.5 / state.zoom, 'paint-order': 'stroke', 'font-size': Math.max(10 / state.zoom, Math.min(radius, 18 / state.zoom)),
        'font-family': 'Arial, sans-serif', 'font-weight': 700, 'pointer-events': 'none'
      });
      label.textContent = sign; children.push(label);
    }
    group.replaceChildren(...children);
    if (!previous) (artboardScene.overlayRoot || refs.artboard).append(group);
    refs.artboard.style.cursor = 'none';
  }

  function updatePathModifierCursor(point) {
    state.pathModifierCursorPoint = point ? { x: point.x, y: point.y } : null;
    renderPathModifierCursor();
  }

  function updateWarpCursor(point) {
    state.warpCursorPoint = point ? { x: point.x, y: point.y } : null;
    if (state.tool === 'liquify') renderEditorOverlays();
  }

  function toolInfoIconMarkup(info) {
    if (info?.tablerIcon) return Tabler.svg(info.tablerIcon);
    return info?.icon ? `<svg aria-hidden="true" viewBox="0 0 24 24">${info.icon}</svg>` : '';
  }

  function updateEraserToolButton(tool = currentEraserTool) {
    if (!eraserToolInfo[tool]) return;
    currentEraserTool = tool;
    const button = $('eraserTool'); const icon = $('eraserToolIcon'); const label = $('eraserToolLabel');
    if (!button || !icon || !label) return;
    const info = eraserToolInfo[tool];
    button.dataset.tool = tool; button.dataset.eraserCurrent = tool; button.title = `${info.title} – gedrückt halten zum Wechseln`;
    button.setAttribute('aria-label', `${info.label}; gedrückt halten zum Wechseln`);
    icon.innerHTML = toolInfoIconMarkup(info);
    label.textContent = info.label;
    document.querySelectorAll('[data-eraser-option]').forEach((option) => option.setAttribute('aria-checked', String(option.dataset.eraserOption === tool)));
    persistEraserToolSettings();
  }

  function closeEraserMenu() {
    const menu = $('eraserMenu'); const button = $('eraserTool');
    if (!menu || menu.hidden) return false;
    menu.hidden = true; button?.setAttribute('aria-expanded', 'false'); return true;
  }

  function openEraserMenu() {
    const menu = $('eraserMenu'); const button = $('eraserTool');
    if (!menu || !button) return;
    updateEraserToolButton(); positionToolMenu(menu, button); button.setAttribute('aria-expanded', 'true');
  }

  function selectEraserTool(tool) {
    if (!eraserToolInfo[tool]) return;
    updateEraserToolButton(tool); closeEraserMenu(); chooseTool(tool);
  }

  function bindEraserToolMenu() {
    const button = $('eraserTool'); const menu = $('eraserMenu'); const wrapper = button?.closest('.eraser-tool-wrap');
    if (!button || !menu || !wrapper) return;
    updateEraserToolButton();
    const clearPress = () => { if (eraserPressTimer) window.clearTimeout(eraserPressTimer); eraserPressTimer = null; };
    button.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      clearPress(); eraserPress = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, opened: false };
      button.setPointerCapture?.(event.pointerId);
      eraserPressTimer = window.setTimeout(() => {
        if (!eraserPress || eraserPress.pointerId !== event.pointerId) return;
        eraserPress.opened = true; openEraserMenu();
      }, 430);
    });
    button.addEventListener('pointermove', (event) => {
      if (!eraserPress || eraserPress.pointerId !== event.pointerId || eraserPress.opened) return;
      if (Math.hypot(event.clientX - eraserPress.x, event.clientY - eraserPress.y) > 10) { clearPress(); eraserPress = null; }
    });
    const finishPress = (event) => {
      if (!eraserPress || eraserPress.pointerId !== event.pointerId) return;
      clearPress(); suppressEraserClick = eraserPress.opened; eraserPress = null;
      if (suppressEraserClick) event.preventDefault();
    };
    button.addEventListener('pointerup', finishPress); button.addEventListener('pointercancel', finishPress);
    button.addEventListener('click', (event) => {
      event.preventDefault(); event.stopPropagation();
      if (suppressEraserClick) { suppressEraserClick = false; return; }
      closeEraserMenu(); chooseTool(currentEraserTool);
    });
    menu.addEventListener('click', (event) => {
      const option = event.target.closest?.('[data-eraser-option]'); if (!option) return;
      event.preventDefault(); event.stopPropagation(); selectEraserTool(option.dataset.eraserOption);
    });
    document.addEventListener('pointerdown', (event) => { if (!wrapper.contains(event.target)) closeEraserMenu(); });
  }

  function updateRetouchToolButton(tool = currentRetouchTool) {
    if (!retouchToolInfo[tool]) return;
    currentRetouchTool = tool;
    const button = $('retouchTool'); const icon = $('retouchToolIcon'); const label = $('retouchToolLabel');
    if (!button || !icon || !label) return;
    const info = retouchToolInfo[tool];
    button.dataset.tool = tool; button.dataset.retouchCurrent = tool; button.title = `${info.title} – gedrückt halten für weitere Retuschewerkzeuge`;
    button.setAttribute('aria-label', `${info.label}; gedrückt halten für weitere Retuschewerkzeuge`);
    icon.innerHTML = toolInfoIconMarkup(info);
    label.textContent = info.label;
    document.querySelectorAll('[data-retouch-option]').forEach((option) => option.setAttribute('aria-checked', String(option.dataset.retouchOption === tool)));
  }

  function closeRetouchMenu() {
    const menu = $('retouchMenu'); const button = $('retouchTool');
    if (!menu || menu.hidden) return false;
    menu.hidden = true; button?.setAttribute('aria-expanded', 'false'); return true;
  }

  function positionToolMenu(menu, button, { compactWhenNeeded = false } = {}) {
    const margin = 8; const viewportWidth = Math.max(1, window.innerWidth); const viewportHeight = Math.max(1, window.innerHeight);
    if (compactWhenNeeded) menu.classList.remove('is-compact', 'is-ultra-compact');
    menu.style.left = `${margin}px`; menu.style.top = `${margin}px`;
    menu.hidden = false;
    const availableHeight = viewportHeight - margin * 2;
    if (compactWhenNeeded && menu.getBoundingClientRect().height > availableHeight) {
      menu.classList.add('is-compact');
      if (menu.getBoundingClientRect().height > availableHeight) menu.classList.add('is-ultra-compact');
    }
    const menuRect = menu.getBoundingClientRect(); const buttonRect = button.getBoundingClientRect();
    let left = buttonRect.right + 6;
    if (left + menuRect.width > viewportWidth - margin) left = buttonRect.left - menuRect.width - 6;
    left = clamp(left, margin, Math.max(margin, viewportWidth - menuRect.width - margin));
    const top = clamp(buttonRect.top - 2, margin, Math.max(margin, viewportHeight - menuRect.height - margin));
    menu.style.left = `${Math.round(left)}px`; menu.style.top = `${Math.round(top)}px`;
  }

  function bindToolMenuKeyboard(button, menu, openMenu, closeMenu) {
    if (!button || !menu) return;
    const items = () => [...menu.querySelectorAll('[role="menuitemradio"]')].filter((item) => !item.disabled);
    const focusItem = (index = 0) => {
      const options = items(); if (!options.length) return;
      options.forEach((item) => { item.tabIndex = -1; });
      const target = options[(index + options.length) % options.length]; target.tabIndex = 0; target.focus();
    };
    const openFromKeyboard = (last = false) => {
      openMenu();
      const options = items(); const checked = options.findIndex((item) => item.getAttribute('aria-checked') === 'true');
      focusItem(last ? options.length - 1 : Math.max(0, checked));
    };
    button.addEventListener('keydown', (event) => {
      if (!['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(event.key)) return;
      event.preventDefault(); event.stopPropagation(); openFromKeyboard(event.key === 'ArrowUp');
    });
    menu.addEventListener('keydown', (event) => {
      const options = items(); const current = options.indexOf(document.activeElement); if (!options.length) return;
      if (event.key === 'Escape') {
        event.preventDefault(); event.stopPropagation(); closeMenu(); button.focus(); return;
      }
      if (event.key === 'Home' || event.key === 'End' || event.key === 'ArrowDown' || event.key === 'ArrowUp') {
        event.preventDefault();
        const index = event.key === 'Home' ? 0 : event.key === 'End' ? options.length - 1 : current + (event.key === 'ArrowDown' ? 1 : -1);
        focusItem(index); return;
      }
      if ((event.key === 'Enter' || event.key === ' ') && current >= 0) {
        event.preventDefault(); options[current].click(); button.focus();
      }
    });
  }

  function openRetouchMenu() {
    const menu = $('retouchMenu'); const button = $('retouchTool');
    if (!menu || !button) return;
    updateRetouchToolButton(); positionToolMenu(menu, button); button.setAttribute('aria-expanded', 'true');
  }

  function selectRetouchTool(tool) {
    if (!retouchToolInfo[tool]) return;
    updateRetouchToolButton(tool); closeRetouchMenu(); chooseTool(tool);
  }

  function bindRetouchToolMenu() {
    const button = $('retouchTool'); const menu = $('retouchMenu'); const wrapper = button?.closest('.retouch-tool-wrap');
    if (!button || !menu || !wrapper) return;
    updateRetouchToolButton();
    const clearPress = () => {
      if (retouchPressTimer) window.clearTimeout(retouchPressTimer);
      retouchPressTimer = null;
    };
    button.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      clearPress(); retouchPress = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, opened: false };
      button.setPointerCapture?.(event.pointerId);
      retouchPressTimer = window.setTimeout(() => {
        if (!retouchPress || retouchPress.pointerId !== event.pointerId) return;
        retouchPress.opened = true; openRetouchMenu();
      }, 430);
    });
    button.addEventListener('pointermove', (event) => {
      if (!retouchPress || retouchPress.pointerId !== event.pointerId || retouchPress.opened) return;
      if (Math.hypot(event.clientX - retouchPress.x, event.clientY - retouchPress.y) > 10) { clearPress(); retouchPress = null; }
    });
    const finishPress = (event) => {
      if (!retouchPress || retouchPress.pointerId !== event.pointerId) return;
      clearPress(); suppressRetouchClick = retouchPress.opened; retouchPress = null;
      if (suppressRetouchClick) event.preventDefault();
    };
    button.addEventListener('pointerup', finishPress); button.addEventListener('pointercancel', finishPress);
    button.addEventListener('click', (event) => {
      event.preventDefault(); event.stopPropagation();
      if (suppressRetouchClick) { suppressRetouchClick = false; return; }
      closeRetouchMenu(); chooseTool(currentRetouchTool);
    });
    menu.addEventListener('click', (event) => {
      const option = event.target.closest?.('[data-retouch-option]'); if (!option) return;
      event.preventDefault(); event.stopPropagation(); selectRetouchTool(option.dataset.retouchOption);
    });
    document.addEventListener('pointerdown', (event) => { if (!wrapper.contains(event.target)) closeRetouchMenu(); });
  }

  function updateWarpToolButton(tool = currentWarpTool) {
    if (!warpToolInfo[tool]) return;
    currentWarpTool = tool;
    const button = $('warpTool'); const icon = $('warpToolIcon'); const label = $('warpToolLabel');
    if (!button || !icon || !label) return;
    const info = warpToolInfo[tool];
    button.dataset.tool = tool; button.dataset.warpCurrent = tool; button.title = `${info.title} – gedrückt halten für weitere Verformungswerkzeuge`;
    button.setAttribute('aria-label', `${info.label}; gedrückt halten für weitere Verformungswerkzeuge`);
    icon.innerHTML = toolInfoIconMarkup(info); label.textContent = info.label;
    document.querySelectorAll('[data-warp-option]').forEach((option) => option.setAttribute('aria-checked', String(option.dataset.warpOption === tool)));
  }

  function closeWarpMenu() {
    const menu = $('warpMenu'); const button = $('warpTool');
    if (!menu || menu.hidden) return false;
    menu.hidden = true; button?.setAttribute('aria-expanded', 'false'); return true;
  }

  function openWarpMenu() {
    const menu = $('warpMenu'); const button = $('warpTool');
    if (!menu || !button) return;
    updateWarpToolButton(); positionToolMenu(menu, button); button.setAttribute('aria-expanded', 'true');
  }

  function selectWarpTool(tool) {
    if (!warpToolInfo[tool]) return;
    updateWarpToolButton(tool); closeWarpMenu(); chooseTool(tool);
  }

  function bindWarpToolMenu() {
    const button = $('warpTool'); const menu = $('warpMenu'); const wrapper = button?.closest('.warp-tool-wrap');
    if (!button || !menu || !wrapper) return;
    updateWarpToolButton();
    const clearPress = () => { if (warpPressTimer) window.clearTimeout(warpPressTimer); warpPressTimer = null; };
    button.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      clearPress(); warpPress = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, opened: false };
      button.setPointerCapture?.(event.pointerId);
      warpPressTimer = window.setTimeout(() => {
        if (!warpPress || warpPress.pointerId !== event.pointerId) return;
        warpPress.opened = true; openWarpMenu();
      }, 430);
    });
    button.addEventListener('pointermove', (event) => {
      if (!warpPress || warpPress.pointerId !== event.pointerId || warpPress.opened) return;
      if (Math.hypot(event.clientX - warpPress.x, event.clientY - warpPress.y) > 10) { clearPress(); warpPress = null; }
    });
    const finishPress = (event) => {
      if (!warpPress || warpPress.pointerId !== event.pointerId) return;
      clearPress(); suppressWarpClick = warpPress.opened; warpPress = null;
      if (suppressWarpClick) event.preventDefault();
    };
    button.addEventListener('pointerup', finishPress); button.addEventListener('pointercancel', finishPress);
    button.addEventListener('click', (event) => {
      event.preventDefault(); event.stopPropagation();
      if (suppressWarpClick) { suppressWarpClick = false; return; }
      closeWarpMenu(); chooseTool(currentWarpTool);
    });
    menu.addEventListener('click', (event) => {
      const option = event.target.closest?.('[data-warp-option]'); if (!option) return;
      event.preventDefault(); event.stopPropagation(); selectWarpTool(option.dataset.warpOption);
    });
    document.addEventListener('pointerdown', (event) => { if (!wrapper.contains(event.target)) closeWarpMenu(); });
  }

  function updateMarqueeToolButton(tool = currentMarqueeTool) {
    if (!marqueeToolInfo[tool]) return;
    currentMarqueeTool = tool;
    const button = $('marqueeTool'); const icon = $('marqueeToolIcon'); const label = $('marqueeToolLabel');
    if (!button || !icon || !label) return;
    const info = marqueeToolInfo[tool];
    button.dataset.tool = tool; button.dataset.marqueeCurrent = tool; button.title = `${info.title} – gedrückt halten für weitere Auswahlformen`;
    button.setAttribute('aria-label', `${info.label}; gedrückt halten für weitere Auswahlformen`);
    icon.innerHTML = toolInfoIconMarkup(info);
    label.textContent = info.label;
    document.querySelectorAll('[data-marquee-option]').forEach((option) => option.setAttribute('aria-checked', String(option.dataset.marqueeOption === tool)));
  }

  function closeMarqueeMenu() {
    const menu = $('marqueeMenu'); const button = $('marqueeTool');
    if (!menu || menu.hidden) return false;
    menu.hidden = true; button?.setAttribute('aria-expanded', 'false'); return true;
  }

  function openMarqueeMenu() {
    const menu = $('marqueeMenu'); const button = $('marqueeTool');
    if (!menu || !button) return;
    updateMarqueeToolButton(); positionToolMenu(menu, button); button.setAttribute('aria-expanded', 'true');
  }

  function selectMarqueeTool(tool) {
    if (!marqueeToolInfo[tool]) return;
    updateMarqueeToolButton(tool); closeMarqueeMenu(); chooseTool(tool);
  }

  function bindMarqueeToolMenu() {
    const button = $('marqueeTool'); const menu = $('marqueeMenu'); const wrapper = button?.closest('.marquee-tool-wrap');
    if (!button || !menu || !wrapper) return;
    updateMarqueeToolButton();
    const clearPress = () => { if (marqueePressTimer) window.clearTimeout(marqueePressTimer); marqueePressTimer = null; };
    button.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      clearPress(); marqueePress = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, opened: false };
      button.setPointerCapture?.(event.pointerId);
      marqueePressTimer = window.setTimeout(() => {
        if (!marqueePress || marqueePress.pointerId !== event.pointerId) return;
        marqueePress.opened = true; openMarqueeMenu();
      }, 430);
    });
    button.addEventListener('pointermove', (event) => {
      if (!marqueePress || marqueePress.pointerId !== event.pointerId || marqueePress.opened) return;
      if (Math.hypot(event.clientX - marqueePress.x, event.clientY - marqueePress.y) > 10) { clearPress(); marqueePress = null; }
    });
    const finishPress = (event) => {
      if (!marqueePress || marqueePress.pointerId !== event.pointerId) return;
      clearPress(); suppressMarqueeClick = marqueePress.opened; marqueePress = null;
      if (suppressMarqueeClick) event.preventDefault();
    };
    button.addEventListener('pointerup', finishPress); button.addEventListener('pointercancel', finishPress);
    button.addEventListener('click', (event) => {
      event.preventDefault(); event.stopPropagation();
      if (suppressMarqueeClick) { suppressMarqueeClick = false; return; }
      closeMarqueeMenu(); chooseTool(currentMarqueeTool);
    });
    menu.addEventListener('click', (event) => {
      const option = event.target.closest?.('[data-marquee-option]'); if (!option) return;
      event.preventDefault(); event.stopPropagation(); selectMarqueeTool(option.dataset.marqueeOption);
    });
    document.addEventListener('pointerdown', (event) => { if (!wrapper.contains(event.target)) closeMarqueeMenu(); });
  }

  function updateLineToolButton(tool = currentLineTool) {
    if (!lineToolInfo[tool]) return;
    currentLineTool = tool;
    const button = $('lineTool'); const icon = $('lineToolIcon'); const label = $('lineToolLabel');
    if (!button || !icon || !label) return;
    const info = lineToolInfo[tool];
    button.dataset.tool = tool; button.dataset.lineCurrent = tool; button.title = `${info.title} – gedrückt halten für weitere Linienwerkzeuge`;
    button.setAttribute('aria-label', `${info.label}; gedrückt halten für weitere Linienwerkzeuge`);
    icon.innerHTML = toolInfoIconMarkup(info);
    label.textContent = info.label;
    document.querySelectorAll('[data-line-option]').forEach((option) => option.setAttribute('aria-checked', String(option.dataset.lineOption === tool)));
  }

  function closeLineMenu() {
    const menu = $('lineMenu'); const button = $('lineTool');
    if (!menu || menu.hidden) return false;
    menu.hidden = true; button?.setAttribute('aria-expanded', 'false'); return true;
  }

  function openLineMenu() {
    const menu = $('lineMenu'); const button = $('lineTool');
    if (!menu || !button) return;
    updateLineToolButton(); positionToolMenu(menu, button); button.setAttribute('aria-expanded', 'true');
  }

  function selectLineTool(tool) {
    if (!lineToolInfo[tool]) return;
    updateLineToolButton(tool); closeLineMenu(); chooseTool(tool);
  }

  function bindLineToolMenu() {
    const button = $('lineTool'); const menu = $('lineMenu'); const wrapper = button?.closest('.line-tool-wrap');
    if (!button || !menu || !wrapper) return;
    updateLineToolButton();
    const clearPress = () => { if (linePressTimer) window.clearTimeout(linePressTimer); linePressTimer = null; };
    button.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      clearPress(); linePress = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, opened: false };
      button.setPointerCapture?.(event.pointerId);
      linePressTimer = window.setTimeout(() => {
        if (!linePress || linePress.pointerId !== event.pointerId) return;
        linePress.opened = true; openLineMenu();
      }, 430);
    });
    button.addEventListener('pointermove', (event) => {
      if (!linePress || linePress.pointerId !== event.pointerId || linePress.opened) return;
      if (Math.hypot(event.clientX - linePress.x, event.clientY - linePress.y) > 10) { clearPress(); linePress = null; }
    });
    const finishPress = (event) => {
      if (!linePress || linePress.pointerId !== event.pointerId) return;
      clearPress(); suppressLineClick = linePress.opened; linePress = null;
      if (suppressLineClick) event.preventDefault();
    };
    button.addEventListener('pointerup', finishPress); button.addEventListener('pointercancel', finishPress);
    button.addEventListener('click', (event) => {
      event.preventDefault(); event.stopPropagation();
      if (suppressLineClick) { suppressLineClick = false; return; }
      closeLineMenu(); chooseTool(currentLineTool);
    });
    menu.addEventListener('click', (event) => {
      const option = event.target.closest?.('[data-line-option]'); if (!option) return;
      event.preventDefault(); event.stopPropagation(); selectLineTool(option.dataset.lineOption);
    });
    document.addEventListener('pointerdown', (event) => { if (!wrapper.contains(event.target)) closeLineMenu(); });
  }

  function updatePathModifierToolButton(tool = currentPathModifierTool) {
    if (!pathModifierToolInfo[tool]) return;
    currentPathModifierTool = tool;
    const button = $('pathModifierTool'); const icon = $('pathModifierToolIcon'); const label = $('pathModifierToolLabel');
    if (!button || !icon || !label) return;
    const info = pathModifierToolInfo[tool];
    button.dataset.tool = tool; button.dataset.pathModifierCurrent = tool; button.title = `${info.title} – gedrückt halten für weitere Pfadformer`;
    button.setAttribute('aria-label', `${info.label}; gedrückt halten für weitere Pfadformer`);
    icon.innerHTML = toolInfoIconMarkup(info); label.textContent = info.label;
    document.querySelectorAll('[data-path-modifier-option]').forEach((option) => option.setAttribute('aria-checked', String(option.dataset.pathModifierOption === tool)));
  }

  function closePathModifierMenu() {
    const menu = $('pathModifierMenu'); const button = $('pathModifierTool');
    if (!menu || menu.hidden) return false;
    menu.hidden = true; button?.setAttribute('aria-expanded', 'false'); return true;
  }

  function openPathModifierMenu() {
    const menu = $('pathModifierMenu'); const button = $('pathModifierTool');
    if (!menu || !button) return;
    updatePathModifierToolButton(); positionToolMenu(menu, button); button.setAttribute('aria-expanded', 'true');
  }

  function selectPathModifierTool(tool) {
    if (!pathModifierToolInfo[tool]) return;
    updatePathModifierToolButton(tool); closePathModifierMenu(); chooseTool(tool);
  }

  function bindPathModifierToolMenu() {
    const button = $('pathModifierTool'); const menu = $('pathModifierMenu'); const wrapper = button?.closest('.path-modifier-tool-wrap');
    if (!button || !menu || !wrapper) return;
    updatePathModifierToolButton();
    const clearPress = () => { if (pathModifierPressTimer) window.clearTimeout(pathModifierPressTimer); pathModifierPressTimer = null; };
    button.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      clearPress(); pathModifierPress = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, opened: false };
      button.setPointerCapture?.(event.pointerId);
      pathModifierPressTimer = window.setTimeout(() => {
        if (!pathModifierPress || pathModifierPress.pointerId !== event.pointerId) return;
        pathModifierPress.opened = true; openPathModifierMenu();
      }, 430);
    });
    button.addEventListener('pointermove', (event) => {
      if (!pathModifierPress || pathModifierPress.pointerId !== event.pointerId || pathModifierPress.opened) return;
      if (Math.hypot(event.clientX - pathModifierPress.x, event.clientY - pathModifierPress.y) > 10) { clearPress(); pathModifierPress = null; }
    });
    const finishPress = (event) => {
      if (!pathModifierPress || pathModifierPress.pointerId !== event.pointerId) return;
      clearPress(); suppressPathModifierClick = pathModifierPress.opened; pathModifierPress = null;
      if (suppressPathModifierClick) event.preventDefault();
    };
    button.addEventListener('pointerup', finishPress); button.addEventListener('pointercancel', finishPress);
    button.addEventListener('click', (event) => {
      event.preventDefault(); event.stopPropagation();
      if (suppressPathModifierClick) { suppressPathModifierClick = false; return; }
      closePathModifierMenu(); chooseTool(currentPathModifierTool);
    });
    menu.addEventListener('click', (event) => {
      const option = event.target.closest?.('[data-path-modifier-option]'); if (!option) return;
      event.preventDefault(); event.stopPropagation(); selectPathModifierTool(option.dataset.pathModifierOption);
    });
    document.addEventListener('pointerdown', (event) => { if (!wrapper.contains(event.target)) closePathModifierMenu(); });
  }

  function updateShapeToolButton(tool = currentShapeTool) {
    if (!shapeToolInfo[tool]) return;
    currentShapeTool = tool;
    const button = $('shapeTool'); const icon = $('shapeToolIcon'); const label = $('shapeToolLabel');
    if (!button || !icon || !label) return;
    const info = shapeToolInfo[tool];
    button.dataset.tool = tool; button.dataset.shapeCurrent = tool; button.title = `${info.title} – gedrückt halten für weitere Formen`;
    button.setAttribute('aria-label', `${info.label}; gedrückt halten für weitere Formen`);
    icon.innerHTML = toolInfoIconMarkup(info);
    label.textContent = info.label;
    document.querySelectorAll('[data-shape-option]').forEach((option) => option.setAttribute('aria-checked', String(option.dataset.shapeOption === tool)));
  }

  function closeShapeMenu() {
    const menu = $('shapeMenu'); const button = $('shapeTool');
    if (!menu || menu.hidden) return false;
    menu.hidden = true; button?.setAttribute('aria-expanded', 'false'); return true;
  }

  function openShapeMenu() {
    const menu = $('shapeMenu'); const button = $('shapeTool');
    if (!menu || !button) return;
    updateShapeToolButton(); positionToolMenu(menu, button, { compactWhenNeeded: true }); button.setAttribute('aria-expanded', 'true');
  }

  function selectShapeTool(tool) {
    if (!shapeToolInfo[tool]) return;
    updateShapeToolButton(tool); closeShapeMenu(); chooseTool(tool);
    if (tool === 'vector-emoji') requestAnimationFrame(openNotoEmojiPicker);
    if (tool === 'vector-flag') requestAnimationFrame(openVectorFlagPicker);
  }

  function bindShapeToolMenu() {
    const button = $('shapeTool'); const menu = $('shapeMenu'); const wrapper = button?.closest('.shape-tool-wrap');
    if (!button || !menu || !wrapper) return;
    updateShapeToolButton();
    const clearPress = () => {
      if (shapePressTimer) window.clearTimeout(shapePressTimer);
      shapePressTimer = null;
    };
    button.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      clearPress(); shapePress = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, opened: false };
      button.setPointerCapture?.(event.pointerId);
      shapePressTimer = window.setTimeout(() => {
        if (!shapePress || shapePress.pointerId !== event.pointerId) return;
        shapePress.opened = true; openShapeMenu();
      }, 430);
    });
    button.addEventListener('pointermove', (event) => {
      if (!shapePress || shapePress.pointerId !== event.pointerId || shapePress.opened) return;
      if (Math.hypot(event.clientX - shapePress.x, event.clientY - shapePress.y) > 10) { clearPress(); shapePress = null; }
    });
    const finishPress = (event) => {
      if (!shapePress || shapePress.pointerId !== event.pointerId) return;
      clearPress(); suppressShapeClick = shapePress.opened; shapePress = null;
      if (suppressShapeClick) event.preventDefault();
    };
    button.addEventListener('pointerup', finishPress); button.addEventListener('pointercancel', finishPress);
    button.addEventListener('click', (event) => {
      event.preventDefault(); event.stopPropagation();
      if (suppressShapeClick) { suppressShapeClick = false; return; }
      closeShapeMenu(); chooseTool(currentShapeTool);
    });
    menu.addEventListener('click', (event) => {
      const option = event.target.closest?.('[data-shape-option]'); if (!option) return;
      event.preventDefault(); event.stopPropagation(); selectShapeTool(option.dataset.shapeOption);
    });
    document.addEventListener('pointerdown', (event) => { if (!wrapper.contains(event.target)) closeShapeMenu(); });
  }

  function chooseTool(tool) {
    if (tool !== state.tool && state.gesture) cancelActiveGesture({ render: false });
    const leftWarpSession = tool !== state.tool && Boolean(state.warpSession || state.warpPreparation);
    if (leftWarpSession) cancelWarpSession({ render: false });
    if (state.inlineTextSession && tool !== 'text') finishInlineText();
    if (tool !== 'vector-emoji') closeNotoEmojiPicker();
    if (tool !== 'vector-flag') closeVectorFlagPicker();
    if (tool === 'vector-select' && !vectorEditObjects().length) {
      toast('Wähle zuerst ein Vektorobjekt mit editierbaren Punkten', 'warning');
      return;
    }
    const previousTool = state.tool;
    if (previousTool === 'liquify' && tool !== 'liquify') state.liquifyBase = null;
    const leftCanvasResizeMode = state.canvasResizeMode && tool !== 'select';
    if (leftCanvasResizeMode) {
      state.canvasResizeMode = false;
      refs.shell.style.transform = '';
    }
    state.tool = tool;
    if (eraserToolInfo[tool]) updateEraserToolButton(tool);
    if (retouchToolInfo[tool]) updateRetouchToolButton(tool);
    if (warpToolInfo[tool]) updateWarpToolButton(tool);
    if (marqueeToolInfo[tool]) updateMarqueeToolButton(tool);
    if (lineToolInfo[tool]) updateLineToolButton(tool);
    if (pathModifierToolInfo[tool]) updatePathModifierToolButton(tool);
    if (shapeToolInfo[tool]) updateShapeToolButton(tool);
    if (previousTool === 'polygon-lasso' && tool !== 'polygon-lasso' && state.gesture?.kind === 'bitmap-polygon-lasso') state.gesture = null;
    if (tool !== 'vector-select') { clearVectorSubselection(); state.vectorClick = null; }
    if (tool !== 'select') state.textClick = null;
    refs.toolRail.querySelectorAll('[data-tool]').forEach((button) => {
      const active = button.dataset.tool === tool; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', String(active));
    });
    refs.artboard.style.cursor = cursorForTool(tool);
    if (!eraserToolInfo[tool]) state.eraserCursorPoint = null;
    if (tool !== 'liquify') state.warpCursorPoint = null;
    if (!pathModifierToolInfo[tool]) state.pathModifierCursorPoint = null;
    setStatus(names[tool] || ({ select: 'Auswahl', pan: 'Hand', fill: 'Füllen', eyedropper: 'Pipette' }[tool] || 'Bereit'));
    syncPixelToolOptions();
    syncManualCanvasResizeButton();
    if (leftWarpSession) renderAll();
    else if (tool === 'vector-select' || previousTool === 'vector-select' || leftCanvasResizeMode) renderArtboard();
    if (tool === 'crop' && primaryObject()?.type === 'image') openCropEditor(primaryObject());
    if (['mesh-warp', 'shape-grid', 'puppet-warp', 'stretch-area'].includes(tool)) void prepareWarpSession(tool);
  }

  function syncVectorPathOptions() {
    const points = vectorPointSelections(); const segments = vectorSegmentSelections(); const paths = selectedWorkbenchObjects();
    const simplifiablePaths = selectedSimplifiableVectorObjects(); const displayedPaths = paths.length ? paths : simplifiablePaths;
    const pointLabel = points.length === 1 ? '1 Punkt' : `${points.length} Punkte`;
    const segmentLabel = segments.length === 1 ? '1 Segment' : `${segments.length} Segmente`;
    $('vectorSelectionHint').textContent = points.length && segments.length ? `${pointLabel} · ${segmentLabel}` : points.length ? pointLabel : segments.length ? segmentLabel : displayedPaths.length ? `${displayedPaths.length} ${displayedPaths.length === 1 ? 'Pfad' : 'Pfade'}` : 'Rahmen ziehen oder Pfad anklicken';
    const modes = points.map((selection) => objectEntry(selection.objectId)?.object?.pointModes?.[selection.index] || 'corner');
    [['vectorPointCornerBtn', 'corner'], ['vectorPointSmoothBtn', 'smooth'], ['vectorPointSymmetricBtn', 'symmetric']].forEach(([id, mode]) => {
      const button = $(id); button.disabled = !points.length || points.some((selection) => !pointVectorTypes.has(objectEntry(selection.objectId)?.object?.type));
      const active = modes.length > 0 && modes.every((value) => value === mode); button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', String(active));
    });
    const endpoints = points.filter((selection) => {
      const object = objectEntry(selection.objectId)?.object; const count = vectorEditablePoints(object).length;
      return object && pointVectorTypes.has(object.type) && !isClosedVectorPath(object) && (selection.index === 0 || selection.index === count - 1);
    });
    $('vectorJoinBtn').disabled = endpoints.length !== 2 || points.length !== 2;
    $('vectorCloseBtn').disabled = paths.filter((object) => !isClosedVectorPath(object) && vectorEditablePoints(object).length >= 3).length !== 1;
    $('vectorOpenBtn').disabled = points.length !== 1 || !isClosedVectorPath(objectEntry(points[0]?.objectId)?.object);
    $('vectorReverseBtn').disabled = !paths.length;
    $('vectorDeleteSegmentBtn').disabled = !segments.length;
    $('vectorSimplifyBtn').disabled = !simplifiablePaths.length;
    $('vectorSmoothBtn').disabled = !paths.length;
    const offsetReady = vectorEditObjects().some(isClosedVectorObject);
    $('vectorOffsetOutsetBtn').disabled = !offsetReady; $('vectorOffsetInsetBtn').disabled = !offsetReady;
    syncVectorCoordinateFields();
  }

  const vectorCoordinateIds = ['vectorAnchorX', 'vectorAnchorY', 'vectorIncomingX', 'vectorIncomingY', 'vectorOutgoingX', 'vectorOutgoingY'];

  function selectedVectorCoordinateContext() {
    const selections = vectorPointSelections(); if (selections.length !== 1) return null;
    const selection = selections[0]; const object = objectEntry(selection.objectId)?.object; const points = vectorEditablePoints(object); const anchor = points[selection.index];
    if (!object || !anchor) return null;
    const documentPoint = (point) => {
      const transformed = objectLocalToDocument(object, { x: point[0], y: point[1] }); return [round(transformed.x, 4), round(transformed.y, 4)];
    };
    let incoming = null; let outgoing = null;
    if (outlineVectorTypes.has(object.type)) {
      const address = outlineControlAddress(object, selection.index);
      if (address?.incoming) {
        const controls = bezierControlsFor(object, address.incoming.segmentIndex, points);
        if (controls) incoming = { ...address.incoming, point: documentPoint(controls.c2) };
      }
      if (address?.outgoing) {
        const controls = bezierControlsFor(object, address.outgoing.segmentIndex, points);
        if (controls) outgoing = { ...address.outgoing, point: documentPoint(controls.c1) };
      }
    } else if (pointVectorTypes.has(object.type)) {
      const previous = selection.index > 0 ? selection.index - 1 : isClosedVectorPath(object) ? points.length - 1 : -1;
      const incomingControls = previous >= 0 ? bezierControlsFor(object, previous, points) : null; const outgoingControls = bezierControlsFor(object, selection.index, points);
      if (incomingControls) incoming = { segmentIndex: previous, control: 'c2', point: documentPoint(incomingControls.c2) };
      if (outgoingControls) outgoing = { segmentIndex: selection.index, control: 'c1', point: documentPoint(outgoingControls.c1) };
    }
    return { object, selection, anchor: documentPoint(anchor), incoming, outgoing };
  }

  function syncVectorCoordinateFields() {
    const context = selectedVectorCoordinateContext(); const pairs = {
      anchor: context?.anchor, incoming: context?.incoming?.point, outgoing: context?.outgoing?.point
    };
    [['Anchor', 'anchor'], ['Incoming', 'incoming'], ['Outgoing', 'outgoing']].forEach(([prefix, key]) => {
      const point = pairs[key]; ['X', 'Y'].forEach((axis, index) => {
        const field = $(`vector${prefix}${axis}`); if (!field) return;
        field.disabled = !point; field.value = point ? String(round(point[index], 3)) : '';
      });
    });
  }

  function applyNumericVectorCoordinate(kind) {
    const context = selectedVectorCoordinateContext(); if (!context) return false;
    const prefix = kind === 'anchor' ? 'Anchor' : kind === 'incoming' ? 'Incoming' : 'Outgoing'; const target = kind === 'anchor' ? { point: context.anchor } : context[kind];
    if (!target?.point) return false;
    const x = num($(`vector${prefix}X`).value, target.point[0]); const y = num($(`vector${prefix}Y`).value, target.point[1]);
    if (Math.hypot(x - target.point[0], y - target.point[1]) <= 1e-7) { syncVectorCoordinateFields(); return false; }
    const origin = deepClone(context.object);
    if (kind === 'anchor') updateVectorPoint(context.object, context.selection.index, { x, y }, origin);
    else updateVectorBezierControl(context.object, target.segmentIndex, target.control, { x, y }, origin);
    commitHistory(kind === 'anchor' ? 'Ankerkoordinaten geändert' : 'Béziergriff numerisch geändert'); renderAll(); return true;
  }

  function syncPixelToolOptions() {
    const show = (id, visible) => { const element = $(id); element.hidden = !visible; element.setAttribute('aria-hidden', String(!visible)); };
    const marquee = marqueeSettings(); const wand = wandSettings();
    const marqueeToolActive = ['marquee', 'ellipse-marquee', 'lasso', 'polygon-lasso'].includes(state.tool);
    const eraserToolActive = Boolean(eraserToolInfo[state.tool]);
    show('eraserOptions', eraserToolActive); show('colorEraserFields', state.tool === 'color-eraser'); show('marqueeOptions', marqueeToolActive);
    show('wandOptions', state.tool === 'wand');
    show('redEyeOptions', state.tool === 'red-eye'); show('retouchOptions', ['clone', 'heal', 'spot-heal'].includes(state.tool));
    const warpToolActive = Boolean(warpToolInfo[state.tool]); const committedWarpTool = ['mesh-warp', 'shape-grid', 'puppet-warp', 'stretch-area'].includes(state.tool);
    show('warpOptions', warpToolActive); show('liquifyOptionFields', state.tool === 'liquify');
    show('warpGridOptionFields', ['mesh-warp', 'shape-grid'].includes(state.tool)); show('shapeGridOptionFields', state.tool === 'shape-grid');
    show('puppetOptionFields', state.tool === 'puppet-warp'); show('warpActionFields', committedWarpTool);
    show('stretchOptionFields', state.tool === 'stretch-area');
    show('polygonOptions', state.tool === 'polygon');
    show('penOptions', state.tool === 'pen');
    show('vectorPathOptions', state.tool === 'vector-select');
    show('pathModifierOptions', Boolean(pathModifierToolInfo[state.tool]));
    const geometricMarquee = ['marquee', 'ellipse-marquee'].includes(state.tool);
    show('marqueeRectangleOptions', geometricMarquee);
    show('marqueeAspectFields', geometricMarquee && marquee.style === 'aspect'); show('marqueeSizeFields', geometricMarquee && marquee.style === 'size'); show('marqueeFeatherField', marquee.edge === 'feather');
    show('wandFeatherField', wand.edge === 'feather');
    $('retouchOptionsHint').textContent = state.tool === 'spot-heal' ? 'Klick: Fleck automatisch aus dem Rand reparieren' : 'Alt-Klick: Quelle';
    $('eraserOptionsLabel').textContent = eraserToolInfo[state.tool]?.label || 'Radierer';
    if (eraserToolActive) renderEraserCursor();
    $('marqueeOptionsLabel').textContent = ({ marquee: 'Rechteckauswahl', 'ellipse-marquee': 'Ellipsenauswahl', lasso: 'Lasso', 'polygon-lasso': 'Polygon-Lasso' })[state.tool] || 'Rechteckauswahl';
    $('marqueeOptions').setAttribute('aria-label', state.tool === 'ellipse-marquee' ? 'Eigenschaften der Ellipsenauswahl' : 'Eigenschaften der Rechteckauswahl');
    if (document.activeElement !== $('wandTolerance') || $('wandTolerance').value !== '') setWandTolerance(wand.tolerance);
    $('wandInvertBtn').disabled = state.tool !== 'wand' || state.bitmapMarquee?.kind !== 'wand';
    const pen = activePenGesture(); const penPoints = pen?.object?.points?.length || 0;
    $('finishPenBtn').disabled = penPoints < 2; $('closePenBtn').disabled = penPoints < 3;
    $('penOptionsHint').textContent = pen
      ? I18n.t(penPoints === 1 ? 'ui.penPointHintOne' : 'ui.penPointHintMany', { count: penPoints })
      : 'Klick: Ecke · Ziehen: Kurve';
    if (pathModifierToolInfo[state.tool]) {
      $('pathModifierOptionsLabel').textContent = pathModifierToolInfo[state.tool].label;
      $('pathModifierOptionsHint').textContent = ({
        freeform: 'Am Pfad ziehen · Punkte werden bei Bedarf ergänzt',
        'reshape-area': 'Innerer Kreis 100 %, zum Außenkreis weich abfallend',
        'path-scrubber-add': 'Über die Kontur ziehen, um sie lokal zu verstärken',
        'path-scrubber-subtract': 'Über die Kontur ziehen, um sie lokal auszudünnen'
      })[state.tool];
      renderPathModifierCursor();
    }
    if (warpToolActive) {
      $('warpOptionsLabel').textContent = warpToolInfo[state.tool].label;
      if (['mesh-warp', 'shape-grid'].includes(state.tool) && document.activeElement !== $('warpGridDensity')) $('warpGridDensity').value = String(warpGridDensity(state.tool));
      const liquifyHint = ({
        push: 'Im ausgewählten Bitmap ziehen · Pixel organisch verschieben',
        bloat: 'Klicken oder ziehen · Bereich nach außen aufblasen',
        pucker: 'Klicken oder ziehen · Bereich zur Mitte zusammenziehen',
        twirl: 'Klicken oder ziehen · Alt kehrt die Wirbelrichtung um',
        smooth: 'Über Verformungen malen · nähert sie weich der Originalform an',
        restore: 'Über Verformungen malen · stellt die Originalform wieder her'
      })[liquifySettings().mode];
      const hint = state.warpPreparation ? 'Vorbereitung …' : state.tool === 'liquify'
        ? `${liquifyHint} · jede Geste ist ein Undo-Schritt`
        : state.tool === 'mesh-warp'
          ? (state.warpSession ? 'Punkte ziehen · Übernehmen rastert die gesamte aktive Ebene' : 'Aktive Ebene wird vorbereitet')
          : state.tool === 'shape-grid'
            ? (state.warpSession ? 'Punkt ziehen · Umschalt wirkt nur auf diesen Punkt' : 'Wähle ein Bitmap für das lokale Formgitter')
            : state.tool === 'puppet-warp'
              ? (state.warpSession ? 'Klick setzt Gelenkpunkt · Ziehen verformt · Alt-Klick löscht' : 'Wähle ein Bitmap für die Gelenkverformung')
              : state.warpSession?.selection
                ? `${state.warpSession.axis === 'vertical' ? 'Vertikaler' : 'Horizontaler'} Bereich gewählt · positive Werte strecken, negative stauchen`
                : (state.warpSession ? `${state.warpSession.axis === 'vertical' ? 'Vertikalen' : 'Horizontalen'} Streifen im Bitmap aufziehen` : 'Wähle ein Bitmap zum Strecken oder Stauchen');
      $('warpOptionsHint').textContent = hint; updateWarpActionState();
    }
    if (state.tool === 'vector-select') syncVectorPathOptions();
    syncPropertyDockContext();
  }

  let paletteTarget = null;
  let paletteDraftColor = null;
  let paletteSampler = null;

  function normalizeColor(value) {
    const raw = String(value || '').trim().replace(/^#/, '');
    if (/^[0-9a-f]{3}$/i.test(raw)) return `#${raw.split('').map((part) => `${part}${part}`).join('').toLowerCase()}`;
    return /^[0-9a-f]{6}$/i.test(raw) ? `#${raw.toLowerCase()}` : null;
  }

  function rgbFromColor(color) {
    const hex = normalizeColor(color);
    if (!hex) return null;
    return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
  }

  function colorFromRgb(red, green, blue) {
    const values = [red, green, blue].map((value) => clamp(Math.round(num(value, 0)), 0, 255));
    return `#${values.map((value) => value.toString(16).padStart(2, '0')).join('')}`;
  }

  function hslToColor(hue, saturation, lightness) {
    const h = ((hue % 360) + 360) % 360;
    const s = clamp(saturation, 0, 100) / 100; const l = clamp(lightness, 0, 100) / 100;
    if (s === 0) return colorFromRgb(l * 255, l * 255, l * 255);
    const chroma = (1 - Math.abs(2 * l - 1)) * s;
    const secondary = chroma * (1 - Math.abs((h / 60 % 2) - 1));
    const match = l - chroma / 2;
    let red = 0; let green = 0; let blue = 0;
    if (h < 60) [red, green] = [chroma, secondary];
    else if (h < 120) [red, green] = [secondary, chroma];
    else if (h < 180) [green, blue] = [chroma, secondary];
    else if (h < 240) [green, blue] = [secondary, chroma];
    else if (h < 300) [red, blue] = [secondary, chroma];
    else [red, blue] = [chroma, secondary];
    return colorFromRgb((red + match) * 255, (green + match) * 255, (blue + match) * 255);
  }

  function makePaletteSwatch(color, label = color) {
    const swatch = document.createElement('button');
    swatch.type = 'button'; swatch.dataset.paletteColor = color; swatch.setAttribute('aria-label', label);
    swatch.style.setProperty('--swatch', color);
    return swatch;
  }

  function buildColorPalette() {
    const grid = $('colorPaletteGrid'); const gray = $('colorPaletteGray');
    if (!grid || !gray || grid.childElementCount) return;
    // Fireworks-Farbmatrix: reine, vollständig gesättigte RGB-Farbtöne in
    // 15°-Schritten von Rot bis Magenta. Die elf Zeilen mischen diese Farben
    // gleichmäßig mit Weiß beziehungsweise Schwarz und vermeiden dadurch den
    // grauen, gedämpften Eindruck der früheren 82-%-HSL-Palette.
    const hues = Array.from({ length: 21 }, (_, index) => index * 15);
    const lightnesses = Array.from({ length: 11 }, (_, index) => 87.5 - index * 7.5);
    grid.dataset.paletteStyle = 'fireworks-rgb';
    grid.dataset.paletteColumns = String(hues.length);
    grid.dataset.paletteRows = String(lightnesses.length);
    lightnesses.forEach((lightness) => hues.forEach((hue) => {
      const color = hslToColor(hue, 100, lightness);
      grid.append(makePaletteSwatch(color, `${color.toUpperCase()}, Fireworks-Farbton ${Math.round(hue)} Grad`));
    }));
    Array.from({ length: 40 }, (_, index) => 100 - index * 100 / 39).forEach((lightness) => {
      const color = colorFromRgb(lightness * 2.55, lightness * 2.55, lightness * 2.55);
      gray.append(makePaletteSwatch(color, `${color.toUpperCase()}, Graustufe`));
    });
  }

  function closeColorPalette({ restoreFocus = false } = {}) {
    const palette = $('colorPalette');
    if (!palette || palette.hidden) return;
    const target = paletteTarget;
    if (palette.open) palette.close();
    palette.hidden = true;
    if (paletteSampler?.url) URL.revokeObjectURL(paletteSampler.url);
    paletteSampler = null; palette.classList.remove('is-sampling');
    paletteTarget?.setAttribute('aria-expanded', 'false');
    paletteTarget = null;
    paletteDraftColor = null;
    if (restoreFocus) target?.focus();
  }

  function setPalettePreview(value) {
    const valid = normalizeColor(value);
    const field = $('colorPaletteHex');
    field.dataset.invalid = valid ? 'false' : 'true';
    $('colorPaletteApply').disabled = !valid;
    document.querySelector('.color-palette__preview').style.background = valid || 'transparent';
    if (valid) $('colorPaletteNativeInput').value = valid;
    const rgb = rgbFromColor(valid);
    if (rgb) {
      $('colorPaletteRed').value = rgb[0]; $('colorPaletteGreen').value = rgb[1]; $('colorPaletteBlue').value = rgb[2];
    }
  }

  function restorePaletteDraft() {
    const color = normalizeColor(paletteDraftColor) || normalizeColor(paletteTarget?.value) || '#000000';
    $('colorPaletteHex').value = color;
    setPalettePreview(color);
  }

  function openNativeColorPicker() {
    const input = $('colorPaletteNativeInput');
    if (!input || !paletteTarget) return;
    input.value = normalizeColor(paletteDraftColor) || normalizeColor(paletteTarget.value) || '#000000';
    try {
      if (typeof input.showPicker === 'function') input.showPicker();
      else input.click();
    } catch {
      input.click();
    }
  }

  function beginPaletteSampling() {
    if (paletteSampler?.url) URL.revokeObjectURL(paletteSampler.url);
    const width = Math.max(1, state.doc.width); const height = Math.max(1, state.doc.height);
    const scale = Math.min(1, Math.sqrt(12_000_000 / (width * height)));
    const canvas = document.createElement('canvas'); canvas.width = Math.max(1, Math.round(width * scale)); canvas.height = Math.max(1, Math.round(height * scale));
    const session = { canvas, scale, ready: false, color: null, url: null };
    paletteSampler = session;
    try {
      const url = URL.createObjectURL(new Blob([cleanSvgString()], { type: 'image/svg+xml' })); session.url = url;
      const image = new Image();
      image.onload = () => {
        if (paletteSampler !== session) { URL.revokeObjectURL(url); return; }
        try { canvas.getContext('2d', { willReadFrequently: true }).drawImage(image, 0, 0, canvas.width, canvas.height); session.ready = true; }
        catch { /* Die Objekt-Fallback-Pipette bleibt verfügbar. */ }
        URL.revokeObjectURL(url); session.url = null;
      };
      image.onerror = () => { if (paletteSampler === session) { URL.revokeObjectURL(url); session.url = null; } };
      image.src = url;
    } catch { /* Die Objekt-Fallback-Pipette bleibt verfügbar. */ }
  }

  function samplePaletteColor(clientX, clientY) {
    const rect = refs.artboard.getBoundingClientRect();
    if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) return null;
    const point = pointFromEvent({ clientX, clientY }); const session = paletteSampler;
    if (session?.ready) {
      try {
        const x = clamp(Math.floor(point.x * session.canvas.width / state.doc.width), 0, session.canvas.width - 1);
        const y = clamp(Math.floor(point.y * session.canvas.height / state.doc.height), 0, session.canvas.height - 1);
        const pixel = session.canvas.getContext('2d', { willReadFrequently: true }).getImageData(x, y, 1, 1).data;
        if (pixel[3]) return colorFromRgb(pixel[0], pixel[1], pixel[2]);
      } catch { /* Ein nicht lesbares Bild fällt auf Objektfarben zurück. */ }
    }
    const object = objectAtPoint(point);
    return normalizeColor(object?.fill) || normalizeColor(object?.stroke) || (state.doc.bg === 'transparent' ? null : normalizeColor(state.doc.bg));
  }

  function applyPaletteColor(value) {
    const color = normalizeColor(value);
    if (!color || !paletteTarget) return;
    const target = paletteTarget;
    if (target.id === 'canvasBg' || target.id === 'newBg') {
      target.dataset.transparent = 'false';
      target.closest('.inline-color, .dialog-color-field')?.classList.remove('is-transparent');
    }
    if (target.id === 'fillColor') $('noFill').checked = false;
    target.value = color;
    target.dispatchEvent(new Event('input', { bubbles: true }));
    target.dispatchEvent(new Event('change', { bubbles: true }));
    if (target.id === 'newBg') target.closest('.dialog-color-field')?.querySelector('em')?.replaceChildren(color);
    closeColorPalette({ restoreFocus: true });
  }

  function applyPaletteTransparency() {
    const target = paletteTarget;
    if (!target) return;
    const object = primaryObject();
    if (target.id === 'propFill' && object) {
      delete object.fillPaint; object.fill = 'none'; commitHistory('Füllung transparent'); renderAll();
    } else if (target.id === 'propStroke' && object) {
      if (supportsStrokeType(object)) updateActiveStroke('enabled', false, true, 'Kontur entfernt');
      else { if (object.type === 'text') object.textStroke = 'none'; else object.stroke = 'none'; commitHistory('Kontur entfernt'); renderAll(); }
    } else if (target.id === 'pixelColor' && state.pixelSession) {
      setPixelTool('eraser');
    } else if (target.id === 'canvasBg') {
      state.doc.bg = 'transparent'; target.dataset.transparent = 'true'; commitHistory('Leinwand transparent'); renderAll();
    } else if (target.id === 'newBg') {
      target.dataset.transparent = 'true'; setColorField('newBg', 'transparent', '#ffffff');
      target.closest('.dialog-color-field')?.querySelector('em')?.replaceChildren('transparent');
      updateNewCanvasPreview();
    } else if (target.id === 'fillColor') {
      $('noFill').checked = true;
      persistToolColors();
      const objects = selectedObjects().filter(supportsObjectFill);
      if (objects.length) { objects.forEach((item) => { delete item.fillPaint; item.fill = 'none'; }); commitHistory('Füllung transparent'); renderAll(); }
    } else if (target.id === 'strokeColor' && object) {
      if (supportsStrokeType(object) && Array.isArray(object.strokeStack) && object.strokeStack.length) {
        const stack = StrokeStyle.ensureStack(object, { createId: uid }); stack[0] = { ...stack[0], enabled: false }; setObjectStrokeStack(object, stack);
      } else if (object.type === 'text') object.textStroke = 'none'; else object.stroke = 'none';
      commitHistory('Kontur entfernt'); renderAll();
    } else {
      toast('Für dieses Feld ist Transparenz nicht verfügbar', 'warning');
    }
    closeColorPalette({ restoreFocus: true });
  }

  function openColorPalette(target, anchor = target) {
    const palette = $('colorPalette');
    if (!palette || target.type !== 'color') return;
    paletteTarget?.setAttribute('aria-expanded', 'false');
    paletteTarget = target;
    paletteTarget.setAttribute('aria-expanded', 'true');
    const color = normalizeColor(target.value) || '#000000';
    $('colorPaletteHex').value = color;
    paletteDraftColor = color;
    setPalettePreview(color);
    beginPaletteSampling();
    palette.hidden = false;
    if (!palette.open) palette.showModal();

    const rect = anchor?.getBoundingClientRect ? anchor.getBoundingClientRect() : anchor;
    if (!rect) return;
    const margin = 8;
    const width = palette.offsetWidth;
    const height = palette.offsetHeight;
    // Die Palette bleibt in Reichweite des Auslösers: links öffnet sie nach rechts, rechts nach links.
    const left = rect.left < window.innerWidth / 2
      ? clamp(rect.right + 8, margin, window.innerWidth - width - margin)
      : clamp(rect.right - width, margin, window.innerWidth - width - margin);
    const below = rect.bottom + 8;
    const top = below + height <= window.innerHeight - margin ? below : clamp(rect.top - height - 8, margin, window.innerHeight - height - margin);
    palette.style.left = `${Math.round(left)}px`;
    palette.style.top = `${Math.round(top)}px`;
    requestAnimationFrame(() => $('colorPaletteHex').focus({ preventScroll: true }));
  }

  function bindColorPalettes() {
    const palette = $('colorPalette');
    if (!palette) return;
    buildColorPalette();
    const header = palette.querySelector('.color-palette__header');
    let paletteDrag = null;
    const stopPaletteDrag = (event) => {
      if (!paletteDrag || (event?.pointerId !== undefined && event.pointerId !== paletteDrag.pointerId)) return;
      header?.releasePointerCapture?.(paletteDrag.pointerId);
      paletteDrag = null; palette.classList.remove('is-dragging');
    };
    header?.addEventListener('pointerdown', (event) => {
      if (event.button !== 0 || event.target.closest('button')) return;
      const rect = palette.getBoundingClientRect();
      paletteDrag = { pointerId: event.pointerId, offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top, width: rect.width, height: rect.height };
      header.setPointerCapture?.(event.pointerId); palette.classList.add('is-dragging'); event.preventDefault();
    });
    header?.addEventListener('pointermove', (event) => {
      if (!paletteDrag || event.pointerId !== paletteDrag.pointerId) return;
      const margin = 8;
      const left = clamp(event.clientX - paletteDrag.offsetX, margin, window.innerWidth - paletteDrag.width - margin);
      const top = clamp(event.clientY - paletteDrag.offsetY, margin, window.innerHeight - paletteDrag.height - margin);
      palette.style.left = `${Math.round(left)}px`; palette.style.top = `${Math.round(top)}px`;
      event.preventDefault();
    });
    header?.addEventListener('pointerup', stopPaletteDrag);
    header?.addEventListener('pointercancel', stopPaletteDrag);
    const isOutsidePalette = (event) => {
      const rect = palette.getBoundingClientRect();
      return event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom;
    };
    const updatePaletteSampling = (event) => {
      if (palette.hidden) return;
      const outside = isOutsidePalette(event);
      palette.classList.toggle('is-sampling', outside);
      if (!outside) {
        if (paletteSampler?.color) { paletteSampler.color = null; restorePaletteDraft(); }
        return;
      }
      const color = samplePaletteColor(event.clientX, event.clientY);
      if (!color) return;
      if (paletteSampler) paletteSampler.color = color;
      $('colorPaletteHex').value = color; setPalettePreview(color);
    };
    palette.addEventListener('pointermove', updatePaletteSampling);
    document.addEventListener('pointermove', updatePaletteSampling, true);
    palette.addEventListener('pointerdown', (event) => {
      if (!isOutsidePalette(event) || event.button !== 0) return;
      event.preventDefault(); event.stopPropagation();
      const color = samplePaletteColor(event.clientX, event.clientY) || paletteSampler?.color;
      if (color) applyPaletteColor(color);
      else closeColorPalette({ restoreFocus: true });
    });
    document.addEventListener('pointerdown', (event) => {
      const input = event.target.closest?.('input[type="color"]:not([data-native-color-picker])');
      if (input) {
        event.preventDefault();
        event.stopPropagation();
        openColorPalette(input);
      } else if (!palette.hidden && !palette.contains(event.target)) {
        const color = samplePaletteColor(event.clientX, event.clientY) || paletteSampler?.color;
        event.preventDefault(); event.stopPropagation();
        if (color) applyPaletteColor(color);
        else closeColorPalette({ restoreFocus: true });
      }
    }, true);
    document.addEventListener('click', (event) => {
      if (event.target.closest?.('input[type="color"]:not([data-native-color-picker])')) event.preventDefault();
    }, true);
    document.addEventListener('keydown', (event) => {
      const input = event.target.closest?.('input[type="color"]:not([data-native-color-picker])');
      if (input && (event.key === 'Enter' || event.key === ' ')) {
        event.preventDefault();
        openColorPalette(input);
      }
      if (event.key === 'Escape' && !palette.hidden) {
        event.preventDefault();
        event.stopPropagation();
        closeColorPalette({ restoreFocus: true });
      }
    });
    palette.addEventListener('click', (event) => {
      if (event.target === palette) { closeColorPalette({ restoreFocus: true }); return; }
      const transparent = event.target.closest('[data-palette-transparent]');
      if (transparent) { applyPaletteTransparency(); return; }
      const swatch = event.target.closest('[data-palette-color]');
      if (swatch) applyPaletteColor(swatch.dataset.paletteColor);
    });
    const previewSwatch = (event) => {
      const swatch = event.target.closest?.('[data-palette-color]');
      if (!swatch || !palette.contains(swatch)) return;
      const color = normalizeColor(swatch.dataset.paletteColor);
      if (!color) return;
      $('colorPaletteHex').value = color;
      setPalettePreview(color);
    };
    const restoreAfterSwatch = (event) => {
      const swatch = event.target.closest?.('[data-palette-color]');
      const nextSwatch = event.relatedTarget?.closest?.('[data-palette-color]');
      if (swatch && !nextSwatch) restorePaletteDraft();
    };
    palette.addEventListener('pointerover', previewSwatch);
    palette.addEventListener('pointerout', restoreAfterSwatch);
    palette.addEventListener('focusin', previewSwatch);
    palette.addEventListener('focusout', restoreAfterSwatch);
    $('colorPaletteHex').addEventListener('input', (event) => { paletteDraftColor = event.target.value; setPalettePreview(event.target.value); });
    $('colorPaletteHex').addEventListener('keydown', (event) => {
      if (event.key === 'Enter') { event.preventDefault(); applyPaletteColor(event.currentTarget.value); }
    });
    $('colorPaletteApply').addEventListener('click', () => applyPaletteColor($('colorPaletteHex').value));
    $('colorPaletteNativeButton').addEventListener('click', openNativeColorPicker);
    $('colorPaletteNativeInput').addEventListener('input', (event) => {
      const color = normalizeColor(event.target.value); if (!color) return;
      paletteDraftColor = color; $('colorPaletteHex').value = color; setPalettePreview(color);
    });
    $('colorPaletteNativeInput').addEventListener('change', (event) => applyPaletteColor(event.target.value));
    $('colorPaletteClose').addEventListener('click', () => closeColorPalette({ restoreFocus: true }));
    palette.addEventListener('close', () => {
      palette.hidden = true;
      paletteTarget?.setAttribute('aria-expanded', 'false');
      paletteTarget = null;
    });
    ['colorPaletteRed', 'colorPaletteGreen', 'colorPaletteBlue'].forEach((id) => {
      $(id).addEventListener('input', () => {
        const color = colorFromRgb($('colorPaletteRed').value, $('colorPaletteGreen').value, $('colorPaletteBlue').value);
        $('colorPaletteHex').value = color; paletteDraftColor = color; setPalettePreview(color);
      });
      $(id).addEventListener('keydown', (event) => {
        if (event.key === 'Enter') { event.preventDefault(); applyPaletteColor($('colorPaletteHex').value); }
      });
    });
    window.addEventListener('resize', () => closeColorPalette());
    document.addEventListener('scroll', () => closeColorPalette(), true);
  }

  const rightSidebarStorageKey = 'pyro-right-sidebar-width';
  const rightSidebarCollapsedStorageKey = 'pyro-right-sidebar-collapsed-v1';
  const toolLabelsStorageKey = 'pyro-tool-labels-mode-v1';
  let rightSidebarResizeGesture = null;
  let toolLabelsMode = 'never';

  function syncToolLabelsToggle() {
    if (!refs.toolLabelsToggle) return;
    const modes = {
      always: { labelKey: 'ui.labels', icon: 'typography', nextKey: 'ui.noLabels' },
      never: { labelKey: 'ui.noLabels', icon: 'typography-off', nextKey: 'ui.automaticLabels' },
      auto: { labelKey: 'ui.automaticLabels', icon: 'arrows-exchange-2', nextKey: 'ui.labels' }
    };
    const mode = modes[toolLabelsMode] || modes.never;
    const label = I18n.t(mode.labelKey);
    const next = I18n.t(mode.nextKey);
    refs.toolLabelsToggle.dataset.labelMode = toolLabelsMode;
    refs.toolLabelsToggle.setAttribute('aria-label', I18n.t('ui.toolNamesMode', { mode: label }));
    refs.toolLabelsToggle.title = I18n.t('ui.toolLabelModeHint', { current: label, next });
    $('toolLabelsToggleIcon').innerHTML = Tabler.svg(mode.icon);
    $('toolLabelsToggleLabel').textContent = label;
  }

  function applyToolLabelsMode(mode, persist = false) {
    toolLabelsMode = ['always', 'never', 'auto'].includes(mode) ? mode : 'never';
    refs.layout?.classList.toggle('is-tool-sidebar-icon-only', toolLabelsMode === 'never');
    refs.layout?.classList.toggle('is-tool-sidebar-with-text', toolLabelsMode === 'always');
    syncToolLabelsToggle();
    if (persist) {
      try { localStorage.setItem(toolLabelsStorageKey, toolLabelsMode); } catch { /* Lokale Einstellung ist optional. */ }
    }
    scheduleRulerRender();
  }

  function restoreToolLabelsMode() {
    let stored = 'never';
    try { stored = localStorage.getItem(toolLabelsStorageKey) || 'never'; } catch { /* Lokale Einstellung ist optional. */ }
    applyToolLabelsMode(stored);
  }

  function syncPropertyPanelsModeToggle() {
    if (!refs.propertyPanelsModeToggle) return;
    const accordion = state.propertyDockAccordion;
    const label = I18n.t(accordion ? 'ui.accordion' : 'ui.freePanels');
    const next = I18n.t(accordion ? 'ui.freePanels' : 'ui.accordion');
    refs.propertyPanelsModeToggle.dataset.panelMode = accordion ? 'accordion' : 'free';
    refs.propertyPanelsModeToggle.setAttribute('aria-pressed', String(accordion));
    refs.propertyPanelsModeToggle.setAttribute('aria-label', I18n.t('ui.propertyPanelsMode', { mode: label }));
    refs.propertyPanelsModeToggle.title = I18n.t('ui.toolLabelModeHint', { current: label, next });
    $('propertyPanelsModeIcon').innerHTML = Tabler.svg(accordion ? 'columns-1' : 'columns-3');
    $('propertyPanelsModeLabel').textContent = label;
  }

  function applyPropertyPanelsMode(accordion, persist = false) {
    state.propertyDockAccordion = Boolean(accordion);
    refs.propertyDock?.classList.toggle('is-accordion', state.propertyDockAccordion);
    syncPropertyPanelsModeToggle();
    applyInspectorSectionStates();
    enforceInspectorAccordion();
    if (persist) {
      try { localStorage.setItem(propertyDockAccordionStorageKey, String(state.propertyDockAccordion)); } catch { /* Lokale Einstellung ist optional. */ }
    }
    scheduleRulerRender();
  }

  function restorePropertyPanelsMode() {
    applyPropertyPanelsMode(state.propertyDockAccordion);
  }

  function applyRightSidebarCollapsed(collapsed, persist = false) {
    if (!refs.layout || !refs.rightSidebar || !refs.rightSidebarToggle) return;
    const next = Boolean(collapsed);
    refs.layout.classList.toggle('is-right-sidebar-collapsed', next);
    refs.rightSidebar.inert = next;
    refs.rightSidebar.setAttribute('aria-hidden', String(next));
    refs.rightSidebarToggle.classList.toggle('is-collapsed', next);
    refs.rightSidebarToggle.setAttribute('aria-expanded', String(!next));
    Tabler.set(refs.rightSidebarToggle.querySelector('svg'), next ? 'layout-sidebar-right-expand' : 'layout-sidebar-right-collapse');
    const description = next ? 'Rechte Leiste einblenden' : 'Rechte Leiste ausblenden';
    refs.rightSidebarToggle.title = description;
    refs.rightSidebarToggle.setAttribute('aria-label', description);
    refs.rightSidebarResize.tabIndex = next ? -1 : 0;
    if (persist) {
      try { localStorage.setItem(rightSidebarCollapsedStorageKey, String(next)); } catch { /* Lokale Einstellung ist optional. */ }
    }
    requestAnimationFrame(syncDocumentTabScroller); scheduleRulerRender();
  }

  function restoreRightSidebarCollapsed() {
    let collapsed = false;
    try { collapsed = localStorage.getItem(rightSidebarCollapsedStorageKey) === 'true'; } catch { /* Lokale Einstellung ist optional. */ }
    applyRightSidebarCollapsed(collapsed);
  }

  function rightSidebarWidthBounds() {
    const layoutWidth = refs.layout?.clientWidth || window.innerWidth;
    const toolWidth = refs.toolSidebar?.getBoundingClientRect().width || 118;
    const minimumWorkspace = window.innerWidth <= 960 ? 280 : window.innerWidth <= 1180 ? 300 : 320;
    return { min: 240, max: Math.max(240, Math.floor(layoutWidth - toolWidth - minimumWorkspace)) };
  }

  function applyRightSidebarWidth(width, persist = false) {
    if (!refs.layout || !refs.rightSidebarResize) return 0;
    const bounds = rightSidebarWidthBounds(); const next = clamp(Math.round(num(width, 306)), bounds.min, bounds.max);
    refs.layout.style.setProperty('--right-sidebar-width', `${next}px`);
    refs.rightSidebarResize.setAttribute('aria-valuemin', String(bounds.min)); refs.rightSidebarResize.setAttribute('aria-valuemax', String(bounds.max)); refs.rightSidebarResize.setAttribute('aria-valuenow', String(next));
    if (persist) {
      try { localStorage.setItem(rightSidebarStorageKey, String(next)); } catch { /* Lokale Einstellung ist optional. */ }
    }
    requestAnimationFrame(syncDocumentTabScroller); scheduleRulerRender(); return next;
  }

  function restoreRightSidebarWidth() {
    let stored = null;
    try { stored = localStorage.getItem(rightSidebarStorageKey); } catch { /* Lokale Einstellung ist optional. */ }
    const current = refs.rightSidebar?.getBoundingClientRect().width || 306;
    applyRightSidebarWidth(stored || current);
  }

  function beginRightSidebarResize(event) {
    if (event.button !== 0 || !refs.layout || !refs.rightSidebarResize) return;
    rightSidebarResizeGesture = { pointerId: event.pointerId };
    refs.rightSidebarResize.classList.add('is-resizing'); document.body.classList.add('is-resizing-sidebar');
    refs.rightSidebarResize.setPointerCapture(event.pointerId); event.preventDefault();
  }

  function moveRightSidebarResize(event) {
    if (!rightSidebarResizeGesture || event.pointerId !== rightSidebarResizeGesture.pointerId || !refs.layout) return;
    const rect = refs.layout.getBoundingClientRect(); applyRightSidebarWidth(rect.right - event.clientX);
  }

  function finishRightSidebarResize(event) {
    if (!rightSidebarResizeGesture || event.pointerId !== rightSidebarResizeGesture.pointerId) return;
    const width = refs.rightSidebar?.getBoundingClientRect().width || 306;
    rightSidebarResizeGesture = null; refs.rightSidebarResize?.classList.remove('is-resizing'); document.body.classList.remove('is-resizing-sidebar');
    applyRightSidebarWidth(width, true);
  }

  let numberScrubberField = null;
  let numberScrubberButton = null;
  let numberScrubberGesture = null;

  function numberStepPrecision(step) {
    const value = String(step); return value.includes('.') ? Math.min(6, value.split('.')[1].length) : 0;
  }

  function closeNumberScrubber() {
    if (numberScrubberGesture) return;
    $('numberScrubber').hidden = true;
    numberScrubberButton?.setAttribute('aria-expanded', 'false');
    numberScrubberField = null; numberScrubberButton = null;
  }

  function positionNumberScrubber(button) {
    const scrubber = $('numberScrubber'); const margin = 7; scrubber.hidden = false;
    const buttonRect = button.getBoundingClientRect(); const popupRect = scrubber.getBoundingClientRect();
    let left = buttonRect.right + 5;
    if (left + popupRect.width > window.innerWidth - margin) left = buttonRect.left - popupRect.width - 5;
    const top = clamp(buttonRect.top + buttonRect.height / 2 - popupRect.height / 2, margin, Math.max(margin, window.innerHeight - popupRect.height - margin));
    scrubber.style.left = `${Math.round(clamp(left, margin, Math.max(margin, window.innerWidth - popupRect.width - margin)))}px`;
    scrubber.style.top = `${Math.round(top)}px`;
  }

  function updateNumberScrubberVisual(field, gesture = null) {
    if (!field) return;
    const value = num(field.value, 0); const minimum = field.min === '' ? -Infinity : num(field.min, -Infinity); const maximum = field.max === '' ? Infinity : num(field.max, Infinity);
    const track = $('numberScrubberTrack'); const thumb = $('numberScrubberThumb'); $('numberScrubberValue').textContent = field.value || '0';
    thumb.style.transform = 'translateY(-50%)';
    if (Number.isFinite(minimum) && Number.isFinite(maximum) && maximum > minimum) {
      const ratio = clamp((value - minimum) / (maximum - minimum), 0, 1); const inset = 6; const height = track.clientHeight || 158;
      thumb.style.top = `${round(inset + (1 - ratio) * Math.max(1, height - inset * 2))}px`;
    } else {
      thumb.style.top = `calc(50% + ${round(clamp(gesture?.visualOffset || 0, -70, 70))}px)`;
    }
  }

  function openNumberScrubber(field, button) {
    if (!field || field.disabled || field.readOnly) return;
    if (numberScrubberButton === button && !$('numberScrubber').hidden) { closeNumberScrubber(); return; }
    if (numberScrubberGesture) return;
    numberScrubberButton?.setAttribute('aria-expanded', 'false');
    numberScrubberField = field; numberScrubberButton = button; button.setAttribute('aria-expanded', 'true');
    positionNumberScrubber(button);
    updateNumberScrubberVisual(field);
  }

  function beginNumberScrub(event, options = {}) {
    if (!numberScrubberField || event.button !== 0) return;
    const field = numberScrubberField; const step = Math.abs(num(field.step, 1)) || 1;
    numberScrubberGesture = {
      pointerId: event.pointerId, lastY: event.clientY, currentValue: num(field.value, 0), pixelRemainder: 0, visualOffset: 0, step,
      minimum: field.min === '' ? -Infinity : num(field.min, -Infinity), maximum: field.max === '' ? Infinity : num(field.max, Infinity),
      precision: numberStepPrecision(step), changed: false,
      direct: Boolean(options.direct), wasOpen: Boolean(options.wasOpen)
    };
    $('numberScrubberTrack').setPointerCapture?.(event.pointerId); event.preventDefault();
  }

  function moveNumberScrub(event) {
    const gesture = numberScrubberGesture; const field = numberScrubberField;
    if (!gesture || !field || event.pointerId !== gesture.pointerId) return;
    const pixelsPerStep = event.ctrlKey || event.metaKey ? .55 : event.shiftKey ? 6 : 1.5;
    const pointerDelta = gesture.lastY - event.clientY; gesture.lastY = event.clientY;
    gesture.pixelRemainder += pointerDelta; gesture.visualOffset -= pointerDelta;
    const steps = Math.trunc(gesture.pixelRemainder / pixelsPerStep);
    if (steps) {
      const requested = gesture.currentValue + steps * gesture.step;
      const value = clamp(requested, gesture.minimum, gesture.maximum); const normalized = Number(value.toFixed(gesture.precision));
      const overflow = requested < gesture.minimum || requested > gesture.maximum;
      gesture.pixelRemainder = overflow ? 0 : gesture.pixelRemainder - steps * pixelsPerStep;
      if (overflow) gesture.visualOffset = 0;
      if (String(normalized) !== field.value) {
        field.value = String(normalized); field.dispatchEvent(new Event('input', { bubbles: true })); gesture.changed = true;
      }
      gesture.currentValue = num(field.value, normalized);
    }
    updateNumberScrubberVisual(field, gesture);
  }

  function finishNumberScrub(event) {
    const gesture = numberScrubberGesture; const field = numberScrubberField;
    if (!gesture || event.pointerId !== gesture.pointerId) return;
    $('numberScrubberTrack').releasePointerCapture?.(event.pointerId); numberScrubberGesture = null;
    updateNumberScrubberVisual(field);
    if (gesture.changed && field) field.dispatchEvent(new Event('change', { bubbles: true }));
    if (gesture.direct ? (gesture.changed || gesture.wasOpen) : field?.dataset.effectField) closeNumberScrubber();
  }

  function attachNumberScrubber(field, container, label = '') {
    if (!field || !container || container.querySelector('.number-scrubber-button')) return;
    const button = document.createElement('button'); button.type = 'button'; button.className = 'number-scrubber-button'; button.textContent = '▾';
    button.title = 'Klicken zum Öffnen oder gedrückt vertikal ziehen · Umschalt fein · Strg schnell'; button.setAttribute('aria-label', `${label || container.querySelector('span')?.textContent || field.getAttribute('aria-label') || 'Wert'} vertikal ändern`);
    button.setAttribute('aria-expanded', 'false'); if (field.id) button.dataset.numberScrubberFor = field.id;
    else { button.dataset.effectIndex = field.dataset.effectIndex || ''; button.dataset.effectField = field.dataset.effectField || ''; }
    button.addEventListener('pointerdown', (event) => {
      if (event.button !== 0 || field.disabled || field.readOnly) return;
      event.preventDefault(); event.stopPropagation();
      const wasOpen = numberScrubberButton === button && !$('numberScrubber').hidden;
      if (!wasOpen) openNumberScrubber(field, button);
      beginNumberScrub(event, { direct: true, wasOpen });
    });
    // Tastaturaktivierung erzeugt keinen Pointerdown und öffnet den Regler daher hier.
    button.addEventListener('click', (event) => {
      event.preventDefault(); event.stopPropagation();
      if (event.detail === 0) openNumberScrubber(field, button);
    });
    container.append(button);
  }

  function bindNumberScrubbers() {
    document.querySelectorAll('.number-field input[type="number"], .unit-input input[type="number"]').forEach((field) => {
      const container = field.closest('.number-field, .unit-input'); attachNumberScrubber(field, container);
    });
    $('numberScrubberTrack').addEventListener('pointerdown', beginNumberScrub);
    $('numberScrubberTrack').addEventListener('pointermove', moveNumberScrub);
    $('numberScrubberTrack').addEventListener('pointerup', finishNumberScrub);
    $('numberScrubberTrack').addEventListener('pointercancel', finishNumberScrub);
    document.addEventListener('pointerdown', (event) => {
      if ($('numberScrubber').hidden || $('numberScrubber').contains(event.target) || numberScrubberButton?.contains(event.target)) return;
      closeNumberScrubber();
    });
    document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !$('numberScrubber').hidden) { closeNumberScrubber(); event.preventDefault(); } });
    window.addEventListener('resize', closeNumberScrubber); refs.workspace.addEventListener('scroll', closeNumberScrubber, { passive: true });
  }

  function bindEnterCommitFields() {
    document.addEventListener('keydown', (event) => {
      if (event.key !== 'Enter' || event.defaultPrevented || event.isComposing) return;
      const field = event.target;
      if (field instanceof HTMLTextAreaElement) {
        // Mehrzeilige Inhalte behalten Enter für einen Zeilenumbruch; Strg/Cmd+Enter übernimmt sie.
        if (!(event.ctrlKey || event.metaKey)) return;
        event.preventDefault(); field.dispatchEvent(new Event('input', { bubbles: true })); field.dispatchEvent(new Event('change', { bubbles: true }));
        return;
      }
      if (!(field instanceof HTMLInputElement) || field.disabled || field.readOnly || event.ctrlKey || event.metaKey || event.altKey) return;
      if (['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit'].includes(field.type)) return;
      event.preventDefault();
      field.dispatchEvent(new Event('input', { bubbles: true }));
      field.dispatchEvent(new Event('change', { bubbles: true }));
    });
  }

  function populateHelpShortcuts() {
    const container = $('helpShortcutGroups');
    if (!container) return;
    const actionLabels = (...keys) => keys.map((key) => I18n.t(key)).join(' · ');
    const toolLabels = (...names) => names.map((name) => I18n.translateSource(Catalog.names[name] || name)).join(' · ');
    const groups = [
      {
        id: 'general',
        title: 'ui.generalActions',
        rows: [
          ['F1', I18n.t('ui.helpAndKeyboardShortcuts')],
          ['Ctrl/Cmd + N / O / S', actionLabels('command.new', 'command.open', 'command.save')],
          ['Ctrl/Cmd + Z / Y', actionLabels('command.undo', 'command.redo')],
          ['Ctrl/Cmd + X / C / V', actionLabels('command.cut', 'command.copy', 'command.paste')],
          ['Ctrl/Cmd + A', I18n.t('ui.selectAllVisibleUnlockedObjects')],
          ['Ctrl/Cmd + D / G', actionLabels('ui.duplicateSelection', 'ui.group')],
          ['Shift + Ctrl/Cmd + G', I18n.t('ui.ungroup')],
          ['Delete / Backspace', I18n.t('ui.deleteSelection')]
        ]
      },
      {
        id: 'tools',
        title: 'ui.tools',
        rows: [
          ['V / P / B / N', [I18n.t('ui.select'), toolLabels('pencil', 'brush', 'pen')].join(' · ')],
          ['M / W / C / E', toolLabels('marquee', 'wand', 'crop', 'eraser')],
          ['Y / K / J', toolLabels('red-eye', 'clone', 'heal')],
          ['L / R / O / A', toolLabels('line', 'rectangle', 'ellipse', 'arrow')],
          ['T / I / G / H', [toolLabels('text'), I18n.t('ui.eyedropper'), I18n.t('ui.fillAction'), I18n.t('ui.hand')].join(' · ')]
        ]
      },
      {
        id: 'navigation',
        title: 'ui.viewAndNavigation',
        rows: [
          ['← ↑ ↓ →', I18n.t('ui.moveSelectionByOnePixel')],
          ['Shift + ← ↑ ↓ →', I18n.t('ui.moveSelectionByTenPixels')],
          ['+ / −', actionLabels('ui.zoomIn', 'ui.zoomOut')],
          ['0', I18n.t('ui.fitCanvas')],
          ['H / Space', I18n.t('ui.panWorkspace')],
          ['Esc', I18n.t('command.cancel')]
        ]
      }
    ];
    const fragment = document.createDocumentFragment();
    groups.forEach((group) => {
      const section = document.createElement('section'); section.className = 'help-shortcut-group';
      const heading = document.createElement('h3'); heading.id = `helpShortcutGroup-${group.id}`; heading.textContent = I18n.t(group.title);
      section.setAttribute('aria-labelledby', heading.id);
      const list = document.createElement('dl'); list.className = 'help-shortcut-list';
      group.rows.forEach(([shortcut, label]) => {
        const row = document.createElement('div'); row.className = 'help-shortcut-row';
        const term = document.createElement('dt'); const key = document.createElement('kbd'); key.textContent = shortcut; term.append(key);
        const description = document.createElement('dd'); description.textContent = label;
        row.append(term, description); list.append(row);
      });
      section.append(heading, list); fragment.append(section);
    });
    container.replaceChildren(fragment);
  }

  function openHelpDialog() {
    populateHelpShortcuts();
    if (!$('helpDialog').open) $('helpDialog').showModal();
  }

  function bindUI() {
    bindPerformanceProfiler();
    buildSpecialCharacterList();
    buildEmojiPicker();
    buildNotoEmojiPicker();
    buildVectorFlagPicker();
    restoreTheme(); updateCanvasAspectLock();
    restoreToolLabelsMode();
    restorePropertyPanelsMode();
    restoreRightSidebarWidth();
    restoreRightSidebarCollapsed();
    bindNumberScrubbers();
    $('themeSelect').addEventListener('change', () => applyTheme($('themeSelect').value));
    document.addEventListener('pyro:languagechange', () => {
      syncLocalizedExampleDocuments();
      syncToolLabelsToggle();
      syncPropertyPanelsModeToggle();
      renderEmojiPicker();
      renderNotoEmojiPicker();
      renderVectorFlagPicker();
      populateHelpShortcuts();
      if (!$('debugConsole')?.hidden) renderDebugConsole();
      renderAll();
      if ($('exportDialog')?.open) syncExportDialog();
    });
    populateHelpShortcuts();
    refs.toolRail.addEventListener('click', (event) => { const button = event.target.closest('[data-tool]'); if (button) chooseTool(button.dataset.tool); });
    refs.emptyHint?.addEventListener('pointerdown', (event) => event.stopPropagation());
    $('emptyOpenImageBtn')?.addEventListener('click', () => $('imageFileInput').click());
    $('emptyDrawShapeBtn')?.addEventListener('click', () => {
      chooseTool('rectangle'); refs.workspace.focus(); toast('Rechteckwerkzeug aktiv – auf der Leinwand ziehen');
    });
    $('emptyOpenExampleBtn')?.addEventListener('click', openExampleDocument);
    $('debugConsoleBtn')?.addEventListener('click', toggleDebugConsole);
    $('debugConsoleCloseBtn')?.addEventListener('click', closeDebugConsole);
    $('debugConsoleLevel')?.addEventListener('change', renderDebugConsole);
    $('debugConsoleSearch')?.addEventListener('input', renderDebugConsole);
    $('performanceProfilerToggleBtn')?.addEventListener('click', () => setPerformanceProfilerEnabled(!performanceProfilerEnabled));
    $('performanceProfilerExportBtn')?.addEventListener('click', exportPerformanceProfilerReport);
    $('debugConsoleCopyBtn')?.addEventListener('click', () => { void copyDebugConsoleJson(); });
    $('debugConsoleClearBtn')?.addEventListener('click', clearDebugConsole);
    $('debugConsole')?.addEventListener('keydown', (event) => { if (event.key === 'Escape') { event.preventDefault(); closeDebugConsole(); } });
    ['svgImportReportCloseBtn', 'svgImportReportConfirmBtn'].forEach((id) => $(id)?.addEventListener('click', () => $('svgImportReportDialog').close()));
    $('svgImportReportDebugBtn')?.addEventListener('click', () => { $('svgImportReportDialog').close(); openDebugConsole(); });
    $('svgImportReportDialog')?.addEventListener('click', (event) => { if (event.target === $('svgImportReportDialog')) $('svgImportReportDialog').close(); });
    window.addEventListener('error', (event) => debugLog('error', 'Laufzeit', 'WINDOW_ERROR', event.message || 'Unbehandelter Laufzeitfehler', { file: event.filename || '', line: event.lineno || 0, column: event.colno || 0, error: debugDetails(event.error) }));
    window.addEventListener('unhandledrejection', (event) => debugLog('error', 'Laufzeit', 'UNHANDLED_REJECTION', event.reason?.message || String(event.reason || 'Unbehandeltes Promise'), debugDetails(event.reason)));
    refs.toolLabelsToggle?.addEventListener('click', () => {
      const modes = ['always', 'never', 'auto'];
      applyToolLabelsMode(modes[(modes.indexOf(toolLabelsMode) + 1) % modes.length], true);
      const visibleWidth = refs.rightSidebar?.getBoundingClientRect().width || 306;
      applyRightSidebarWidth(visibleWidth);
    });
    refs.propertyPanelsModeToggle?.addEventListener('click', () => applyPropertyPanelsMode(!state.propertyDockAccordion, true));
    bindEraserToolMenu();
    bindRetouchToolMenu();
    bindWarpToolMenu();
    bindMarqueeToolMenu();
    bindLineToolMenu();
    bindPathModifierToolMenu();
    bindShapeToolMenu();
    bindToolMenuKeyboard($('eraserTool'), $('eraserMenu'), openEraserMenu, closeEraserMenu);
    bindToolMenuKeyboard($('retouchTool'), $('retouchMenu'), openRetouchMenu, closeRetouchMenu);
    bindToolMenuKeyboard($('warpTool'), $('warpMenu'), openWarpMenu, closeWarpMenu);
    bindToolMenuKeyboard($('marqueeTool'), $('marqueeMenu'), openMarqueeMenu, closeMarqueeMenu);
    bindToolMenuKeyboard($('lineTool'), $('lineMenu'), openLineMenu, closeLineMenu);
    bindToolMenuKeyboard($('pathModifierTool'), $('pathModifierMenu'), openPathModifierMenu, closePathModifierMenu);
    bindToolMenuKeyboard($('shapeTool'), $('shapeMenu'), openShapeMenu, closeShapeMenu);
    refs.workspace.addEventListener('pointerdown', onPointerDown); refs.workspace.addEventListener('pointermove', onPointerMove);
    refs.workspace.addEventListener('pointerup', onPointerUp); refs.workspace.addEventListener('pointercancel', onPointerUp);
    refs.workspace.addEventListener('dblclick', onDoubleClick);
    refs.workspace.addEventListener('scroll', scheduleRulerRender, { passive: true });
    refs.guideLock?.addEventListener('click', toggleGuideLock);
    refs.rightSidebarToggle?.addEventListener('click', () => {
      applyRightSidebarCollapsed(!refs.layout?.classList.contains('is-right-sidebar-collapsed'), true);
    });
    refs.tabs?.addEventListener('scroll', () => {
      closeDocumentTabContextMenu();
      syncDocumentTabScroller();
    }, { passive: true });
    refs.tabs?.addEventListener('dragstart', onDocumentTabDragStart);
    refs.tabs?.addEventListener('dragover', onDocumentTabDragOver);
    refs.tabs?.addEventListener('drop', onDocumentTabDrop);
    refs.tabs?.addEventListener('dragend', onDocumentTabDragEnd);
    refs.tabContextMenu?.addEventListener('click', (event) => {
      const button = event.target.closest('[data-document-tab-action]');
      if (button && !button.disabled) runDocumentTabContextAction(button.dataset.documentTabAction);
    });
    refs.tabContextMenu?.addEventListener('keydown', (event) => {
      const buttons = [...refs.tabContextMenu.querySelectorAll('button:not(:disabled)')];
      const index = buttons.indexOf(document.activeElement);
      if (event.key === 'Escape') { event.preventDefault(); closeDocumentTabContextMenu({ restoreFocus: true }); return; }
      if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || !buttons.length) return;
      event.preventDefault();
      const next = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
        : (index + (event.key === 'ArrowDown' ? 1 : -1) + buttons.length) % buttons.length;
      buttons[next].focus();
    });
    document.addEventListener('pointerdown', (event) => {
      if (!refs.tabContextMenu?.hidden && !refs.tabContextMenu.contains(event.target)) closeDocumentTabContextMenu();
    });
    document.addEventListener('keydown', (event) => {
      if (event.key === 'Escape' && !refs.tabContextMenu?.hidden) closeDocumentTabContextMenu({ restoreFocus: true });
    });
    refs.tabsScrollLeft?.addEventListener('click', () => scrollDocumentTabs(-1));
    refs.tabsScrollRight?.addEventListener('click', () => scrollDocumentTabs(1));
    refs.rightSidebarResize?.addEventListener('pointerdown', beginRightSidebarResize);
    refs.rightSidebarResize?.addEventListener('pointermove', moveRightSidebarResize);
    refs.rightSidebarResize?.addEventListener('pointerup', finishRightSidebarResize);
    refs.rightSidebarResize?.addEventListener('pointercancel', finishRightSidebarResize);
    refs.rightSidebarResize?.addEventListener('keydown', (event) => {
      const current = refs.rightSidebar?.getBoundingClientRect().width || 306; const bounds = rightSidebarWidthBounds();
      if (event.key === 'ArrowLeft') { event.preventDefault(); applyRightSidebarWidth(current + (event.shiftKey ? 40 : 16), true); }
      else if (event.key === 'ArrowRight') { event.preventDefault(); applyRightSidebarWidth(current - (event.shiftKey ? 40 : 16), true); }
      else if (event.key === 'Home') { event.preventDefault(); applyRightSidebarWidth(bounds.min, true); }
      else if (event.key === 'End') { event.preventDefault(); applyRightSidebarWidth(bounds.max, true); }
    });
    window.addEventListener('resize', () => {
      closeDocumentTabContextMenu();
      syncToolLabelsToggle();
      const configured = num(refs.layout?.style.getPropertyValue('--right-sidebar-width'), 306);
      const visibleWidth = refs.rightSidebar?.getBoundingClientRect().width || configured;
      applyRightSidebarWidth(visibleWidth); syncDocumentTabScroller(); scheduleRulerRender();
    });
    window.addEventListener('blur', () => closeDocumentTabContextMenu());
    refs.workspace.addEventListener('dblclick', (event) => {
      if (state.tool !== 'select') return;
      const object = objectEntry(event.target.closest?.('[data-object-id]')?.dataset.objectId)?.object || objectAtPoint(pointFromEvent(event));
      if (object?.type === 'text' && state.inlineTextSession?.objectId !== object.id) { event.preventDefault(); openInlineTextEditor(pointFromEvent(event), object); }
    });
    refs.workspace.addEventListener('pointerleave', () => { refs.cursor.textContent = I18n.t('ui.cursorPositionEmpty'); updateEraserCursor(null); updatePathModifierCursor(null); updateWarpCursor(null); });

    $('newBtn').addEventListener('click', () => {
      $('newWidth').value = state.doc.width; $('newHeight').value = state.doc.height; $('newDpi').value = state.doc.dpi;
      setColorField('newBg', state.doc.bg, '#ffffff'); updateNewCanvasPreview(); $('newDialog').showModal();
    });
    $('newBgWhite').addEventListener('click', () => setNewBackground('#ffffff'));
    $('newBgTransparent').addEventListener('click', () => setNewBackground('transparent'));
    $('brandSplashBtn').addEventListener('click', openSplash);
    $('splashCloseBtn').addEventListener('click', () => $('splashDialog').close());
    $('splashDialog').addEventListener('click', (event) => { if (event.target === $('splashDialog')) $('splashDialog').close(); });
    $('helpBtn').addEventListener('click', openHelpDialog);
    ['helpDialogCloseBtn', 'helpDialogConfirmBtn'].forEach((id) => $(id).addEventListener('click', () => $('helpDialog').close()));
    $('helpDialog').addEventListener('click', (event) => { if (event.target === $('helpDialog')) $('helpDialog').close(); });
    $('infoBtn').addEventListener('click', () => $('aboutDialog').showModal());
    ['aboutDialogCloseBtn', 'aboutDialogConfirmBtn'].forEach((id) => $(id).addEventListener('click', () => $('aboutDialog').close()));
    $('aboutDialog').addEventListener('click', (event) => { if (event.target === $('aboutDialog')) $('aboutDialog').close(); });
    $('openProjectBtn').addEventListener('click', openProjectPicker); $('saveProjectBtn').addEventListener('click', saveProject);
    $('autoSaveToggle').addEventListener('change', (event) => setAutoSaveEnabled(event.target.checked));
    $('saveTabsToggle').addEventListener('change', (event) => setTabSessionEnabled(event.target.checked));
    $('importImageBtn').addEventListener('click', () => $('imageFileInput').click());
    $('exportBtn').addEventListener('click', openExportDialog);
    $('exportForm').addEventListener('submit', submitExportDialog);
    ['exportDialogCloseBtn', 'exportCancelBtn'].forEach((id) => $(id).addEventListener('click', closeExportDialog));
    ['exportFormatPng', 'exportFormatSvg', 'exportBackground', 'exportPngQuality', 'exportPngSmoothing', 'exportSvgEmbedFonts', 'exportSvgResponsive', 'exportSvgKeepMetadata']
      .forEach((id) => $(id).addEventListener('change', syncExportDialog));
    $('exportFilename').addEventListener('input', syncExportDialog);
    $('exportDialog').addEventListener('cancel', (event) => { event.preventDefault(); closeExportDialog(); });
    $('exportDialog').addEventListener('click', (event) => { if (event.target === $('exportDialog')) closeExportDialog(); });
    $('projectFileInput').addEventListener('change', (event) => { openFile(event.target.files[0]); event.target.value = ''; });
    $('imageFileInput').addEventListener('change', (event) => { importImage(event.target.files[0]); event.target.value = ''; });
    $('bitmapImportPixelsBtn').addEventListener('click', () => settleBitmapImportChoice('pixels'));
    $('bitmapImportPhysicalBtn').addEventListener('click', () => settleBitmapImportChoice('physical'));
    ['bitmapImportCancelBtn', 'bitmapImportCloseBtn'].forEach((id) => $(id).addEventListener('click', () => settleBitmapImportChoice(null)));
    $('bitmapImportDialog').addEventListener('cancel', (event) => { event.preventDefault(); settleBitmapImportChoice(null); });
    $('bitmapRasterizeConfirmBtn').addEventListener('click', () => settleBitmapRasterize($('bitmapRasterizeInterpolation').value));
    ['bitmapRasterizeCancelBtn', 'bitmapRasterizeCloseBtn'].forEach((id) => $(id).addEventListener('click', () => settleBitmapRasterize(null)));
    $('bitmapRasterizeDialog').addEventListener('cancel', (event) => { event.preventDefault(); settleBitmapRasterize(null); });
    $('undoBtn').addEventListener('click', undo); $('redoBtn').addEventListener('click', redo); $('cutBtn').addEventListener('click', () => copySelection(true));
    $('copyBtn').addEventListener('click', () => copySelection(false)); $('pasteBtn').addEventListener('click', pasteSelection);
    $('deleteBtn').addEventListener('click', () => { if (!deleteSelectedVectorSegments() && !deleteSelectedVectorPoint()) deleteSelection(); });
    document.addEventListener('paste', handleClipboardPaste);

    const applyTopStrokeWidth = (commit = false) => {
      const value = clamp(Math.round(num($('strokeWidth').value, 3)), 1, 64);
      $('strokeWidth').value = value;
      const objects = selectedObjects().filter((object) => !['image', ...containerObjectTypes, 'text'].includes(object.type));
      if (!objects.length) return;
      objects.forEach((object) => {
        if (Array.isArray(object.strokeStack) && object.strokeStack.length) {
          const stack = StrokeStyle.ensureStack(object, { createId: uid }); stack[0] = StrokeStyle.normalizeStyle({ ...stack[0], width: value }, { fallback: stack[0] }); setObjectStrokeStack(object, stack);
        } else object.strokeWidth = value;
      });
      if (commit) { commitHistory('Konturstärke geändert'); renderAll(); }
      else { scheduleInteractionRender(objects, { inspector: true }); }
    };
    $('strokeWidth').addEventListener('input', () => applyTopStrokeWidth());
    $('strokeWidth').addEventListener('change', () => applyTopStrokeWidth(true));
    ['input', 'change'].forEach((eventName) => $('polygonSides').addEventListener(eventName, () => {
      if ($('polygonSides').value === '') return;
      $('polygonSides').value = polygonSideCount($('polygonSides').value);
    }));
    const syncEraserToolSettings = () => { syncPixelToolOptions(); persistEraserToolSettings(); };
    ['eraserSize', 'eraserSoftness', 'colorEraserTolerance'].forEach((id) => $(id).addEventListener('input', syncEraserToolSettings));
    $('eraserShape').addEventListener('change', syncEraserToolSettings);
    ['pathModifierSize', 'pathModifierStrength'].forEach((id) => $(id).addEventListener('input', syncPixelToolOptions));
    $('redEyeSize').addEventListener('input', syncPixelToolOptions);
    ['retouchSize', 'retouchSoftness'].forEach((id) => $(id).addEventListener('input', syncPixelToolOptions));
    $('retouchShape').addEventListener('change', syncPixelToolOptions);
    ['liquifySize', 'liquifyStrength'].forEach((id) => $(id).addEventListener('input', () => {
      syncPixelToolOptions(); if (state.tool === 'liquify') renderEditorOverlays();
    }));
    $('liquifyMode').addEventListener('change', () => {
      syncPixelToolOptions(); if (state.tool === 'liquify') renderEditorOverlays();
    });
    $('warpGridDensity').addEventListener('change', () => {
      if (!['mesh-warp', 'shape-grid'].includes(state.tool)) return;
      const density = clamp(Math.round(num($('warpGridDensity').value, warpGridDensity(state.tool))), 2, 16);
      $('warpGridDensity').value = String(density); warpGridDensities[state.tool] = density;
      if (state.warpSession?.tool === state.tool) rebuildWarpGrid();
    });
    $('shapeGridRadius').addEventListener('input', () => { if (state.tool === 'shape-grid') renderEditorOverlays(); });
    $('puppetRadius').addEventListener('input', () => {
      if (state.warpSession?.tool !== 'puppet-warp') return;
      scheduleWarpPreview(state.warpSession); renderEditorOverlays();
    });
    $('puppetShowMesh').addEventListener('change', () => { if (state.tool === 'puppet-warp') renderEditorOverlays(); });
    $('stretchAxis').addEventListener('change', () => {
      const session = state.warpSession; if (session?.tool !== 'stretch-area') return;
      session.axis = $('stretchAxis').value === 'horizontal' ? 'horizontal' : 'vertical'; clearStretchSelection();
    });
    const updateStretchAmount = (normalize = false) => {
      const field = $('stretchAmount'); const amount = clamp(num(field.value, 25), -90, 200);
      if (normalize) field.value = String(Math.round(amount)); $('stretchAmountRange').value = String(amount);
      const session = state.warpSession; if (session?.tool !== 'stretch-area') { syncPixelToolOptions(); return; }
      session.amount = amount; session.dirty = Boolean(session.selection) && Math.abs(amount) > .001;
      if (session.dirty) scheduleWarpPreview(session);
      else { session.context.clearRect(0, 0, session.width, session.height); session.context.drawImage(session.sourceCanvas, 0, 0); }
      updateWarpActionState(); syncPixelToolOptions(); renderEditorOverlays();
    };
    $('stretchAmount').addEventListener('input', () => updateStretchAmount());
    $('stretchAmount').addEventListener('change', () => updateStretchAmount(true));
    $('stretchAmountRange').addEventListener('input', () => { $('stretchAmount').value = $('stretchAmountRange').value; updateStretchAmount(); });
    $('stretchReselectBtn').addEventListener('click', () => clearStretchSelection());
    $('warpResetBtn').addEventListener('click', resetWarpSession);
    $('warpCancelBtn').addEventListener('click', () => cancelWarpSession());
    $('warpApplyBtn').addEventListener('click', applyWarpSession);
    ['marqueeStyle', 'marqueeEdge'].forEach((id) => $(id).addEventListener('change', () => {
      syncPixelToolOptions(); if (['marquee', 'ellipse-marquee', 'lasso', 'polygon-lasso'].includes(state.tool)) scheduleInteractionRender();
    }));
    ['marqueeAspect', 'marqueeWidth', 'marqueeHeight', 'marqueeFeather'].forEach((id) => $(id).addEventListener('input', () => {
      if (['marquee', 'ellipse-marquee', 'lasso', 'polygon-lasso'].includes(state.tool)) scheduleInteractionRender();
    }));
    const refreshWandFromControls = () => {
      syncPixelToolOptions();
      if (state.tool === 'wand' && state.wandSeed) void refreshBitmapWandSelection();
    };
    $('wandTolerance').addEventListener('input', () => {
      if ($('wandTolerance').value === '') return;
      setWandTolerance($('wandTolerance').value); refreshWandFromControls();
    });
    $('wandTolerance').addEventListener('change', () => { setWandTolerance($('wandTolerance').value); refreshWandFromControls(); });
    $('wandEdge').addEventListener('change', refreshWandFromControls);
    $('wandFeather').addEventListener('input', refreshWandFromControls);
    $('wandInvertBtn').addEventListener('click', invertWandSelection);
    $('finishPenBtn').addEventListener('click', () => finishPenPath(false));
    $('closePenBtn').addEventListener('click', () => finishPenPath(true));
    $('vectorPointCornerBtn').addEventListener('click', () => applySelectedPointMode('corner'));
    $('vectorPointSmoothBtn').addEventListener('click', () => applySelectedPointMode('smooth'));
    $('vectorPointSymmetricBtn').addEventListener('click', () => applySelectedPointMode('symmetric'));
    $('vectorJoinBtn').addEventListener('click', joinSelectedVectorEndpoints);
    $('vectorCloseBtn').addEventListener('click', closeSelectedVectorPath);
    $('vectorOpenBtn').addEventListener('click', openSelectedVectorPath);
    $('vectorReverseBtn').addEventListener('click', reverseSelectedVectorPaths);
    $('vectorDeleteSegmentBtn').addEventListener('click', deleteSelectedVectorSegments);
    $('vectorSimplifyBtn').addEventListener('click', simplifySelectedVectorPaths);
    $('vectorSmoothBtn').addEventListener('click', smoothSelectedVectorPaths);
    $('vectorOffsetOutsetBtn').addEventListener('click', () => offsetSelectedVectorPaths(1));
    $('vectorOffsetInsetBtn').addEventListener('click', () => offsetSelectedVectorPaths(-1));
    const clampVectorWorkbenchField = (id, minimum, maximum, fallback) => {
      const field = $(id); field.value = String(clamp(num(field.value, fallback), minimum, maximum));
    };
    $('vectorSimplifyTolerance').addEventListener('change', () => clampVectorWorkbenchField('vectorSimplifyTolerance', .1, 100, 2));
    $('vectorSmoothStrength').addEventListener('change', () => clampVectorWorkbenchField('vectorSmoothStrength', 0, 100, 50));
    $('vectorOffsetDistance').addEventListener('change', () => clampVectorWorkbenchField('vectorOffsetDistance', .1, 1000, 10));
    $('vectorSimplifyTolerance').addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); simplifySelectedVectorPaths(); } });
    $('vectorSmoothStrength').addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); smoothSelectedVectorPaths(); } });
    vectorCoordinateIds.forEach((id) => {
      const kind = id.includes('Anchor') ? 'anchor' : id.includes('Incoming') ? 'incoming' : 'outgoing';
      $(id).addEventListener('change', () => applyNumericVectorCoordinate(kind));
      $(id).addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); applyNumericVectorCoordinate(kind); } });
    });
    const applySelectedColor = (fieldId, property, types, label) => {
      const color = $(fieldId).value;
      if (fieldId === 'fillColor') $('noFill').checked = false;
      persistToolColors();
      const targets = selectedObjects().flatMap(objectTree).filter((object) => types.includes(object.type) && (property !== 'fill' || supportsObjectFill(object)));
      if (!targets.length) return;
      let changed = false;
      targets.forEach((object) => {
        const targetProperty = object.type === 'text' && property === 'stroke' ? 'textStroke' : property;
        if (property === 'fill' && object.fillPaint) { delete object.fillPaint; changed = true; }
        if (property === 'stroke' && Array.isArray(object.strokeStack) && object.strokeStack.length) {
          const stack = StrokeStyle.ensureStack(object, { createId: uid });
          if (stack[0].color !== color || !stack[0].enabled) {
            stack[0] = StrokeStyle.normalizeStyle({ ...stack[0], color, enabled: true, strokeType: stack[0].strokeType === 'none' ? DEFAULT_STROKE_TYPE : stack[0].strokeType }, { fallback: stack[0] });
            setObjectStrokeStack(object, stack); changed = true;
          }
        } else if (object[targetProperty] !== color) { object[targetProperty] = color; changed = true; }
      });
      if (changed) { commitHistory(label); renderAll(); }
    };
    ['strokeColor', 'fillColor'].forEach((id) => $(id).addEventListener('input', persistToolColors));
    $('strokeColor').addEventListener('change', () => applySelectedColor('strokeColor', 'stroke', [...colorableVectorTypes, 'text'], 'Konturfarbe geändert'));
    $('fillColor').addEventListener('change', () => applySelectedColor('fillColor', 'fill', [...fillableVectorShapeTypes, 'polyline', 'text'], 'Füllfarbe geändert'));
    $('swapColorsBtn').addEventListener('click', () => {
      const stroke = $('strokeColor').value; const fill = $('fillColor').value;
      $('strokeColor').value = fill; $('fillColor').value = stroke; $('noFill').checked = false;
      persistToolColors();
      const targets = selectedObjects().flatMap(objectTree); let changed = false;
      targets.forEach((object) => {
        if (supportsStrokeType(object) && Array.isArray(object.strokeStack) && object.strokeStack.length) {
          const stack = StrokeStyle.ensureStack(object, { createId: uid });
          if (stack[0].color !== fill || !stack[0].enabled) {
            stack[0] = StrokeStyle.normalizeStyle({ ...stack[0], color: fill, enabled: true, strokeType: stack[0].strokeType === 'none' ? DEFAULT_STROKE_TYPE : stack[0].strokeType }, { fallback: stack[0] });
            setObjectStrokeStack(object, stack); changed = true;
          }
        } else if (object.type === 'text' && object.textStroke !== fill) {
          object.textStroke = fill; changed = true;
        } else if (colorableVectorTypes.has(object.type) && object.stroke !== fill) {
          object.stroke = fill; changed = true;
        }
        if (supportsObjectFill(object) && (object.fill !== stroke || object.fillPaint)) {
          delete object.fillPaint; object.fill = stroke; changed = true;
        }
      });
      if (changed) { commitHistory('Kontur und Füllung getauscht'); renderAll(); }
    });
    $('noFill').addEventListener('change', () => {
      persistToolColors();
      const objects = selectedObjects().filter(supportsObjectFill);
      if (!objects.length) return;
      objects.forEach((object) => { delete object.fillPaint; object.fill = $('noFill').checked ? 'none' : $('fillColor').value; });
      commitHistory($('noFill').checked ? 'Füllung entfernt' : 'Füllung gesetzt'); renderAll();
    });
    $('rotateLeftBtn').addEventListener('click', () => rotateSelection(-90));
    $('rotateRightBtn').addEventListener('click', () => rotateSelection(90));
    $('flipHorizontalBtn').addEventListener('click', () => flipSelection('horizontal'));
    $('flipVerticalBtn').addEventListener('click', () => flipSelection('vertical'));
    document.querySelectorAll('[data-free-transform-mode]').forEach((button) => button.addEventListener('click', () => selectFreeTransformMode(button.dataset.freeTransformMode)));
    $('freeTransformSkewX').addEventListener('change', () => shearSelection('x', $('freeTransformSkewX').value));
    $('freeTransformSkewY').addEventListener('change', () => shearSelection('y', $('freeTransformSkewY').value));
    $('applyFreeTransformBtn').addEventListener('click', applyFreeTransformToPath);
    $('resetFreeTransformBtn').addEventListener('click', resetFreeTransformSelection);
    const applyTopTextProperty = (property, value, commit = false) => {
      const object = primaryObject(); if (object?.type !== 'text') return;
      object[property] = value; if (['fontFamily', 'fontSize'].includes(property)) sizeTextObject(object);
      if (commit) { commitHistory('Textstil geändert'); renderAll(); } else scheduleInteractionRender([object]);
    };
    $('fontFamily').addEventListener('change', () => { applyFontPreview($('fontFamily')); applyTopTextProperty('fontFamily', $('fontFamily').value, true); });
    $('propFontFamily').addEventListener('change', () => applyFontPreview($('propFontFamily')));
    $('fontSize').addEventListener('input', () => applyTopTextProperty('fontSize', clamp(num($('fontSize').value, 32), 6, 400)));
    $('fontSize').addEventListener('change', () => applyTopTextProperty('fontSize', clamp(num($('fontSize').value, 32), 6, 400), true));
    [['fontBold', 'bold'], ['fontItalic', 'italic']].forEach(([id, property]) => $(id).addEventListener('click', () => {
      const next = !$(id).classList.contains('is-active'); const object = primaryObject();
      if (object?.type === 'text') { object[property] = next; commitHistory('Textstil geändert'); renderAll(); }
      else { $(id).classList.toggle('is-active', next); $(id).setAttribute('aria-pressed', String(next)); }
    }));
    refs.propertyDockSizeBtn?.addEventListener('click', togglePropertyDockSize);
    refs.propertyDockHeading?.addEventListener('click', editPropertyDockObjectName);
    refs.propertyDockHeading?.addEventListener('keydown', (event) => {
      if (!['Enter', ' '].includes(event.key)) return;
      event.preventDefault(); event.stopPropagation(); editPropertyDockObjectName();
    });
    $('canvasAspectLockBtn').addEventListener('click', toggleCanvasAspectLock);
    $('canvasWidth').addEventListener('input', () => syncCanvasAspect('width'));
    $('canvasHeight').addEventListener('input', () => syncCanvasAspect('height'));
    [['canvasWidth', 'width'], ['canvasHeight', 'height']].forEach(([id]) => $(id).addEventListener('keydown', (event) => {
      if (event.key !== 'Enter') return;
      event.preventDefault(); resizeCanvas();
    }));
    $('resizeCanvasBtn').addEventListener('click', fitCanvasToObjects);
    $('trimCanvasBtn').addEventListener('click', trimCanvasToContent);
    $('manualCanvasResizeBtn').addEventListener('click', () => setManualCanvasResizeMode(!state.canvasResizeMode));
    $('canvasBg').addEventListener('change', () => { $('canvasBg').dataset.transparent = 'false'; $('canvasBg').closest('.inline-color')?.classList.remove('is-transparent'); resizeCanvas(state.doc.width, state.doc.height, $('canvasBg').value); });
    const applyCanvasDpi = () => {
      const dpi = clamp(Math.round(num($('canvasDpi').value, 72)), 1, 2400);
      if (dpi === state.doc.dpi) { $('canvasDpi').value = dpi; return; }
      state.doc.dpi = dpi; $('canvasDpi').value = dpi; commitHistory('Leinwandauflösung geändert'); renderAll();
    };
    $('canvasDpi').addEventListener('change', applyCanvasDpi);
    $('canvasDpi').addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); applyCanvasDpi(); } });
    $('canvasGridVisible').addEventListener('change', () => {
      const grid = documentGrid(); const visible = $('canvasGridVisible').checked;
      if (grid.visible === visible) return;
      grid.visible = visible; commitHistory(visible ? 'Gitter eingeblendet' : 'Gitter ausgeblendet'); renderAll();
    });
    $('canvasGridSnap').addEventListener('change', () => {
      const grid = documentGrid(); const snap = $('canvasGridSnap').checked;
      if (grid.snap === snap) return;
      grid.snap = snap; state.snapGuides = []; commitHistory(snap ? 'Gittereinrasten aktiviert' : 'Gittereinrasten deaktiviert'); renderAll();
    });
    const applyCanvasGridSize = () => {
      const grid = documentGrid(); const size = clamp(Math.round(num($('canvasGridSize').value, grid.size)), 1, 10000);
      $('canvasGridSize').value = size;
      if (grid.size === size) return;
      grid.size = size; state.snapGuides = []; commitHistory('Gittergröße geändert'); renderAll();
    };
    $('canvasGridSize').addEventListener('change', applyCanvasGridSize);
    $('canvasGridSize').addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); applyCanvasGridSize(); } });
    ['newWidth', 'newHeight'].forEach((id) => $(id).addEventListener('input', updateNewCanvasPreview));
    $('newBg').addEventListener('change', updateNewCanvasPreview);

    $('zoomOutBtn').addEventListener('click', () => applyZoom(state.zoom / 1.25, { x: refs.workspace.clientWidth / 2, y: refs.workspace.clientHeight / 2 }));
    $('zoomInBtn').addEventListener('click', () => applyZoom(state.zoom * 1.25, { x: refs.workspace.clientWidth / 2, y: refs.workspace.clientHeight / 2 }));
    $('zoomRange').addEventListener('input', () => scheduleZoom(num($('zoomRange').value) / 100));
    $('zoomPreset').addEventListener('change', () => applyZoom(num($('zoomPreset').value) / 100));
    $('zoomFitBtn').addEventListener('click', zoomFit);
    $('pixelGridToggle').addEventListener('change', () => { state.pixelGrid = $('pixelGridToggle').checked; renderArtboard(); });
    refs.workspace.addEventListener('wheel', (event) => {
      if (!event.ctrlKey && !event.metaKey) return;
      event.preventDefault();
      const base = pendingZoom?.next ?? state.zoom;
      scheduleZoom(base * (event.deltaY < 0 ? 1.15 : 1 / 1.15), { x: event.clientX - refs.workspace.getBoundingClientRect().left, y: event.clientY - refs.workspace.getBoundingClientRect().top });
    }, { passive: false });

    $('snapToggle').addEventListener('change', () => {
      state.snap = $('snapToggle').checked; state.snapGuides = []; syncSnapControls(); renderArtboard();
    });
    Object.entries(snapSettingFields).forEach(([setting, id]) => $(id).addEventListener('change', () => {
      state.snapSettings = Snap.normalizeSettings({ ...state.snapSettings, [setting]: $(id).checked });
      state.snapGuides = []; persistSnapSettings(); renderArtboard();
    }));
    [['alignLeftBtn', 'left'], ['alignHCenterBtn', 'hcenter'], ['alignRightBtn', 'right'], ['alignTopBtn', 'top'], ['alignVCenterBtn', 'vcenter'], ['alignBottomBtn', 'bottom']].forEach(([id, mode]) => $(id).addEventListener('click', () => alignSelection(mode)));
    $('alignTarget').addEventListener('change', renderSelectionState);
    $('distributeHorizontalBtn').addEventListener('click', () => distributeSelection('horizontal'));
    $('distributeVerticalBtn').addEventListener('click', () => distributeSelection('vertical'));
    $('bringFrontBtn').addEventListener('click', () => reorderObject('front')); $('bringForwardBtn').addEventListener('click', () => reorderObject('forward'));
    $('sendBackwardBtn').addEventListener('click', () => reorderObject('backward')); $('sendBackBtn').addEventListener('click', () => reorderObject('back'));
    $('duplicateBtn').addEventListener('click', duplicateSelection);
    $('groupBtn').addEventListener('click', groupSelection); $('ungroupBtn').addEventListener('click', ungroupSelection); $('ungroupAllBtn').addEventListener('click', ungroupAllSelection); $('flattenBtn').addEventListener('click', flattenGroup);
    $('createSymbolBtn').addEventListener('click', createSymbolFromSelection); $('insertSymbolBtn').addEventListener('click', insertSelectedSymbol);
    $('editSymbolBtn').addEventListener('click', editSelectedSymbolMaster); $('finishSymbolEditBtn').addEventListener('click', finishSelectedSymbolMaster);
    $('deleteSymbolBtn').addEventListener('click', () => removeSelectedReusableResource('symbols', 'symbolLibrarySelect', 'Symbol'));
    $('saveObjectStyleBtn').addEventListener('click', saveSelectedObjectStyle); $('applyObjectStyleBtn').addEventListener('click', applySelectedObjectStyle);
    $('deleteObjectStyleBtn').addEventListener('click', () => removeSelectedReusableResource('objectStyles', 'objectStyleLibrarySelect', 'Objektstil'));
    $('saveFillPresetBtn').addEventListener('click', saveSelectedFillPreset); $('applyFillPresetBtn').addEventListener('click', applySelectedFillPreset);
    $('deleteFillPresetBtn').addEventListener('click', () => removeSelectedReusableResource('fillPresets', 'fillPresetLibrarySelect', 'Füllungsvorgabe'));
    $('saveStrokePresetBtn').addEventListener('click', saveSelectedStrokePreset); $('applyStrokePresetBtn').addEventListener('click', applySelectedStrokePreset);
    $('deleteStrokePresetBtn').addEventListener('click', () => removeSelectedReusableResource('strokePresets', 'strokePresetLibrarySelect', 'Konturvorgabe'));
    $('copyLibraryBtn').addEventListener('click', copyReusableLibrary); $('pasteLibraryBtn').addEventListener('click', () => { void pasteReusableLibrary(); });
    $('booleanUnionBtn').addEventListener('click', () => booleanSelection('union'));
    $('booleanSubtractBtn').addEventListener('click', () => booleanSelection('subtract'));
    $('booleanIntersectBtn').addEventListener('click', () => booleanSelection('intersection'));
    $('booleanExcludeBtn').addEventListener('click', () => booleanSelection('exclude'));
    $('maskSelectionBtn').addEventListener('click', createVectorMask); $('releaseMaskBtn').addEventListener('click', releaseVectorMask); $('invertMaskBtn').addEventListener('click', invertVectorMask);
    $('clipSelectionBtn').addEventListener('click', createClippingPath); $('releaseClipBtn').addEventListener('click', releaseClippingPath);

    $('layerBlendMode').addEventListener('change', () => {
      const layer = activeLayer(); if (!layer) return;
      const mode = normalizeBlendMode($('layerBlendMode').value); $('layerBlendMode').value = mode;
      if (normalizeBlendMode(layer.blendMode) === mode) return;
      layer.blendMode = mode; commitHistory('Ebenen-Mischmodus geändert'); renderAll();
    });
    $('addLayerBtn').addEventListener('click', addLayer); $('duplicateLayerBtn').addEventListener('click', duplicateLayer); $('deleteLayerBtn').addEventListener('click', deleteLayer);
    $('layerUpBtn').addEventListener('click', () => moveLayer(1)); $('layerDownBtn').addEventListener('click', () => moveLayer(-1));
    refs.layerList.addEventListener('dragstart', beginLayerObjectDrag);
    refs.layerList.addEventListener('dragover', dragLayerObjectOver);
    refs.layerList.addEventListener('drop', dropLayerObject);
    refs.layerList.addEventListener('dragend', finishLayerObjectDrag);
    refs.inlineText.addEventListener('input', positionInlineTextEditor);
    refs.inlineText.addEventListener('keydown', (event) => {
      if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
        event.preventDefault();
        finishInlineText({ showObjectProperties: true });
      }
      if (event.key === 'Escape') { event.preventDefault(); finishInlineText({ cancel: true }); }
    });
    refs.inlineText.addEventListener('blur', (event) => {
      if (event.relatedTarget?.closest?.('.font-controls')) return;
      finishInlineText();
    });
    $('openTextDialogBtn').addEventListener('click', () => {
      const object = primaryObject(); if (object?.type === 'text') openTextDialog(null, object);
    });
    $('loadSystemFontsBtn').addEventListener('click', loadSystemFonts);
    $('installGoogleFontBtn').addEventListener('click', openGoogleFontDialog);
    $('googleFontForm').addEventListener('submit', (event) => { event.preventDefault(); void installGoogleFontFromDialog(); });
    $('googleFontCancelBtn').addEventListener('click', closeGoogleFontDialog);
    $('googleFontCloseBtn').addEventListener('click', closeGoogleFontDialog);
    $('googleFontDialog').addEventListener('click', (event) => { if (event.target === $('googleFontDialog')) closeGoogleFontDialog(); });
    $('convertTextToPathBtn').addEventListener('click', convertSelectedTextToPath);
    $('propTextTextureMode').addEventListener('change', () => setTextTextureMode($('propTextTextureMode').value));
    $('propTextTextureScale').addEventListener('input', () => setTextTextureNumber('scale', $('propTextTextureScale').value));
    $('propTextTextureScale').addEventListener('change', () => setTextTextureNumber('scale', $('propTextTextureScale').value, true));
    $('propTextTextureDistortion').addEventListener('input', () => setTextTextureNumber('distortion', $('propTextTextureDistortion').value));
    $('propTextTextureDistortion').addEventListener('change', () => setTextTextureNumber('distortion', $('propTextTextureDistortion').value, true));
    $('propTextTextureAmount').addEventListener('input', () => setTextTextureNumber('amount', $('propTextTextureAmount').value));
    $('propTextTextureAmount').addEventListener('change', () => setTextTextureNumber('amount', $('propTextTextureAmount').value, true));
    $('propTextTextureTransparent').addEventListener('change', () => {
      const object = primaryObject(); if (!supportsObjectTexture(object)) return;
      const texture = textTextureSettings(object); if (!surfaceTextTextureModes.has(texture.mode)) return;
      object.textTexture = { ...texture, transparent: $('propTextTextureTransparent').checked };
      commitHistory(object.textTexture.transparent ? 'Transparente Textur aktiviert' : 'Transparente Textur deaktiviert'); renderAll();
    });
    $('propTextTextureFullCells').addEventListener('change', () => {
      const object = primaryObject(); if (object?.type !== 'text') return;
      const texture = textTextureSettings(object); if (texture.mode !== 'pixel') return;
      object.textTexture = { ...texture, pixelFullCells: $('propTextTextureFullCells').checked };
      commitHistory('Randpixel geändert'); renderAll();
    });
    $('propTextTexturePixelShape').addEventListener('change', () => {
      const object = primaryObject(); if (!supportsObjectTexture(object)) return;
      const texture = textTextureSettings(object); if (texture.mode !== 'pixel') return;
      object.textTexture = { ...texture, pixelShape: $('propTextTexturePixelShape').value === 'round' ? 'round' : 'square' };
      commitHistory('Pixelform geändert'); renderAll();
    });
    $('randomizeTextTextureBtn').addEventListener('click', () => {
      const object = primaryObject(); if (!supportsObjectTexture(object)) return;
      const texture = textTextureSettings(object); if (texture.mode === 'none') return;
      let seed = Math.floor(Math.random() * 9999) + 1; if (seed === texture.seed) seed = seed % 9999 + 1;
      object.textTexture = { ...texture, seed }; commitHistory('Texturmuster neu erzeugt'); renderAll();
    });
    document.fonts?.addEventListener?.('loadingdone', () => {
      // Der erste Render eines Projektfonts kann noch mit einer Ersatzschrift erfolgen.
      // Sobald der Browser die Glyphen geladen hat, wird der sichtbare Rahmen neu vermessen.
      textPixelTextureCache.clear();
      if (!state.gesture && selectedObjects().some((object) => object.type === 'text')) renderArtboard();
    });
    $('bindTextPathBtn').addEventListener('click', bindTextToShape); $('unbindTextPathBtn').addEventListener('click', unbindTextFromShape);
    $('propTextPathPlacement').addEventListener('change', () => setTextPathProperty('placement', $('propTextPathPlacement').value, true));
    $('propTextPathOrientation').addEventListener('change', () => setTextPathProperty('orientation', $('propTextPathOrientation').value, true));
    $('propTextPathDistance').addEventListener('input', () => setTextPathProperty('distance', clamp(num($('propTextPathDistance').value, 0), 0, 1000)));
    $('propTextPathDistance').addEventListener('change', () => setTextPathProperty('distance', clamp(num($('propTextPathDistance').value, 0), 0, 1000), true));
    $('propTextPathOffset').addEventListener('input', () => setTextPathProperty('offset', clamp(num($('propTextPathOffset').value, 0), -360, 360)));
    $('propTextPathOffset').addEventListener('change', () => setTextPathProperty('offset', clamp(num($('propTextPathOffset').value, 0), -360, 360), true));
    $('propTextPathSpacing').addEventListener('input', () => setTextPathProperty('spacing', clamp(num($('propTextPathSpacing').value, 0), -100, 500)));
    $('propTextPathSpacing').addEventListener('change', () => setTextPathProperty('spacing', clamp(num($('propTextPathSpacing').value, 0), -100, 500), true));
    $('propTextPathReversed').addEventListener('change', () => setTextPathProperty('reversed', $('propTextPathReversed').checked, true));
    $('propTextPathTargetVisible').addEventListener('change', () => setTextPathProperty('targetVisible', $('propTextPathTargetVisible').checked, true));
    $('textDialogConfirm').addEventListener('click', confirmText); $('textDialogCancel').addEventListener('click', closeTextDialog);
    $('textDialogInput').addEventListener('keydown', (event) => {
      if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); confirmText(); }
    });
    document.querySelector('[data-dialog-cancel="text"]')?.addEventListener('click', closeTextDialog);
    $('newConfirm').addEventListener('click', createNewDocument); $('newCancel').addEventListener('click', () => $('newDialog').close());
    ['newWidth', 'newHeight', 'newDpi'].forEach((id) => $(id).addEventListener('keydown', (event) => {
      if (event.key !== 'Enter') return;
      event.preventDefault();
      if (!$('newDialog').querySelector('form')?.reportValidity()) return;
      createNewDocument();
    }));
    document.querySelector('[data-dialog-cancel="new"]')?.addEventListener('click', () => $('newDialog').close());
    $('pixelCanvas').addEventListener('pointerdown', onPixelPointerDown); $('pixelCanvas').addEventListener('pointermove', onPixelPointerMove);
    $('pixelCanvas').addEventListener('pointerup', onPixelPointerUp); $('pixelCanvas').addEventListener('pointercancel', onPixelPointerUp);
    document.querySelectorAll('[data-pixel-tool]').forEach((button) => button.addEventListener('click', () => setPixelTool(button.dataset.pixelTool)));
    $('pixelZoom').addEventListener('input', () => renderPixelEditor()); $('pixelGrid').addEventListener('change', () => renderPixelEditor());
    $('pixelApplyBtn').addEventListener('click', applyPixelEditor); $('pixelCancelBtn').addEventListener('click', closePixelEditor); $('pixelCloseBtn').addEventListener('click', closePixelEditor);
    $('pixelDialog').addEventListener('pointerdown', (event) => { if (event.target.matches('[data-pixel-close]')) closePixelEditor(); });
    $('pixelDialog').addEventListener('keydown', (event) => overlayDialogKeydown(event, $('pixelDialog'), closePixelEditor));
    $('cropStage').addEventListener('pointerdown', onCropPointerDown); $('cropStage').addEventListener('pointermove', onCropPointerMove);
    $('cropStage').addEventListener('pointerup', onCropPointerUp); $('cropStage').addEventListener('pointercancel', onCropPointerUp);
    $('cropZoom').addEventListener('input', renderCropEditor); $('cropApplyBtn').addEventListener('click', applyCropEditor); $('cropCancelBtn').addEventListener('click', closeCropEditor); $('cropCloseBtn').addEventListener('click', closeCropEditor);
    $('cropDialog').addEventListener('pointerdown', (event) => { if (event.target.matches('[data-crop-close]')) closeCropEditor(); });
    $('cropDialog').addEventListener('keydown', (event) => overlayDialogKeydown(event, $('cropDialog'), closeCropEditor));
    $('cropSelection').addEventListener('keydown', onCropKeyboard);
    bindInspector();
    bindColorPalettes();
    bindEnterCommitFields();
    refreshFontPreviews();
  }

  function bindKeyboard() {
    const toolKeys = { v: 'select', p: 'pencil', n: 'pen', b: 'brush', m: 'marquee', w: 'wand', c: 'crop', y: 'red-eye', k: 'clone', j: 'heal', l: 'line', r: 'rectangle', o: 'ellipse', a: 'arrow', t: 'text', i: 'eyedropper', g: 'fill', h: 'pan' };
    window.addEventListener('keydown', (event) => {
      if (event.key === 'F1' && !document.querySelector('dialog[open]:not(#helpDialog)')) {
        event.preventDefault(); openHelpDialog(); return;
      }
      if (!$('cropDialog').hidden) {
        if (event.key === 'Escape') { event.preventDefault(); closeCropEditor(); }
        return;
      }
      if (!$('pixelDialog').hidden) {
        if (event.key === 'Escape') { event.preventDefault(); closePixelEditor(); }
        else if (!/INPUT|TEXTAREA|SELECT/.test(event.target.tagName)) {
          const pixelKeys = { p: 'pencil', f: 'fill', e: 'eraser' };
          if (pixelKeys[event.key.toLowerCase()]) { event.preventDefault(); setPixelTool(pixelKeys[event.key.toLowerCase()]); }
        }
        return;
      }
      const editing = /INPUT|TEXTAREA|SELECT/.test(event.target.tagName) || event.target.isContentEditable;
      if (event.code === 'Space' && !editing) { state.spaceDown = true; refs.artboard.style.cursor = 'grab'; event.preventDefault(); }
      if (editing) return;
      if (event.key === 'Escape' && (closeNotoEmojiPicker() || closeShapeMenu() || closeEraserMenu() || closeRetouchMenu() || closeWarpMenu() || closeMarqueeMenu() || closeLineMenu() || closePathModifierMenu())) { event.preventDefault(); return; }
      if (event.key === 'Enter' && state.gesture?.kind === 'bitmap-polygon-lasso') { event.preventDefault(); finishBitmapPolygonLassoGesture(); return; }
      if (event.key === 'Enter' && activePenGesture()) { event.preventDefault(); finishPenPath(false); return; }
      if (event.key === 'Enter' && activePolylineGesture()) { event.preventDefault(); finishPolylinePath(); return; }
      if (event.key === 'Enter' && state.warpSession?.dirty) { event.preventDefault(); applyWarpSession(); return; }
      const mod = event.ctrlKey || event.metaKey; const key = event.key.toLowerCase();
      if (mod && key === 'z') { event.preventDefault(); event.shiftKey ? redo() : undo(); return; }
      if (mod && key === 'y') { event.preventDefault(); redo(); return; }
      if (mod && key === 'c') { event.preventDefault(); copySelection(); return; }
      if (mod && key === 'x') { event.preventDefault(); copySelection(true); return; }
      if (mod && key === 'v') return;
      if (mod && key === 'd') { event.preventDefault(); duplicateSelection(); return; }
      if (mod && key === 'g') { event.preventDefault(); event.shiftKey ? ungroupSelection() : groupSelection(); return; }
      if (mod && key === 'a') {
        event.preventDefault();
        select(allObjects().filter((object) => { const layer = layerForObject(object.id); return layer?.visible && !layer.locked && object.visible !== false && !object.locked; }).map((object) => object.id));
        return;
      }
      if (mod && key === 's') { event.preventDefault(); saveProject(); return; }
      if (mod && key === 'o') { event.preventDefault(); openProjectPicker(); return; }
      if (mod && key === 'n') { event.preventDefault(); $('newBtn').click(); return; }
      if (mod && (event.key === '+' || event.key === '=')) { event.preventDefault(); applyZoom(state.zoom * 1.25); return; }
      if (mod && event.key === '-') { event.preventDefault(); applyZoom(state.zoom / 1.25); return; }
      if (!mod && (event.key === '+' || event.key === '=')) { event.preventDefault(); applyZoom(state.zoom * 1.25); return; }
      if (!mod && event.key === '-') { event.preventDefault(); applyZoom(state.zoom / 1.25); return; }
      if (!mod && event.key === '0') { event.preventDefault(); zoomFit(); return; }
      if (event.key === 'Delete' || event.key === 'Backspace') { event.preventDefault(); if (!removeLastPenAnchor() && !removeLastPolylineAnchor() && !deleteSelectedVectorSegments() && !deleteSelectedVectorPoint()) deleteSelection(); return; }
      if (event.key === 'Escape') {
        if (state.gesture) { event.preventDefault(); cancelActiveGesture(); return; }
        if (state.warpSession || state.warpPreparation) { event.preventDefault(); cancelWarpSession(); return; }
        if (state.canvasResizeMode) { event.preventDefault(); setManualCanvasResizeMode(false); return; }
        if (state.bitmapMarquee) {
          state.bitmapMarquee = null; state.gesture = null; state.snapGuides = []; syncPixelToolOptions(); renderAll(); return;
        }
        state.gesture = null; state.snapGuides = []; select([]); return;
      }
      const shortcutTool = key === 'e' ? currentEraserTool : toolKeys[key];
      if (shortcutTool && !mod) { chooseTool(shortcutTool); return; }
      if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key) && state.selected.length) {
        const step = event.shiftKey ? 10 : 1; const dx = event.key === 'ArrowLeft' ? -step : event.key === 'ArrowRight' ? step : 0; const dy = event.key === 'ArrowUp' ? -step : event.key === 'ArrowDown' ? step : 0;
        selectedObjects().forEach((object) => { object.x += dx; object.y += dy; }); commitHistory('Objekte verschoben'); renderAll(); event.preventDefault();
      }
    });
    window.addEventListener('keyup', (event) => { if (event.code === 'Space') { state.spaceDown = false; chooseTool(state.tool); } });
    window.addEventListener('blur', () => { state.spaceDown = false; if (state.gesture) cancelActiveGesture(); });
    window.addEventListener('beforeunload', (event) => {
      finishInlineText();
      persistActiveDocument();
      saveTabSession({ navigation: true });
      if (state.documents.some((document) => document.dirty)) { event.preventDefault(); event.returnValue = ''; }
    });
    window.addEventListener('pagehide', () => {
      finishInlineText();
      saveTabSession({ navigation: true });
    });
  }

  restoreToolColors();
  restoreEraserToolSettings();
  setupRightSidebarCollapsibles();
  const restoredTabSession = restoreTabSession();
  bindUI(); bindKeyboard();
  installProjectFileLaunchHandler();
  if (!restoredTabSession) initializeHistory('Start');
  renderAll(); chooseTool('select');
  requestAnimationFrame(() => {
    if (restoredTabSession) {
      const document = activeDocument();
      refs.workspace.scrollLeft = document?.scrollLeft || 0;
      refs.workspace.scrollTop = document?.scrollTop || 0;
      scheduleRulerRender();
    } else zoomFit();
    requestAnimationFrame(() => {
      if (tabSessionStartupWarning) toast(tabSessionStartupWarning, 'warning');
      void (async () => {
        await startupIdleTurn();
        await registerBundledFonts();
        await startupIdleTurn();
        await registerInstalledGoogleFonts();
      })();
    });
  });
})();
