﻿{"id":487,"date":"2026-03-04T13:07:07","date_gmt":"2026-03-04T13:07:07","guid":{"rendered":"https:\/\/perfectchroma.com\/testbild-generator\/"},"modified":"2026-05-09T05:52:18","modified_gmt":"2026-05-09T05:52:18","slug":"testbild-generator","status":"publish","type":"page","link":"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/","title":{"rendered":"Testbild-Generator"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"487\" class=\"elementor elementor-487 elementor-68\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-ae6bca0 e-con-full e-flex e-con e-parent\" data-id=\"ae6bca0\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-8207a37 elementor-widget elementor-widget-shortcode\" data-id=\"8207a37\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\"><style>\n\/* =====================================================\n   Test Pattern Generator \u2014 3-Column Apple-Dark UI\n   ===================================================== *\/\n\n:root {\n  --tpg-bg: #0b0b0d;               \/* Deep black background for the app *\/\n  --tpg-header: #141416;           \/* Titlebar \/ Sidebar color *\/\n  --tpg-panel: #18181b;            \/* Main settings panel color *\/\n  --tpg-card: #232326;             \/* Card\/Section background *\/\n  --tpg-input: #29292c;            \/* Control background *\/\n  --tpg-border: rgba(255,255,255,0.06);\n  --tpg-border-hover: rgba(255,255,255,0.12);\n  --tpg-accent: #0a84ff;           \/* Pro Blue *\/\n  --tpg-accent-glow: rgba(10,132,255,0.2);\n  --tpg-text: #efeff1;\n  --tpg-text2: #efeff1;           \/* Fallback for old variables *\/\n  --tpg-text3: #9a9a9e;           \/* Fallback for old variables *\/\n  --tpg-text-secondary: #9a9a9e;\n  --tpg-text-dim: #64646a;\n  --tpg-hover: rgba(255,255,255,0.08);\n  --tpg-padding: 16px;\n  --tpg-radius: 8px;\n  --tpg-radius-sm: 6px;\n  \n  --tpg-col-l-w: 320px;\n  --tpg-col-r-w: 320px;\n  --tpg-col-l: #0b1120;\n}\n\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\n\/* \u2500\u2500 Wrap \u2500\u2500 *\/\n.tpg-wrap {\n  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif;\n  color: var(--tpg-text);\n  width: 100%;\n  -webkit-font-smoothing: antialiased;\n}\n\n\/* \u2500\u2500 Outer shell \u2500\u2500 *\/\n\/* In normal page flow (pre-modal) the app is hidden *\/\n#tpg-focus-wrap {\n  display: none; \/* always hidden in page \u2014 only visible inside modal *\/\n}\n\n\/* Inside the modal it fills the modal-inner container *\/\n#tpg-modal-inner #tpg-focus-wrap {\n  display: block;\n  height: 100%;\n}\n\n#app {\n  width: 100%;\n  max-width: 100%;\n  margin: 0;\n  border-radius: 0;\n  overflow: hidden;\n  border: none;\n  background: var(--tpg-bg);\n  box-shadow: none;\n  display: flex;\n  flex-direction: column;\n  height: 100vh;\n  min-height: 0;\n  position: relative;\n}\n\n\/* \u2500\u2500 Header \/ Toolbar \u2500\u2500 *\/\n#header {\n  display: flex;\n  align-items: center;\n  background: #09090b; \/* Pitch black for premium feel *\/\n  border-bottom: 1px solid rgba(255,255,255,0.08);\n  height: 56px;\n  min-height: 56px;\n  flex-shrink: 0;\n  z-index: 100;\n  padding: 0 20px;\n}\n\n.header-left {\n  width: 320px;\n  display: flex;\n  align-items: center;\n  flex-shrink: 0;\n}\n.header-center {\n  flex: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 16px;\n}\n.header-right {\n  width: 320px;\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  gap: 12px;\n  flex-shrink: 0;\n}\n\n#header h1 {\n  font-size: 13px;\n  font-weight: 800;\n  letter-spacing: 0.05em;\n  text-transform: uppercase;\n  color: #fff;\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  margin: 0;\n}\n#header h1 .tpg-title-icon { color: var(--tpg-accent); display: flex; }\n\n.tpg-toolbar-btn {\n  background: transparent !important;\n  color: #ffffff !important;\n  border: none !important;\n  width: 40px !important;\n  height: 40px !important;\n  border-radius: 10px !important;\n  cursor: pointer !important;\n  display: flex !important;\n  align-items: center !important;\n  justify-content: center !important;\n  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;\n  outline: none !important;\n  flex-shrink: 0 !important;\n  opacity: 0.8;\n}\n.tpg-toolbar-btn:hover {\n  background: rgba(255,255,255,0.12) !important;\n  opacity: 1;\n}\n.tpg-toolbar-btn svg {\n  width: 22px !important;\n  height: 22px !important;\n  min-width: 22px !important;\n  min-height: 22px !important;\n  stroke: #ffffff !important;\n  stroke-width: 2.2 !important;\n  fill: none !important;\n  display: block !important;\n  pointer-events: none !important;\n  flex-shrink: 0 !important;\n}\n\n\/* Zoom Container *\/\n.tpg-zoom-control {\n  display: flex;\n  align-items: center;\n  background: rgba(255,255,255,0.05);\n  border: 1px solid rgba(255,255,255,0.1);\n  border-radius: 12px;\n  padding: 3px;\n  height: 44px;\n  flex-shrink: 0 !important;\n}\n.tpg-zoom-control .tpg-toolbar-btn {\n  width: 38px !important;\n  height: 38px !important;\n}\n#zoom-label {\n  font-size: 12px;\n  font-weight: 700;\n  color: #fff;\n  min-width: 50px;\n  text-align: center;\n  font-variant-numeric: tabular-nums;\n  padding: 0 4px;\n}\n\n.tpg-btn-close {\n  background: transparent !important;\n  border: none !important;\n  color: rgba(255,255,255,0.5) !important;\n  display: flex !important;\n  align-items: center !important;\n  gap: 8px !important;\n  font-size: 14px !important;\n  font-weight: 600 !important;\n  cursor: pointer !important;\n  padding: 8px 12px !important;\n  border-radius: 8px !important;\n  transition: all 0.2s !important;\n  height: auto !important;\n  width: auto !important;\n  text-transform: none !important;\n}\n.tpg-btn-close:hover {\n  color: #fff !important;\n  background: rgba(255,255,255,0.08) !important;\n}\n.tpg-btn-close svg {\n  width: 18px !important;\n  height: 18px !important;\n  stroke: #ffffff !important;\n  stroke-width: 2.5 !important;\n  display: block !important;\n}\n\n.tpg-v-sep {\n  width: 1px;\n  height: 20px;\n  background: rgba(255,255,255,0.1);\n  margin: 0 4px;\n}\n\n\/* \u2500\u2500 Keyboard Shortcuts Modal \u2500\u2500 *\/\n#shortcuts-overlay {\n  position: fixed; inset: 0;\n  background: rgba(0,0,0,0.72);\n  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n  display: flex; align-items: center; justify-content: center;\n  z-index: 99999;\n  opacity: 0; pointer-events: none;\n  transition: opacity 0.25s cubic-bezier(0.4,0,0.2,1);\n}\n#shortcuts-overlay.tpg-show {\n  opacity: 1; pointer-events: all;\n}\n.shortcuts-modal {\n  background: #1c1c1f;\n  border: 1px solid rgba(255,255,255,0.1);\n  border-radius: 20px;\n  padding: 0;\n  width: 400px;\n  max-width: calc(100vw - 32px);\n  box-shadow:\n    0 0 0 1px rgba(255,255,255,0.04),\n    0 32px 80px rgba(0,0,0,0.85),\n    0 8px 24px rgba(0,0,0,0.5);\n  transform: translateY(16px) scale(0.97);\n  transition: transform 0.28s cubic-bezier(0.34,1.2,0.64,1), opacity 0.25s;\n  opacity: 0;\n  overflow: hidden;\n}\n#shortcuts-overlay.tpg-show .shortcuts-modal {\n  transform: translateY(0) scale(1);\n  opacity: 1;\n}\n\/* Header bar *\/\n.shortcuts-modal-header {\n  display: flex; align-items: center; justify-content: space-between;\n  padding: 20px 24px 16px;\n  border-bottom: 1px solid rgba(255,255,255,0.07);\n}\n.shortcuts-modal-header h2 {\n  font-size: 15px; font-weight: 700; color: #fff;\n  letter-spacing: -0.3px; margin: 0;\n  display: flex; align-items: center; gap: 9px;\n}\n.shortcuts-modal-header h2 svg {\n  color: var(--tpg-accent);\n  flex-shrink: 0;\n}\n.shortcuts-modal-close {\n  background: transparent !important;\n  border: none !important;\n  color: rgba(255,255,255,0.4) !important;\n  cursor: pointer; display: flex; align-items: center; justify-content: center;\n  transition: color 0.15s;\n  flex-shrink: 0;\n  width: 24px; height: 24px; padding: 0;\n}\n.shortcuts-modal-close:hover {\n  color: #fff !important;\n}\n\/* Body *\/\n.shortcuts-modal-body { padding: 16px 24px 24px; }\n.shortcut-section-label {\n  font-size: 9.5px; font-weight: 700; letter-spacing: 0.7px;\n  text-transform: uppercase; color: rgba(255,255,255,0.3);\n  padding: 4px 0 8px; margin-top: 4px;\n}\n.shortcut-row {\n  display: flex; justify-content: space-between; align-items: center;\n  padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,0.04);\n  color: rgba(255,255,255,0.65); font-size: 13px; font-weight: 500;\n}\n.shortcut-row:last-of-type { border-bottom: none; }\n.shortcut-keys { display: flex; align-items: center; gap: 4px; }\n.shortcut-key {\n  background: #2a2a2d;\n  padding: 3px 9px; border-radius: 6px;\n  border: 1px solid rgba(255,255,255,0.12);\n  border-bottom-width: 2px;\n  font-family: 'SF Mono', 'Fira Mono', 'Cascadia Code', monospace;\n  color: rgba(255,255,255,0.9); font-size: 11px; font-weight: 600;\n  box-shadow: 0 1px 3px rgba(0,0,0,0.35);\n  white-space: nowrap;\n}\n.shortcut-hr {\n  border: none; border-top: 1px solid rgba(255,255,255,0.06);\n  margin: 8px 0;\n}\n.shortcut-dismiss {\n  margin-top: 20px; width: 100%; height: 42px; border-radius: 10px;\n  background: var(--tpg-accent); color: #fff; border: none;\n  font-weight: 700; font-size: 13px; cursor: pointer;\n  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;\n  transition: background 0.15s, transform 0.1s;\n  box-shadow: 0 4px 14px rgba(10,132,255,0.3);\n}\n.shortcut-dismiss:hover { background: #409cff; }\n.shortcut-dismiss:active { transform: scale(0.98); }\n\n\/* Ensure modal icons are visible *\/\n.shortcuts-modal svg {\n  stroke: currentColor !important;\n  stroke-width: 2.2 !important;\n  display: block !important;\n}\n.shortcuts-modal-header h2 svg {\n  color: #0a84ff !important;\n}\n\n.tpg-zoom-group {\n  display: flex; align-items: center; gap: 0;\n  background: var(--tpg-input); \n  border: 1px solid var(--tpg-border);\n  border-radius: 6px; overflow: hidden; height: 32px;\n}\n.tpg-zoom-group .tpg-btn { \n  width: 32px; height: 30px; border: none; border-radius: 0; background: transparent; \n}\n.tpg-zoom-group .tpg-btn:hover { background: rgba(255,255,255,0.05); }\n#zoom-label {\n  font-size: 11px; color: var(--tpg-text-secondary);\n  min-width: 44px; text-align: center; font-variant-numeric: tabular-nums;\n  border-left: 1px solid var(--tpg-border); border-right: 1px solid var(--tpg-border);\n  height: 30px; display: flex; align-items: center; justify-content: center;\n  font-weight: 600;\n}\n\n\/* \u2500\u2500 3-Column Body \u2500\u2500 *\/\n#tpg-body {\n  display: flex;\n  flex: 1;\n  overflow: hidden;\n}\n\n\/* \u2500\u2500 LEFT COL: Pattern list \u2014 Shopall-inspired dark sidebar \u2500\u2500 *\/\n#tpg-left {\n  width: var(--tpg-col-l-w);\n  min-width: var(--tpg-col-l-w);\n  background: #0b1120;\n  border-right: 1px solid rgba(255,255,255,0.06);\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n#tpg-left-header {\n  padding: 18px 18px 14px;\n  font-size: 15px;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n  color: #ffffff;\n  flex-shrink: 0;\n  border-bottom: 1px solid rgba(255,255,255,0.06);\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n#tpg-left-header::before {\n  content: '';\n  display: inline-block;\n  width: 28px; height: 28px;\n  background: #3b82f6;\n  border-radius: 7px;\n  flex-shrink: 0;\n  background-image: url(\"data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='3' width='20' height='18' rx='2'\/%3E%3Cline x1='8' y1='3' x2='8' y2='21'\/%3E%3Cline x1='16' y1='3' x2='16' y2='21'\/%3E%3C\/svg%3E\");\n  background-repeat: no-repeat;\n  background-position: center;\n  background-size: 16px;\n}\n#sidebar {\n  flex: 1;\n  overflow-y: auto;\n  padding: 8px 0;\n}\n#sidebar::-webkit-scrollbar { width: 5px; }\n#sidebar::-webkit-scrollbar-track { background: transparent; }\n#sidebar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 4px; }\n#sidebar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }\n\/* override elementor leakage *\/\n#sidebar.tpg-sidebar { display: block !important; }\n#sidebar.tpg-sidebar > .nav-section { display: flex !important; }\n#sidebar.tpg-sidebar > .nav-group { display: grid !important; }\n\n#sidebar .nav-section {\n  display: flex !important;\n  align-items: center !important;\n  justify-content: flex-start !important;\n  padding: 10px 14px !important;\n  margin: 4px 10px 2px !important;\n  font-size: 11px !important; font-weight: 600 !important;\n  text-transform: uppercase !important; letter-spacing: 0.08em !important;\n  color: #4a6fa5 !important;\n  cursor: pointer !important;\n  user-select: none !important;\n  transition: color 0.2s, background 0.2s !important;\n  border-radius: 8px !important;\n}\n#sidebar .nav-section:hover {\n  color: #8bb4e0 !important;\n  background: rgba(59,130,246,0.08) !important;\n}\n#sidebar .nav-section.expanded {\n  color: #93b5e0 !important;\n}\n\n\/* \u2500\u2500 Nav group \u2014 CSS grid slide \u2500\u2500 *\/\n.nav-group {\n  display: grid !important;\n  grid-template-rows: 0fr !important;\n  overflow: hidden !important;\n  transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;\n}\n.nav-group.tpg-open {\n  grid-template-rows: 1fr !important;\n}\n.nav-group > .nav-group-inner {\n  overflow: hidden !important;\n  min-height: 0 !important;\n}\n\n\/* \u2500\u2500 Chevron icon \u2500\u2500 *\/\n.nav-section .nav-section-chevron {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  margin-left: auto;\n  width: 18px; height: 18px;\n  opacity: 0.35;\n  color: #4a6fa5;\n  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),\n              opacity 0.2s ease,\n              color 0.2s ease;\n  transform: rotate(0deg);\n}\n.nav-section .nav-section-chevron svg { display: block; }\n.nav-section:hover .nav-section-chevron { opacity: 0.55; }\n.nav-section.expanded .nav-section-chevron {\n  transform: rotate(90deg);\n  opacity: 0.8;\n  color: #6b8fc7;\n}\n\n\/* \u2500\u2500 Nav items \u2500\u2500 *\/\n#sidebar .nav-item {\n  display: flex !important; align-items: center !important;\n  gap: 12px !important; padding: 0 14px !important;\n  margin: 2px 10px !important; height: 40px !important;\n  min-height: 40px !important;\n  cursor: pointer !important; border-radius: 10px !important;\n  font-size: 13.5px !important; font-weight: 500 !important;\n  color: #8899b4 !important;\n  transition: background 0.15s ease, color 0.15s ease !important;\n  line-height: 1 !important; border-left: none !important;\n  position: relative !important;\n}\n#sidebar .nav-item:hover {\n  background: rgba(59,130,246,0.08) !important;\n  color: #c8d6e8 !important;\n}\n#sidebar .nav-item.active {\n  background: rgba(59,130,246,0.15) !important;\n  color: #ffffff !important;\n}\n#sidebar .nav-icon {\n  width: 22px !important; height: 22px !important;\n  display: flex !important; align-items: center !important; justify-content: center !important;\n  flex-shrink: 0 !important; opacity: 0.55 !important;\n  transition: opacity 0.15s ease, color 0.15s ease !important;\n}\n#sidebar .nav-item:hover .nav-icon { opacity: 0.8 !important; }\n#sidebar .nav-item.active .nav-icon { opacity: 1 !important; color: #3b82f6 !important; }\n#sidebar .nav-label {\n  flex: 1 !important; margin: 0 !important; padding: 0 !important;\n  white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important;\n}\n\n\/* \u2500\u2500 Nav Thumbnail Grid \u2500\u2500 *\/\n.nav-group-thumbnail-grid {\n  display: grid !important;\n  grid-template-columns: repeat(3, 1fr) !important;\n  gap: 10px !important;\n  padding: 8px 12px 14px 12px !important;\n}\n\n#sidebar .nav-thumb-item {\n  display: flex !important;\n  flex-direction: column !important;\n  align-items: center !important;\n  cursor: pointer !important;\n  border-radius: 8px !important;\n  transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease !important;\n  position: relative !important;\n  padding: 6px !important;\n  background: rgba(255, 255, 255, 0.02) !important;\n  border: 1px solid rgba(255, 255, 255, 0.05) !important;\n}\n\n#sidebar .nav-thumb-item:hover {\n  background: rgba(59, 130, 246, 0.08) !important;\n  border-color: rgba(59, 130, 246, 0.2) !important;\n  transform: translateY(-2px) !important;\n}\n\n#sidebar .nav-thumb-item.active {\n  background: rgba(59, 130, 246, 0.15) !important;\n  border-color: #3b82f6 !important;\n}\n\n#sidebar .nav-thumb-img {\n  width: 100% !important;\n  aspect-ratio: 16 \/ 9 !important;\n  background-size: cover !important;\n  background-position: center !important;\n  border-radius: 4px !important;\n  margin-bottom: 8px !important;\n  border: 1px solid rgba(0, 0, 0, 0.5) !important;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.3) !important;\n}\n\n#sidebar .nav-thumb-label {\n  font-size: 10px !important;\n  font-weight: 600 !important;\n  color: #8899b4 !important;\n  text-align: center !important;\n  line-height: 1.25 !important;\n  \n  \/* Multiline clamp (2 lines max) *\/\n  display: -webkit-box !important;\n  -webkit-line-clamp: 2 !important;\n  -webkit-box-orient: vertical !important;\n  overflow: hidden !important;\n  text-overflow: ellipsis !important;\n  \n  width: 100% !important;\n}\n\n#sidebar .nav-thumb-item:hover .nav-thumb-label {\n  color: #c8d6e8 !important;\n}\n\n#sidebar .nav-thumb-item.active .nav-thumb-label {\n  color: #ffffff !important;\n}\n\n\/* \u2500\u2500 CENTER COL: Preview \u2500\u2500 *\/\n#tpg-center {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  background: #000;\n  position: relative;\n}\n#canvas-wrap {\n  flex: 1;\n  position: relative;\n  background: #000;\n  overflow: hidden;\n  cursor: default;\n}\n#canvas-wrap.tpg-panning { cursor: grabbing !important; }\n#canvas-wrap.tpg-zoomable { cursor: grab; }\n\n#canvas-pan {\n  position: absolute;\n  top: 0; left: 0;\n  width: 100%; height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  will-change: transform;\n}\n\n#patternCanvas {\n  display: block;\n  transform-origin: center center;\n  image-rendering: optimizeSpeed;\n  image-rendering: -moz-crisp-edges;\n  image-rendering: -webkit-optimize-contrast;\n  image-rendering: crisp-edges;\n  image-rendering: pixelated;\n  flex-shrink: 0;\n}\n\n\/* \u2500\u2500 Custom scrollbars \u2500\u2500 *\/\n#tpg-scroll-x, #tpg-scroll-y {\n  position: absolute;\n  background: rgba(255,255,255,0.04);\n  border-radius: 6px;\n  opacity: 0;\n  transition: opacity 0.25s ease;\n  z-index: 10;\n  pointer-events: none;\n}\n#tpg-scroll-x {\n  bottom: 6px; left: 6px; right: 16px;\n  height: 6px;\n}\n#tpg-scroll-y {\n  top: 6px; right: 6px; bottom: 16px;\n  width: 6px;\n}\n#canvas-wrap.tpg-scrollable #tpg-scroll-x,\n#canvas-wrap.tpg-scrollable #tpg-scroll-y,\n#canvas-wrap:hover #tpg-scroll-x,\n#canvas-wrap:hover #tpg-scroll-y { opacity: 1; pointer-events: auto; }\n\n#tpg-thumb-x, #tpg-thumb-y {\n  position: absolute;\n  background: rgba(255,255,255,0.30);\n  border-radius: 6px;\n  transition: background 0.15s;\n  cursor: pointer;\n}\n#tpg-thumb-x { height: 100%; top: 0; min-width: 24px; }\n#tpg-thumb-y { width: 100%; left: 0; min-height: 24px; }\n#tpg-thumb-x:hover, #tpg-thumb-y:hover { background: rgba(255,255,255,0.55); }\n#tpg-thumb-x:active, #tpg-thumb-y:active { background: rgba(59,130,246,0.75); }\n\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   RIGHT COLUMN \u2014 Config Panel\n   \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n\n#tpg-right {\n  width: var(--tpg-col-r-w);\n  min-width: var(--tpg-col-r-w);\n  background: var(--tpg-panel);\n  border-left: 1px solid var(--tpg-border);\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n#tpg-right-header {\n  display: flex;\n  align-items: center;\n  padding: 0 16px;\n  height: 48px;\n  border-bottom: 1px solid var(--tpg-border);\n  font-size: 11px;\n  font-weight: 700;\n  text-transform: uppercase;\n  letter-spacing: 0.8px;\n  color: var(--tpg-text-secondary);\n}\n\n#tpg-right-scroll {\n  flex: 1;\n  overflow-y: auto;\n  padding: 12px;\n}\n#tpg-right-scroll::-webkit-scrollbar { width: 5px; }\n#tpg-right-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }\n\n\/* \u2500\u2500 Collapsible Section \u2500\u2500 *\/\n.rpanel-section {\n  background: rgba(255,255,255,0.03);\n  border-radius: 12px;\n  border: 1px solid rgba(255,255,255,0.06);\n  margin-bottom: 20px;\n  \/* overflow: hidden removed to allow custom select dropdowns to overflow *\/\n}\n\n.rpanel-header {\n  padding: 12px 14px;\n  background: rgba(255,255,255,0.02);\n  border-bottom: 1px solid rgba(255,255,255,0.04);\n}\n.rpanel-title {\n  font-size: 11px;\n  font-weight: 800;\n  text-transform: uppercase;\n  letter-spacing: 0.8px;\n  color: rgba(255,255,255,0.9);\n}\n.rpanel-arrow {\n  font-size: 10px;\n  color: var(--tpg-text-dim);\n  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n.rpanel-section.collapsed .rpanel-arrow { transform: rotate(-90deg); }\n.rpanel-content {\n  padding: 16px;\n  display: flex;\n  flex-direction: column;\n  gap: 14px;\n}\n.rpanel-section.collapsed .rpanel-content { display: none; }\n\n\/* \u2500\u2500 Form Layout \u2500\u2500 *\/\n.rpanel-field {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n}\n.rpanel-label {\n  font-size: 10px;\n  font-weight: 700;\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  color: rgba(255,255,255,0.4);\n}\n\n\/* \u2500\u2500 Controls \u2500\u2500 *\/\n.rp-input, .rp-select {\n  width: 100%;\n  height: 38px;\n  background: rgba(255,255,255,0.04);\n  border: 1px solid rgba(255,255,255,0.08);\n  border-radius: 12px;\n  color: #fff;\n  padding: 0 14px;\n  font-size: 13px;\n  font-family: inherit;\n  outline: none;\n  transition: all 0.2s;\n  color-scheme: dark;\n}\n.rp-input:hover, .rp-select:hover {\n  background: rgba(255,255,255,0.07);\n  border-color: rgba(255,255,255,0.15);\n}\n.rp-input:focus, .rp-select:focus {\n  background: rgba(255,255,255,0.03);\n  border-color: var(--tpg-accent);\n  box-shadow: 0 0 0 3px var(--tpg-accent-glow);\n}\n\n.rp-select-wrap { position: relative; }\n.rp-select-wrap::after {\n  content: '';\n  position: absolute; right: 10px; top: 50%;\n  transform: translateY(-50%) rotate(45deg);\n  width: 5px; height: 5px;\n  border-right: 1.5px solid var(--tpg-text-dim);\n  border-bottom: 1.5px solid var(--tpg-text-dim);\n  pointer-events: none;\n}\n.rp-select {\n  padding-right: 32px;\n  -webkit-appearance: none;\n  appearance: none;\n  cursor: pointer;\n  color-scheme: dark; \/* Forces native picker to use dark theme if supported *\/\n}\n.rp-select option {\n  background: #18181b; \/* Matches --tpg-panel *\/\n  color: #fff;\n}\n\n\/* Inline controls (Row style) *\/\n.rpanel-row {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n}\n.rpanel-row .rpanel-label { margin-bottom: 0; flex-shrink: 0; }\n\n\/* Toggle *\/\n.rp-toggle {\n  position: relative;\n  width: 38px; height: 20px;\n  background: rgba(255,255,255,0.1);\n  border-radius: 10px;\n  cursor: pointer;\n  transition: background 0.3s;\n  border: none;\n  flex-shrink: 0;\n}\n.rp-toggle.on { background: var(--tpg-accent); }\n.rp-toggle::after {\n  content: '';\n  position: absolute; top: 3px; left: 3px;\n  width: 14px; height: 14px;\n  background: #fff;\n  border-radius: 50%;\n  transition: transform 0.25s cubic-bezier(0.25, 0.1, 0.25, 1);\n  box-shadow: 0 1px 3px rgba(0,0,0,0.3);\n}\n.rp-toggle.on::after { transform: translateX(18px); }\n\n\/* \u2500\u2500 Custom Select (Custom Dropdown) \u2500\u2500 *\/\n.tpg-custom-select {\n  position: relative;\n  width: 100%;\n  user-select: none;\n}\n.tpg-select-trigger {\n  width: 100%;\n  height: 38px;\n  background: rgba(255,255,255,0.04);\n  border: 1px solid rgba(255,255,255,0.08);\n  border-radius: 12px;\n  color: #fff;\n  padding: 0 14px;\n  font-size: 13px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  cursor: pointer;\n  transition: all 0.2s;\n}\n.tpg-custom-select.active .tpg-select-trigger {\n  border-color: var(--tpg-accent);\n  box-shadow: 0 0 0 3px var(--tpg-accent-glow);\n  background: rgba(255,255,255,0.02);\n}\n.tpg-select-trigger:hover {\n  background: rgba(255,255,255,0.07);\n  border-color: rgba(255,255,255,0.15);\n}\n.tpg-select-arrow {\n  width: 0; height: 0;\n  border-left: 4.5px solid transparent;\n  border-right: 4.5px solid transparent;\n  border-top: 5px solid rgba(255,255,255,0.4);\n  transition: transform 0.2s;\n}\n.tpg-custom-select.active .tpg-select-arrow {\n  transform: rotate(180deg);\n  border-top-color: #fff;\n}\n.tpg-custom-select.active {\n  z-index: 1001; \/* Stay above other sections when open *\/\n}\n.tpg-select-dropdown {\n  position: absolute;\n  top: calc(100% + 8px);\n  left: 0; right: 0;\n  background: #232326;\n  border: 1px solid rgba(255,255,255,0.1);\n  border-radius: 14px;\n  padding: 6px;\n  z-index: 9999;\n  box-shadow: 0 12px 32px rgba(0,0,0,0.5);\n  display: none;\n  opacity: 0;\n  transform: translateY(-8px);\n  transition: opacity 0.2s, transform 0.2s;\n}\n.tpg-custom-select.active .tpg-select-dropdown {\n  display: block;\n  opacity: 1;\n  transform: translateY(0);\n}\n.tpg-select-option {\n  padding: 8px 12px;\n  border-radius: 8px;\n  font-size: 13px;\n  color: rgba(255,255,255,0.7);\n  cursor: pointer;\n  transition: all 0.15s;\n}\n.tpg-select-option:hover {\n  background: rgba(255,255,255,0.06);\n  color: #fff;\n}\n.tpg-select-option.selected {\n  background: rgba(10,132,255,0.12);\n  color: var(--tpg-accent);\n  font-weight: 600;\n}\n\n\/* Slider *\/\n.rp-slider-wrap { flex: 1; display: flex; align-items: center; gap: 8px; }\n.rp-slider {\n  -webkit-appearance: none; appearance: none;\n  flex: 1; height: 3px;\n  background: rgba(255,255,255,0.1); border-radius: 2px;\n  outline: none; cursor: pointer;\n  margin: 10px 0;\n}\n.rp-slider::-webkit-slider-thumb {\n  -webkit-appearance: none; width: 14px; height: 14px;\n  background: #0a84ff; border-radius: 50%;\n  cursor: pointer;\n  box-shadow: 0 0 0 4px rgba(10,132,255,0.15), 0 2px 4px rgba(0,0,0,0.3);\n  transition: transform 0.1s, box-shadow 0.1s;\n}\n.rp-slider::-webkit-slider-thumb:hover { transform: scale(1.1); box-shadow: 0 0 0 6px rgba(10,132,255,0.2), 0 2px 6px rgba(0,0,0,0.4); }\n.rp-slider-val { font-size: 11px; color: var(--tpg-text-secondary); min-width: 24px; text-align: right; font-variant-numeric: tabular-nums; }\n\n\/* Position Grid *\/\n.rp-pos-grid {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 5px;\n  background: rgba(0,0,0,0.2);\n  border-radius: 12px;\n  padding: 6px;\n  width: 120px;\n  flex-shrink: 0;\n  margin: 0 auto;\n}\n.rp-pos-btn {\n  width: 100% !important; height: 32px;\n  background: rgba(255,255,255,0.04);\n  border: 1px solid rgba(255,255,255,0.08);\n  cursor: pointer; display: flex; align-items: center; justify-content: center;\n  border-radius: 8px;\n  transition: all 0.15s;\n  padding: 0;\n}\n.rp-pos-btn:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.1); }\n.rp-pos-btn.active {\n  background: #0a84ff !important;\n  border-color: #0a84ff !important;\n}\n.rp-pos-btn::before {\n  content: ''; width: 4px; height: 4px; background: rgba(255,255,255,0.25); border-radius: 50%;\n}\n.rp-pos-btn.active::before {\n  background: #fff; width: 5px; height: 5px;\n}\n.rp-pos-dot { display: none; } \/* handled by ::before *\/\n\n.rpanel-group {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n\/* Buttons *\/\n.rp-btn {\n  display: flex; align-items: center; justify-content: center; gap: 8px;\n  height: 36px; padding: 0 16px;\n  border: none; border-radius: var(--tpg-radius);\n  font-family: inherit; font-size: 13px; font-weight: 600;\n  cursor: pointer; transition: transform 0.12s, opacity 0.15s, background 0.15s;\n}\n.rp-btn.primary {\n  background: #0a84ff;\n  color: #fff;\n  box-shadow: 0 4px 14px rgba(10,132,255,0.3);\n}\n.rp-btn.primary:hover { opacity: 0.9; transform: translateY(-1px); }\n.rp-btn.secondary {\n  background: rgba(255,255,255,0.04);\n  color: #fff;\n  border: 1px solid rgba(255,255,255,0.08);\n}\n.rp-btn.secondary:hover {\n  background: rgba(255,255,255,0.08);\n  border-color: rgba(255,255,255,0.15);\n}\n\n\/* Color Picker *\/\n.rp-color-wrap { display: flex; align-items: center; gap: 6px; }\n.rp-color {\n  -webkit-appearance: none; appearance: none;\n  width: 24px; height: 24px; border-radius: 4px;\n  border: 1px solid var(--tpg-border);\n  background: none; padding: 0; cursor: pointer;\n}\n.rp-color::-webkit-color-swatch-wrapper { padding: 0; }\n.rp-color::-webkit-color-swatch { border: none; border-radius: 3px; }\n\n\/* Swatch Grid *\/\n.rp-swatch-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }\n.rp-swatch {\n  height: 28px; border-radius: 4px; border: 1px solid var(--tpg-border);\n  cursor: pointer; transition: transform 0.1s;\n}\n.rp-swatch.active { border-color: var(--tpg-accent); box-shadow: 0 0 0 2px var(--tpg-accent-glow); }\n\n\/* Multi-field rows *\/\n.rpanel-group { display: flex; gap: 8px; align-items: center; }\n\/* \u2500\u2500 Color Swatch Custom Trigger \u2500\u2500 *\/\n.tpg-color-trigger {\n  width: 52px; height: 38px;\n  border-radius: 12px;\n  border: 1px solid rgba(255,255,255,0.08);\n  background: rgba(255,255,255,0.04);\n  cursor: pointer;\n  padding: 4px;\n  transition: all 0.2s;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.tpg-color-trigger:hover {\n  background: rgba(255,255,255,0.07);\n  border-color: rgba(255,255,255,0.15);\n}\n.tpg-color-swatch {\n  width: 100%; height: 100%;\n  border-radius: 8px;\n  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05);\n}\n\n\/* \u2500\u2500 Iro.js Popover \u2500\u2500 *\/\n.tpg-iro-popover {\n  position: absolute;\n  z-index: 10000;\n  background: #1c1c1f;\n  border: 1px solid rgba(255,255,255,0.1);\n  border-radius: 20px;\n  padding: 14px;\n  box-shadow: 0 24px 60px rgba(0,0,0,0.7);\n  display: none;\n  opacity: 0;\n  transform: translateX(10px);\n  transition: opacity 0.25s, transform 0.25s;\n  pointer-events: none;\n  width: 240px; \/* More compact width *\/\n  right: 12px;\n}\n.tpg-iro-popover.active {\n  display: block;\n  opacity: 1;\n  transform: translateX(0);\n  pointer-events: all;\n}\n.IroColorPicker {\n  margin: 0 auto;\n}\n\n\/* \u2500\u2500 Description \u2500\u2500 *\/\n#pattern-info {\n  font-size: 11px;\n  color: var(--tpg-text-secondary);\n  line-height: 1.5;\n  padding: 0 4px 12px;\n}\n\n}\n.rp-segment-btn {\n  flex: 1;\n  background: transparent;\n  color: var(--tpg-text2);\n  border: none;\n  font-size: 11.5px;\n  font-family: inherit;\n  font-weight: 500;\n  cursor: pointer;\n  transition: background 0.12s, color 0.12s;\n  padding: 0 6px;\n}\n.rp-segment-btn:not(:last-child) {\n  border-right: 1px solid var(--rp-input-border);\n}\n.rp-segment-btn:hover { background: rgba(255,255,255,0.08); color: var(--tpg-text); }\n.rp-segment-btn.active {\n  background: rgba(10,132,255,0.22);\n  color: #409cff;\n  font-weight: 600;\n}\n\n\/* Unify button radius *\/\n.rp-btn, .rp-btn-upload, .rp-btn.primary, .rp-btn.secondary, .rp-btn.danger {\n  border-radius: var(--rp-input-radius) !important;\n}\n\n\/* \u2500\u2500 Slider track (accent-colored fill) \u2500\u2500 *\/\n.rp-slider-wrap {\n  flex: 1;\n  position: relative;\n  display: flex;\n  align-items: center;\n  min-width: 0;\n}\n.rp-slider {\n  -webkit-appearance: none;\n  appearance: none;\n  width: 100%;\n  height: 4px;\n  border-radius: 4px;\n  background: rgba(255,255,255,0.12);\n  outline: none;\n  border: none;\n  cursor: pointer;\n}\n.rp-slider::-webkit-slider-thumb {\n  -webkit-appearance: none;\n  width: 18px; height: 18px;\n  background: var(--tpg-accent);\n  border-radius: 50%;\n  cursor: pointer;\n  box-shadow: 0 0 0 2px rgba(10,132,255,0.25), 0 2px 6px rgba(0,0,0,0.45);\n  transition: transform 0.1s;\n}\n.rp-slider::-webkit-slider-thumb:hover { transform: scale(1.15); }\n.rp-slider::-moz-range-thumb {\n  width: 18px; height: 18px;\n  background: var(--tpg-accent); border-radius: 50%; border: none;\n  box-shadow: 0 0 0 2px rgba(10,132,255,0.25), 0 2px 6px rgba(0,0,0,0.45);\n  cursor: pointer;\n}\n.rp-slider-val {\n  font-size: 11px;\n  color: var(--tpg-text3);\n  font-variant-numeric: tabular-nums;\n  min-width: 32px;\n  text-align: right;\n  flex-shrink: 0;\n  padding-left: 6px;\n}\n\n\/* \u2500\u2500 Color swatch picker \u2500\u2500 *\/\n.rp-color {\n  -webkit-appearance: none; appearance: none;\n  width: 52px; height: 38px;\n  border-radius: 12px;\n  border: 1px solid rgba(255,255,255,0.08);\n  background: rgba(255,255,255,0.04);\n  cursor: pointer;\n  padding: 4px;\n  transition: all 0.2s;\n  flex-shrink: 0;\n  color-scheme: dark;\n}\n.rp-color:hover { \n  background: rgba(255,255,255,0.07);\n  border-color: rgba(255,255,255,0.15);\n}\n.rp-color::-webkit-color-swatch-wrapper { padding: 0; }\n.rp-color::-webkit-color-swatch { border: none; border-radius: 6px; }\n\n\/* \u2500\u2500 Solid-color swatch palette \u2500\u2500 *\/\n.rp-swatch-grid {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 5px;\n  padding: 2px 0 4px;\n}\n.rp-swatch {\n  width: 26px; height: 26px;\n  border-radius: 7px;\n  border: 1.5px solid rgba(255,255,255,0.1);\n  cursor: pointer;\n  transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;\n  flex-shrink: 0;\n}\n.rp-swatch:hover { transform: scale(1.12); border-color: rgba(255,255,255,0.45); }\n.rp-swatch.active {\n  border-color: #fff;\n  transform: scale(1.15);\n  box-shadow: 0 0 0 2.5px rgba(255,255,255,0.25);\n}\n\n\/* \u2500\u2500 Logo position 3\u00d73 grid \u2500\u2500 *\/\n.rp-pos-grid {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 5px;\n  background: rgba(0,0,0,0.2);\n  border-radius: 12px;\n  padding: 6px;\n  width: 120px;\n}\n.rp-pos-btn {\n  width: 100%; aspect-ratio: 1;\n  border-radius: 8px;\n  border: 1px solid rgba(255,255,255,0.12);\n  background: rgba(255,255,255,0.04);\n  color: rgba(255,255,247,0.3);\n  cursor: pointer; font-size: 7px;\n  display: flex; align-items: center; justify-content: center;\n  transition: background 0.12s, border-color 0.12s;\n}\n.rp-pos-btn:hover { border-color: var(--tpg-accent); background: rgba(10,132,255,0.1); color: var(--tpg-accent); }\n.rp-pos-btn.active { background: var(--tpg-accent); color: #fff; border-color: var(--tpg-accent); }\n\n\/* \u2500\u2500 Primary action button \u2500\u2500 *\/\n.rp-btn {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 7px;\n  width: 100%;\n  height: 36px;\n  border-radius: var(--rp-input-radius);\n  border: none;\n  font-family: inherit;\n  font-size: 13px;\n  font-weight: 600;\n  cursor: pointer;\n  transition: background 0.12s, transform 0.1s, box-shadow 0.12s;\n}\n.rp-btn.primary {\n  background: var(--tpg-accent);\n  color: #fff;\n  box-shadow: 0 4px 16px rgba(10,132,255,0.3);\n}\n.rp-btn.primary:hover {\n  background: var(--tpg-accent2);\n  box-shadow: 0 6px 20px rgba(10,132,255,0.4);\n}\n.rp-btn.primary:active { transform: scale(0.98); }\n.rp-btn.secondary {\n  background: rgba(255,255,255,0.07);\n  color: var(--tpg-text);\n  border: 1.5px solid rgba(255,255,255,0.12);\n}\n.rp-btn.secondary:hover { background: rgba(255,255,255,0.12); }\n.rp-btn.danger {\n  background: rgba(255,69,58,0.08);\n  color: #ff453a;\n  border: 1.5px solid rgba(255,69,58,0.28);\n}\n.rp-btn.danger:hover { background: rgba(255,69,58,0.18); }\n\n\/* Upload dashed button *\/\n.rp-btn-upload {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 7px;\n  width: 100%;\n  height: 34px;\n  border-radius: var(--rp-input-radius);\n  border: 1.5px dashed rgba(255,255,255,0.18);\n  background: rgba(255,255,255,0.03);\n  color: var(--tpg-text2);\n  font-family: inherit;\n  font-size: 12px;\n  cursor: pointer;\n  transition: border-color 0.12s, background 0.12s, color 0.12s;\n}\n.rp-btn-upload:hover {\n  border-color: var(--tpg-accent);\n  background: rgba(10,132,255,0.07);\n  color: var(--tpg-accent);\n}\n\n\/* \u2500\u2500 Pattern description \u2500\u2500 *\/\n#pattern-info {\n  font-size: 11px;\n  color: var(--tpg-text3);\n  line-height: 1.6;\n  padding: 10px 14px;\n  border-bottom: 1px solid var(--tpg-border);\n}\n\n\/* \u2500\u2500 x separator label \u2500\u2500 *\/\n.rp-x {\n  font-size: 11px;\n  color: var(--tpg-text3);\n  flex-shrink: 0;\n}\n\n\/* \u2500\u2500 Resolution stepper pair \u2500\u2500 *\/\n.rp-res-pair {\n  display: flex;\n  align-items: center;\n  gap: 5px;\n  flex: 1;\n}\n\n\/* \u2500\u2500 Controls (hidden \u2014 legacy, JS now writes to right panel) \u2500\u2500 *\/\n#controls { display: none; }\n#controls-inner { display: none; }\n#custom-panel { display: none; }\n\n\/* \u2500\u2500 Legacy ctrl-* classes \u2014 map to rp-* visuals \u2500\u2500 *\/\n.ctrl-group { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n.ctrl-label {\n  font-size: 12px; color: var(--tpg-text2);\n  white-space: nowrap; font-weight: 400; min-width: 80px; flex-shrink: 0;\n}\n.ctrl-slider {\n  -webkit-appearance: none; appearance: none;\n  flex: 1; height: 4px;\n  background: rgba(255,255,255,0.12); border-radius: 4px;\n  outline: none; border: none; cursor: pointer; min-width: 60px;\n}\n.ctrl-slider::-webkit-slider-thumb {\n  -webkit-appearance: none; width: 18px; height: 18px;\n  background: var(--tpg-accent); border-radius: 50%; cursor: pointer;\n  box-shadow: 0 0 0 2px rgba(10,132,255,0.25), 0 2px 6px rgba(0,0,0,0.45);\n  transition: transform 0.1s;\n}\n.ctrl-slider::-webkit-slider-thumb:hover { transform: scale(1.12); }\n.ctrl-select {\n  flex: 1;\n  background: var(--rp-input-bg); color: var(--tpg-text);\n  border: 1.5px solid var(--rp-input-border);\n  border-radius: var(--rp-input-radius);\n  padding: 0 10px; height: 32px;\n  font-size: 12.5px; outline: none; -webkit-appearance: none;\n  cursor: pointer; font-family: inherit;\n  transition: border-color 0.15s;\n}\n.ctrl-select:focus { border-color: var(--rp-input-border-focus); box-shadow: 0 0 0 3px rgba(10,132,255,0.18); }\n.ctrl-value {\n  font-size: 11px; color: var(--tpg-text3);\n  min-width: 32px; text-align: right;\n  font-variant-numeric: tabular-nums;\n}\n.ctrl-sep { width: 100%; height: 1px; background: var(--tpg-border); margin: 2px 0; }\n.color-btn {\n  width: 24px; height: 24px; border-radius: 7px;\n  border: 1.5px solid rgba(255,255,255,0.1); cursor: pointer;\n  transition: transform 0.12s, border-color 0.12s;\n}\n.color-btn:hover, .color-btn.active {\n  border-color: rgba(255,255,255,0.7); transform: scale(1.12);\n}\n\n\/* \u2500\u2500 Custom panel (cp-*) legacy classes \u2014 restyled \u2500\u2500 *\/\n.cp-row { display: flex; align-items: center; gap: 8px; min-height: 32px; }\n.cp-label {\n  font-size: 12px; color: var(--tpg-text2);\n  min-width: 88px; white-space: nowrap; flex-shrink: 0;\n}\n.cp-input {\n  flex: 1;\n  background: var(--rp-input-bg); color: var(--tpg-text);\n  border: 1.5px solid var(--rp-input-border);\n  border-radius: var(--rp-input-radius);\n  padding: 0 10px; height: 32px;\n  font-size: 12.5px; outline: none; font-family: inherit;\n  transition: border-color 0.15s, box-shadow 0.15s;\n  -moz-appearance: textfield;\n}\n.cp-input:focus { border-color: var(--rp-input-border-focus); box-shadow: 0 0 0 3px rgba(10,132,255,0.18); }\n.cp-input::-webkit-outer-spin-button,\n.cp-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n.cp-input-sm { flex: none; width: 62px; text-align: center; }\n.cp-input-name { width: 100%; }\n.cp-x { font-size: 11px; color: var(--tpg-text3); flex-shrink: 0; }\n.cp-select {\n  background: var(--rp-input-bg); color: var(--tpg-text);\n  border: 1.5px solid var(--rp-input-border);\n  border-radius: var(--rp-input-radius);\n  padding: 0 8px; height: 32px; font-size: 12px;\n  outline: none; font-family: inherit; -webkit-appearance: none;\n  min-width: 52px; cursor: pointer;\n  transition: border-color 0.15s;\n}\n.cp-select:focus { border-color: var(--rp-input-border-focus); }\n.cp-color {\n  -webkit-appearance: none; appearance: none;\n  width: 32px; height: 28px; border-radius: 8px;\n  border: 1.5px solid rgba(255,255,255,0.13); cursor: pointer;\n  background: none; padding: 0; flex-shrink: 0;\n}\n.cp-color::-webkit-color-swatch-wrapper { padding: 0; }\n.cp-color::-webkit-color-swatch { border: none; border-radius: 6px; }\n.cp-slider {\n  -webkit-appearance: none; appearance: none;\n  flex: 1; height: 4px;\n  background: rgba(255,255,255,0.12); border-radius: 4px;\n  outline: none; min-width: 60px; cursor: pointer;\n}\n.cp-slider::-webkit-slider-thumb {\n  -webkit-appearance: none; width: 18px; height: 18px;\n  background: var(--tpg-accent); border-radius: 50%;\n  box-shadow: 0 0 0 2px rgba(10,132,255,0.25), 0 2px 6px rgba(0,0,0,0.45);\n  cursor: pointer; transition: transform 0.1s;\n}\n.cp-slider::-webkit-slider-thumb:hover { transform: scale(1.12); }\n.cp-slider-val { font-size: 11px; color: var(--tpg-text3); min-width: 28px; text-align: right; flex-shrink: 0; }\n.cp-toggle {\n  position: relative; width: 42px; height: 24px;\n  background: rgba(255,255,255,0.14); border-radius: 12px;\n  cursor: pointer; transition: background 0.22s; flex-shrink: 0; border: none;\n}\n.cp-toggle.on { background: #0a84ff; }\n.cp-toggle::after {\n  content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px;\n  background: #fff; border-radius: 50%;\n  box-shadow: 0 1px 5px rgba(0,0,0,0.45);\n  transition: transform 0.22s cubic-bezier(0.34,1.56,0.64,1);\n}\n.cp-toggle.on::after { transform: translateX(18px); }\n.cp-refresh {\n  background: rgba(255,255,255,0.06); color: var(--tpg-text2);\n  border: 1.5px solid rgba(255,255,255,0.1);\n  width: 30px; height: 30px; border-radius: 8px;\n  cursor: pointer; font-size: 14px;\n  display: flex; align-items: center; justify-content: center;\n  transition: background 0.12s; flex-shrink: 0;\n}\n.cp-refresh:hover { background: rgba(255,255,255,0.13); }\n.cp-btn-save {\n  background: var(--tpg-accent); color: #fff; border: none;\n  height: 36px; border-radius: var(--rp-input-radius); cursor: pointer;\n  font-size: 13px; font-weight: 600;\n  display: flex; align-items: center; justify-content: center; gap: 7px;\n  width: 100%; font-family: inherit;\n  transition: background 0.12s;\n  box-shadow: 0 4px 16px rgba(10,132,255,0.3);\n}\n.cp-btn-save:hover { background: var(--tpg-accent2); }\n.cp-btn-file {\n  background: rgba(255,255,255,0.03); color: var(--tpg-text2);\n  border: 1.5px dashed rgba(255,255,255,0.16);\n  height: 34px; border-radius: var(--rp-input-radius); cursor: pointer;\n  font-size: 12px; transition: all 0.12s;\n  flex: 1; display: flex; align-items: center; justify-content: center;\n  font-family: inherit;\n}\n.cp-btn-file:hover { border-color: var(--tpg-accent); background: rgba(10,132,255,0.07); color: var(--tpg-accent); }\n\n\/* \u2500\u2500 Download resolution \u2500\u2500 *\/\n.dl-res-group {\n  display: flex; align-items: center; gap: 4px;\n  background: var(--rp-input-bg);\n  border: 1.5px solid var(--rp-input-border);\n  border-radius: var(--rp-input-radius);\n  padding: 0 8px; height: 32px;\n}\n.dl-res-group input {\n  background: transparent; color: var(--tpg-text); border: none;\n  font-family: -apple-system, monospace; font-size: 12px;\n  text-align: center; width: 46px; outline: none;\n  -moz-appearance: textfield;\n}\n.dl-res-group input::-webkit-outer-spin-button,\n.dl-res-group input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n\n\/* \u2500\u2500 Logo badge in titlebar \u2500\u2500 *\/\n#logo-badge {\n  display: flex; align-items: center; gap: 5px;\n  background: rgba(255,255,255,0.06);\n  border: 1px solid rgba(255,255,255,0.1);\n  border-radius: 7px; padding: 3px 8px;\n  font-size: 11px; color: var(--tpg-text2);\n}\n#logo-badge img { width: 16px; height: 16px; object-fit: contain; border-radius: 3px; }\n\n\/* \u2500\u2500 Apple-style Toggle (kept for backward compat) \u2500\u2500 *\/\n.tpg-toggle {\n  position: relative; width: 42px; height: 24px;\n  background: rgba(255,255,255,0.14); border-radius: 12px;\n  cursor: pointer; transition: background 0.22s; flex-shrink: 0;\n  border: none; outline: none;\n}\n.tpg-toggle.on { background: #0a84ff; }\n.tpg-toggle::after {\n  content: ''; position: absolute;\n  top: 3px; left: 3px; width: 18px; height: 18px;\n  background: #fff; border-radius: 50%;\n  box-shadow: 0 1px 5px rgba(0,0,0,0.4);\n  transition: transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n.tpg-toggle.on::after { transform: translateX(18px); }\n\n\/* \u2500\u2500 Apple-style Button (kept for backward compat) \u2500\u2500 *\/\n.tpg-action-btn {\n  display: flex; align-items: center; justify-content: center;\n  gap: 7px;\n  background: rgba(255,255,255,0.07); color: var(--tpg-text);\n  border: 1.5px solid rgba(255,255,255,0.12);\n  padding: 0 14px; height: 34px; border-radius: var(--rp-input-radius);\n  cursor: pointer; font-size: 12.5px; font-weight: 500;\n  transition: background 0.12s; white-space: nowrap; font-family: inherit;\n}\n.tpg-action-btn:hover { background: rgba(255,255,255,0.12); }\n.tpg-action-btn.primary {\n  background: var(--tpg-accent); color: #fff;\n  border-color: var(--tpg-accent); font-weight: 600;\n  box-shadow: 0 4px 16px rgba(10,132,255,0.3);\n}\n.tpg-action-btn.primary:hover { background: var(--tpg-accent2); border-color: var(--tpg-accent2); }\n.tpg-action-btn.danger {\n  color: #ff453a; border-color: rgba(255,69,58,0.28);\n  background: rgba(255,69,58,0.07);\n}\n.tpg-action-btn.danger:hover { background: rgba(255,69,58,0.16); }\n.tpg-action-btn.full { width: 100%; }\n\n\/* \u2500\u2500 Generic select wrapper (kept for backward compat) \u2500\u2500 *\/\n.tpg-select-wrap { position: relative; flex: 1; }\n.tpg-select-wrap::after {\n  content: '';\n  position: absolute; right: 10px; top: 50%;\n  transform: translateY(-60%) rotate(45deg);\n  width: 5px; height: 5px;\n  border-right: 1.5px solid var(--tpg-text3);\n  border-bottom: 1.5px solid var(--tpg-text3);\n  pointer-events: none;\n}\n.tpg-select {\n  width: 100%;\n  background: var(--rp-input-bg); color: var(--tpg-text);\n  border: 1.5px solid var(--rp-input-border);\n  border-radius: var(--rp-input-radius);\n  padding: 0 30px 0 10px; height: 32px;\n  font-size: 12.5px; outline: none; -webkit-appearance: none;\n  cursor: pointer; font-family: inherit;\n  transition: border-color 0.15s, box-shadow 0.15s;\n}\n.tpg-select:focus {\n  border-color: var(--rp-input-border-focus);\n  box-shadow: 0 0 0 3px rgba(10,132,255,0.18);\n}\n\n\/* \u2500\u2500 Swatch row (backward compat) \u2500\u2500 *\/\n.tpg-swatch-row { display: flex; gap: 5px; flex-wrap: wrap; }\n.tpg-swatch {\n  width: 26px; height: 26px; border-radius: 7px;\n  border: 1.5px solid rgba(255,255,255,0.1); cursor: pointer;\n  transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;\n}\n.tpg-swatch:hover { transform: scale(1.12); border-color: rgba(255,255,255,0.5); }\n.tpg-swatch.active {\n  border-color: #fff; transform: scale(1.15);\n  box-shadow: 0 0 0 2.5px rgba(255,255,255,0.25);\n}\n\n\/* \u2500\u2500 Color input (backward compat) \u2500\u2500 *\/\n.tpg-color {\n  -webkit-appearance: none; appearance: none;\n  width: 32px; height: 28px; border-radius: 8px;\n  border: 1.5px solid rgba(255,255,255,0.14); cursor: pointer;\n  background: none; padding: 0; flex-shrink: 0;\n  transition: border-color 0.12s;\n}\n.tpg-color:hover { border-color: rgba(255,255,255,0.4); }\n.tpg-color::-webkit-color-swatch-wrapper { padding: 0; }\n.tpg-color::-webkit-color-swatch { border: none; border-radius: 6px; }\n\n\/* \u2500\u2500 Slider (backward compat) \u2500\u2500 *\/\n.tpg-slider {\n  -webkit-appearance: none; appearance: none;\n  flex: 1; height: 4px; border-radius: 4px;\n  background: rgba(255,255,255,0.12); outline: none; border: none;\n  cursor: pointer; min-width: 60px;\n}\n.tpg-slider::-webkit-slider-thumb {\n  -webkit-appearance: none; width: 18px; height: 18px;\n  background: var(--tpg-accent); border-radius: 50%; cursor: pointer;\n  box-shadow: 0 0 0 2px rgba(10,132,255,0.25), 0 2px 6px rgba(0,0,0,0.45);\n  transition: transform 0.1s;\n}\n.tpg-slider::-webkit-slider-thumb:hover { transform: scale(1.12); }\n.tpg-slider-val {\n  font-size: 11px; color: var(--tpg-text3);\n  font-variant-numeric: tabular-nums;\n  min-width: 32px; text-align: right; flex-shrink: 0; padding-left: 4px;\n}\n\n\/* \u2500\u2500 Logo position grid (backward compat) \u2500\u2500 *\/\n.logo-pos-grid {\n  display: grid; grid-template-columns: repeat(3, 1fr);\n  gap: 5px; width: 120px; margin: 0 auto;\n  background: rgba(0,0,0,0.2); border-radius: 12px; padding: 6px;\n}\n.logo-pos-btn {\n  width: 24px; height: 24px; border-radius: 5px;\n  border: 1px solid rgba(255,255,255,0.12);\n  background: rgba(255,255,255,0.04);\n  color: rgba(245,245,247,0.25);\n  cursor: pointer; font-size: 8px;\n  display: flex; align-items: center; justify-content: center;\n  transition: all 0.12s;\n}\n.logo-pos-btn:hover { border-color: var(--tpg-accent); background: rgba(10,132,255,0.1); color: var(--tpg-accent); }\n.logo-pos-btn.active { background: var(--tpg-accent); color: #fff; border-color: var(--tpg-accent); }\n\n\/* Download section *\/\n.tpg-dl-row {\n  padding: 12px 14px;\n  border-top: 1px solid var(--tpg-border);\n  display: flex; flex-direction: column; gap: 8px;\n}\n\n\/* \u2500\u2500 Fullscreen overrides \u2500\u2500 *\/\n#app.fullscreen #header,\n#app.fullscreen #tpg-left,\n#app.fullscreen #tpg-right { display: none !important; }\n#app.fullscreen #tpg-body { height: 100%; }\n#app.fullscreen #tpg-center { flex: 1; }\n#app.fullscreen #canvas-wrap { cursor: none; }\n#app.fullscreen #canvas-wrap:hover { cursor: default; }\n\n\/* \u2500\u2500 Fullscreen hint \u2500\u2500 *\/\n#fs-hint {\n  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);\n  background: rgba(22,22,24,0.92); backdrop-filter: blur(14px);\n  -webkit-backdrop-filter: blur(14px); color: var(--tpg-text2);\n  padding: 7px 18px; border-radius: 20px; font-size: 11.5px;\n  z-index: 200; border: 1px solid var(--tpg-border-s);\n  pointer-events: none; opacity: 0; transition: opacity 0.35s;\n}\n#fs-hint.show { opacity: 1; }\n\n\/* Hidden file inputs *\/\n#logoFileInput, #customLogoInput { display: none; }\n\n\/* \u2500\u2500 Responsive \u2500\u2500 *\/\n@media (max-width: 960px) {\n  :root { --tpg-col-l-w: 170px; --tpg-col-r-w: 230px; }\n}\n@media (max-width: 700px) {\n  #tpg-left { display: none; }\n  :root { --tpg-col-r-w: 200px; }\n}\n@media (max-width: 540px) {\n  #tpg-right { display: none; }\n}\n\n\/* ============================================================\n   LAUNCH BUTTON \u2014 shown in the inline section\n   ============================================================ *\/\n#tpg-launch-area {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 2.5rem 1rem 3rem;\n  background: linear-gradient(180deg, #060d1e 0%, #050c1a 100%);\n}\n#tpg-launch-btn {\n  display: inline-flex;\n  align-items: center;\n  gap: 10px;\n  padding: 14px 28px;\n  background: var(--tpg-accent, #0a84ff);\n  color: #fff;\n  border: none;\n  border-radius: 14px;\n  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;\n  font-size: 15px;\n  font-weight: 600;\n  cursor: pointer;\n  letter-spacing: -0.1px;\n  box-shadow: 0 8px 32px rgba(10, 132, 255, 0.38), 0 2px 8px rgba(0,0,0,0.4);\n  transition: background 0.15s, transform 0.15s, box-shadow 0.15s;\n  -webkit-font-smoothing: antialiased;\n}\n#tpg-launch-btn:hover {\n  background: var(--tpg-accent2, #409cff);\n  transform: translateY(-1px);\n  box-shadow: 0 12px 40px rgba(10, 132, 255, 0.45), 0 2px 8px rgba(0,0,0,0.4);\n}\n#tpg-launch-btn:active {\n  transform: translateY(0);\n  box-shadow: 0 4px 16px rgba(10, 132, 255, 0.3);\n}\n#tpg-launch-btn svg { flex-shrink: 0; }\n\n\/* ============================================================\n   MODAL OVERLAY\n   ============================================================ *\/\n#tpg-modal-overlay {\n  position: fixed;\n  inset: 0;\n  z-index: 9000;\n  \/* Backdrop *\/\n  background: rgba(0, 0, 0, 0.72);\n  backdrop-filter: blur(6px);\n  -webkit-backdrop-filter: blur(6px);\n  \/* Hidden by default *\/\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 0.32s ease;\n}\n#tpg-modal-overlay.tpg-modal-open {\n  opacity: 1;\n  pointer-events: all;\n}\n\n\/* \u2500\u2500 Modal container \u2500\u2500 *\/\n#tpg-modal {\n  position: fixed;\n  inset: 0;\n  z-index: 9010;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  pointer-events: none;\n}\n#tpg-modal-inner {\n  width: 100vw;\n  height: 100vh;\n  background: #111113;\n  border-radius: 0;\n  box-shadow:\n    0 0 0 1px rgba(255,255,255,0.07),\n    0 40px 120px rgba(0,0,0,0.7),\n    0 0 80px rgba(10,132,255,0.06);\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  \/* Entrance: starts scaled down + transparent *\/\n  opacity: 0;\n  transform: scale(0.96);\n  transition:\n    opacity 0.32s cubic-bezier(0.4, 0, 0.2, 1),\n    transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);\n  pointer-events: none;\n}\n#tpg-modal.tpg-modal-open {\n  pointer-events: all;\n}\n#tpg-modal.tpg-modal-open #tpg-modal-inner {\n  opacity: 1;\n  transform: scale(1);\n  pointer-events: all;\n}\n\/* Exit animation class *\/\n#tpg-modal-inner.tpg-modal-closing {\n  opacity: 0 !important;\n  transform: scale(0.96) !important;\n}\n\n\/* \u2500\u2500 Close button in modal titlebar \u2500\u2500 *\/\n#tpg-modal-close {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 28px;\n  height: 28px;\n  border-radius: 50%;\n  background: rgba(255,255,255,0.08);\n  border: 1px solid rgba(255,255,255,0.12);\n  color: rgba(245,245,247,0.7);\n  cursor: pointer;\n  transition: background 0.12s, color 0.12s;\n  flex-shrink: 0;\n}\n#tpg-modal-close:hover {\n  background: rgba(255,59,48,0.2);\n  border-color: rgba(255,59,48,0.4);\n  color: #ff453a;\n}\n\n\/* \u2500\u2500 ESC hint inside modal \u2500\u2500 *\/\n#tpg-esc-hint {\n  position: absolute;\n  bottom: 18px;\n  left: 50%;\n  transform: translateX(-50%);\n  display: flex;\n  align-items: center;\n  gap: 7px;\n  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;\n  font-size: 11px;\n  color: rgba(245,245,247,0.28);\n  pointer-events: none;\n  z-index: 10;\n  white-space: nowrap;\n  transition: opacity 0.5s;\n}\n#tpg-esc-hint kbd {\n  display: inline-flex;\n  align-items: center;\n  font-family: -apple-system, monospace;\n  font-size: 9.5px;\n  background: rgba(255,255,255,0.07);\n  border: 1px solid rgba(255,255,255,0.14);\n  border-radius: 4px;\n  padding: 2px 6px;\n  color: rgba(245,245,247,0.4);\n}\n#tpg-esc-hint.hidden { opacity: 0; }\n\n<\/style>\n<div class=\"tpg-wrap\">\n\n<!-- SEO Hero Section -->\n<section style=\"background:radial-gradient(ellipse 80% 60% at 50% 0%, #0e2a5c 0%, #060d1e 55%, #050c1a 100%); padding:120px 2rem 3rem; text-align:center;\">\n    <div style=\"max-width:800px; margin:0 auto;\">\n        <div class=\"badge\" style=\"display:inline-flex;align-items:center;gap:.5rem;margin-bottom:1.5rem;\">\n            <span class=\"pulse-dot\"><\/span><span>Kostenloses Online-Tool<\/span>\n        <\/div>\n        <h1 style=\"font-size:2.8rem; font-family:var(--font-heading); font-weight:700; color:#fff; line-height:1.2; margin-bottom:1rem; user-select:none; -webkit-user-select:none;\">\n            Monitor Test Pattern<br>\n            <span style=\"display:inline-block; background:linear-gradient(135deg,#60a5fa 0%,#0ea5e9 50%,#0284c7 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; color:transparent; user-select:none; -webkit-user-select:none;\">\n                Generator            <\/span>\n        <\/h1>\n        <p style=\"color:rgba(255,255,255,0.55); font-size:1.05rem; line-height:1.8; margin-bottom:1.5rem;\">\n            Nutzen Sie diesen kostenlosen <strong>Monitor-Testmuster-Generator<\/strong>, um die Farbgenauigkeit, Gamma-Antwort und Pixelintegrit\u00e4t Ihres Displays zu bewerten. Zudem generiert er SMPTE-Farbblaken, Graustufenrampen, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Checkerboard_pattern\" target=\"_blank\" rel=\"noopener\" style=\"color:#60a5fa;text-decoration:underline\">Schachbrettmuster<\/a>, Dead-Pixel-Detektoren und benutzerdefinierte Testbilder direkt in Ihrem Browser.        <\/p>\n        <p style=\"color:rgba(255,255,255,0.4); font-size:0.95rem; line-height:1.75;\">\n            Ob Sie sich auf eine <a href=\"https:\/\/perfectchroma.com\/de\/monitor-calibration-user-guide\/\" style=\"color:#60a5fa;text-decoration:underline\">vollst\u00e4ndige Monitorkalibrierung<\/a> vorbereiten oder Ihr Display nach einem Treiberupdate schnell \u00fcberpr\u00fcfen \u2013 dieser <strong>Monitor-Testmuster-Generator<\/strong> hilft Ihnen, Probleme zu erkennen, bevor sie Ihre Arbeit beeintr\u00e4chtigen. Alle Muster k\u00f6nnen als hochaufl\u00f6sende Bilder f\u00fcr die Offline-Nutzung heruntergeladen werden.        <\/p>\n    <\/div>\n<\/section>\n\n<!-- \u2500\u2500 Launch button \u2014 shown in page, triggers modal \u2500\u2500 -->\n<div id=\"tpg-launch-area\">\n  <button id=\"tpg-launch-btn\" onclick=\"tpgOpenModal()\">\n    <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"3\" width=\"20\" height=\"14\" rx=\"2\"\/><line x1=\"8\" y1=\"21\" x2=\"16\" y2=\"21\"\/><line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"21\"\/><\/svg>\n    Open Pattern Generator\n  <\/button>\n<\/div>\n\n<!-- \u2500\u2500 Modal backdrop \u2500\u2500 -->\n<div id=\"tpg-modal-overlay\" onclick=\"tpgCloseModal()\"><\/div>\n\n<!-- \u2500\u2500 Modal container \u2500\u2500 -->\n<div id=\"tpg-modal\">\n  <div id=\"tpg-modal-inner\">\n\n<!-- ====================== APP SHELL ====================== -->\n<div id=\"tpg-focus-wrap\">\n<div id=\"app\">\n\n  <!-- \u2500\u2500 Header \/ Toolbar \u2500\u2500 -->\n  <div id=\"header\">\n    <div class=\"header-left\">\n      <h1 style=\"color:#fff !important;\">\n        <span class=\"tpg-title-icon\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#0a84ff\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block !important;\"><rect x=\"2\" y=\"3\" width=\"20\" height=\"14\" rx=\"2\"\/><line x1=\"8\" y1=\"21\" x2=\"16\" y2=\"21\"\/><line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"21\"\/><\/svg>\n        <\/span>\n        TestPatternLib\n      <\/h1>\n    <\/div>\n\n    <div class=\"header-center\">\n      <!-- Zoom Control Group -->\n      <div class=\"tpg-zoom-control\">\n        <button class=\"tpg-toolbar-btn\" onclick=\"zoomOut()\" title=\"Zoom Out\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block !important;\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"\/><line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\"\/><\/svg>\n        <\/button>\n        <div id=\"zoom-label\">100%<\/div>\n        <button class=\"tpg-toolbar-btn\" onclick=\"zoomIn()\" title=\"Zoom In\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block !important;\"><circle cx=\"11\" cy=\"11\" r=\"8\"\/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"\/><line x1=\"11\" y1=\"8\" x2=\"11\" y2=\"14\"\/><line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\"\/><\/svg>\n        <\/button>\n      <\/div>\n\n      <div class=\"tpg-v-sep\"><\/div>\n\n      <!-- Maximize -->\n      <button class=\"tpg-toolbar-btn\" onclick=\"toggleFullscreen()\" title=\"Fullscreen (F)\">\n        <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block !important;\"><polyline points=\"15 3 21 3 21 9\"\/><polyline points=\"9 21 3 21 3 15\"\/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"\/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"\/><\/svg>\n      <\/button>\n    <\/div>\n\n    <div class=\"header-right\">\n      <!-- Keyboard -->\n      <button class=\"tpg-toolbar-btn\" onclick=\"showShortcuts()\" title=\"Keyboard Shortcuts\">\n        <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block !important;\"><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\"\/><path d=\"M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M8 16h8\"\/><\/svg>\n      <\/button>\n\n      <div class=\"tpg-v-sep\"><\/div>\n\n      <button id=\"tpg-modal-close\" class=\"tpg-btn-close\" onclick=\"tpgCloseModal()\">\n        <span>Close<\/span>\n        <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:block !important;\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"\/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"\/><\/svg>\n      <\/button>\n    <\/div>\n  <\/div>\n\n  <!-- Shortcuts Modal Overlay -->\n  <div id=\"shortcuts-overlay\" onclick=\"tpgCloseShortcuts()\">\n    <div class=\"shortcuts-modal\" onclick=\"event.stopPropagation()\">\n      <div class=\"shortcuts-modal-header\">\n        <h2>\n          <!-- Lucide Keyboard icon -->\n          <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\"\/><path d=\"M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M8 16h8\"\/><\/svg>\n          Keyboard Shortcuts\n        <\/h2>\n        <button class=\"shortcuts-modal-close\" onclick=\"tpgCloseShortcuts()\" title=\"Close\">\n          <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"\/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"\/><\/svg>\n        <\/button>\n      <\/div>\n      <div class=\"shortcuts-modal-body\">\n        <div class=\"shortcut-section-label\">Navigation<\/div>\n        <div class=\"shortcut-row\"><span>Next Pattern<\/span><span class=\"shortcut-keys\"><span class=\"shortcut-key\">\u2192<\/span><\/span><\/div>\n        <div class=\"shortcut-row\"><span>Previous Pattern<\/span><span class=\"shortcut-keys\"><span class=\"shortcut-key\">\u2190<\/span><\/span><\/div>\n        <div class=\"shortcut-row\"><span>Select Pattern 1\u201312<\/span><span class=\"shortcut-keys\"><span class=\"shortcut-key\">1\u20139<\/span><span class=\"shortcut-key\">0<\/span><span class=\"shortcut-key\">-<\/span><span class=\"shortcut-key\">=<\/span><\/span><\/div>\n        <hr class=\"shortcut-hr\">\n        <div class=\"shortcut-section-label\">View<\/div>\n        <div class=\"shortcut-row\"><span>Toggle Fullscreen<\/span><span class=\"shortcut-keys\"><span class=\"shortcut-key\">F<\/span><\/span><\/div>\n        <div class=\"shortcut-row\"><span>Pause \/ Play Animation<\/span><span class=\"shortcut-keys\"><span class=\"shortcut-key\">Space<\/span><\/span><\/div>\n        <hr class=\"shortcut-hr\">\n        <div class=\"shortcut-section-label\">General<\/div>\n        <div class=\"shortcut-row\"><span>Close Tool \/ Exit Fullscreen<\/span><span class=\"shortcut-keys\"><span class=\"shortcut-key\">ESC<\/span><\/span><\/div>\n        <button class=\"shortcut-dismiss\" onclick=\"tpgCloseShortcuts()\">Dismiss<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- \u2500\u2500 3-Column Body \u2500\u2500 -->\n  <div id=\"tpg-body\">\n\n    <!-- LEFT: Pattern list -->\n    <div id=\"tpg-left\">\n      <div id=\"tpg-left-header\">Patterns<\/div>\n      <div id=\"sidebar\" class=\"tpg-sidebar\"><\/div>\n    <\/div>\n\n    <!-- CENTER: Preview canvas -->\n    <div id=\"tpg-center\">\n      <div id=\"canvas-wrap\">\n        <div id=\"canvas-pan\">\n          <canvas id=\"patternCanvas\"><\/canvas>\n        <\/div>\n        <!-- Custom scrollbars -->\n        <div id=\"tpg-scroll-x\"><div id=\"tpg-thumb-x\"><\/div><\/div>\n        <div id=\"tpg-scroll-y\"><div id=\"tpg-thumb-y\"><\/div><\/div>\n      <\/div>\n    <\/div>\n\n    <!-- RIGHT: Controls panel -->\n    <div id=\"tpg-right\">\n      <div id=\"tpg-right-header\">Settings<\/div>\n      <div id=\"tpg-right-scroll\">\n        <!-- Pattern-specific controls rendered here by buildRightPanel() -->\n        <div id=\"rpanel-controls\"><\/div>\n        <!-- Pattern description -->\n        <div id=\"pattern-info\" style=\"color:rgba(255,255,255,0.4); font-size:12px; line-height:1.5; padding:12px 16px 20px;\"><\/div>\n              <\/div>\n    <\/div>\n\n  <\/div><!-- #tpg-body -->\n\n  <!-- Hidden legacy containers (JS still references these) -->\n  <div id=\"main\" style=\"display:none\">\n    <div id=\"sidebar-legacy\"><\/div>\n    <div id=\"content\" style=\"display:none\">\n      <div id=\"controls\"><div id=\"controls-inner\"><\/div><\/div>\n      <div id=\"custom-panel\"><\/div>\n    <\/div>\n  <\/div>\n\n<\/div><!-- #app -->\n<\/div><!-- #tpg-focus-wrap -->\n\n    <!-- ESC hint \u2014 subtle, auto-hides after a few seconds -->\n    <div id=\"tpg-esc-hint\">\n      <kbd>ESC<\/kbd>\n      <span>to close<\/span>\n      &nbsp;\u00b7&nbsp;\n      <kbd>F<\/kbd>\n      <span>fullscreen<\/span>\n    <\/div>\n\n  <\/div><!-- #tpg-modal-inner -->\n<\/div><!-- #tpg-modal -->\n\n<div id=\"fs-hint\">Press <b>ESC<\/b> or <b>F<\/b> to exit fullscreen &nbsp;|&nbsp; <b>\u2190\u2192<\/b> change pattern &nbsp;|&nbsp; <b>Space<\/b> pause<\/div>\n\n<!-- SEO Closing Section -->\n<section style=\"background:linear-gradient(180deg, #060d1e 0%, #050c1a 100%); padding:4rem 2rem;\">\n    <div style=\"max-width:800px; margin:0 auto; text-align:center;\">\n        <h2 style=\"font-size:1.8rem; font-family:var(--font-heading); color:#fff; margin-bottom:1.25rem;\">Was dieser Monitor-Testmuster-Generator enth\u00e4lt<\/h2>\n        <p style=\"color:rgba(255,255,255,0.5); font-size:1rem; line-height:1.8; margin-bottom:1.5rem;\">\n            Dieser <strong>Monitor-Testmuster-Generator<\/strong> bietet 13 professionelle Muster: SMPTE Color Bars, Graustufenrampe, RGB-Gradient, Vollfarben, Gitterausrichtung, Schachbrett, Sch\u00e4rfefokus, Dead-Pixel-Detektor, Antwortzeit, Betrachtungswinkel, Einbrenntest, Kontrastverh\u00e4ltnis und einen vollst\u00e4ndig benutzerdefinierten Generator. So k\u00f6nnen Sie jeden Aspekt Ihres Displays mit einem einzigen Tool testen.        <\/p>\n        <p style=\"color:rgba(255,255,255,0.4); font-size:0.95rem; line-height:1.8; margin-bottom:2rem;\">\n            Zus\u00e4tzlich erlaubt der benutzerdefinierte Generator Multi-Display-Setups mit konfigurierbaren \u00dcberlappungsbereichen, Gitterlinien und Logo-Wasserzeichen. Er dient als perfekte Erg\u00e4nzung zu PerfectChromas <a href=\"https:\/\/perfectchroma.com\/de\/monitor-calibration-presets\/\" style=\"color:#60a5fa;text-decoration:underline\">Smart Calibration Presets<\/a>. Bereit f\u00fcr die vollst\u00e4ndige Hardwarekalibrierung? <a href=\"https:\/\/perfectchroma.com\/de\/preise\/\" style=\"color:#60a5fa;text-decoration:underline\">Unsere Preispl\u00e4ne ansehen<\/a>.        <\/p>\n    <\/div>\n<\/section>\n<script>\n\/\/ ===== TESTPATTERNLIB ICONS =====\nconst ICONS = {\n  qc: `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"\/><path d=\"M3 9h18M3 15h18M9 3v18M15 3v18\"\/><\/svg>`,\n  ramp: `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\"\/><path d=\"M7 6v12M12 6v12M17 6v12\"\/><\/svg>`,\n  circle: `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"\/><circle cx=\"12\" cy=\"12\" r=\"3\"\/><\/svg>`,\n  image: `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"\/><circle cx=\"8\" cy=\"10\" r=\"2\"\/><path d=\"m21 15-5-5L5 19\"\/><\/svg>`,\n  _default: `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" rx=\"2\"\/><\/svg>`\n};\n\n\/\/ ===== PATTERN DEFINITIONS FROM TestPatternLib::PatternCreator =====\nconst TPL_ASSET_URL = \"https:\/\/perfectchroma.com\/wp-content\/themes\/hello-elementor-child\/assets\/images\/testpatternlib\";\nconst patterns = [\n  { id:'TG18-QC', name:'TG18-QC', section:'aapm', icon:'qc', desc:'AAPM TG18 quality control pattern.' },\n  { id:'TG18-OIQ', name:'TG18-OIQ', section:'aapm', icon:'qc', desc:'AAPM TG18 overall image quality pattern.' },\n  { id:'SMPTE', name:'SMPTE', section:'aapm', icon:'qc', desc:'SMPTE medical display test pattern.' },\n  { id:'TG18-AD', name:'TG18-AD', section:'aapm', icon:'qc', desc:'TG18 angular response pattern.' },\n  { id:'TG18-AFC', name:'TG18-AFC', section:'aapm', icon:'qc', desc:'TG18 anatomic feature contrast pattern.' },\n  { id:'TG18-BR', name:'TG18-BR', section:'aapm', icon:'qc', desc:'TG18 Briggs pattern.' },\n  { id:'TG18-CT', name:'TG18-CT', section:'aapm', icon:'qc', desc:'TG18 contrast threshold pattern.' },\n  { id:'TG18-CX', name:'TG18-CX', section:'aapm', icon:'qc', desc:'TG18 contrast-detail \/ resolution pattern.' },\n  { id:'TG18-MP', name:'TG18-MP', section:'aapm', icon:'qc', desc:'TG18 multi-purpose pattern.' },\n  { id:'TG18-PDC', name:'TG18-PDC', section:'aapm', icon:'qc', desc:'TG18 pixel defect \/ display check pattern.' },\n  { id:'TG18-PX', name:'TG18-PX', section:'aapm', icon:'qc', desc:'TG18 pixel pattern.' },\n\n  ...Array.from({length:18}, (_,i)=>({ id:`TG18-LN8-${String(i+1).padStart(2,'0')}`, name:`TG18-LN8-${String(i+1).padStart(2,'0')}`, section:'luminance', icon:'ramp', desc:'TG18 luminance response square on gray background.' })),\n  ...['TG18-UN10','TG18-UN80','TG18-UNL10','TG18-UNL50','TG18-UNL80'].map(id=>({ id, name:id, section:'luminance', icon:'qc', desc:'TG18 uniformity pattern.' })),\n  ...['TG18-GV','TG18-GVN','TG18-GQ','TG18-GQN','TG18-GQB'].map(id=>({ id, name:id, section:'luminance', icon:'circle', desc:'TG18 grayscale visibility circle pattern.' })),\n  ...['TG18-GA03','TG18-GA05','TG18-GA08','TG18-GA10','TG18-GA15','TG18-GA20','TG18-GA25','TG18-GA30'].map(id=>({ id, name:id, section:'luminance', icon:'circle', desc:'TG18 grayscale visibility pattern with variable center radius.' })),\n  ...['TG18-RH10','TG18-RH50','TG18-RH89','TG18-RV10','TG18-RV50','TG18-RV89'].map(id=>({ id, name:id, section:'resolution', icon:'qc', desc:'TG18 line-pair resolution pattern.' })),\n  ...['TG18-LPH10','TG18-LPH50','TG18-LPH89','TG18-LPV10','TG18-LPV50','TG18-LPV89'].map(id=>({ id, name:id, section:'resolution', icon:'qc', desc:'TG18 line-pair pattern.' })),\n  ...['TG18-NS10','TG18-NS50','TG18-NS89'].map(id=>({ id, name:id, section:'resolution', icon:'qc', desc:'TG18 noise \/ resolution pattern.' })),\n\n  { id:'8bit_Grayramp', name:'8bit Grayramp', section:'utility', icon:'ramp', desc:'0-255 horizontal grayscale ramp.' },\n  { id:'GRAYSCALE', name:'GRAYSCALE', section:'utility', icon:'ramp', desc:'Grayscale bars and measurement strips.' },\n  { id:'RESOLUTION', name:'RESOLUTION', section:'utility', icon:'qc', desc:'Resolution grid pattern.' },\n  { id:'IEC_DIN6868_57', name:'IEC DIN6868-57', section:'iec', icon:'qc', desc:'IEC\/DIN display test pattern.' },\n  { id:'IEC-ANG', name:'IEC-ANG', section:'iec', icon:'circle', desc:'IEC angle circle pattern.' },\n  { id:'IEC-GD', name:'IEC-GD \/ TG18-GD', section:'iec', icon:'qc', desc:'IEC geometric distortion pattern.' },\n  { id:'TG270-sQC', name:'TG270-sQC', section:'tg270', icon:'qc', desc:'TG270 small QC pattern.' },\n  ...[0,16,32,64,96,128,160,192,224,255].map(v=>({ id:`Solid_${v}`, name:`Solid ${v}`, section:'utility', icon:'qc', desc:`Solid grayscale field ${v}.` })),\n  { id:'QBX-3COLORS', name:'QBX-3COLORS', section:'utility', icon:'ramp', desc:'Red, green, and blue ramps.' },\n  { id:'QBX-4COLORS', name:'QBX-4COLORS', section:'utility', icon:'ramp', desc:'Red, green, blue, and grayscale ramps.' },\n  { id:'AVEC', name:'AVEC', section:'video', icon:'qc', desc:'Audio Video Equipment Check pattern.' },\n  { id:'Brightness', name:'Brightness', section:'video', icon:'ramp', desc:'BUROSCH-style brightness pattern.' },\n  { id:'Contrast', name:'Contrast', section:'video', icon:'ramp', desc:'BUROSCH-style contrast pattern.' },\n  { id:'BRIGHTNESS_BLACK_LEVEL', name:'Brightness Black Level', section:'video', icon:'ramp', desc:'Black level code value visibility pattern.' },\n  { id:'GamutStripes', name:'Gamut Stripes', section:'video', icon:'ramp', desc:'Gamut stripe gradient pattern.' },\n  { id:'FORMAT_OVERSCAN', name:'Format Overscan', section:'video', icon:'qc', desc:'Format overscan line-zone pattern.' },\n  { id:'TG18-CH', name:'TG18-CH', section:'images', icon:'image', desc:'Image-based TG18 chest pattern from PerfectLum assets.', asset:'TG18-CH.png' },\n  { id:'TG18-KN', name:'TG18-KN', section:'images', icon:'image', desc:'Image-based TG18 knee pattern from PerfectLum assets.', asset:'TG18-KN.png' },\n  { id:'TG18-MM01', name:'TG18-MM01', section:'images', icon:'image', desc:'Image-based TG18 mammography pattern from PerfectLum assets.', asset:'TG18-MM01.png' },\n  { id:'TG18-MM02', name:'TG18-MM02', section:'images', icon:'image', desc:'Image-based TG18 mammography pattern from PerfectLum assets.', asset:'TG18-MM02.png' },\n  { id:'FUJIFILM', name:'FUJIFILM', section:'images', icon:'image', desc:'Image-based pattern from TestPatternLib assets.', asset:'fuji_test.jpg' },\n  { id:'PressProof_01', name:'PressProof 01', section:'images', icon:'image', desc:'Image-based pattern from TestPatternLib assets.', asset:'PressProof_01.jpg' },\n  { id:'RealImages_01', name:'RealImages 01', section:'images', icon:'image', desc:'Image-based pattern from TestPatternLib assets.', asset:'RealImages_01.jpg' }\n];\n\nconst sectionLabels = {\n  aapm: 'AAPM TG18',\n  luminance: 'Luminance',\n  resolution: 'Resolution',\n  iec: 'IEC \/ DIN',\n  tg270: 'TG270',\n  utility: 'Utility',\n  video: 'Video \/ BUROSCH',\n  images: 'Image Based'\n};\n\/\/ ===== STATE =====\nlet currentPattern = 0;\nlet isFullscreen = false;\nlet animFrame = null;\nlet isPaused = false;\nlet sectionStates = { setup: true, grid: true, overlays: true, branding: false, export: true };\nlet sidebarState = { aapm: true, luminance: true, resolution: false, iec: false, tg270: false, utility: false, video: false, images: false };\nlet patternState = {\n  solidColor:'#ff0000', gridSize:50, checkerSize:20,\n  deadPixelInterval:1500, deadPixelColor:0, responseSpeed:3,\n  gradientDir:'horizontal', gradientChannel:'all',\n  burninPhase:0, burninInterval:2000, contrastMode:'steps',\n  \/\/ Grid Alignment expanded state\n  gridColor: '#333333', gridBg: '#000000',\n  gridCircleSize: 0.3, gridCircleColor: '#ff3333',\n  gridCrossSize: 15, gridCrossPos: 30, gridCrossColor: '#4a9eff',\n  \/\/ Checkerboard expanded state\n  checkerColor1: '#ffffff', checkerColor2: '#000000',\n  \/\/ Global Resolution \/ Aspect Ratio\n  resW: 1920, resH: 1080,\n  pieColorFieldPreset: 'blue',\n  pieColorBarsMode: 'vertical',\n  pieColorRampPreset: 'blue',\n  pieCosineScale: 1.0,\n  pieEletterScale: 1.0,\n  pieSiemensRays: 32,\n  pieSiemensColor1: '#ffffff',\n  pieSiemensColor2: '#000000',\n  pieColorStripesSteps: 15,\n  pieChvContrastScale: 1,\n  pieGammaPreset: 2.0,\n  pieGammaMixScale: 1,\n  pieSiemensSpecialPreset: '64',\n  pieSiemensSpecialRays: 64,\n  pieSiemensSpecialColor1: '#ffffff',\n  pieSiemensSpecialColor2: '#000000',\n  pieMultiColorStarPreset: '128',\n  pieMultiColorStarRays: 128,\n  pie7ColorVaDirection: 'horizontal',\n  pie7ColorVaLineSize: 1,\n  pie7ColorVaGapSize: 1,\n  pieDualZoneplateScale: 0.8,\n  pieDualZoneplateColor1: '#ff0000',\n  pieDualZoneplateColor2: '#0000ff',\n  pieZoneplateSpecialScale: 0.8,\n  pieZoneplateSpecialColor1: '#ffffff',\n  pieZoneplateSpecialColor2: '#000000',\n  pieHilbertZonesScale: 1,\n  pieHilbertZonesColor1: '#7f7f7f',\n  pieHilbertZonesColor2: '#101010',\n  pieMultiColorSqCols: 8,\n  pieMultiColorSqRows: 4,\n  pieMultiColorSqPreset: 'rgb_classic',\n  pieMultiColorSqGlowH: 1.0,\n  pieMultiColorSqGlowV: 1.0,\n  pieContrastSweepScale: 1.0,\n  pieContrastSweepColor1: '#ffffff',\n  pieContrastSweepColor2: '#000000',\n  pieLinesHvLineColor: '#000000',\n  pieLinesHvBgColor: '#ffffff',\n  pieLinesHvLineWeight: 1,\n  pieLinesHvGap: 1,\n  btpOverscanLineColor: '#000000',\n  btpOverscanBgColor: '#d0d0d0',\n  tplShowDescription: true\n};\nlet deadPixelTimer = null;\nlet burninTimer = null;\n\nconst btpColorImg = new Image();\nbtpColorImg.crossOrigin = \"Anonymous\";\nbtpColorImg.src = 'https:\/\/perfectchroma.com\/wp-content\/themes\/hello-elementor-child\/assets\/skintones.png';\n\nconst umtzFirstImg = new Image();\numtzFirstImg.crossOrigin = \"Anonymous\";\numtzFirstImg.src = 'https:\/\/perfectchroma.com\/wp-content\/themes\/hello-elementor-child\/assets\/umtz_first_women.png';\n\nlet zoomLevel = 1.0;\nlet defImg = new Image();\nlet industrialImg = new Image();\nindustrialImg.src = '\/wp-content\/themes\/hello-elementor-child\/assets\/images\/industrial_bg.png';\nlet reichstagImg = new Image();\nreichstagImg.src = '\/wp-content\/themes\/hello-elementor-child\/assets\/images\/reichstag_bg.png';\nlet landImg = new Image();\nlandImg.src = '\/wp-content\/themes\/hello-elementor-child\/assets\/images\/landimage.jpg';\n\nfunction zoomIn() {\n  if (zoomLevel < 3.0) { zoomLevel += 0.25; updateZoomLabel(); resizeCanvas(); }\n}\nfunction zoomOut() {\n  if (zoomLevel > 0.25) { zoomLevel -= 0.25; updateZoomLabel(); resizeCanvas(); }\n}\nfunction resetZoom() {\n  zoomLevel = 1.0; tpgPanX = 0; tpgPanY = 0; updateZoomLabel(); resizeCanvas();\n}\nfunction updateZoomLabel() {\n  document.getElementById('zoom-label').innerText = Math.round(zoomLevel * 100) + '%';\n}\n\n\/\/ Logo state (for preset patterns)\nlet logoState = {\n  image:null, fileName:'', position:'bottom-right',\n  size:80, opacity:0.5, enabled:true,\n  x: 0, y: 0, manual: false, isDragging: false,\n  dragOffX: 0, dragOffY: 0\n};\n\n\/\/ Custom Generator state \u2014 VIOSO-style\nlet custom = {\n  imageName: 'My Testpattern',\n  displayW: 1920,\n  displayH: 1080,\n  displaysX: 1,\n  displaysY: 1,\n  overlapX: 0,\n  overlapY: 0,\n  \/\/ computed\n  get totalW() { return this.displayW * this.displaysX - this.overlapX * Math.max(0, this.displaysX - 1); },\n  get totalH() { return this.displayH * this.displaysY - this.overlapY * Math.max(0, this.displaysY - 1); },\n  \/\/ grid\n  gridSize: 80,\n  gridWidth: 1,\n  gridColor: '#ff0000',\n  textColor: '#ffffff',\n  \/\/ toggles\n  circles: true,\n  circleColor: '#ffffff',\n  colorbar: true,\n  hatching: false,\n  invert: false,\n  overlap: false,\n  \/\/ logo\n  logoEnabled: true,\n  logoImage: null,\n  logoSize: 80,\n};\n\n\/\/ ===== CANVAS =====\nconst canvas = document.getElementById('patternCanvas');\nconst ctx = canvas.getContext('2d');\n\nfunction resizeCanvas() {\n  const wrap = document.getElementById('canvas-wrap');\n  if (!wrap) return;\n\n  const targetW = patternState.resW || 1920;\n  const targetH = patternState.resH || 1080;\n  \n  \/\/ 1. Set internal canvas resolution (1:1 with target output)\n  canvas.width = targetW;\n  canvas.height = targetH;\n\n  \/\/ 2. Calculate scale to fit container while preserving ratio\n  const containerW = wrap.clientWidth - 40; \/\/ minus padding\n  const containerH = wrap.clientHeight - 40;\n  \n  const scaleW = containerW \/ targetW;\n  const scaleH = containerH \/ targetH;\n  const fitScale = Math.min(scaleW, scaleH);\n  \n  \/\/ 3. Apply scale + zoom via transform\n  const finalScale = fitScale * zoomLevel;\n  canvas.style.transform = `scale(${finalScale})`;\n  tpgCurrentScale = finalScale;\n  tpgFitScale = fitScale;\n\n  \/\/ Clamp pan offset when zoom changes\n  tpgClampPan();\n  tpgApplyPan();\n  tpgUpdateScrollbars();\n\n  \/\/ Update cursor & scrollable class\n  const wrap2 = document.getElementById('canvas-wrap');\n  if (zoomLevel > 1.0) {\n    wrap2.classList.add('tpg-zoomable');\n  } else {\n    wrap2.classList.remove('tpg-zoomable');\n    wrap2.classList.remove('tpg-scrollable');\n  }\n\n  \/\/ Internal draws are always at full resolution\n  drawCurrent();\n}\n\/\/ ===== PAN \/ DRAG STATE =====\nlet tpgPanX = 0, tpgPanY = 0;\nlet tpgCurrentScale = 1, tpgFitScale = 1;\nlet tpgIsDragging = false;\nlet tpgDragStartX = 0, tpgDragStartY = 0;\nlet tpgPanStartX = 0, tpgPanStartY = 0;\n\nfunction tpgGetMaxPan() {\n  const wrap = document.getElementById('canvas-wrap');\n  if (!wrap) return { x: 0, y: 0 };\n  const scaledW = canvas.width * tpgCurrentScale;\n  const scaledH = canvas.height * tpgCurrentScale;\n  const maxX = Math.max(0, (scaledW - wrap.clientWidth) \/ 2);\n  const maxY = Math.max(0, (scaledH - wrap.clientHeight) \/ 2);\n  return { x: maxX, y: maxY };\n}\n\nfunction tpgClampPan() {\n  const max = tpgGetMaxPan();\n  tpgPanX = Math.max(-max.x, Math.min(max.x, tpgPanX));\n  tpgPanY = Math.max(-max.y, Math.min(max.y, tpgPanY));\n}\n\nfunction tpgApplyPan() {\n  const pan = document.getElementById('canvas-pan');\n  if (pan) pan.style.transform = `translate(${tpgPanX}px, ${tpgPanY}px)`;\n}\n\nfunction tpgUpdateScrollbars() {\n  const wrap = document.getElementById('canvas-wrap');\n  const thumbX = document.getElementById('tpg-thumb-x');\n  const thumbY = document.getElementById('tpg-thumb-y');\n  const trackX = document.getElementById('tpg-scroll-x');\n  const trackY = document.getElementById('tpg-scroll-y');\n  if (!wrap || !thumbX || !thumbY) return;\n\n  const max = tpgGetMaxPan();\n  const canPanX = max.x > 0;\n  const canPanY = max.y > 0;\n\n  if (canPanX || canPanY) {\n    wrap.classList.add('tpg-scrollable');\n  } else {\n    wrap.classList.remove('tpg-scrollable');\n    return;\n  }\n\n  const trackXW = trackX.clientWidth;\n  const trackYH = trackY.clientHeight;\n  const scaledW = canvas.width * tpgCurrentScale;\n  const scaledH = canvas.height * tpgCurrentScale;\n  const wrapW = wrap.clientWidth;\n  const wrapH = wrap.clientHeight;\n\n  \/\/ Horizontal thumb\n  if (canPanX) {\n    const ratioW = wrapW \/ scaledW;\n    const thumbW = Math.max(24, trackXW * ratioW);\n    const posX = ((tpgPanX + max.x) \/ (max.x * 2)) * (trackXW - thumbW);\n    thumbX.style.width = thumbW + 'px';\n    thumbX.style.left = posX + 'px';\n    trackX.style.display = '';\n  } else {\n    trackX.style.display = 'none';\n  }\n\n  \/\/ Vertical thumb\n  if (canPanY) {\n    const ratioH = wrapH \/ scaledH;\n    const thumbH = Math.max(24, trackYH * ratioH);\n    const posY = ((tpgPanY + max.y) \/ (max.y * 2)) * (trackYH - thumbH);\n    thumbY.style.height = thumbH + 'px';\n    thumbY.style.top = posY + 'px';\n    trackY.style.display = '';\n  } else {\n    trackY.style.display = 'none';\n  }\n}\n\n\/\/ Check if mouse event is over the logo on canvas (accounts for scale\/pan)\nfunction tpgIsMouseOverLogo(e) {\n  if (!logoState || !logoState.image || !logoState.enabled) return false;\n  const rect = canvas.getBoundingClientRect();\n  const scaleX = canvas.width \/ rect.width;\n  const scaleY = canvas.height \/ rect.height;\n  const mx = (e.clientX - rect.left) * scaleX;\n  const my = (e.clientY - rect.top) * scaleY;\n  const b = getLogoBounds();\n  const pad = 10;\n  return mx >= b.x - pad && mx <= b.x + b.w + pad &&\n         my >= b.y - pad && my <= b.y + b.h + pad;\n}\n\n\/\/ \u2500\u2500 Mouse drag on canvas-wrap \u2500\u2500\n(function() {\n  const wrap = document.getElementById('canvas-wrap');\n\n  wrap.addEventListener('mousedown', (e) => {\n    if (!document.getElementById('canvas-wrap').classList.contains('tpg-zoomable')) return;\n    if (e.button !== 0) return;\n    \/\/ Don't drag if clicking scrollbar thumbs\n    if (e.target.id === 'tpg-thumb-x' || e.target.id === 'tpg-thumb-y') return;\n    \/\/ Don't start pan if mouse is over logo \u2014 let logo drag take priority\n    if (tpgIsMouseOverLogo(e)) return;\n    tpgIsDragging = true;\n    tpgDragStartX = e.clientX;\n    tpgDragStartY = e.clientY;\n    tpgPanStartX = tpgPanX;\n    tpgPanStartY = tpgPanY;\n    wrap.classList.add('tpg-panning');\n    e.preventDefault();\n  });\n\n  window.addEventListener('mousemove', (e) => {\n    if (!tpgIsDragging) return;\n    tpgPanX = tpgPanStartX + (e.clientX - tpgDragStartX);\n    tpgPanY = tpgPanStartY + (e.clientY - tpgDragStartY);\n    tpgClampPan();\n    tpgApplyPan();\n    tpgUpdateScrollbars();\n  });\n\n  window.addEventListener('mouseup', () => {\n    if (!tpgIsDragging) return;\n    tpgIsDragging = false;\n    document.getElementById('canvas-wrap').classList.remove('tpg-panning');\n  });\n\n  \/\/ Scroll wheel to pan vertically (shift = horizontal)\n  wrap.addEventListener('wheel', (e) => {\n    const max = tpgGetMaxPan();\n    if (max.x === 0 && max.y === 0) return;\n    e.preventDefault();\n    if (e.shiftKey) {\n      tpgPanX -= e.deltaY * 0.8;\n    } else {\n      tpgPanY -= e.deltaY * 0.8;\n    }\n    tpgClampPan();\n    tpgApplyPan();\n    tpgUpdateScrollbars();\n  }, { passive: false });\n\n  \/\/ Scrollbar drag \u2014 X\n  const thumbX = document.getElementById('tpg-thumb-x');\n  const trackX = document.getElementById('tpg-scroll-x');\n  let sbDraggingX = false, sbStartX = 0, sbPanStartX = 0;\n  thumbX.addEventListener('mousedown', (e) => {\n    sbDraggingX = true; sbStartX = e.clientX; sbPanStartX = tpgPanX;\n    e.stopPropagation(); e.preventDefault();\n  });\n  window.addEventListener('mousemove', (e) => {\n    if (!sbDraggingX) return;\n    const max = tpgGetMaxPan();\n    const trackW = trackX.clientWidth;\n    const thumbW = thumbX.offsetWidth;\n    const ratio = (e.clientX - sbStartX) \/ (trackW - thumbW);\n    tpgPanX = sbPanStartX + ratio * max.x * 2;\n    tpgClampPan(); tpgApplyPan(); tpgUpdateScrollbars();\n  });\n  window.addEventListener('mouseup', () => { sbDraggingX = false; });\n\n  \/\/ Scrollbar drag \u2014 Y\n  const thumbY = document.getElementById('tpg-thumb-y');\n  const trackY = document.getElementById('tpg-scroll-y');\n  let sbDraggingY = false, sbStartY = 0, sbPanStartY = 0;\n  thumbY.addEventListener('mousedown', (e) => {\n    sbDraggingY = true; sbStartY = e.clientY; sbPanStartY = tpgPanY;\n    e.stopPropagation(); e.preventDefault();\n  });\n  window.addEventListener('mousemove', (e) => {\n    if (!sbDraggingY) return;\n    const max = tpgGetMaxPan();\n    const trackH = trackY.clientHeight;\n    const thumbH = thumbY.offsetHeight;\n    const ratio = (e.clientY - sbStartY) \/ (trackH - thumbH);\n    tpgPanY = sbPanStartY + ratio * max.y * 2;\n    tpgClampPan(); tpgApplyPan(); tpgUpdateScrollbars();\n  });\n  window.addEventListener('mouseup', () => { sbDraggingY = false; });\n})();\n\n\/\/ Reset pan when zoom returns to fit\nconst _origZoomReset = window.zoomReset;\nfunction tpgResetPan() { tpgPanX = 0; tpgPanY = 0; tpgApplyPan(); tpgUpdateScrollbars(); }\n\n\/\/ Helper to get logo bounds\nfunction getLogoBounds() {\n  if (!logoState.image) return { x:0, y:0, w:0, h:0 };\n  const h = logoState.size;\n  const w = (logoState.image.width \/ logoState.image.height) * h;\n  return { x: logoState.x, y: logoState.y, w: w, h: h };\n}\n\n\/\/ Drag Handlers\nfunction handleLogoDown(e) {\n  if (!logoState.image || !logoState.enabled) return;\n  const rect = canvas.getBoundingClientRect();\n  const scaleX = canvas.width \/ rect.width;\n  const scaleY = canvas.height \/ rect.height;\n  const clientX = e.clientX !== undefined ? e.clientX : e.touches[0].clientX;\n  const clientY = e.clientY !== undefined ? e.clientY : e.touches[0].clientY;\n  const mouseX = (clientX - rect.left) * scaleX;\n  const mouseY = (clientY - rect.top) * scaleY;\n\n  const b = getLogoBounds();\n  const pad = 10; \/\/ extra hit area\n  if (mouseX >= b.x - pad && mouseX <= b.x + b.w + pad &&\n      mouseY >= b.y - pad && mouseY <= b.y + b.h + pad) {\n    logoState.isDragging = true;\n    logoState.manual = true;\n    logoState.dragOffX = mouseX - b.x;\n    logoState.dragOffY = mouseY - b.y;\n    canvas.style.cursor = 'grabbing';\n    e.stopPropagation(); \/\/ prevent pan from activating\n  }\n}\n\nfunction handleLogoMove(e) {\n  const rect = canvas.getBoundingClientRect();\n  const scaleX = canvas.width \/ rect.width;\n  const scaleY = canvas.height \/ rect.height;\n  const clientX = e.clientX !== undefined ? e.clientX : (e.touches && e.touches[0] ? e.touches[0].clientX : 0);\n  const clientY = e.clientY !== undefined ? e.clientY : (e.touches && e.touches[0] ? e.touches[0].clientY : 0);\n  const mouseX = (clientX - rect.left) * scaleX;\n  const mouseY = (clientY - rect.top) * scaleY;\n\n  if (!logoState.isDragging) {\n    const b = getLogoBounds();\n    const pad = 10;\n    if (logoState.image && logoState.enabled &&\n        mouseX >= b.x - pad && mouseX <= b.x + b.w + pad &&\n        mouseY >= b.y - pad && mouseY <= b.y + b.h + pad) {\n      canvas.style.cursor = 'grab';\n    } else {\n      canvas.style.cursor = 'default';\n    }\n    return;\n  }\n  e.preventDefault();\n  logoState.x = mouseX - logoState.dragOffX;\n  logoState.y = mouseY - logoState.dragOffY;\n  drawCurrent();\n}\n\nfunction handleLogoUp() {\n  if (logoState.isDragging) {\n    logoState.isDragging = false;\n    canvas.style.cursor = 'grab';\n  }\n}\n\ncanvas.addEventListener('mousedown', handleLogoDown);\nwindow.addEventListener('mousemove', handleLogoMove);\nwindow.addEventListener('mouseup', handleLogoUp);\n\ncanvas.addEventListener('touchstart', handleLogoDown, { passive: false });\nwindow.addEventListener('touchmove', handleLogoMove, { passive: false });\nwindow.addEventListener('touchend', handleLogoUp);\n\n\/\/ ===== UI HELPERS =====\nfunction tpgCreateSelect(options, currentVal, onSelect) {\n  const currentOption = options.find(o => o.value == currentVal) || options[0];\n  return `\n    <div class=\"tpg-custom-select\" onclick=\"tpgToggleSelect(this, event)\">\n      <div class=\"tpg-select-trigger\">\n        <span>${currentOption.label}<\/span>\n        <div class=\"tpg-select-arrow\"><\/div>\n      <\/div>\n      <div class=\"tpg-select-dropdown\">\n        ${options.map(o => `\n          <div class=\"tpg-select-option ${o.value == currentVal ? 'selected' : ''}\" \n               onclick=\"tpgSelectOption('${o.value}', '${o.label.replace(\/'\/g, \"\\\\'\")}', this, event, ${onSelect})\">\n            ${o.label}\n          <\/div>\n        `).join('')}\n      <\/div>\n    <\/div>\n  `;\n}\n\nwindow.tpgToggleSelect = function(el, e) {\n  e.stopPropagation();\n  const isActive = el.classList.contains('active');\n  document.querySelectorAll('.tpg-custom-select').forEach(s => s.classList.remove('active'));\n  if (!isActive) el.classList.add('active');\n};\n\nwindow.tpgSelectOption = function(val, label, el, e, callback) {\n  e.stopPropagation();\n  const parent = el.closest('.tpg-custom-select');\n  parent.querySelector('.tpg-select-trigger span').innerText = label;\n  parent.querySelectorAll('.tpg-select-option').forEach(opt => opt.classList.remove('selected'));\n  el.classList.add('selected');\n  parent.classList.remove('active');\n  callback(val);\n};\n\ndocument.addEventListener('click', () => {\n  document.querySelectorAll('.tpg-custom-select').forEach(s => s.classList.remove('active'));\n});\n\nlet iroPicker = null;\n\nfunction initIroPicker() {\n  if (iroPicker) return;\n  iroPicker = new iro.ColorPicker(\"#iro-picker-anchor\", {\n    width: 210,\n    color: \"#ffffff\", \/\/ temporary, will be set on toggle\n    handleRadius: 8,\n    layout: [\n      { component: iro.ui.Wheel, options: { margin: 12 } },\n      { component: iro.ui.Slider, options: { sliderType: 'saturation', sliderShape: 'box', height: 24, margin: 10 } },\n      { component: iro.ui.Slider, options: { sliderType: 'value', sliderShape: 'box', height: 24, margin: 10 } },\n    ]\n  });\n\n  iroPicker.on(['color:change', 'input:change'], function(color) {\n    const hex = color.hexString;\n    const target = iroPicker._tpgTarget;\n    const isSolid = patterns[currentPattern].id === 'solid';\n    \n    if (isSolid) {\n        patternState.solidColor = hex;\n        const hexInp = document.getElementById('solid-hex-input');\n        if (hexInp) hexInp.value = hex.toUpperCase();\n    } else {\n        patternState[target] = hex;\n    }\n    \n    drawCurrent();\n    \n    \/\/ Update swatch background in real-time\n    const swatch = document.querySelector(isSolid ? '.tpg-color-swatch' : `.tpg-swatch-${target}`);\n    if (swatch) swatch.style.background = hex;\n    \n    if (isSolid) {\n        const sc = [{c:'#ff0000'},{c:'#00ff00'},{c:'#0000ff'},{c:'#ffffff'},{c:'#000000'},{c:'#00ffff'},{c:'#ff00ff'},{c:'#ffff00'}];\n        document.querySelectorAll('.rp-swatch').forEach((btn, i) => {\n            if (sc[i]) btn.classList.toggle('active', hex.toLowerCase() === sc[i].c.toLowerCase());\n        });\n    }\n  });\n\n  \/\/ Smart close\n  const anchor = document.getElementById('iro-picker-anchor');\n  anchor.addEventListener('click', (e) => {\n      if (e.target.closest('.IroWheel')) {\n          setTimeout(() => {\n              document.getElementById('tpg-iro-popover').classList.remove('active');\n          }, 150);\n      }\n  });\n}\n\nwindow.tpgToggleIro = function(btn, e, target = 'solidColor') {\n  e.stopPropagation();\n  initIroPicker();\n  const pop = document.getElementById('tpg-iro-popover');\n  const isActive = pop.classList.contains('active') && iroPicker._tpgTarget === target;\n  \n  document.querySelectorAll('.tpg-iro-popover, .tpg-custom-select').forEach(el => el.classList.remove('active'));\n  \n  if (!isActive) {\n    const group = btn.closest('.rpanel-group') || btn.parentElement;\n    const gRect = group.getBoundingClientRect();\n    const center = document.getElementById('tpg-center').getBoundingClientRect();\n    \n    pop.style.top = (gRect.top - center.top) + 'px';\n    pop.style.right = '12px';\n    \n    pop.classList.add('active');\n    iroPicker._tpgTarget = target;\n    iroPicker.color.set(patternState[target]);\n  }\n};\n\nwindow.tpgUpdateSolidColor = function(v) {\n  patternState.solidColor = v;\n  if (iroPicker) iroPicker.color.set(v);\n  drawCurrent();\n  \n  \/\/ Re-build panel to ensure all elements (HEX text, hex in popover) are synced\n  const hexInp = document.getElementById('solid-hex-input');\n  if (hexInp) hexInp.value = v.toUpperCase();\n  \n  const sc = [{c:'#ff0000'},{c:'#00ff00'},{c:'#0000ff'},{c:'#ffffff'},{c:'#000000'},{c:'#00ffff'},{c:'#ff00ff'},{c:'#ffff00'}];\n  document.querySelectorAll('.rp-swatch').forEach((btn, i) => {\n      if (sc[i]) btn.classList.toggle('active', v.toLowerCase() === sc[i].c.toLowerCase());\n  });\n  \n  \/\/ Update swatch bg\n  const swatch = document.querySelector('.tpg-color-swatch');\n  if (swatch) swatch.style.background = v;\n};\n\ndocument.addEventListener('click', (e) => {\n    \/\/ Don't close if clicking inside the popover or on the trigger\n    if (e.target.closest('.tpg-iro-popover') || e.target.closest('.tpg-color-trigger')) return;\n    document.querySelectorAll('.tpg-iro-popover').forEach(p => p.classList.remove('active'));\n});\n\nwindow.tpgUpdateCheckerPreset = function(c1, c2) {\n  patternState.checkerColor1 = c1;\n  patternState.checkerColor2 = c2;\n  drawCurrent();\n  \n  \/\/ Update swatches in UI\n  const s1 = document.querySelector('.tpg-swatch-checkerColor1');\n  const s2 = document.querySelector('.tpg-swatch-checkerColor2');\n  if (s1) s1.style.background = c1;\n  if (s2) s2.style.background = c2;\n  \n  \/\/ Highlighting active preset\n  document.querySelectorAll('.checker-preset-btn').forEach(btn => {\n      const b1 = btn.getAttribute('data-c1').toLowerCase();\n      const b2 = btn.getAttribute('data-c2').toLowerCase();\n      btn.classList.toggle('active', b1 === c1.toLowerCase() && b2 === c2.toLowerCase());\n  });\n};\n\nwindow.tpgUpdatePieColorFieldPreset = function(v) {\n  patternState.pieColorFieldPreset = v;\n  drawCurrent();\n  document.querySelectorAll('.pie-cf-preset-btn').forEach(btn => {\n    btn.classList.toggle('active', btn.getAttribute('data-id') === v);\n  });\n};\n\nwindow.tpgUpdatePieColorBarsMode = function(v) {\n  patternState.pieColorBarsMode = v;\n  drawCurrent();\n  document.querySelectorAll('.pie-cb-mode-btn').forEach(btn => {\n    btn.classList.toggle('active', btn.getAttribute('data-id') === v);\n  });\n};\n\nwindow.tpgUpdatePieColorRampPreset = function(v) {\n  patternState.pieColorRampPreset = v;\n  drawCurrent();\n  document.querySelectorAll('.pie-cr-preset-btn').forEach(btn => {\n    btn.classList.toggle('active', btn.getAttribute('data-id') === v);\n  });\n};\n\n\/\/ ===== SIDEBAR =====\nlet sidebarBuilt = false;\n\nfunction toggleSidebarSection(sectionId) {\n  if (!sidebarBuilt) { buildSidebar(); return; }\n  const sb = document.getElementById('sidebar');\n  Object.keys(sectionLabels).forEach(k => {\n    const sec = sb.querySelector(`.nav-section[data-section=\"${k}\"]`);\n    const grp = sb.querySelector(`.nav-group[data-group=\"${k}\"]`);\n    if (!sec || !grp) return;\n    const open = k === sectionId ? !grp.classList.contains('tpg-open') : false;\n    grp.classList.toggle('tpg-open', open);\n    sec.classList.toggle('expanded', open);\n    sidebarState[k] = open;\n  });\n}\n\nfunction buildSidebar() {\n  const sb = document.getElementById('sidebar');\n  if (!sb) return;\n  const groups = {};\n  patterns.forEach((p, i) => {\n    if (!groups[p.section]) groups[p.section] = [];\n    groups[p.section].push({ ...p, index: i });\n  });\n\n  let html = '';\n  Object.keys(sectionLabels).forEach(secId => {\n    const isExpanded = sidebarState[secId] !== false;\n    const groupItems = groups[secId] || [];\n    html += `\n      <div class=\"nav-section ${isExpanded ? 'expanded' : ''}\" data-section=\"${secId}\" onclick=\"event.stopPropagation(); toggleSidebarSection('${secId}')\">\n        <span>${sectionLabels[secId]}<\/span>\n        <span class=\"nav-section-chevron\">\n          <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 18 15 12 9 6\"\/><\/svg>\n        <\/span>\n      <\/div>\n      <div class=\"nav-group ${isExpanded ? 'tpg-open' : ''}\" data-group=\"${secId}\">\n        <div class=\"nav-group-inner\">\n          ${groupItems.map(item => `\n            <div class=\"nav-item ${item.index === currentPattern ? 'active' : ''}\" data-index=\"${item.index}\" onclick=\"selectPattern(${item.index}); event.stopPropagation();\">\n              <div class=\"nav-icon\">${ICONS[item.icon] || ICONS._default}<\/div>\n              <div class=\"nav-label\">${item.name}<\/div>\n            <\/div>\n          `).join('')}\n        <\/div>\n      <\/div>`;\n  });\n  sb.innerHTML = html;\n  sidebarBuilt = true;\n}\n\nfunction updateSidebarActive() {\n  if (!sidebarBuilt) { buildSidebar(); return; }\n  const sb = document.getElementById('sidebar');\n  const p = patterns[currentPattern];\n  sb.querySelectorAll('.nav-item').forEach(item => {\n    item.classList.toggle('active', parseInt(item.getAttribute('data-index'), 10) === currentPattern);\n  });\n  const targetGroup = sb.querySelector(`.nav-group[data-group=\"${p.section}\"]`);\n  if (targetGroup && !targetGroup.classList.contains('tpg-open')) {\n    toggleSidebarSection(p.section);\n  }\n}\n\/\/ ===== PATTERN SELECTION =====\nfunction selectPattern(idx) {\n  cancelAnimations();\n  currentPattern = Math.max(0, Math.min(patterns.length - 1, idx));\n  updateSidebarActive();\n  buildRightPanel();\n  drawCurrent();\n}\n\nfunction buildRightPanel() {\n  const p = patterns[currentPattern] || patterns[0];\n  const container = document.getElementById('rpanel-controls');\n  if (!container || !p) return;\n\n  const resPresets = [\n    { label: 'FHD - 1920x1080', w: 1920, h: 1080 },\n    { label: 'QHD - 2560x1440', w: 2560, h: 1440 },\n    { label: '4K UHD - 3840x2160', w: 3840, h: 2160 },\n    { label: 'DCI 4K - 4096x2160', w: 4096, h: 2160 }\n  ];\n  if (!resPresets.some(item => patternState.resW === item.w && patternState.resH === item.h)) {\n    patternState.resW = 1920;\n    patternState.resH = 1080;\n  }\n\n  const presetOptions = resPresets.map(item => {\n    const selected = patternState.resW === item.w && patternState.resH === item.h ? 'selected' : '';\n    return `<option value=\"${item.w}x${item.h}\" ${selected}>${item.label}<\/option>`;\n  }).join('');\n\n  container.innerHTML = `\n    <div class=\"rpanel-section\">\n      <div class=\"rpanel-header\"><span class=\"rpanel-title\" style=\"font-weight:800;font-size:11px;\">DISPLAY SETUP<\/span><\/div>\n      <div class=\"rpanel-content\">\n        <div class=\"rpanel-field\">\n          <span class=\"rpanel-label\">DISPLAY PRESET<\/span>\n          <div class=\"rp-select-wrap\">\n            <select class=\"rp-select\" onchange=\"const v=this.value.split('x'); patternState.resW=parseInt(v[0],10); patternState.resH=parseInt(v[1],10); buildRightPanel(); resizeCanvas();\">\n              ${presetOptions}\n            <\/select>\n          <\/div>\n        <\/div>\n        <div class=\"rpanel-field\">\n          <span class=\"rpanel-label\">OUTPUT RESOLUTION (PX)<\/span>\n          <div style=\"display:flex;align-items:center;gap:8px;\">\n            <input type=\"number\" class=\"rp-input\" style=\"flex:1;opacity:.68;cursor:not-allowed;\" value=\"${patternState.resW}\" disabled>\n            <span style=\"color:rgba(255,255,255,.24);font-size:11px;\">x<\/span>\n            <input type=\"number\" class=\"rp-input\" style=\"flex:1;opacity:.68;cursor:not-allowed;\" value=\"${patternState.resH}\" disabled>\n          <\/div>\n          <div style=\"font-size:10px;color:rgba(255,255,255,.34);margin-top:5px;\">Locked to native C++ reference presets. Aspect ratio: ${(patternState.resW \/ patternState.resH).toFixed(2)}:1<\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n    <div class=\"rpanel-section\">\n      <div class=\"rpanel-header\"><span class=\"rpanel-title\" style=\"font-weight:800;font-size:11px;\">TESTPATTERNLIB SOURCE<\/span><\/div>\n      <div class=\"rpanel-content\">\n        <div class=\"rpanel-field\">\n          <span class=\"rpanel-label\">PATTERN TYPE<\/span>\n          <div style=\"color:#fff;font-size:13px;font-weight:700;\">${p.name}<\/div>\n          <div style=\"color:rgba(255,255,255,.42);font-size:11px;line-height:1.5;margin-top:6px;\">Factory key: <code>${p.id}<\/code><\/div>\n        <\/div>\n        <label class=\"rpanel-field\" style=\"display:flex;gap:10px;align-items:center;justify-content:space-between;\">\n          <span>\n            <span class=\"rpanel-label\" style=\"display:block;\">DESCRIPTION LABEL<\/span>\n            <span style=\"display:block;color:rgba(255,255,255,.36);font-size:11px;line-height:1.4;\">Matches the C++ label overlay behavior for most generated patterns.<\/span>\n          <\/span>\n          <button class=\"rp-toggle ${patternState.tplShowDescription !== false ? 'on' : ''}\" onclick=\"patternState.tplShowDescription = patternState.tplShowDescription === false; buildRightPanel(); drawCurrent();\"><\/button>\n        <\/label>\n      <\/div>\n    <\/div>\n  `;\n\n  const info = document.getElementById('pattern-info');\n  if (info) info.textContent = p.desc || '';\n}\n\nfunction prevPattern() { selectPattern((currentPattern - 1 + patterns.length) % patterns.length); }\nfunction nextPattern() { selectPattern((currentPattern + 1) % patterns.length); }\n\nfunction cancelAnimations() {\n  if (animFrame) { cancelAnimationFrame(animFrame); animFrame = null; }\n  if (deadPixelTimer) { clearInterval(deadPixelTimer); deadPixelTimer = null; }\n  if (burninTimer) { clearInterval(burninTimer); burninTimer = null; }\n  isPaused = false;\n}\n\/\/ ===== DRAW DISPATCHER =====\nfunction drawCurrent() {\n  const p = patterns[currentPattern] || patterns[0];\n  if (!p) return;\n  ctx.save();\n  ctx.setTransform(1, 0, 0, 1, 0, 0);\n  ctx.clearRect(0, 0, canvas.width, canvas.height);\n  drawTestPatternLibPattern(p);\n  tplLabel(p);\n  ctx.restore();\n}\n\nfunction drawComingSoon(name) {\n  const w = canvas.width, h = canvas.height;\n  tplFill(0);\n  tplText(name, w \/ 2, h \/ 2, Math.min(w, h) * 0.045, 230, 'center', '700');\n}\n\/\/ ===== TESTPATTERNLIB CANVAS RENDERER =====\nconst tplImageCache = {};\nconst TPL_NATIVE_BASE_URL = \"https:\/\/perfectchroma.com\/wp-content\/themes\/hello-elementor-child\/assets\/images\/testpatternlib\";\nconst tplNativeMissing = new Set(['FUJIFILM', 'PressProof_01', 'RealImages_01']);\n\nfunction tplGray(v) {\n  v = Math.max(0, Math.min(255, Math.round(v)));\n  return `rgb(${v},${v},${v})`;\n}\nfunction tplFill(gray) {\n  ctx.fillStyle = tplGray(gray);\n  ctx.fillRect(0, 0, canvas.width, canvas.height);\n}\nfunction tplRect(x, y, w, h, gray) {\n  ctx.fillStyle = tplGray(gray);\n  ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));\n}\nfunction tplStrokeRect(x, y, w, h, gray, lw = 1) {\n  ctx.strokeStyle = tplGray(gray);\n  ctx.lineWidth = lw;\n  ctx.strokeRect(Math.round(x) + 0.5, Math.round(y) + 0.5, Math.round(w), Math.round(h));\n}\nfunction tplLine(x1, y1, x2, y2, gray, lw = 1) {\n  ctx.strokeStyle = tplGray(gray);\n  ctx.lineWidth = lw;\n  ctx.beginPath();\n  ctx.moveTo(Math.round(x1) + 0.5, Math.round(y1) + 0.5);\n  ctx.lineTo(Math.round(x2) + 0.5, Math.round(y2) + 0.5);\n  ctx.stroke();\n}\nfunction tplPreviewLineWidth(base = 1) {\n  if (window.tpgExporting) return base;\n  const scale = Math.max(0.2, Number(window.tpgCurrentScale || tpgCurrentScale || 1));\n  return Math.max(base, Math.ceil(base \/ scale));\n}\nfunction tplNativeAssetName(id) {\n  if (tplNativeMissing.has(id)) return null;\n  return `${String(id).replace(\/[^a-z0-9._-]+\/gi, '_')}.png`;\n}\nfunction tplNativeFolderName() {\n  const key = `${canvas.width}x${canvas.height}`;\n  return ['1920x1080', '2560x1440', '3840x2160', '4096x2160'].includes(key) ? `native-${key}` : null;\n}\nfunction tplDrawNativeReference(p) {\n  if (window.tpgUseNativeReferences === false) return false;\n  const folder = tplNativeFolderName();\n  if (!folder) return false;\n  const file = tplNativeAssetName(p.id);\n  if (!file) return false;\n  const src = `${TPL_NATIVE_BASE_URL}\/${folder}\/${file}`;\n  let img = tplImageCache[src];\n  if (!img) {\n    img = new Image();\n    img.onload = () => drawCurrent();\n    img.onerror = () => { img._failed = true; drawCurrent(); };\n    img.src = src;\n    tplImageCache[src] = img;\n  }\n  if (img._failed) return false;\n  tplFill(0);\n  if (img.complete && img.naturalWidth) {\n    ctx.imageSmoothingEnabled = false;\n    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n  }\n  return true;\n}\nfunction tplEllipse(cx, cy, rx, ry, gray, fill = true, lw = 1) {\n  ctx.beginPath();\n  ctx.ellipse(cx, cy, Math.abs(rx), Math.abs(ry), 0, 0, Math.PI * 2);\n  if (fill) {\n    ctx.fillStyle = tplGray(gray);\n    ctx.fill();\n  } else {\n    ctx.strokeStyle = tplGray(gray);\n    ctx.lineWidth = lw;\n    ctx.stroke();\n  }\n}\nfunction tplText(text, x, y, size, gray = 255, align = 'center', weight = 'normal') {\n  ctx.fillStyle = tplGray(gray);\n  ctx.font = `${weight} ${Math.max(2, Math.round(size))}px Arial, sans-serif`;\n  ctx.textAlign = align;\n  ctx.textBaseline = 'middle';\n  ctx.fillText(text, x, y);\n}\nfunction tplLabel(p, fg = 230, bg = 80) {\n  if (p.hideLabel || patternState.tplShowDescription === false) return;\n  const w = canvas.width, h = canvas.height;\n  const boxW = Math.min(520, w * 0.56);\n  const boxH = Math.max(42, Math.min(64, h * 0.065));\n  const x = (w - boxW) \/ 2;\n  const y = h - boxH - Math.max(12, h * 0.025);\n  ctx.fillStyle = `rgba(${bg},${bg},${bg},0.86)`;\n  ctx.fillRect(x, y, boxW, boxH);\n  tplText(`Generated and scaled by PerfectChroma.`, w \/ 2, y + boxH * 0.34, Math.max(9, Math.min(14, h * 0.014)), fg, 'center', '600');\n  tplText(`Test pattern based on ${p.name}.`, w \/ 2, y + boxH * 0.68, Math.max(9, Math.min(13, h * 0.013)), fg);\n}\nfunction tplCenteredSquare(sizeRatio, gray, bg = 153) {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  tplFill(bg);\n  const ss = s * sizeRatio;\n  tplRect((w - ss) \/ 2, (h - ss) \/ 2, ss, ss, gray);\n}\nfunction tplCheckerCell(x, y, w, h, base, delta = 4) {\n  tplRect(x, y, w, h, base);\n  const q = Math.max(2, Math.min(w, h) * 0.13);\n  tplRect(x, y, q, q, base + delta);\n  tplRect(x + w - q, y + h - q, q, q, base + delta);\n  tplRect(x + w - q, y, q, q, base - delta);\n  tplRect(x, y + h - q, q, q, base - delta);\n}\n\nconst tplCX = [\n  [[1,1,1,1,1,1,1],[1,0,0,0,0,0,0],[1,0,1,0,1,0,0],[1,0,0,1,0,0,0],[1,0,1,0,1,0,0],[1,0,0,0,0,0,0],[1,1,1,1,1,1,1]],\n  [[1,1,1,1,1,1,1],[1,0,0,0,0,0,1],[1,0,1,0,1,0,1],[1,0,0,1,0,0,1],[1,0,1,0,1,0,1],[1,0,0,0,0,0,1],[1,0,0,0,0,0,1]],\n  [[1,1,1,1,1,1,1],[0,0,0,0,0,0,1],[0,0,1,0,1,0,1],[0,0,0,1,0,0,1],[0,0,1,0,1,0,1],[0,0,0,0,0,0,1],[1,1,1,1,1,1,1]],\n  [[1,0,0,0,0,0,1],[1,0,0,0,0,0,1],[1,0,1,0,1,0,1],[1,0,0,1,0,0,1],[1,0,1,0,1,0,1],[1,0,0,0,0,0,1],[1,1,1,1,1,1,1]]\n];\nconst tplCxGray = [255, 191, 128, 64];\nconst tplCxTileCache = new Map();\n\nfunction tplCxConfig() {\n  const s = Math.min(canvas.width, canvas.height);\n  if (s >= 1800) return { big: 4, margin: 4, small: 8 };\n  if (s >= 1600) return { big: 3, margin: 8, small: 6 };\n  if (s >= 1536) return { big: 3, margin: 5, small: 6 };\n  if (s >= 1200) return { big: 2, margin: 15, small: 4 };\n  if (s >= 960) return { big: 2, margin: 7, small: 4 };\n  if (s >= 780) return { big: 1, margin: 15, small: 3 };\n  if (s >= 600) return { big: 1, margin: 15, small: 2 };\n  return { big: 1, margin: 7, small: 2 };\n}\n\nfunction tplSmallCxAt(x, y, blockSize, colorOffset = 0) {\n  const tile = tplSmallCxTile(blockSize, colorOffset);\n  ctx.drawImage(tile, Math.round(x), Math.round(y));\n}\n\nfunction tplSmallCxTile(blockSize, colorOffset = 0) {\n  const key = `${blockSize}:${colorOffset % 4}`;\n  if (tplCxTileCache.has(key)) return tplCxTileCache.get(key);\n  const cellPitch = Math.floor(19 \/ 2);\n  const px = blockSize * 7 + Math.floor(blockSize * 5 \/ 2);\n  const c = document.createElement('canvas');\n  c.width = px;\n  c.height = px;\n  const cctx = c.getContext('2d');\n  for (let cj = 0; cj < blockSize; cj++) {\n    for (let ci = 0; ci < blockSize; ci++) {\n      const gray = tplCxGray[(ci + cj + colorOffset) % 4];\n      cctx.fillStyle = tplGray(gray);\n      const mat = tplCX[(ci + cj) % 4];\n      const bx = Math.round(ci * cellPitch + 3);\n      const by = Math.round(cj * cellPitch + 3);\n      for (let j = 0; j < 7; j++) {\n        for (let i = 0; i < 7; i++) {\n          if (mat[j][i]) cctx.fillRect(bx + i, by + j, 1, 1);\n        }\n      }\n    }\n  }\n  tplCxTileCache.set(key, c);\n  return c;\n}\n\nfunction tplDrawSmallCxField(blockSize) {\n  const px = blockSize * 7 + Math.floor(blockSize * 5 \/ 2);\n  for (let x = 0, ix = 0; x <= canvas.width; x += px, ix++) {\n    for (let y = 0, jy = 0; y <= canvas.height; y += px, jy++) {\n      tplSmallCxAt(x, y, blockSize, ix + jy);\n    }\n  }\n  return px;\n}\n\nfunction tplDrawBigCx(cx, cy, blockSize, margin, paint = true) {\n  const px = 5 * (Math.floor(blockSize * 19 \/ 2) - 3) + 2 * margin;\n  if (!paint) return px;\n  const places = [[1,3],[0,3],[0,2],[0,1],[0,0],[1,0],[2,0],[3,0],[3,1],[3,2],[3,3],[2,3]];\n  const left = Math.round(cx - 2 * px), top = Math.round(cy - 2 * px);\n  places.forEach(([pi, pj], profile) => {\n    const x = left + pi * px, y = top + pj * px;\n    const alpha = profile === 2 ? 1 : Math.max(0.22, 1 - Math.abs(profile - 2) * 0.055);\n    tplDrawScaledCxBlock(x, y, blockSize, margin, px, 255 * alpha);\n    tplRect(x + px - 18, y + px - 18, 16, 16, 128);\n    tplText(String(profile - 2), x + px - 10, y + px - 10, Math.max(7, px \/ 5), 0, 'center', '700');\n  });\n  return px;\n}\n\nfunction tplDrawScaledCxBlock(x, y, blockSize, margin, px, gray) {\n  tplRect(x, y, px, px, 0);\n  const scale = 5;\n  for (let cj = 0; cj < blockSize; cj++) {\n    for (let ci = 0; ci < blockSize; ci++) {\n      const mat = tplCX[(ci + cj) % 4];\n      const baseX = x + margin + scale * (ci * Math.floor(19 \/ 2));\n      const baseY = y + margin + scale * (cj * Math.floor(19 \/ 2));\n      for (let j = 0; j < 7; j++) for (let i = 0; i < 7; i++) {\n        if (mat[j][i]) tplRect(baseX + i * scale, baseY + j * scale, scale, scale, gray);\n      }\n    }\n  }\n}\n\nfunction tplDrawCxPatch(x, y, blockSize, withCx = true, flipX = false, flipY = false) {\n  const px = blockSize * 7 + Math.floor(blockSize * 5 \/ 2);\n  const border = 2;\n  const patchW = 4 * px + 2 * border;\n  const patchH = (withCx ? 3 * px + 2 * border + 4 : 2 * px + 2 * border);\n  ctx.save();\n  ctx.translate(x + (flipX ? patchW : 0), y + (flipY ? patchH : 0));\n  ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1);\n  tplRect(0, 0, patchW, patchH, 0);\n  tplStrokeRect(-1, -1, patchW + 2, patchH + 2, 64, 2);\n  if (withCx) tplDrawSmallCxRow(border, border, blockSize, 4);\n  const stripY = withCx ? px + border + 4 : border;\n  tplDrawResolutionStrips(border, stripY, px);\n  ctx.restore();\n  return { w: patchW, h: patchH, cxH: px + border + 4 };\n}\n\nfunction tplDrawSmallCxRow(x, y, blockSize, count) {\n  const px = blockSize * 7 + Math.floor(blockSize * 5 \/ 2);\n  for (let i = 0; i < count; i++) tplSmallCxAt(x + i * px, y, blockSize, i);\n}\n\nfunction tplDrawResolutionStrips(x, y, ss) {\n  drawTplLineZone(Math.round(x), Math.round(y), ss, ss, 'v');\n  drawTplLineZone(Math.round(x), Math.round(y + ss), ss, ss, 'h');\n  drawTplLineZone(Math.round(x + ss), Math.round(y), ss, ss, 'v2');\n  drawTplLineZone(Math.round(x + ss), Math.round(y + ss), ss, ss, 'h2');\n  tplRect(x + 2 * ss, y, 2 * ss + 1, 2 * ss + 1, 128);\n  for (let xx = 0; xx < ss; xx += 2) tplLine(x + 2 * ss + xx, y, x + 2 * ss + xx, y + ss - 1, 130, 1);\n  for (let yy = ss; yy < 2 * ss; yy += 2) tplLine(x + 2 * ss, y + yy, x + 3 * ss - 1, y + yy, 130, 1);\n  for (let xx = ss + 1; xx < 2 * ss; xx += 4) tplLine(x + 2 * ss + xx, y, x + 2 * ss + xx, y + ss - 1, 130, 2);\n  for (let yy = ss + 1; yy < 2 * ss; yy += 4) tplLine(x + 3 * ss, y + yy, x + 4 * ss - 1, y + yy, 130, 2);\n}\n\nfunction tplDrawLuminanceSq(x, y, ss, gray, corners = true) {\n  tplRect(x, y, ss - 1, ss - 1, gray);\n  if (!corners) return;\n  const q = Math.max(1, Math.floor(10 * ss \/ 102));\n  tplRect(x, y, q - 1, q - 1, gray + 4);\n  tplRect(x + ss - q, y + ss - q, q - 1, q - 1, gray + 4);\n  tplRect(x + ss - q, y, q - 1, q - 1, gray - 4);\n  tplRect(x, y + ss - q, q - 1, q - 1, gray - 4);\n}\n\nfunction drawTestPatternLibPattern(p) {\n  const id = p.id;\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  ctx.imageSmoothingEnabled = false;\n  ctx.lineCap = 'butt';\n  ctx.lineJoin = 'miter';\n  if (tplDrawNativeReference(p)) return;\n\n  if (id.startsWith('Solid_')) return tplFill(Number(id.split('_')[1] || 0));\n  if (id.startsWith('TG18-LN8-')) return drawTplTG18LN8(id);\n  if (id.startsWith('TG18-GA')) return drawTplTG18GA(id);\n  if (id.startsWith('TG18-R') || id.startsWith('TG18-LP') || id.startsWith('TG18-NS')) return drawTplLinePair(id);\n  if (id.startsWith('TG18-UN')) return drawTplUniformity(id);\n\n  switch (id) {\n    case '8bit_Grayramp': return drawTplGrayRamp();\n    case 'QBX-3COLORS': return drawTplQbxColors(3);\n    case 'QBX-4COLORS': return drawTplQbxColors(4);\n    case 'TG18-GV': return drawTplTg18Gv(false, true, true);\n    case 'TG18-GVN': return drawTplTg18Gv(true, false, true);\n    case 'TG18-GQ': return drawTplTg18Gv(false, true, false);\n    case 'TG18-GQN': return tplFill(0);\n    case 'TG18-GQB': return drawTplTg18Gv(false, true, false, true);\n    case 'TG18-CT': return drawTplTG18CT();\n    case 'TG18-QC': return drawTplTG18QC(false);\n    case 'TG18-OIQ': return drawTplTG18QC(true);\n    case 'SMPTE': return drawTplSMPTE();\n    case 'TG18-PX': return drawTplPixelGrid();\n    case 'TG18-PDC': return drawTplPDC();\n    case 'TG18-CX': return drawTplCX();\n    case 'TG18-MP': return drawTplMP();\n    case 'TG18-AD': return drawTplAD();\n    case 'TG18-AFC': return drawTplAFC();\n    case 'TG18-BR': return drawTplBR();\n    case 'GRAYSCALE': return drawTplGrayscale();\n    case 'RESOLUTION': return drawTplResolution();\n    case 'IEC_DIN6868_57': return drawTplIEC57();\n    case 'IEC-ANG': return drawTplIECANG();\n    case 'IEC-GD': return drawTplIECGD();\n    case 'TG270-sQC': return drawTplTG270sQC();\n    case 'AVEC': return drawTplAVEC();\n    case 'Brightness': return drawTplBrightness();\n    case 'Contrast': return drawTplContrast();\n    case 'BRIGHTNESS_BLACK_LEVEL': return drawTplBrightnessBlackLevel();\n    case 'GamutStripes': return drawTplGamutStripes();\n    case 'FORMAT_OVERSCAN': return drawTplFormatOverscan();\n    case 'FUJIFILM':\n    case 'PressProof_01':\n    case 'RealImages_01':\n    case 'TG18-CH':\n    case 'TG18-KN':\n    case 'TG18-MM01':\n    case 'TG18-MM02': return drawTplImagePattern(p);\n    default:\n      tplFill(0);\n      tplText(p.name || id, w \/ 2, h \/ 2, Math.min(w, h) * 0.05, 220, 'center', '700');\n      tplText('Pattern registered from TestPatternLib factory.', w \/ 2, h \/ 2 + Math.min(w, h) * 0.07, Math.min(w, h) * 0.018, 150);\n      return;\n  }\n}\n\nfunction drawTplGrayRamp() {\n  const w = canvas.width, h = canvas.height;\n  for (let x = 0; x < w; x++) {\n    const v = 255 * x \/ Math.max(1, w - 1);\n    ctx.fillStyle = tplGray(v);\n    ctx.fillRect(x, 0, 1, h);\n  }\n}\n\nfunction drawTplQbxColors(rows) {\n  const w = canvas.width, h = canvas.height;\n  const rowH = h \/ rows;\n  for (let x = 0; x < w; x++) {\n    const v = Math.round(255 * x \/ Math.max(1, w - 1));\n    ctx.fillStyle = `rgb(${v},0,0)`; ctx.fillRect(x, 0, 1, Math.ceil(rowH));\n    ctx.fillStyle = `rgb(0,${v},0)`; ctx.fillRect(x, rowH, 1, Math.ceil(rowH));\n    ctx.fillStyle = `rgb(0,0,${v})`; ctx.fillRect(x, rowH * 2, 1, Math.ceil(rowH));\n    if (rows === 4) { ctx.fillStyle = tplGray(v); ctx.fillRect(x, rowH * 3, 1, Math.ceil(rowH)); }\n  }\n}\n\nfunction drawTplTG18LN8(id) {\n  const n = Number(id.slice(-2));\n  tplCenteredSquare(324 \/ 1024, (n - 1) * 15, 153);\n}\n\nfunction drawTplTg18Gv(blackField, whiteCircle, targets, noBlackCenter = false) {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  tplFill(0);\n  const cx = w \/ 2, cy = h \/ 2;\n  if (whiteCircle) tplEllipse(cx, cy, s * 300 \/ 1000, s * 300 \/ 1000, 255);\n  if (!noBlackCenter) tplEllipse(cx, cy, s * 15 \/ 1000, s * 15 \/ 1000, 0);\n  if (targets) {\n    const r = s * 15 \/ 1000, R = s * 4.5 \/ 1000;\n    [10, 8, 6, 4, 2].forEach((g, i) => {\n      const a = -Math.PI \/ 2 + i * Math.PI * 2 \/ 5;\n      tplEllipse(cx + Math.cos(a) * r, cy + Math.sin(a) * r, R, R, g);\n    });\n  }\n}\n\nfunction drawTplTG18GA(id) {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h), r = Number(id.slice(-2));\n  tplFill(0);\n  tplEllipse(w \/ 2, h \/ 2, s * 300 \/ 1000, s * 300 \/ 1000, 255);\n  tplEllipse(w \/ 2, h \/ 2, s * r \/ 1000, s * r \/ 1000, 0);\n}\n\nfunction drawTplTG18CT() {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  tplFill(128);\n  const side = s * 102 \/ 1024, gap = s * 51 \/ 1024;\n  const startX = w \/ 2 - (2 * side + 1.5 * gap);\n  const startY = h \/ 2 - (2 * side + 1.5 * gap);\n  const row = [0,0,1,0,1,2,0,1,2,3,1,2,3,3,2,3];\n  const col = [0,1,0,2,1,0,3,2,1,0,3,2,1,2,3,3];\n  for (let i = 0, gray = 8; i < 16; i++, gray += 16) {\n    const x = startX + col[i] * (side + gap), y = startY + row[i] * (side + gap);\n    tplCheckerCell(x, y, side, side, gray, 4);\n    const r = s * 32 \/ 1024 \/ 2;\n    ctx.beginPath(); ctx.arc(x + side \/ 2, y + side \/ 2, r, -Math.PI \/ 2, Math.PI \/ 2); ctx.fillStyle = tplGray(gray + 2); ctx.fill();\n    ctx.beginPath(); ctx.arc(x + side \/ 2, y + side \/ 2, r, Math.PI \/ 2, Math.PI * 1.5); ctx.fillStyle = tplGray(gray - 2); ctx.fill();\n  }\n}\n\nfunction drawTplTG18QC(oiq) {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h), cfg = tplCxConfig();\n  tplFill(128);\n  const ss = tplDrawBigCx(w \/ 2, h \/ 2, cfg.big, cfg.margin, !oiq);\n  for (let x = w \/ 2; x <= w; x += ss) { tplLine(x, 0, x, h, 191); tplLine(w - x, 0, w - x, h, 191); }\n  for (let y = h \/ 2; y <= h; y += ss) { tplLine(0, y, w, y, 191); tplLine(0, h - y, w, h - y, 191); }\n  tplStrokeRect(w \/ 2 - 3 * ss + 1, h \/ 2 - 3 * ss + 1, 6 * ss - 2, 6 * ss - 2, 191, 3);\n  tplStrokeRect(1, 1, w - 3, h - 3, 191, 3);\n\n  const patch = tplDrawCxPatch(5, 5, cfg.small, !oiq);\n  tplDrawCxPatch(5, h - 5 - patch.h, cfg.small, !oiq, false, true);\n  tplDrawCxPatch(w - 5 - patch.w, h - 5 - patch.h, cfg.small, !oiq, true, true);\n  tplDrawCxPatch(w - 5 - patch.w, 5, cfg.small, !oiq, true, false);\n  tplDrawCxPatch(w \/ 2 - patch.w \/ 2, h \/ 2 - patch.h \/ 2, cfg.small, !oiq);\n\n  let gray = 8, step = oiq ? (248 - 8 + 1) \/ 16 : (248 - 8) \/ 15, i = 0;\n  let x = w \/ 2 - 3 * ss + 1, y = h \/ 2 + 2 * ss + 1;\n  for (let k = 0; k < 6; k++, y -= ss) tplDrawLuminanceSq(x, y, ss - 1, gray + i++ * step);\n  y += ss; x += ss;\n  for (let k = 0; k < 5; k++, x += ss) tplDrawLuminanceSq(x, y, ss - 1, gray + i++ * step);\n  x -= ss; y += ss;\n  for (let k = 0; k < 5; k++, y += ss) tplDrawLuminanceSq(x, y, ss - 1, gray + i++ * step);\n\n  const sss = Math.floor(51 * ss \/ 102);\n  tplRect(w \/ 2 - 2 * ss + 1, h \/ 2 + 2 * ss + 1, ss - 2, ss - 2, 0);\n  tplRect(w \/ 2 - 2 * ss + 1 + (ss - sss) \/ 2 - 1, h \/ 2 + 2 * ss + 1 + (ss - sss) \/ 2 - 1, sss - 1, sss - 1, 13);\n  tplRect(w \/ 2 + ss + 1, h \/ 2 + 2 * ss + 1, ss - 2, ss - 2, 255);\n  tplRect(w \/ 2 + ss + 1 + (ss - sss) \/ 2 - 1, h \/ 2 + 2 * ss + 1 + (ss - sss) \/ 2 - 1, sss - 1, sss - 1, 242);\n\n  tplQcBox(w \/ 2 - ss + 1, h \/ 2 + 3 * ss + 1, 2 * ss - 2, ss - 2, 128, 142, -1);\n  tplQcBox(w \/ 2 - 3 * ss + 1, h \/ 2 + 3 * ss + 1, 2 * ss - 2, ss - 2, 0, 14, -1);\n  tplQcBox(w \/ 2 + ss + 1, h \/ 2 + 3 * ss + 1, 2 * ss - 2, ss - 2, 255, 241, 1);\n\n  const bwW = 815 * ss \/ 102, bwInner = 407 * ss \/ 102, bwH = 50 * ss \/ 102, bwInnerH = 33 * ss \/ 102;\n  const bwX = w \/ 2 - bwW \/ 2, bwY = h \/ 2 - (3 * ss + 1 + bwH);\n  tplRect(bwX, bwY, bwW, bwH \/ 2, 255); tplRect(bwX, bwY + bwH \/ 2, bwW, bwH \/ 2, 0);\n  tplRect(w \/ 2 - bwInner \/ 2, h \/ 2 - (3 * ss + 1 + bwH \/ 2 + bwInnerH \/ 2), bwInner, bwInnerH \/ 2 - 1, 0);\n  tplRect(w \/ 2 - bwInner \/ 2, h \/ 2 - (3 * ss + 1 + bwH \/ 2), bwInner, bwInnerH \/ 2 - 1, 255);\n\n  tplDrawCrosstalk(patch.w);\n  const gh = 5 * ss, gw = 64 * w \/ 1024, mar = (102 - 64) * w \/ 1024 \/ 2;\n  for (let row = 0; row < gh; row++) {\n    const g = Math.round(255 * row \/ Math.max(1, gh - 1));\n    tplLine(3 + mar, h \/ 2 - gh \/ 2 + row, 3 + mar + gw, h \/ 2 - gh \/ 2 + row, g);\n    tplLine(w - (3 + mar), h \/ 2 + gh \/ 2 - row, w - (3 + mar + gw), h \/ 2 + gh \/ 2 - row, g);\n  }\n  tplStrokeRect(w \/ 2 - 3 * ss + 1, h \/ 2 - 3 * ss + 1, 6 * ss - 2, 6 * ss - 2, 191, 3);\n}\n\nfunction tplQcBox(x, y, w, h, gray, textGray, inc) {\n  tplRect(x, y, w, h, gray);\n  const chars = 'QUALITYCONTROL';\n  const size = Math.max(6, Math.floor(h * 0.26));\n  const top = y + h * 0.36, bot = y + h * 0.68;\n  const row1 = 'QUALITY', row2 = 'CONTROL';\n  [...row1].forEach((ch, i) => { tplText(ch, x + w * (.18 + i * .105), top, size, textGray + i * inc, 'center', '700'); });\n  [...row2].forEach((ch, i) => { tplText(ch, x + w * (.19 + i * .105), bot, size, textGray + (i + row1.length) * inc, 'center', '700'); });\n}\n\nfunction tplDrawCrosstalk(patchW) {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  const marRight = 30 * w \/ 1024 \/ 2, lineH = Math.max(1, 3 * s \/ 1024), marTop = 7 * h \/ 1024, marBottom = 9 * h \/ 1024, marLeft = 17 * w \/ 1024;\n  let maxW = w \/ 2 - 3 - patchW - marLeft - marRight - 10 * w \/ 1024, n = 1, seg = 1;\n  for (n = 1; 2 * seg < maxW; n++) seg *= 2;\n  const hh = marTop + n * lineH + marBottom, ww = marLeft + seg + marRight;\n  const cx = w \/ 2, cy = 6 + hh;\n  tplRect(cx - ww, cy - hh, 2 * ww - 1, hh - 1, 255);\n  tplRect(cx - ww, cy, 2 * ww - 1, hh - 1, 0);\n  const halfBar = Math.max(1, 6 * 2 * marRight \/ 30 \/ 2);\n  tplRect(cx - halfBar, cy - hh, 2 * halfBar, hh - 1, 242);\n  tplRect(cx - halfBar, cy, 2 * halfBar, hh - 1, 13);\n  seg = 1;\n  for (let i = 1; i <= n; i++) {\n    tplLine(cx - marRight, cy - (i * lineH + marBottom), cx - (marRight + seg), cy - (i * lineH + marBottom), 0, lineH);\n    tplLine(cx + marRight, cy - (i * lineH + marBottom), cx + (marRight + seg), cy - (i * lineH + marBottom), 0, lineH);\n    tplLine(cx + marRight, cy + (i * lineH + marBottom), cx + (marRight + seg), cy + (i * lineH + marBottom), 255, lineH);\n    tplLine(cx - marRight, cy + (i * lineH + marBottom), cx - (marRight + seg), cy + (i * lineH + marBottom), 255, lineH);\n    seg *= 2;\n  }\n}\n\nfunction drawTplSMPTE() {\n  const w = canvas.width, h = canvas.height, ss = h * 102 \/ 1024;\n  tplFill(128);\n  for (let x = w \/ 2; x <= w; x += ss) { tplLine(x, 0, x, h, 191); tplLine(w - x, 0, w - x, h, 191); }\n  for (let y = h \/ 2; y <= h; y += ss) { tplLine(0, y, w, y, 191); tplLine(0, h - y, w, h - y, 191); }\n  tplStrokeRect(4.5, 4.5, w - 9, h - 9, 191, 3);\n\n  const patchH = ss * 2 - 2, patchW = ss * 2.5;\n  tplSmptePatch(6, 6, patchW, patchH, false, false);\n  tplSmptePatch(w - 6 - patchW, 6, patchW, patchH, true, false);\n  tplSmptePatch(w - 6 - patchW, h - 6 - patchH, patchW, patchH, true, true);\n  tplSmptePatch(6, h - 6 - patchH, patchW, patchH, false, true);\n  tplSmptePatch(w \/ 2 - patchW \/ 2, h \/ 2 - patchH \/ 2, patchW, patchH, false, false);\n\n  const pcx = w \/ 2, pcy = h \/ 2;\n  const lum = [\n    [-3, 1, 0],[-3, 0, .1],[-3,-1,.2],[-3,-2,.3],\n    [-2,-2,.4],[-1,-2,.5],[0,-2,.5],[1,-2,.6],\n    [2,-2,.7],[2,-1,.8],[2,0,.9],[2,1,1]\n  ];\n  lum.forEach(([gx, gy, v]) => tplSmpteLum(pcx + gx * ss + 1, pcy + gy * ss + 1, ss, Math.round(255 * v), `${Math.round(v * 100)}%`));\n  const sss = 51 * ss \/ 102;\n  tplRect(pcx - 2 * ss + 1, pcy + ss + 1, ss - 2, ss - 2, 0);\n  tplRect(pcx - 2 * ss + 1 + (ss - sss) \/ 2 - 1, pcy + ss + 1 + (ss - sss) \/ 2 - 1, sss - 1, sss - 1, 13);\n  tplText('0 \/ 5', pcx - 1.5 * ss, pcy + ss + ss * .16, Math.max(7, ss \/ 5), 100, 'center', '700');\n  tplRect(pcx + ss + 1, pcy + ss + 1, ss - 2, ss - 2, 255);\n  tplRect(pcx + ss + 1 + (ss - sss) \/ 2 - 1, pcy + ss + 1 + (ss - sss) \/ 2 - 1, sss - 1, sss - 1, 242);\n  tplText('100 \/ 95', pcx + 1.5 * ss, pcy + ss + ss * .16, Math.max(7, ss \/ 5), 150, 'center', '700');\n\n  const bwW = w - 2 * ss, bwInner = bwW \/ 2, bwH = ss * .8, bwInnerH = bwH \/ 2;\n  tplRect(pcx - bwW \/ 2, pcy - (3 * ss - (ss - bwH) \/ 2), bwW, bwH, 255);\n  tplRect(pcx - bwInner \/ 2, pcy - (3 * ss - (ss - bwH) \/ 2 - (bwH - bwInnerH) \/ 2), bwInner, bwInnerH, 0);\n  tplRect(pcx - bwW \/ 2, pcy + (2 * ss + (ss - bwH) \/ 2), bwW, bwH, 0);\n  tplRect(pcx - bwInner \/ 2, pcy + (2 * ss + (ss - bwH) \/ 2 + (bwH - bwInnerH) \/ 2), bwInner, bwInnerH, 255);\n}\n\nfunction tplSmpteLum(x, y, ss, gray, label) {\n  tplRect(x, y, ss - 2, ss - 2, gray);\n  tplText(label, x + ss \/ 2, y + ss \/ 2, Math.max(8, ss \/ 3), 255, 'center', '700');\n}\n\nfunction tplSmptePatch(x, y, ww, hh, flipX, flipY) {\n  const border = 0, ss = Math.floor((hh - 2 * border) \/ 3);\n  const pw = 4 * ss + 2 * border, ph = 3 * ss + 2 * border;\n  ctx.save();\n  ctx.translate(x + (flipX ? pw : 0), y + (flipY ? ph : 0));\n  ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1);\n  tplRect(0, 0, pw - 1, ph - 1, 0);\n  [1, 2, 4].forEach((lw, row) => {\n    for (let xx = Math.floor(lw \/ 2); xx < ss - lw \/ 2; xx += 2 * lw) tplLine(xx, row * ss, xx, row * ss + ss, 255, lw);\n    for (let yy = row * ss + Math.floor(lw \/ 2); yy < row * ss + ss - lw \/ 2; yy += 2 * lw) tplLine(ss, yy, 2 * ss, yy, 255, lw);\n  });\n  tplRect(2 * ss, 0, 2 * ss - 1, 3 * ss - 1, 122);\n  [1, 2, 4].forEach((lw, row) => {\n    for (let xx = 0 + Math.floor(lw \/ 2); xx < ss - lw \/ 2; xx += 2 * lw) tplLine(2 * ss + xx, row * ss, 2 * ss + xx, row * ss + ss, 135, lw);\n    for (let yy = row * ss + Math.floor(lw \/ 2); yy < row * ss + ss - lw \/ 2; yy += 2 * lw) tplLine(3 * ss, yy, 4 * ss, yy, 135, lw);\n  });\n  ctx.restore();\n}\n\nfunction drawTplGraySteps(x, y, ww, hh, n) {\n  for (let i = 0; i < n; i++) tplRect(x + i * ww \/ n, y, ww \/ n + 1, hh, 255 * i \/ (n - 1));\n}\n\nfunction drawTplUniformity(id) {\n  const w = canvas.width, h = canvas.height;\n  const level = id.endsWith('10') ? 26 : id.endsWith('50') ? 127 : 204;\n  const qtLine = (x1, y1, x2, y2, gray) => {\n    const lw = tplPreviewLineWidth(1);\n    const off = Math.floor(lw \/ 2);\n    x1 = Math.round(x1); y1 = Math.round(y1); x2 = Math.round(x2); y2 = Math.round(y2);\n    if (y1 === y2) tplRect(Math.min(x1, x2), y1 - off, Math.abs(x2 - x1) + 1, lw, gray);\n    else if (x1 === x2) tplRect(x1 - off, Math.min(y1, y2), lw, Math.abs(y2 - y1) + 1, gray);\n    else tplLine(x1, y1, x2, y2, gray);\n  };\n  tplFill(level);\n  if (id.includes('UNL')) {\n    const pen = id.endsWith('10') ? 204 : id.endsWith('80') ? 26 : 0;\n    if (id.endsWith('50')) {\n      const ww = Math.trunc(w * 324 \/ 1024), hh = Math.trunc(h * 324 \/ 1024);\n      [[(w - ww) \/ 2, (h - hh) \/ 2], [0, 0], [w - ww, 0], [w - ww, h - hh], [0, h - hh]].forEach(([x, y]) => {\n        x = Math.round(x); y = Math.round(y);\n        qtLine(x, y, x + ww, y, pen);\n        qtLine(x + ww, y, x + ww, y + hh, pen);\n        qtLine(x + ww, y + hh, x, y + hh, pen);\n        qtLine(x, y + hh, x, y, pen);\n      });\n      return;\n    }\n\n    const center = [Math.trunc(w \/ 2), Math.trunc(h \/ 2)];\n    const shift = Math.trunc(h \/ 256);\n    const rsize = Math.trunc((h - shift * 4) \/ 3);\n    const v = [\n      [-Math.trunc(rsize \/ 2) + shift, -Math.trunc(rsize \/ 2) + shift],\n      [ Math.trunc(rsize \/ 2) - shift, -Math.trunc(rsize \/ 2) + shift],\n      [ Math.trunc(rsize \/ 2) - shift,  Math.trunc(rsize \/ 2) - shift],\n      [-Math.trunc(rsize \/ 2) + shift,  Math.trunc(rsize \/ 2) - shift]\n    ];\n    const step = rsize + 2 * shift;\n    const centers = [\n      center,\n      [center[0] + step * 2, center[1]],\n      [center[0] - step * 2, center[1]],\n      [center[0] + step, Math.trunc(rsize \/ 2) + 1 - shift],\n      [center[0] - step, Math.trunc(rsize \/ 2) + 1 - shift],\n      [center[0] + step, h + shift - 1 - Math.trunc(rsize \/ 2)],\n      [center[0] - step, h + shift - 1 - Math.trunc(rsize \/ 2)]\n    ];\n    centers.forEach(([cx, cy]) => {\n      qtLine(cx + v[0][0], cy + v[0][1], cx + v[1][0], cy + v[1][1], pen);\n      qtLine(cx + v[1][0], cy + v[1][1], cx + v[2][0], cy + v[2][1], pen);\n      qtLine(cx + v[2][0], cy + v[2][1], cx + v[3][0], cy + v[3][1], pen);\n      qtLine(cx + v[3][0], cy + v[3][1], cx + v[0][0], cy + v[0][1], pen);\n    });\n  }\n}\n\nfunction drawTplLinePair(id) {\n  const w = canvas.width, h = canvas.height;\n  const level = id.endsWith('10') ? 26 : id.endsWith('50') ? 128 : 228;\n  const line = id.startsWith('TG18-LP') ? (id.includes('10') ? 29 : id.includes('50') ? 143 : 255) : (id.endsWith('10') ? 128 : id.endsWith('50') ? 26 : 128);\n  tplFill(level);\n  if (id.startsWith('TG18-LP')) {\n    const vertical = id.includes('LPV');\n    for (let i = 0; i < (vertical ? w : h); i += 2) {\n      if (vertical) tplLine(i, 0, i, h, line); else tplLine(0, i, w, i, line);\n    }\n    return;\n  }\n  drawTplFiveAreas(id, id.startsWith('TG18-R') ? 'r' : 'ns');\n}\n\nfunction drawTplFiveAreas(id, mode) {\n  const w = canvas.width, h = canvas.height;\n  tplFill(51);\n  const ww = w * 324 \/ 1024, hh = h * 324 \/ 1024;\n  const rects = [\n    [(w - ww) \/ 2, (h - hh) \/ 2], [0, 0], [w - ww, 0], [w - ww, h - hh], [0, h - hh]\n  ];\n  rects.forEach(([x, y]) => {\n    const bg = id.endsWith('10') ? 26 : id.endsWith('50') ? 128 : 228;\n    const fg = id.endsWith('10') ? 29 : id.endsWith('50') ? 143 : 255;\n    const dot = id.endsWith('10') ? 128 : id.endsWith('50') ? 26 : 128;\n    tplRect(x, y, ww, hh, bg);\n    if (mode === 'r') {\n      if (id.includes('RH')) tplLine(x, y + hh \/ 2, x + ww, y + hh \/ 2, fg);\n      else tplLine(x + ww \/ 2, y, x + ww \/ 2, y + hh, fg);\n    }\n    const d = Math.min(w, h) * 60 \/ 1024;\n    [[-d,-d],[d,-d],[-d,d],[d,d]].forEach(([dx,dy]) => tplRect(x + ww \/ 2 + dx, y + hh \/ 2 + dy, 1, 1, dot));\n  });\n}\n\nfunction drawTplTG18PX() {\n  tplFill(0);\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h), d = s * 100 \/ 1024, gray = [255, 191, 128, 64];\n  for (let x = d \/ 2, i = 0; x < w; x += d, i++) {\n    for (let y = d \/ 2, j = 0; y < h; y += d, j++) {\n      tplRect(x, y, 1, 1, gray[(i + j) % 4]);\n    }\n  }\n}\n\nfunction drawTplPixelGrid() {\n  drawTplTG18PX();\n}\n\nfunction drawTplPDC() {\n  const w = canvas.width, h = canvas.height;\n  tplFill(128);\n  for (let x = 0; x < w; x += 2) tplLine(x, 0, x, h * 0.08, 255);\n  for (let y = h * 0.9; y < h; y += 2) tplLine(0, y, w, y, 255);\n  for (let y = 0; y < h; y += 8) tplLine(0, y, w * 0.09, y, 255 * y \/ h);\n  for (let x = w * 0.91; x < w; x += 8) tplLine(x, 0, x, h, 255 * (x - w * 0.91) \/ (w * 0.09));\n}\n\nfunction drawTplCX() {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  const small = s > 1600 ? 8 : 4, big = s > 1600 ? 4 : 2, margin = 5;\n  tplFill(0);\n  tplDrawSmallCxField(small);\n  tplDrawBigCx(w \/ 2, h \/ 2, big, margin, true);\n}\n\nfunction drawTplMP() {\n  const w = canvas.width, h = canvas.height;\n  tplFill(0);\n  const barW = Math.floor((768 * w \/ 1024) \/ 16), ww = barW * 16, hh = 768 * h \/ 1024;\n  const ox = w \/ 2 - ww \/ 2, oy = h \/ 2 - hh \/ 2;\n  tplStrokeRect(ox - 1, oy - 1, ww + 1, hh + 1, 32);\n  for (let bar = 0; bar < 16; bar++) {\n    let prev = -1;\n    for (let ln = 0; ln < hh; ln++) {\n      const gray = Math.floor(bar * 16 + ln * 16 \/ hh);\n      tplLine(ox + bar * barW, oy + ln, ox + (bar + 1) * barW, oy + ln, gray);\n      if (ln !== 0 && ln !== hh - 1 && gray !== prev) tplRect(ox + bar * barW, oy + ln, 3, 1, bar < 8 ? gray + 16 : gray - 16);\n      prev = gray;\n    }\n  }\n}\n\nfunction drawTplAD() {\n  const w = canvas.width, h = canvas.height;\n  tplFill(0);\n  const wside = w * 60 \/ 1024, hside = h * 60 \/ 1024;\n  const ox = w \/ 2 - 7 * wside \/ 2, oy = h \/ 2 - 7 * hside \/ 2;\n  for (let row = 0; row < 7; row++) {\n    for (let col = 1; col <= 7; col++) {\n      const x = ox + (col - 1) * wside, y = oy + row * hside, g = col + 7 * row;\n      for (let yy = 4; yy < hside - 1; yy += 4) tplLine(x, y + yy, x + wside, y + yy, g);\n      tplStrokeRect(x, y, wside, hside, 50);\n      if (row === 0) tplText(String(col), x + wside \/ 2, y - 8, Math.max(7, Math.min(w,h)\/90), 50);\n      if (col === 1) tplText(String(row), x - 10, y + hside \/ 2, Math.max(7, Math.min(w,h)\/90), 50);\n    }\n  }\n}\n\nfunction drawTplAFC() {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h), scale = s > 1600 ? 2 : 1;\n  tplFill(128);\n  const ln = scale * 2, sq = scale * 64, gridG = 143;\n  for (let x = w\/2 + sq - 2; x < w; x += sq) { tplLine(x, 0, x, h, gridG, ln); tplLine(w - x, 0, w - x, h, gridG, ln); }\n  for (let y = h\/2 + sq - 2; y < h; y += sq) { tplLine(0, y, w, y, gridG, ln); tplLine(0, h - y, w, h - y, gridG, ln); }\n  tplLine(w\/2, 0, w\/2, h, gridG, 2*ln); tplLine(0, h\/2, w, h\/2, gridG, 2*ln);\n  const size = scale * 128 - 4 * ln;\n  [[w\/2-size\/2,h\/2-size\/2],[0,0],[w-size,0],[w-size,h-size],[0,h-size]].forEach(([x,y]) => tplAfcSquare(x, y, size, scale));\n}\n\nfunction tplAfcSquare(x, y, size, scale) {\n  const ln = scale * 2, step = scale * 128 \/ 4 - 2 * 2 * ln \/ 4;\n  tplRect(x, y, size, size, 128); tplStrokeRect(x, y, size, size, 143, 2 * ln);\n  for (let yy = step \/ 2; yy <= scale * 4 * 32; yy += step) {\n    [[128+2,scale*2],[128+3,scale*3],[128+4,scale*4],[128+6,scale*6]].forEach(([g,sz],i) => {\n      tplRect(x + step \/ 2 + i * step - sz \/ 2, y + yy - sz \/ 2, sz, sz, g);\n    });\n  }\n}\n\nfunction drawTplBR() {\n  const w = canvas.width, h = canvas.height;\n  tplFill(128);\n  const cols = 4, rows = 4;\n  for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) {\n    tplCheckerCell(x*w\/cols, y*h\/rows, w\/cols, h\/rows, (x+y)%2 ? 180 : 75, 20);\n  }\n}\n\nfunction drawTplGrayscale() {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  tplFill(127);\n  const squareH = 240 * s \/ 1200, squareY = 150 * h \/ 1200, stripH = (720 - 480) * s \/ 1200, steps = 16;\n  tplRect(w\/2 - squareH\/2, squareY, squareH, squareH, 0);\n  tplRect(w\/2 - squareH\/2, h - squareY - squareH, squareH, squareH, 255);\n  for (let i=0; i<steps; i++) tplRect(i*w\/steps, h\/2-stripH\/2, w\/steps+1, stripH, i*255\/steps);\n}\n\nfunction drawTplResolution() {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  const ww = 150 * s \/ 1024, hh = 150 * s \/ 1024;\n  tplFill(127);\n  const hv = (x,y,label) => {\n    tplRect(x,y,ww,hh,g1); drawTplLineZone(Math.round(x),Math.round(y),Math.round(ww),Math.round(hh),'h');\n    tplRect(x+ww,y,ww,hh,g1); drawTplLineZone(Math.round(x+ww),Math.round(y),Math.round(ww),Math.round(hh),'v');\n    if (label) tplText(label, x + 3*ww\/2, y + hh\/2, Math.max(9, s\/48), 255, 'left');\n  };\n  const g1 = 255;\n  [[0,0],[w-ww*2,0],[0,h-2*hh],[w-2*ww,h-2*hh]].forEach(([x,y]) => hv(x,y));\n  hv(w\/2-ww, h\/2-hh\/2, '100%');\n  \/\/ 25% and 6.25% use low-contrast stripes like C++.\n  tplLowContrastPair(w\/2-ww, h\/2-3*hh\/2, ww, hh, 95, 159, '25%');\n  tplLowContrastPair(w\/2-ww, h\/2+hh\/2, ww, hh, 119, 135, '6.25%');\n}\n\nfunction tplLowContrastPair(x, y, ww, hh, bg, fg, label) {\n  tplRect(x,y,ww,hh,bg); for (let yy=0; yy<hh; yy+=2) tplLine(x,y+yy,x+ww,y+yy,fg);\n  tplRect(x+ww,y,ww,hh,bg); for (let xx=0; xx<ww; xx+=2) tplLine(x+ww+xx,y,x+ww+xx,y+hh,fg);\n  tplText(label, x + 3*ww\/2, y + hh\/2, Math.max(9, Math.min(canvas.width,canvas.height)\/48), 255, 'left');\n}\n\nfunction drawTplLineZone(x, y, ww, hh, type) {\n  if (ww <= 0 || hh <= 0) return;\n\n  if (type === 'v') {\n    const stripe = ctx.createPattern(tplPatternCanvas(2, 1, (px) => px === 0 ? 0 : 255), 'repeat');\n    ctx.fillStyle = stripe;\n    ctx.fillRect(x, y, ww, hh);\n    return;\n  }\n\n  if (type === 'h') {\n    const stripe = ctx.createPattern(tplPatternCanvas(1, 2, (_px, py) => py === 0 ? 0 : 255), 'repeat');\n    ctx.fillStyle = stripe;\n    ctx.fillRect(x, y, ww, hh);\n    return;\n  }\n\n  if (type === 'v2') {\n    const stripe = ctx.createPattern(tplPatternCanvas(4, 1, (px) => px < 2 ? 255 : 0), 'repeat');\n    ctx.fillStyle = stripe;\n    ctx.fillRect(x, y, ww, hh);\n    return;\n  }\n\n  if (type === 'h2') {\n    const stripe = ctx.createPattern(tplPatternCanvas(1, 4, (_px, py) => py < 2 ? 255 : 0), 'repeat');\n    ctx.fillStyle = stripe;\n    ctx.fillRect(x, y, ww, hh);\n    return;\n  }\n\n  if (type === 'diag') {\n    const diag = ctx.createPattern(tplPatternCanvas(2, 2, (px, py) => ((px + py) & 1) ? 255 : 0), 'repeat');\n    ctx.fillStyle = diag;\n    ctx.fillRect(x, y, ww, hh);\n    return;\n  }\n\n  const checker = ctx.createPattern(tplPatternCanvas(2, 2, (px, py) => ((px + py) & 1) === 0 ? 0 : 255), 'repeat');\n  ctx.fillStyle = checker;\n  ctx.fillRect(x, y, ww, hh);\n}\n\nfunction tplPatternCanvas(w, h, valueFn) {\n  const c = document.createElement('canvas');\n  c.width = w;\n  c.height = h;\n  const cctx = c.getContext('2d');\n  const img = cctx.createImageData(w, h);\n  for (let y = 0; y < h; y++) {\n    for (let x = 0; x < w; x++) {\n      const v = valueFn(x, y);\n      const i = (y * w + x) * 4;\n      img.data[i] = v;\n      img.data[i + 1] = v;\n      img.data[i + 2] = v;\n      img.data[i + 3] = 255;\n    }\n  }\n  cctx.putImageData(img, 0, 0);\n  return c;\n}\n\nfunction drawTplIEC57() {\n  const w = canvas.width, h = canvas.height, s = Math.min(w, h);\n  const ss = s \/ 12, d = s - 2;\n  tplFill(127);\n  for (let x = w \/ 2; x <= w; x += ss) { tplLine(x, 0, x, h, 255); tplLine(w - x, 0, w - x, h, 255); }\n  for (let y = h \/ 2; y <= h; y += ss) { tplLine(0, y, w, y, 255); tplLine(0, h - y, w, h - y, 255); }\n\n  const markSize = Math.max(10, Math.floor(s \/ 40));\n  ctx.font = `${markSize}px Arial, sans-serif`;\n  ctx.textAlign = 'left';\n  ctx.textBaseline = 'top';\n  ctx.fillStyle = '#000';\n  const m = ctx.measureText('@');\n  const rtW = Math.ceil(m.width), rtH = Math.ceil(markSize * 1.15), gap = 3;\n  const horD = (w - rtW) % (rtW + gap), verD = (h - rtH) % (rtH + gap);\n  let jumped = false;\n  for (let x = 0; x + rtW <= w; x += rtW + gap) {\n    if (!jumped && x >= w \/ 2) { x += horD; jumped = true; }\n    ctx.fillText('@', x, 0); ctx.fillText('@', x, h - rtH);\n  }\n  jumped = false;\n  for (let y = rtH + gap; y + rtH <= h; y += rtH + gap) {\n    if (!jumped && y >= h \/ 2) { y += verD; jumped = true; }\n    ctx.fillText('@', 0, y); ctx.fillText('@', w - rtW, y);\n  }\n\n  tplEllipse(w \/ 2, h \/ 2, d \/ 2, d \/ 2, 255, false, 1);\n  tplRect(w \/ 2 - ss, h \/ 2 - ss, 2 * ss, 2 * ss, 127);\n  const x = ss * (Math.floor((w - 50) \/ 2 \/ ss) - 1);\n  const y = ss * (Math.floor((h - 50) \/ 2 \/ ss) - 1);\n  [[x,y],[-x,-y],[-x,y],[x,-y]].forEach(([dx,dy]) => tplRect(w\/2 + dx - ss, h\/2 + dy - ss, 2*ss, 2*ss, 127));\n}\nfunction drawTplIECANG() { drawTplAD(); }\nfunction drawTplIECGD() {\n  const w = canvas.width, h = canvas.height;\n  tplFill(128);\n  const wm = Math.trunc(w \/ 4), hm = Math.trunc(h \/ 4);\n  const cx = Math.trunc(w \/ 2), cy = Math.trunc(h \/ 2);\n  const r = Math.trunc(Math.min(w, h) \/ 2 * 11 \/ 24);\n  const lw = tplPreviewLineWidth(1);\n  const dark = 64;\n\n  [[-wm, -hm], [-wm, hm], [wm, -hm], [wm, hm]].forEach(([dx, dy]) => {\n    tplEllipse(cx + dx, cy + dy, r, r, dark, false, lw);\n  });\n\n  tplLine(0, 0, w, h, dark, lw);\n  tplLine(0, h, w, 0, dark, lw);\n  tplLine(0, cy, cx, 0, dark, lw);\n  tplLine(0, cy, cx, h, dark, lw);\n  tplLine(w, cy, cx, 0, dark, lw);\n  tplLine(w, cy, cx, h, dark, lw);\n  tplLine(cx - wm, 0, cx - wm, h, dark, lw);\n  tplLine(cx + wm, 0, cx + wm, h, dark, lw);\n  tplLine(0, cy + hm, w, cy + hm, dark, lw);\n  tplLine(0, cy - hm, w, cy - hm, dark, lw);\n\n  const edge = Math.max(10, Math.ceil(10 \/ Math.max(0.2, Number(window.tpgCurrentScale || tpgCurrentScale || 1))));\n  for (let y = 0; y < h; y += 2) {\n    const g = 255 * ((Math.floor(y \/ 2)) % 2);\n    tplRect(0, y, edge, 2, g);\n    tplRect(w - edge, y, edge, 2, g);\n  }\n  for (let x = 0; x < w; x += 2) {\n    const g = 255 * ((Math.floor(x \/ 2)) % 2);\n    tplRect(x, 0, 2, edge, g);\n    tplRect(x, h - edge, 2, edge, g);\n  }\n}\n\nfunction drawTplTG270sQC() {\n  const w = canvas.width, h = canvas.height;\n  tplFill(128);\n  const cols = 6, rows = 3, gap = Math.min(w,h)*.015;\n  const cw = (w*.82 - gap*(cols-1))\/cols, ch = (h*.55-gap*(rows-1))\/rows;\n  const sx = w*.09, sy = h*.17;\n  for (let i=0;i<18;i++) {\n    const g = i*15, x=sx+(i%cols)*(cw+gap), y=sy+Math.floor(i\/cols)*(ch+gap);\n    tplRect(x,y,cw,ch,g); tplText(String(g), x+cw\/2, y+ch\/2, Math.min(cw,ch)*.22, g>128?0:255, 'center', '700');\n    tplRect(x+cw*.08,y+ch*.08,cw*.22,ch*.14,Math.max(0,g-5));\n    tplRect(x+cw*.70,y+ch*.78,cw*.22,ch*.14,Math.min(255,g+5));\n  }\n}\n\nfunction drawTplAVEC() {\n  const w = canvas.width, h = canvas.height;\n  tplFill(128);\n  for (let i=0;i<33;i++) tplLine(i*w\/32,0,i*w\/32,h,255);\n  for (let i=0;i<19;i++) tplLine(0,i*h\/18,w,i*h\/18,255);\n  const x=w*4\/32, ww=w*24\/32, y=h*3\/18, row=h*12\/18\/5;\n  ['#fff','#ff0','#0ff','#0f0','#f0f','#f00','#00f','#000'].forEach((c,i)=>{ctx.fillStyle=c;ctx.fillRect(x+i*ww\/8,y,ww\/8+1,row);});\n  drawTplGraySteps(x,y+row,ww,row,24);\n  for(let i=0;i<ww;i++){ctx.fillStyle=(i%8<4)?'#000':'#fff';ctx.fillRect(x+i,y+row*2,1,row);}\n  ctx.fillStyle='#fff';ctx.fillRect(x,y+row*3,ww,row);\n  ['#f00','#0f0','#00f'].forEach((c,ch)=>{const yy=y+row*4+ch*row\/3;for(let i=0;i<24;i++){let v=i<12?255:Math.round(255*(23-i)\/11);ctx.fillStyle=ch===0?`rgb(${v},0,0)`:ch===1?`rgb(0,${v},0)`:`rgb(0,0,${v})`;ctx.fillRect(x+i*ww\/24,yy,ww\/24+1,row\/3);}});\n  tplEllipse(w\/2,h\/2,h\/2,h\/2,255,false,1);\n}\n\nfunction drawTplBrightness() {\n  tplFill(0);\n  const w=canvas.width,h=canvas.height, levels=[13,23,33,43,53,63], bw=w*.094,bh=h*.274,gap=w*.054;\n  const start=(w-(levels.length*bw+(levels.length-1)*gap))\/2, y=h*.272;\n  levels.forEach((v,i)=>{tplRect(start+i*(bw+gap),y,bw,bh,v);tplText(String(v),start+i*(bw+gap)+bw\/2,y+bh+h*.06,Math.min(w,h)*.028,220);});\n}\nfunction drawTplContrast() {\n  tplFill(255);\n  const w=canvas.width,h=canvas.height, levels=[242,232,222,212,202,192], bw=w*.094,bh=h*.274,gap=w*.054;\n  const start=(w-(levels.length*bw+(levels.length-1)*gap))\/2, y=h*.272;\n  levels.forEach((v,i)=>{tplRect(start+i*(bw+gap),y,bw,bh,v);tplText(String(v),start+i*(bw+gap)+bw\/2,y+bh+h*.06,Math.min(w,h)*.028,0);});\n}\nfunction drawTplBrightnessBlackLevel() {\n  const w=canvas.width,h=canvas.height; tplFill(0);\n  const n=31, mx=Math.max(8,w\/80), avail=w-2*mx, sw=avail\/n, bw=sw*.8, top=h*.18, bh=h*.64;\n  for(let i=1;i<=n;i++){tplRect(mx+(i-1)*sw,top,bw,bh,i); tplText(String(i),mx+(i-1)*sw+bw\/2,top+bh+h*.035,Math.max(7,Math.min(w,h)*.018),210);}\n  tplText('Adjust brightness so 17 - 31 are visible, 16 and lower are invisible',w\/2,h*.08,Math.min(w,h)*.018,210);\n}\nfunction drawTplGamutStripes() {\n  const w=canvas.width,h=canvas.height, low=16, high=235, midA=115, midB=136, pairs=[[1,1,1],[1,0,0],[0,1,0],[0,0,1]], sh=h\/8, rectW=w*.109, rectX=(w-rectW)\/2;\n  pairs.forEach((m,pair)=>{[[low,high],[midA,midB]].forEach(([a,b],k)=>{const y=(pair*2+k)*sh,g=ctx.createLinearGradient(0,0,w,0);g.addColorStop(0,`rgb(${a*m[0]},${a*m[1]},${a*m[2]})`);g.addColorStop(1,`rgb(${b*m[0]},${b*m[1]},${b*m[2]})`);ctx.fillStyle=g;ctx.fillRect(0,y,w,sh);});tplStrokeRect(rectX,pair*2*sh,rectW,sh,255,Math.max(2,Math.min(w,h)\/400));});\n}\nfunction drawTplFormatOverscan() {\n  const w=canvas.width,h=canvas.height; tplFill(255);\n  const margin=Math.max(12,w\/30), gap=Math.max(12,w\/16), size=Math.max(24,Math.min((w-2*margin-2*gap)\/3,h-2*Math.max(12,h\/12)));\n  const sx=(w-(3*size+2*gap))\/2,y=(h-size)\/2, rects=[[sx,y],[sx+size+gap,y],[sx+2*(size+gap),y]];\n  rects.forEach(([x,yy],idx)=>{\n    drawTplLineZone(Math.round(x), Math.round(yy), Math.round(size), Math.round(size), idx===0 ? 'v' : idx===1 ? 'diag' : 'h');\n    tplStrokeRect(x,yy,size,size,210);\n  });\n}\n\nfunction drawTplImagePattern(p) {\n  const src = `${TPL_ASSET_URL}\/${p.asset}`;\n  let img = tplImageCache[src];\n  if (!img) {\n    img = new Image();\n    img.onload = () => drawCurrent();\n    img.onerror = () => { img._failed = true; drawCurrent(); };\n    img.src = src;\n    tplImageCache[src] = img;\n  }\n  tplFill(0);\n  if (img.complete && img.naturalWidth && !img._failed) {\n    const scale = Math.min(canvas.width\/img.naturalWidth, canvas.height\/img.naturalHeight);\n    const iw = img.naturalWidth*scale, ih=img.naturalHeight*scale;\n    ctx.imageSmoothingEnabled = true;\n    ctx.drawImage(img,(canvas.width-iw)\/2,(canvas.height-ih)\/2,iw,ih);\n    ctx.imageSmoothingEnabled = false;\n  } else {\n    tplText(p.name, canvas.width\/2, canvas.height\/2 - 18, Math.min(canvas.width,canvas.height)*.04, 230, 'center', '700');\n    tplText(`Missing asset: assets\/images\/testpatternlib\/${p.asset}`, canvas.width\/2, canvas.height\/2 + 24, Math.min(canvas.width,canvas.height)*.018, 160);\n  }\n}\n\/\/ ===== SAVE CUSTOM HIGH-RES =====\nfunction saveCustomImage() {\n  const c = custom;\n  const totalW = c.totalW, totalH = c.totalH;\n\n  \/\/ Save original canvas ref\n  const origW = canvas.width, origH = canvas.height;\n\n  \/\/ Resize main canvas to full resolution temporarily\n  canvas.width = totalW;\n  canvas.height = totalH;\n  drawCustomPattern();\n\n  \/\/ Download\n  const link = document.createElement('a');\n  link.download = (c.imageName || 'testpattern').replace(\/[^a-zA-Z0-9_\\- ]\/g, '') + '.jpg';\n  link.href = canvas.toDataURL('image\/jpeg', 0.95);\n  link.click();\n\n  \/\/ Restore canvas size\n  canvas.width = origW;\n  canvas.height = origH;\n  drawCurrent();\n}\n\n\/\/ ===== SAVE PRESET IMAGE =====\nfunction savePresetImage() {\n  const p = patterns[currentPattern];\n  if (p.id === 'custom') { saveCustomImage(); return; }\n  \n  const dlW = patternState.resW || 1920;\n  const dlH = patternState.resH || 1080;\n\n  \/\/ ---- IMAGE EXPORT LOGIC ----\n  \/\/ Download standard pattern as what you see\n  const link = document.createElement('a');\n  link.download = `perfectchroma-${p.id}-${dlW}x${dlH}.jpg`;\n  \n  \/\/ Swap context and canvas size temporarily\n  const oldCanvasW = canvas.width; const oldCanvasH = canvas.height;\n  canvas.width = dlW; canvas.height = dlH;\n  \n  const oldExporting = window.tpgExporting;\n  window.tpgExporting = true;\n  drawCurrent(); \/\/ draws to Main canvas but using input dimensions\n  window.tpgExporting = oldExporting;\n  \n  const imgData = canvas.toDataURL('image\/jpeg', 0.95);\n  \n  \/\/ Restore\n  canvas.width = oldCanvasW; canvas.height = oldCanvasH;\n  drawCurrent();\n  \n  link.href = imgData;\n  link.click();\n}\n\/\/ ===== FULLSCREEN =====\nfunction toggleFullscreen() {\n  const app = document.getElementById('app');\n  if (!document.fullscreenElement) {\n    app.requestFullscreen().then(()=>{ app.classList.add('fullscreen'); isFullscreen=true; showFsHint(); setTimeout(resizeCanvas,100); }).catch(()=>{});\n  } else document.exitFullscreen();\n}\ndocument.addEventListener('fullscreenchange',()=>{\n  const app=document.getElementById('app');\n  if(!document.fullscreenElement){ app.classList.remove('fullscreen'); isFullscreen=false; setTimeout(resizeCanvas,100); }\n});\nfunction showFsHint() {\n  const h=document.getElementById('fs-hint'); h.classList.add('show'); setTimeout(()=>h.classList.remove('show'),3000);\n}\n\n\/\/ ===== KEYBOARD =====\ndocument.addEventListener('keydown',(e)=>{\n  if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT'||e.target.tagName==='TEXTAREA') return;\n  switch(e.key){\n    case 'f':case 'F':e.preventDefault();toggleFullscreen();break;\n    case 'ArrowLeft':prevPattern();break;\n    case 'ArrowRight':nextPattern();break;\n    case ' ':e.preventDefault();isPaused=!isPaused;if(!isPaused){cancelAnimations();drawCurrent();}break;\n    case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9':selectPattern(+e.key-1);break;\n    case '0':selectPattern(9);break;\n    case '-':selectPattern(10);break;\n    case '=':selectPattern(11);break;\n  }\n});\nfunction showShortcuts() {\n  document.getElementById('shortcuts-overlay').classList.add('tpg-show');\n}\nfunction tpgCloseShortcuts() {\n  document.getElementById('shortcuts-overlay').classList.remove('tpg-show');\n}\n\n\/\/ ===== INIT =====\nwindow.addEventListener('resize', resizeCanvas);\n\nbuildSidebar();\nbuildRightPanel();\nsetTimeout(resizeCanvas, 50);\n\n\/\/ =====================================================================\n\/\/ MODAL WORKSPACE\n\/\/ =====================================================================\n(function () {\n  const overlay     = document.getElementById('tpg-modal-overlay');\n  const modal       = document.getElementById('tpg-modal');\n  const modalInner  = document.getElementById('tpg-modal-inner');\n  const escHint     = document.getElementById('tpg-esc-hint');\n  const launchBtn   = document.getElementById('tpg-launch-btn');\n\n  if (!overlay || !modal || !modalInner) return;\n\n  let isOpen        = false;\n  let savedScroll   = 0;\n  let escHintTimer  = null;\n  let closingTimer  = null;\n\n  \/\/ \u2500\u2500 Open \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  window.tpgOpenModal = function () {\n    if (isOpen) return;\n    isOpen = true;\n\n    \/\/ Save scroll position so we can restore it on close\n    savedScroll = window.scrollY;\n\n    \/\/ Lock body scroll\n    document.body.style.overflow = 'hidden';\n    document.body.style.top      = '-' + savedScroll + 'px';\n    document.body.style.position = 'fixed';\n    document.body.style.width    = '100%';\n\n    \/\/ Show overlay + modal (rAF ensures transition fires)\n    overlay.classList.add('tpg-modal-open');\n    modal.classList.add('tpg-modal-open');\n    \/\/ Small delay so browser paints the initial state before transitioning\n    requestAnimationFrame(() => {\n      requestAnimationFrame(() => {\n        modalInner.classList.remove('tpg-modal-closing');\n        \/\/ Force reflow then apply open state (already handled by .tpg-modal-open on #tpg-modal)\n      });\n    });\n\n    \/\/ Resize canvas to fill modal\n    setTimeout(resizeCanvas, 350);\n\n    \/\/ Show ESC hint, then fade it out after 3.5 s\n    if (escHint) {\n      escHint.classList.remove('hidden');\n      clearTimeout(escHintTimer);\n      escHintTimer = setTimeout(() => escHint.classList.add('hidden'), 3500);\n    }\n  };\n\n  \/\/ \u2500\u2500 Close \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  window.tpgCloseModal = function () {\n    if (!isOpen) return;\n    isOpen = false;\n\n    \/\/ Trigger exit animation\n    modalInner.classList.add('tpg-modal-closing');\n    overlay.classList.remove('tpg-modal-open');\n\n    clearTimeout(closingTimer);\n    closingTimer = setTimeout(() => {\n      modal.classList.remove('tpg-modal-open');\n      modalInner.classList.remove('tpg-modal-closing');\n\n      \/\/ Unlock body scroll and restore position\n      document.body.style.overflow = '';\n      document.body.style.top      = '';\n      document.body.style.position = '';\n      document.body.style.width    = '';\n      window.scrollTo(0, savedScroll);\n\n      \/\/ Resize canvas back to inline size\n      setTimeout(resizeCanvas, 50);\n    }, 340); \/\/ matches transition duration\n  };\n\n  \/\/ \u2500\u2500 ESC key \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  document.addEventListener('keydown', (e) => {\n    if (e.key === 'Escape' && isOpen) {\n      \/\/ Let the existing fullscreen ESC handler fire first if in fullscreen\n      if (!document.fullscreenElement) {\n        tpgCloseModal();\n      }\n    }\n  });\n\n  \/\/ \u2500\u2500 Resize: keep canvas correct while modal is open \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  window.addEventListener('resize', () => {\n    if (isOpen) resizeCanvas();\n  });\n\n  \/\/ \u2500\u2500 IntersectionObserver: show\/hide launch button gracefully \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const launchArea = document.getElementById('tpg-launch-area');\n  if (launchArea && 'IntersectionObserver' in window) {\n    const io = new IntersectionObserver((entries) => {\n      \/\/ purely cosmetic \u2014 button is always visible, observer unused for now\n    }, { threshold: 0.1 });\n    io.observe(launchArea);\n  }\n\n})();\n<\/script>\n\n<\/div><!-- .tpg-wrap -->\n<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"","protected":false},"author":2,"featured_media":0,"parent":499,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-487","page","type-page","status-publish","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.0 (Yoast SEO v27.4) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Kostenloser Monitor-Testbild-Generator | PerfectChroma<\/title>\n<meta name=\"description\" content=\"Nutzen Sie diesen kostenlosen Monitor-Testbild-Generator, um Farbgenauigkeit, Gamma, Pixelfehler, Sch\u00e4rfe und herunterladbare Bilder Ihres Displays zu pr\u00fcfen.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/\" \/>\n<meta property=\"og:locale\" content=\"de_DE\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Testbild-Generator\" \/>\n<meta property=\"og:description\" content=\"Nutzen Sie diesen kostenlosen Monitor-Testbild-Generator, um Farbgenauigkeit, Gamma, Pixelfehler, Sch\u00e4rfe und herunterladbare Bilder Ihres Displays zu pr\u00fcfen.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/\" \/>\n<meta property=\"og:site_name\" content=\"PerfectChroma\" \/>\n<meta property=\"article:modified_time\" content=\"2026-05-09T05:52:18+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/ressourcen\\\/testbild-generator\\\/\",\"url\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/ressourcen\\\/testbild-generator\\\/\",\"name\":\"Kostenloser Monitor-Testbild-Generator | PerfectChroma\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/#website\"},\"datePublished\":\"2026-03-04T13:07:07+00:00\",\"dateModified\":\"2026-05-09T05:52:18+00:00\",\"description\":\"Nutzen Sie diesen kostenlosen Monitor-Testbild-Generator, um Farbgenauigkeit, Gamma, Pixelfehler, Sch\u00e4rfe und herunterladbare Bilder Ihres Displays zu pr\u00fcfen.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/ressourcen\\\/testbild-generator\\\/#breadcrumb\"},\"inLanguage\":\"de\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/perfectchroma.com\\\/de\\\/ressourcen\\\/testbild-generator\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/ressourcen\\\/testbild-generator\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/startseite\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Ressourcen\",\"item\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/resources\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Testbild-Generator\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/#website\",\"url\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/\",\"name\":\"PerfectChroma\",\"description\":\"Professional Display Calibration Software Powered by Qubyx\",\"publisher\":{\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"de\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/#organization\",\"name\":\"PerfectChroma\",\"url\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/perfectchroma.com\\\/wp-content\\\/uploads\\\/2026\\\/03\\\/PerfectChroma-logo-scaled.png\",\"contentUrl\":\"https:\\\/\\\/perfectchroma.com\\\/wp-content\\\/uploads\\\/2026\\\/03\\\/PerfectChroma-logo-scaled.png\",\"width\":2560,\"height\":449,\"caption\":\"PerfectChroma\"},\"image\":{\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/de\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Kostenloser Monitor-Testbild-Generator | PerfectChroma","description":"Nutzen Sie diesen kostenlosen Monitor-Testbild-Generator, um Farbgenauigkeit, Gamma, Pixelfehler, Sch\u00e4rfe und herunterladbare Bilder Ihres Displays zu pr\u00fcfen.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/","og_locale":"de_DE","og_type":"article","og_title":"Testbild-Generator","og_description":"Nutzen Sie diesen kostenlosen Monitor-Testbild-Generator, um Farbgenauigkeit, Gamma, Pixelfehler, Sch\u00e4rfe und herunterladbare Bilder Ihres Displays zu pr\u00fcfen.","og_url":"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/","og_site_name":"PerfectChroma","article_modified_time":"2026-05-09T05:52:18+00:00","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/","url":"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/","name":"Kostenloser Monitor-Testbild-Generator | PerfectChroma","isPartOf":{"@id":"https:\/\/perfectchroma.com\/de\/#website"},"datePublished":"2026-03-04T13:07:07+00:00","dateModified":"2026-05-09T05:52:18+00:00","description":"Nutzen Sie diesen kostenlosen Monitor-Testbild-Generator, um Farbgenauigkeit, Gamma, Pixelfehler, Sch\u00e4rfe und herunterladbare Bilder Ihres Displays zu pr\u00fcfen.","breadcrumb":{"@id":"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/#breadcrumb"},"inLanguage":"de","potentialAction":[{"@type":"ReadAction","target":["https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/perfectchroma.com\/de\/ressourcen\/testbild-generator\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/perfectchroma.com\/de\/startseite\/"},{"@type":"ListItem","position":2,"name":"Ressourcen","item":"https:\/\/perfectchroma.com\/de\/resources\/"},{"@type":"ListItem","position":3,"name":"Testbild-Generator"}]},{"@type":"WebSite","@id":"https:\/\/perfectchroma.com\/de\/#website","url":"https:\/\/perfectchroma.com\/de\/","name":"PerfectChroma","description":"Professional Display Calibration Software Powered by Qubyx","publisher":{"@id":"https:\/\/perfectchroma.com\/de\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/perfectchroma.com\/de\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"de"},{"@type":"Organization","@id":"https:\/\/perfectchroma.com\/de\/#organization","name":"PerfectChroma","url":"https:\/\/perfectchroma.com\/de\/","logo":{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/perfectchroma.com\/de\/#\/schema\/logo\/image\/","url":"https:\/\/perfectchroma.com\/wp-content\/uploads\/2026\/03\/PerfectChroma-logo-scaled.png","contentUrl":"https:\/\/perfectchroma.com\/wp-content\/uploads\/2026\/03\/PerfectChroma-logo-scaled.png","width":2560,"height":449,"caption":"PerfectChroma"},"image":{"@id":"https:\/\/perfectchroma.com\/de\/#\/schema\/logo\/image\/"}}]}},"_hostinger_reach_plugin_has_subscription_block":false,"_hostinger_reach_plugin_is_elementor":false,"_links":{"self":[{"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/pages\/487","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/comments?post=487"}],"version-history":[{"count":17,"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/pages\/487\/revisions"}],"predecessor-version":[{"id":1337,"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/pages\/487\/revisions\/1337"}],"up":[{"embeddable":true,"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/pages\/499"}],"wp:attachment":[{"href":"https:\/\/perfectchroma.com\/de\/wp-json\/wp\/v2\/media?parent=487"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}