﻿{"id":489,"date":"2026-03-04T13:07:07","date_gmt":"2026-03-04T13:07:07","guid":{"rendered":"https:\/\/perfectchroma.com\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/"},"modified":"2026-05-09T05:52:18","modified_gmt":"2026-05-09T05:52:18","slug":"%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0","status":"publish","type":"page","link":"https:\/\/perfectchroma.com\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/","title":{"rendered":"\ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"489\" class=\"elementor elementor-489 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>\ubb34\ub8cc \uc628\ub77c\uc778 \ub3c4\uad6c<\/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            \uc774 \ubb34\ub8cc <strong>\ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30<\/strong>\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc0c9\uc0c1 \uc815\ud655\ub3c4, \uac10\ub9c8 \uc751\ub2f5 \ubc0f \ud53d\uc140 \ubb34\uacb0\uc131\uc744 \ud3c9\uac00\ud558\uc138\uc694. \ub610\ud55c \ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c \uc9c1\uc811 SMPTE \uceec\ub7ec\ubc14, \uadf8\ub808\uc774\uc2a4\ucf00\uc77c \ub7a8\ud504, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Checkerboard_pattern\" target=\"_blank\" rel=\"noopener\" style=\"color:#60a5fa;text-decoration:underline\">\uccb4\ucee4\ubcf4\ub4dc \ud328\ud134<\/a>, \ub370\ub4dc \ud53d\uc140 \ud0d0\uc9c0\uae30, \uc0ac\uc6a9\uc790 \uc815\uc758 \ud14c\uc2a4\ud2b8 \uc774\ubbf8\uc9c0\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.        <\/p>\n        <p style=\"color:rgba(255,255,255,0.4); font-size:0.95rem; line-height:1.75;\">\n            <a href=\"https:\/\/perfectchroma.com\/ko\/monitor-calibration-user-guide\/\" style=\"color:#60a5fa;text-decoration:underline\">\uc804\uccb4 \ubaa8\ub2c8\ud130 \uce98\ub9ac\ube0c\ub808\uc774\uc158<\/a> \uc138\uc158\uc744 \uc900\ube44\ud558\uac70\ub098 \ub4dc\ub77c\uc774\ubc84 \uc5c5\ub370\uc774\ud2b8 \ud6c4 \ub514\uc2a4\ud50c\ub808\uc774\ub97c \ube60\ub974\uac8c \ud655\uc778\ud560 \ub54c, \uc774 <strong>\ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30<\/strong>\ub294 \ubb38\uc81c\uac00 \uc791\uc5c5\uc5d0 \uc601\ud5a5\uc744 \ubbf8\uce58\uae30 \uc804\uc5d0 \uc2dd\ubcc4\ud558\ub294 \ub370 \ub3c4\uc6c0\uc744 \uc90d\ub2c8\ub2e4. \ubaa8\ub4e0 \ud328\ud134\uc740 \uc624\ud504\ub77c\uc778 \uc0ac\uc6a9\uc744 \uc704\ud574 \uace0\ud574\uc0c1\ub3c4 \uc774\ubbf8\uc9c0\ub85c \ub2e4\uc6b4\ub85c\ub4dc\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.        <\/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;\">\uc774 \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\uc5d0 \ud3ec\ud568\ub41c \uac83<\/h2>\n        <p style=\"color:rgba(255,255,255,0.5); font-size:1rem; line-height:1.8; margin-bottom:1.5rem;\">\n            \uc774 <strong>\ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30<\/strong>\ub294 13\uac00\uc9c0 \uc804\ubb38\uae09 \ud328\ud134\uc744 \uc81c\uacf5\ud569\ub2c8\ub2e4: SMPTE \uceec\ub7ec\ubc14, \uadf8\ub808\uc774\uc2a4\ucf00\uc77c \ub7a8\ud504, RGB \uadf8\ub798\ub514\uc5b8\ud2b8, \ub2e8\uc0c9, \uadf8\ub9ac\ub4dc \uc815\ub82c, \uccb4\ucee4\ubcf4\ub4dc, \uc120\uba85\ub3c4 \ucd08\uc810, \ub370\ub4dc \ud53d\uc140 \ud0d0\uc9c0\uae30, \uc751\ub2f5 \uc2dc\uac04, \uc2dc\uc57c\uac01, \ubc88\uc778 \ud14c\uc2a4\ud2b8, \uba85\uc554\ube44, \uadf8\ub9ac\uace0 \uc644\uc804\ud55c \uc0ac\uc6a9\uc790 \uc815\uc758 \uc0dd\uc131\uae30. \ud558\ub098\uc758 \ub3c4\uad6c\ub85c \ub514\uc2a4\ud50c\ub808\uc774\uc758 \ubaa8\ub4e0 \uce21\uba74\uc744 \ud14c\uc2a4\ud2b8\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.        <\/p>\n        <p style=\"color:rgba(255,255,255,0.4); font-size:0.95rem; line-height:1.8; margin-bottom:2rem;\">\n            \ub610\ud55c \uc0ac\uc6a9\uc790 \uc815\uc758 \uc0dd\uc131\uae30\ub294 \uad6c\uc131 \uac00\ub2a5\ud55c \uacb9\uce68 \uc601\uc5ed, \uadf8\ub9ac\ub4dc \ub77c\uc778 \ubc0f \ub85c\uace0 \uc6cc\ud130\ub9c8\ud06c\uac00 \uc788\ub294 \uba40\ud2f0 \ub514\uc2a4\ud50c\ub808\uc774 \uc124\uc815\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4. PerfectChroma\uc758 <a href=\"https:\/\/perfectchroma.com\/ko\/monitor-calibration-presets\/\" style=\"color:#60a5fa;text-decoration:underline\">\uc2a4\ub9c8\ud2b8 \uce98\ub9ac\ube0c\ub808\uc774\uc158 \ud504\ub9ac\uc14b<\/a>\uc758 \uc644\ubcbd\ud55c \ubcf4\uc644\uc7ac\uc785\ub2c8\ub2e4. \uc804\uccb4 \ud558\ub4dc\uc6e8\uc5b4 \uce98\ub9ac\ube0c\ub808\uc774\uc158\uc774 \ud544\uc694\ud558\uc2e0\uac00\uc694? <a href=\"https:\/\/perfectchroma.com\/ko\/%ea%b0%80%ea%b2%a9\/\" style=\"color:#60a5fa;text-decoration:underline\">\uc694\uae08\uc81c \ubcf4\uae30<\/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":503,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-489","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>\ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30 | PerfectChroma<\/title>\n<meta name=\"description\" content=\"\uc774 \ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\ub85c \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc0c9\uc0c1 \uc815\ud655\ub3c4, \uac10\ub9c8, \ub370\ub4dc \ud53d\uc140, \uc120\uba85\ub3c4\uc640 \ub2e4\uc6b4\ub85c\ub4dc \uac00\ub2a5\ud55c \uc774\ubbf8\uc9c0\ub97c \ud655\uc778\ud558\uc138\uc694.\" \/>\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\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/\" \/>\n<meta property=\"og:locale\" content=\"ko_KR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"\ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\" \/>\n<meta property=\"og:description\" content=\"\uc774 \ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\ub85c \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc0c9\uc0c1 \uc815\ud655\ub3c4, \uac10\ub9c8, \ub370\ub4dc \ud53d\uc140, \uc120\uba85\ub3c4\uc640 \ub2e4\uc6b4\ub85c\ub4dc \uac00\ub2a5\ud55c \uc774\ubbf8\uc9c0\ub97c \ud655\uc778\ud558\uc138\uc694.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/perfectchroma.com\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/\" \/>\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\\\/ko\\\/%ec%9e%90%eb%a3%8c\\\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\\\/\",\"url\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/%ec%9e%90%eb%a3%8c\\\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\\\/\",\"name\":\"\ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30 | PerfectChroma\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/#website\"},\"datePublished\":\"2026-03-04T13:07:07+00:00\",\"dateModified\":\"2026-05-09T05:52:18+00:00\",\"description\":\"\uc774 \ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\ub85c \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc0c9\uc0c1 \uc815\ud655\ub3c4, \uac10\ub9c8, \ub370\ub4dc \ud53d\uc140, \uc120\uba85\ub3c4\uc640 \ub2e4\uc6b4\ub85c\ub4dc \uac00\ub2a5\ud55c \uc774\ubbf8\uc9c0\ub97c \ud655\uc778\ud558\uc138\uc694.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/%ec%9e%90%eb%a3%8c\\\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\\\/#breadcrumb\"},\"inLanguage\":\"ko-KR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/%ec%9e%90%eb%a3%8c\\\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/%ec%9e%90%eb%a3%8c\\\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/%ed%99%88\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"\ub9ac\uc18c\uc2a4\",\"item\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/resources\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"\ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/#website\",\"url\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/\",\"name\":\"PerfectChroma\",\"description\":\"Professional Display Calibration Software Powered by Qubyx\",\"publisher\":{\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"ko-KR\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/#organization\",\"name\":\"PerfectChroma\",\"url\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"ko-KR\",\"@id\":\"https:\\\/\\\/perfectchroma.com\\\/ko\\\/#\\\/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\\\/ko\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"\ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30 | PerfectChroma","description":"\uc774 \ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\ub85c \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc0c9\uc0c1 \uc815\ud655\ub3c4, \uac10\ub9c8, \ub370\ub4dc \ud53d\uc140, \uc120\uba85\ub3c4\uc640 \ub2e4\uc6b4\ub85c\ub4dc \uac00\ub2a5\ud55c \uc774\ubbf8\uc9c0\ub97c \ud655\uc778\ud558\uc138\uc694.","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\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/","og_locale":"ko_KR","og_type":"article","og_title":"\ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30","og_description":"\uc774 \ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\ub85c \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc0c9\uc0c1 \uc815\ud655\ub3c4, \uac10\ub9c8, \ub370\ub4dc \ud53d\uc140, \uc120\uba85\ub3c4\uc640 \ub2e4\uc6b4\ub85c\ub4dc \uac00\ub2a5\ud55c \uc774\ubbf8\uc9c0\ub97c \ud655\uc778\ud558\uc138\uc694.","og_url":"https:\/\/perfectchroma.com\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/","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\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/","url":"https:\/\/perfectchroma.com\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/","name":"\ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30 | PerfectChroma","isPartOf":{"@id":"https:\/\/perfectchroma.com\/ko\/#website"},"datePublished":"2026-03-04T13:07:07+00:00","dateModified":"2026-05-09T05:52:18+00:00","description":"\uc774 \ubb34\ub8cc \ubaa8\ub2c8\ud130 \ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30\ub85c \ub514\uc2a4\ud50c\ub808\uc774\uc758 \uc0c9\uc0c1 \uc815\ud655\ub3c4, \uac10\ub9c8, \ub370\ub4dc \ud53d\uc140, \uc120\uba85\ub3c4\uc640 \ub2e4\uc6b4\ub85c\ub4dc \uac00\ub2a5\ud55c \uc774\ubbf8\uc9c0\ub97c \ud655\uc778\ud558\uc138\uc694.","breadcrumb":{"@id":"https:\/\/perfectchroma.com\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/#breadcrumb"},"inLanguage":"ko-KR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/perfectchroma.com\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/perfectchroma.com\/ko\/%ec%9e%90%eb%a3%8c\/%ed%85%8c%ec%8a%a4%ed%8a%b8-%ed%8c%a8%ed%84%b4-%ec%83%9d%ec%84%b1%ea%b8%b0\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/perfectchroma.com\/ko\/%ed%99%88\/"},{"@type":"ListItem","position":2,"name":"\ub9ac\uc18c\uc2a4","item":"https:\/\/perfectchroma.com\/ko\/resources\/"},{"@type":"ListItem","position":3,"name":"\ud14c\uc2a4\ud2b8 \ud328\ud134 \uc0dd\uc131\uae30"}]},{"@type":"WebSite","@id":"https:\/\/perfectchroma.com\/ko\/#website","url":"https:\/\/perfectchroma.com\/ko\/","name":"PerfectChroma","description":"Professional Display Calibration Software Powered by Qubyx","publisher":{"@id":"https:\/\/perfectchroma.com\/ko\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/perfectchroma.com\/ko\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"ko-KR"},{"@type":"Organization","@id":"https:\/\/perfectchroma.com\/ko\/#organization","name":"PerfectChroma","url":"https:\/\/perfectchroma.com\/ko\/","logo":{"@type":"ImageObject","inLanguage":"ko-KR","@id":"https:\/\/perfectchroma.com\/ko\/#\/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\/ko\/#\/schema\/logo\/image\/"}}]}},"_hostinger_reach_plugin_has_subscription_block":false,"_hostinger_reach_plugin_is_elementor":false,"_links":{"self":[{"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/pages\/489","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/comments?post=489"}],"version-history":[{"count":17,"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/pages\/489\/revisions"}],"predecessor-version":[{"id":1339,"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/pages\/489\/revisions\/1339"}],"up":[{"embeddable":true,"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/pages\/503"}],"wp:attachment":[{"href":"https:\/\/perfectchroma.com\/ko\/wp-json\/wp\/v2\/media?parent=489"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}