{
    "version": "https://jsonfeed.org/version/1",
    "title": "Пэйнт.рф",
    "description": "",
    "home_page_url": "https://пэйнт.рф",
    "feed_url": "https://пэйнт.рф/feed.json",
    "user_comment": "",
    "author": {
        "name": "пэйнт.рф"
    },
    "items": [
        {
            "id": "https://пэйнт.рф/servis-7.html",
            "url": "https://пэйнт.рф/servis-7.html",
            "title": "Сервис по поиску относительных цветов",
            "summary": "🎨 Поиск относительных цветов Найдите идеальный оттенок на человеческом языке с помощью модели CIE LCh Настройте параметры и нажмите \"Найти\", чтобы получить список релевантных цветов&hellip;",
            "content_html": "<header class=\"header\">\n<h1>🎨 Поиск относительных цветов</h1>\n<p>Найдите идеальный оттенок на человеческом языке с помощью модели CIE LCh</p>\n</header><main class=\"container\">\n<div class=\"main-content-wrapper\"><!-- Левая колонка: Ввод и Управление -->\n<aside class=\"controls-section\">\n<div class=\"card\">\n<h2 class=\"card-title\">🖌️ 1. Исходный цвет</h2>\n<div class=\"color-input-group\">\n<div class=\"color-picker-wrapper\"><input type=\"color\" id=\"colorPicker\" value=\"#3498db\"></div>\n<div class=\"hex-input-wrapper\"><input type=\"text\" id=\"hexInput\" placeholder=\"#3498db\" value=\"#3498db\"></div>\n</div>\n<div id=\"initialColorPreview\" class=\"initial-color-preview\"></div>\n<div class=\"color-values\">\n<div class=\"value-box\"><label>HEX</label> <span id=\"hexValue\">#3498db</span></div>\n<div class=\"value-box\"><label>LCh</label> <span id=\"lchValue\">-</span></div>\n</div>\n</div>\n<div class=\"card\">\n<h2 class=\"card-title\">🎛️ 2. Настройки</h2>\n<div class=\"slider-container\">\n<div class=\"slider-label\">Светлее / Темнее<span id=\"lightnessValue\">0</span></div>\n<input type=\"range\" id=\"lightnessSlider\" min=\"-50\" max=\"50\" value=\"0\"></div>\n<div class=\"slider-container\">\n<div class=\"slider-label\">Насыщеннее / Глухее<span id=\"chromaValue\">0</span></div>\n<input type=\"range\" id=\"chromaSlider\" min=\"-50\" max=\"50\" value=\"0\"></div>\n<div class=\"slider-container\">\n<div class=\"slider-label\">Теплее / Холоднее<span id=\"hueValue\">0°</span></div>\n<input type=\"range\" id=\"hueSlider\" min=\"-180\" max=\"180\" value=\"0\"></div>\n<div class=\"action-buttons\"><button id=\"resetBtn\" class=\"btn btn-secondary\">Сброс</button> <button id=\"findBtn\" class=\"btn btn-primary\">Найти</button></div>\n</div>\n</aside>\n<!-- Правая колонка: Результаты -->\n<section class=\"results-section\">\n<div class=\"card\">\n<h2 class=\"card-title\">🎯 Результаты поиска</h2>\n<div id=\"resultsContainer\" class=\"results-grid\">\n<p style=\"color: var(--light-text-color); grid-column: 1/-1; text-align: center; padding: 2rem 0;\">Настройте параметры и нажмите \"Найти\", чтобы получить список релевантных цветов из базы.</p>\n</div>\n</div>\n</section>\n</div>\n<section class=\"card info-section\">\n<h2 class=\"card-title\">📖 Что такое CIE LCh?</h2>\n<p><strong>CIE LCh (CIELCh)</strong> — это цветовое пространство, которое описывает цвет так, как его воспринимает человеческий глаз, в отличие от технических форматов RGB или HEX.</p>\n<ul>\n<li><strong>L* (Lightness) — Светлота:</strong> Яркость цвета (от 0 до 100).</li>\n<li><strong>C* (Chroma) — Насыщенность:</strong> \"Чистота\" или интенсивность цвета (от 0 до 100+).</li>\n<li><strong>h* (Hue) — Цветовой тон:</strong> Оттенок цвета по кругу (от 0° до 360°). Это и есть \"теплота\".</li>\n</ul>\n<p>Именно поэтому, меняя эти параметры, мы можем говорить о цвете на понятном языке: <em>\"сделай его теплее (увеличь h*) и светлее (увеличь L*)\"</em>.</p>\n</section>\n</main>\n<div id=\"copyTooltip\" class=\"copy-tooltip\">Скопировано!</div>\n<p>\n<script>\n\t\tdocument.addEventListener('DOMContentLoaded', () => {\n\t\t\t// --- DOM Элементы ---\n\t\t\tconst colorPicker = document.getElementById('colorPicker');\n\t\t\tconst hexInput = document.getElementById('hexInput');\n\t\t\tconst initialColorPreview = document.getElementById('initialColorPreview');\n\t\t\tconst hexValueSpan = document.getElementById('hexValue');\n\t\t\tconst lchValueSpan = document.getElementById('lchValue');\n\n\t\t\tconst lightnessSlider = document.getElementById('lightnessSlider');\n\t\t\tconst chromaSlider = document.getElementById('chromaSlider');\n\t\t\tconst hueSlider = document.getElementById('hueSlider');\n\t\t\tconst lightnessValue = document.getElementById('lightnessValue');\n\t\t\tconst chromaValue = document.getElementById('chromaValue');\n\t\t\tconst hueValue = document.getElementById('hueValue');\n\n\t\t\tconst findBtn = document.getElementById('findBtn');\n\t\t\tconst resetBtn = document.getElementById('resetBtn');\n\t\t\tconst resultsContainer = document.getElementById('resultsContainer');\n\t\t\tconst copyTooltip = document.getElementById('copyTooltip');\n\n\t\t\t// --- Состояние и данные ---\n\t\t\tlet baseColorHex = '#3498db';\n\t\t\tlet baseColorLCh = {\n\t\t\t\tl: 0,\n\t\t\t\tc: 0,\n\t\t\t\th: 0\n\t\t\t};\n\t\t\tlet paintDatabase = [];\n\n\t\t\tasync function loadPalettes() {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await fetch('/data/palettes-list.json');\n\t\t\t\t\tconst palettes = await response.json();\n\n\t\t\t\t\tconst results = [];\n\t\t\t\t\tfor (const p of palettes) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst res = await fetch(p.file);\n\t\t\t\t\t\t\tif (res.ok) {\n\t\t\t\t\t\t\t\tconst data = await res.json();\n\t\t\t\t\t\t\t\tresults.push(...data);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tconsole.warn('Пропущен файл:', p.file);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tpaintDatabase = results.map(c => ({\n\t\t\t\t\t\tname: c.name,\n\t\t\t\t\t\tbrand: c.series || c.ColorFamily || c.collection?.split('/')[0] || '',\n\t\t\t\t\t\thex: c.hex\n\t\t\t\t\t})).filter(c => c.hex);\n\n\t\t\t\t\tresultsContainer.innerHTML = '<p style=\"color: var(--light-text-color); grid-column: 1/-1; text-align: center; padding: 2rem 0;\">Загружено ' + paintDatabase.length + ' цветов. Настройте параметры и нажмите \"Найти\".</p>';\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconsole.error('Ошибка загрузки палитр:', e);\n\t\t\t\t\tresultsContainer.innerHTML = '<p style=\"color: red; grid-column: 1/-1;\">Ошибка загрузки базы цветов</p>';\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// --- Функции преобразования цветов (Hex <-> LCh) ---\n\t\t\tfunction hex2lch(hex) {\n\t\t\t\tlet r = parseInt(hex.slice(1, 3), 16) / 255,\n\t\t\t\t\tg = parseInt(hex.slice(3, 5), 16) / 255,\n\t\t\t\t\tb = parseInt(hex.slice(5, 7), 16) / 255;\n\t\t\t\tr = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;\n\t\t\t\tg = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;\n\t\t\t\tb = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;\n\t\t\t\tlet x = r * 0.4124 + g * 0.3576 + b * 0.1805,\n\t\t\t\t\ty = r * 0.2126 + g * 0.7152 + b * 0.0722,\n\t\t\t\t\tz = r * 0.0193 + g * 0.1192 + b * 0.9505;\n\t\t\t\tx /= 0.95047;\n\t\t\t\ty /= 1.00000;\n\t\t\t\tz /= 1.08883;\n\t\t\t\tlet l = 116 * y - 16;\n\t\t\t\tlet a = 500 * (x - y);\n\t\t\t\tlet b_lab = 200 * (y - z);\n\t\t\t\tlet c = Math.sqrt(a * a + b_lab * b_lab);\n\t\t\t\tlet h = Math.atan2(b_lab, a) * (180 / Math.PI);\n\t\t\t\tif (h < 0) h += 360;\n\t\t\t\treturn [l, c, h];\n\t\t\t}\n\n\t\t\tfunction lch2hex(l, c, h) {\n\t\t\t\tconst h_rad = h * (Math.PI / 180);\n\t\t\t\tconst a = c * Math.cos(h_rad);\n\t\t\t\tconst b = c * Math.sin(h_rad);\n\t\t\t\tlet y = (l + 16) / 116;\n\t\t\t\tlet x = a / 500 + y;\n\t\t\t\tlet z = y - b / 200;\n\t\t\t\tconst x2 = x * x * x,\n\t\t\t\t\ty2 = y * y * y,\n\t\t\t\t\tz2 = z * z * z;\n\t\t\t\tx = (x2 > 0.008856) ? x2 : (x - 16 / 116) / 7.787;\n\t\t\t\ty = (y2 > 0.008856) ? y2 : (y - 16 / 116) / 7.787;\n\t\t\t\tz = (z2 > 0.008856) ? z2 : (z - 16 / 116) / 7.787;\n\t\t\t\tx *= 0.95047;\n\t\t\t\ty *= 1.00000;\n\t\t\t\tz *= 1.08883;\n\t\t\t\tlet r = x * 3.2406 - y * 1.5372 - z * 0.4986;\n\t\t\t\tlet g = -x * 0.9689 + y * 1.8758 + z * 0.0415;\n\t\t\t\tlet b_s = x * 0.0557 - y * 0.2040 + z * 1.0570;\n\t\t\t\tr = (r > 0.0031308) ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;\n\t\t\t\tg = (g > 0.0031308) ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;\n\t\t\t\tb_s = (b_s > 0.0031308) ? 1.055 * Math.pow(b_s, 1 / 2.4) - 0.055 : 12.92 * b_s;\n\t\t\t\tconst toHex = c => Math.round(Math.max(0, Math.min(1, c)) * 255).toString(16).padStart(2, '0');\n\t\t\t\treturn `#${toHex(r)}${toHex(g)}${toHex(b_s)}`;\n\t\t\t}\n\n\t\t\t// --- Основная логика ---\n\t\t\tfunction updateColorDisplay(hex) {\n\t\t\t\tbaseColorHex = hex;\n\t\t\t\tbaseColorLCh = hex2lch(hex);\n\t\t\t\tcolorPicker.value = hex;\n\t\t\t\thexInput.value = hex;\n\t\t\t\thexValueSpan.textContent = hex;\n\t\t\t\tlchValueSpan.textContent = `L:${baseColorLCh[0].toFixed(1)} C:${baseColorLCh[1].toFixed(1)} h:${baseColorLCh[2].toFixed(1)}°`;\n\t\t\t\tinitialColorPreview.style.backgroundColor = hex;\n\t\t\t\tupdatePreviewFromSliders();\n\t\t\t}\n\n\t\t\tfunction updatePreviewFromSliders() {\n\t\t\t\tconst lOffset = parseFloat(lightnessSlider.value),\n\t\t\t\t\tcOffset = parseFloat(chromaSlider.value),\n\t\t\t\t\thOffset = parseFloat(hueSlider.value);\n\t\t\t\tlightnessValue.textContent = `+${lOffset}`;\n\t\t\t\tchromaValue.textContent = `+${cOffset}`;\n\t\t\t\thueValue.textContent = `${hOffset > 0 ? '+' : ''}${hOffset}°`;\n\n\t\t\t\tif (lOffset === 0 && cOffset === 0 && hOffset === 0) {\n\t\t\t\t\tinitialColorPreview.style.backgroundColor = baseColorHex;\n\t\t\t\t} else {\n\t\t\t\t\tconst newL = Math.max(0, Math.min(100, baseColorLCh[0] + lOffset));\n\t\t\t\t\tconst newC = Math.max(0, baseColorLCh[1] + cOffset);\n\t\t\t\t\tlet newH = baseColorLCh[2] + hOffset;\n\t\t\t\t\tif (newH < 0) newH += 360;\n\t\t\t\t\tif (newH > 360) newH -= 360;\n\t\t\t\t\tinitialColorPreview.style.backgroundColor = lch2hex(newL, newC, newH);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction findRelativeColors() {\n\t\t\t\tconst targetL = Math.max(0, Math.min(100, baseColorLCh[0] + parseFloat(lightnessSlider.value)));\n\t\t\t\tconst targetC = Math.max(0, baseColorLCh[1] + parseFloat(chromaSlider.value));\n\t\t\t\tlet targetH = baseColorLCh[2] + parseFloat(hueSlider.value);\n\t\t\t\tif (targetH < 0) targetH += 360;\n\t\t\t\tif (targetH > 360) targetH -= 360;\n\n\t\t\t\tconst colorsWithDistance = paintDatabase.map(paint => {\n\t\t\t\t\tconst paintLch = hex2lch(paint.hex);\n\t\t\t\t\tconst distance = Math.sqrt(Math.pow(targetL - paintLch[0], 2) + Math.pow(targetC - paintLch[1], 2) + Math.pow(targetH - paintLch[2], 2));\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...paint,\n\t\t\t\t\t\tdistance\n\t\t\t\t\t};\n\t\t\t\t}).sort((a, b) => a.distance - b.distance);\n\n\t\t\t\trenderResults(colorsWithDistance.slice(0, 12));\n\t\t\t}\n\n\t\t\tfunction renderResults(results) {\n\t\t\t\tresultsContainer.innerHTML = '';\n\t\t\t\tif (results.length === 0) {\n\t\t\t\t\tresultsContainer.innerHTML = '<p>Ничего не найдено.</p>';\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tresults.forEach(paint => {\n\t\t\t\t\tconst card = document.createElement('div');\n\t\t\t\t\tcard.className = 'color-card';\n\t\t\t\t\tconst paintLch = hex2lch(paint.hex);\n\t\t\t\t\tcard.innerHTML = `\n                <div class=\"color-card-swatch\" style=\"background-color: ${paint.hex};\"></div>\n                <div class=\"color-card-body\">\n                    <h3 class=\"color-card-title\">${paint.name}</h3>\n                    <p class=\"color-card-brand\">${paint.brand}</p>\n                    <div class=\"color-card-codes\" data-hex=\"${paint.hex}\">${paint.hex}</div>\n                </div>\n            `;\n\t\t\t\t\tresultsContainer.appendChild(card);\n\t\t\t\t});\n\t\t\t\tdocument.querySelectorAll('.color-card-codes').forEach(codeBox => {\n\t\t\t\t\tcodeBox.addEventListener('click', function() {\n\t\t\t\t\t\tnavigator.clipboard.writeText(this.dataset.hex).then(() => {\n\t\t\t\t\t\t\tcopyTooltip.classList.add('show');\n\t\t\t\t\t\t\tsetTimeout(() => copyTooltip.classList.remove('show'), 1500);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tfunction resetControls() {\n\t\t\t\tlightnessSlider.value = 0;\n\t\t\t\tchromaSlider.value = 0;\n\t\t\t\thueSlider.value = 0;\n\t\t\t\tupdatePreviewFromSliders();\n\t\t\t}\n\n\t\t\t// --- Слушатели событий ---\n\t\t\tcolorPicker.addEventListener('input', (e) => updateColorDisplay(e.target.value));\n\t\t\thexInput.addEventListener('input', (e) => {\n\t\t\t\tconst hex = e.target.value;\n\t\t\t\tif (/^#[0-9A-F]{6}$/i.test(hex)) updateColorDisplay(hex);\n\t\t\t});\n\t\t\t[lightnessSlider, chromaSlider, hueSlider].forEach(slider => slider.addEventListener('input', updatePreviewFromSliders));\n\t\t\tfindBtn.addEventListener('click', findRelativeColors);\n\t\t\tresetBtn.addEventListener('click', resetControls);\n\n\t\t\t// --- Инициализация ---\n\t\t\tupdateColorDisplay(baseColorHex);\n\t\t\tloadPalettes();\n\t\t});\n\t</script>\n</p>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-15T13:38:07+03:00",
            "date_modified": "2026-03-21T15:39:26+03:00"
        },
        {
            "id": "https://пэйнт.рф/servis-5.html",
            "url": "https://пэйнт.рф/servis-5.html",
            "title": "Сервис 5",
            "summary": "⭐️ LCh Сервис цветов Выберите цвет L (светлота)50 C (насыщенность)20 h (тон)210 L 50 a 0 b 0 Корректировка #7A92A3 Светлота50% Насыщенность20 Тон210° ☀️ +Светлее🌑&hellip;",
            "content_html": "<div class=\"c\">\n<h1>⭐️ LCh Сервис цветов</h1>\n<div class=\"g\">\n<div class=\"p\">\n<h3>Выберите цвет</h3>\n<div class=\"i\"><input type=\"color\" id=\"p\" value=\"#7a92a3\"><input type=\"text\" id=\"h\" value=\"#7A92A3\"></div>\n<div class=\"v\">\n<div class=\"r\">L (светлота)<span id=\"lv\">50</span></div>\n<div class=\"b\">\n<div id=\"lb\" class=\"f\" style=\"width: 50%; background: linear-gradient(90deg,#000,#fff);\"></div>\n</div>\n<div class=\"r\">C (насыщенность)<span id=\"cv\">20</span></div>\n<div class=\"b\">\n<div id=\"cb\" class=\"f\" style=\"width: 20%; background: linear-gradient(90deg,#94a3b8,#1e293b);\"></div>\n</div>\n<div class=\"r\">h (тон)<span id=\"hv\">210</span></div>\n<div class=\"b\">\n<div id=\"hb\" class=\"f\" style=\"width: 58%; background: linear-gradient(90deg,#ef4444,#22c55e,#3b82f6);\"></div>\n</div>\n</div>\n<div class=\"lab\">\n<div class=\"lb\">\n<div class=\"lb_l\">L</div>\n<div id=\"labL\" class=\"lb_v\">50</div>\n</div>\n<div class=\"lb\">\n<div class=\"lb_l\">a</div>\n<div id=\"labA\" class=\"lb_v\">0</div>\n</div>\n<div class=\"lb\">\n<div class=\"lb_l\">b</div>\n<div id=\"labB\" class=\"lb_v\">0</div>\n</div>\n</div>\n</div>\n<div class=\"p\">\n<h3>Корректировка</h3>\n<div id=\"pv\" style=\"background: #7a92a3;\"><span id=\"ph\">#7A92A3</span></div>\n<div class=\"sl\">\n<div class=\"sh\">Светлота<span id=\"slv\">50%</span></div>\n<input type=\"range\" id=\"sL\" min=\"0\" max=\"100\" value=\"50\"></div>\n<div class=\"sl\">\n<div class=\"sh\">Насыщенность<span id=\"scv\">20</span></div>\n<input type=\"range\" id=\"sC\" min=\"0\" max=\"100\" value=\"20\"></div>\n<div class=\"sl\">\n<div class=\"sh\">Тон<span id=\"shv\">210°</span></div>\n<input type=\"range\" id=\"sH\" min=\"0\" max=\"360\" value=\"210\"></div>\n<div class=\"bt\">☀️ +Светлее🌑 -Темнее🌈 +Насыщеннее🌫 -Бледнее🔥 +Теплее❄️ -Холоднее</div>\n</div>\n<div class=\"p\">\n<div class=\"rs\">Ближайшие цвета</div>\n<select id=\"paletteSelect\" style=\"width: 100%; padding: .4rem; margin-bottom: .5rem; border: 1px solid var(--border); border-radius: 6px;\">\n<option>Загрузка палитр...</option>\n</select>\n<div id=\"ml\" class=\"ml\"></div>\n</div>\n</div>\n</div>\n<p>\n<script>\n\t\tfunction hexToRgb(h) {\n\t\t\tvar r = parseInt(h.slice(1, 3), 16),\n\t\t\t\tg = parseInt(h.slice(3, 5), 16),\n\t\t\t\tb = parseInt(h.slice(5, 7), 16);\n\t\t\treturn {\n\t\t\t\tr: r,\n\t\t\t\tg: g,\n\t\t\t\tb: b\n\t\t\t}\n\t\t}\n\n\t\tfunction rgbToHex(r, g, b) {\n\t\t\tvar t = function(n) {\n\t\t\t\tn = Math.max(0, Math.min(255, Math.round(n))).toString(16);\n\t\t\t\treturn n.length === 1 ? '0' + n : n\n\t\t\t};\n\t\t\treturn '#' + t(r) + t(g) + t(b)\n\t\t}\n\n\t\tfunction rgbToXyz(r, g, b) {\n\t\t\tr = r / 255;\n\t\t\tg = g / 255;\n\t\t\tb = b / 255;\n\t\t\tr = r > .04045 ? Math.pow((r + .055) / 1.055, 2.4) : r / 12.92;\n\t\t\tg = g > .04045 ? Math.pow((g + .055) / 1.055, 2.4) : g / 12.92;\n\t\t\tb = b > .04045 ? Math.pow((b + .055) / 1.055, 2.4) : b / 12.92;\n\t\t\treturn {\n\t\t\t\tx: (r * .4124 + g * .3576 + b * .1805) * 100,\n\t\t\t\ty: (r * .2126 + g * .7152 + b * .0722) * 100,\n\t\t\t\tz: (r * .0193 + g * .1192 + b * .9505) * 100\n\t\t\t}\n\t\t}\n\n\t\tfunction xyzToLab(x, y, z) {\n\t\t\tvar refX = 95.047,\n\t\t\t\trefY = 100,\n\t\t\t\trefZ = 108.883;\n\t\t\tx /= refX;\n\t\t\ty /= refY;\n\t\t\tz /= refZ;\n\t\t\tx = x > .008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116;\n\t\t\ty = y > .008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116;\n\t\t\tz = z > .008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116;\n\t\t\treturn {\n\t\t\t\tL: 116 * y - 16,\n\t\t\t\ta: 500 * (x - y),\n\t\t\t\tb: 200 * (y - z)\n\t\t\t}\n\t\t}\n\n\t\tfunction labToXyz(L, a, b) {\n\t\t\tvar refX = 95.047,\n\t\t\t\trefY = 100,\n\t\t\t\trefZ = 108.883,\n\t\t\t\ty = (L + 16) / 116,\n\t\t\t\tx = a / 500 + y,\n\t\t\t\tz = y - b / 200,\n\t\t\t\tx3 = Math.pow(x, 3),\n\t\t\t\ty3 = Math.pow(y, 3),\n\t\t\t\tz3 = Math.pow(z, 3);\n\t\t\treturn {\n\t\t\t\tx: (x3 > .008856 ? x3 : (x - 16 / 116) / 7.787) * refX,\n\t\t\t\ty: (y3 > .008856 ? y3 : (y - 16 / 116) / 7.787) * refY,\n\t\t\t\tz: (z3 > .008856 ? z3 : (z - 16 / 116) / 7.787) * refZ\n\t\t\t}\n\t\t}\n\n\t\tfunction xyzToRgb(x, y, z) {\n\t\t\tx /= 100;\n\t\t\ty /= 100;\n\t\t\tz /= 100;\n\t\t\tvar r = x * 3.2406 + y * -1.5372 + z * -.4986,\n\t\t\t\tg = x * -.9689 + y * 1.8758 + z * .0415,\n\t\t\t\tb = x * .0557 + y * -.204 + z * 1.057;\n\t\t\tr = r > .0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - .055 : 12.92 * r;\n\t\t\tg = g > .0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - .055 : 12.92 * g;\n\t\t\tb = b > .0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - .055 : 12.92 * b;\n\t\t\treturn {\n\t\t\t\tr: r * 255,\n\t\t\t\tg: g * 255,\n\t\t\t\tb: b * 255\n\t\t\t}\n\t\t}\n\n\t\tfunction labToLch(L, a, b) {\n\t\t\tvar C = Math.sqrt(a * a + b * b),\n\t\t\t\th = Math.atan2(b, a) * 180 / Math.PI;\n\t\t\treturn {\n\t\t\t\tL: L,\n\t\t\t\tC: C,\n\t\t\t\th: h < 0 ? h + 360 : h\n\t\t\t}\n\t\t}\n\n\t\tfunction lchToLab(L, C, h) {\n\t\t\tvar hRad = h * Math.PI / 180;\n\t\t\treturn {\n\t\t\t\tL: L,\n\t\t\t\ta: C * Math.cos(hRad),\n\t\t\t\tb: C * Math.sin(hRad)\n\t\t\t}\n\t\t}\n\n\t\tfunction hexToLch(hex) {\n\t\t\tvar rgb = hexToRgb(hex),\n\t\t\t\txyz = rgbToXyz(rgb.r, rgb.g, rgb.b),\n\t\t\t\tlab = xyzToLab(xyz.x, xyz.y, xyz.z);\n\t\t\treturn labToLch(lab.L, lab.a, lab.b)\n\t\t}\n\n\t\tfunction lchToHex(L, C, h) {\n\t\t\tvar lab = lchToLab(L, C, h),\n\t\t\t\txyz = labToXyz(lab.L, lab.a, lab.b),\n\t\t\t\trgb = xyzToRgb(xyz.x, xyz.y, xyz.z);\n\t\t\treturn rgbToHex(rgb.r, rgb.g, rgb.b)\n\t\t}\n\t\tvar L = 50,\n\t\t\tC = 20,\n\t\t\tH = 210,\n\t\t\tcolors = [];\n\t\tvar p = document.getElementById('p'),\n\t\t\th = document.getElementById('h'),\n\t\t\tpv = document.getElementById('pv'),\n\t\t\tph = document.getElementById('ph'),\n\t\t\tsL = document.getElementById('sL'),\n\t\t\tsC = document.getElementById('sC'),\n\t\t\tsH = document.getElementById('sH'),\n\t\t\tml = document.getElementById('ml'),\n\t\t\tps = document.getElementById('paletteSelect');\n\n\t\tfunction init(hex) {\n\t\t\tvar lch = hexToLch(hex);\n\t\t\tL = Math.max(0, Math.min(100, lch.L));\n\t\t\tC = Math.max(0, lch.C);\n\t\t\tH = lch.h;\n\t\t\tdraw()\n\t\t}\n\n\t\tfunction draw() {\n\t\t\tvar hex = lchToHex(L, C, H),\n\t\t\t\trgb = hexToRgb(hex),\n\t\t\t\txyz = rgbToXyz(rgb.r, rgb.g, rgb.b),\n\t\t\t\tlab = xyzToLab(xyz.x, xyz.y, xyz.z);\n\t\t\tpv.style.background = hex;\n\t\t\tph.textContent = hex.toUpperCase();\n\t\t\th.value = hex.toUpperCase();\n\t\t\tp.value = hex;\n\t\t\tsL.value = L;\n\t\t\tsC.value = C;\n\t\t\tsH.value = H;\n\t\t\tdocument.getElementById('lv').textContent = Math.round(L);\n\t\t\tdocument.getElementById('cv').textContent = Math.round(C);\n\t\t\tdocument.getElementById('hv').textContent = Math.round(H);\n\t\t\tdocument.getElementById('slv').textContent = Math.round(L) + '%';\n\t\t\tdocument.getElementById('scv').textContent = Math.round(C);\n\t\t\tdocument.getElementById('shv').textContent = Math.round(H) + '°';\n\t\t\tdocument.getElementById('lb').style.width = L + '%';\n\t\t\tdocument.getElementById('cb').style.width = Math.min(100, C) + '%';\n\t\t\tdocument.getElementById('hb').style.width = (H / 360 * 100) + '%';\n\t\t\tdocument.getElementById('labL').textContent = Math.round(lab.L);\n\t\t\tdocument.getElementById('labA').textContent = Math.round(lab.a);\n\t\t\tdocument.getElementById('labB').textContent = Math.round(lab.b);\n\t\t\tfind(lab)\n\t\t}\n\n\t\tfunction find(tl) {\n\t\t\tvar m = colors.map(function(c) {\n\t\t\t\tvar dr = hexToRgb(c.hex),\n\t\t\t\t\tdx = rgbToXyz(dr.r, dr.g, dr.b),\n\t\t\t\t\tdl = xyzToLab(dx.x, dx.y, dx.z),\n\t\t\t\t\tde = Math.sqrt(Math.pow(tl.L - dl.L, 2) + Math.pow(tl.a - dl.a, 2) + Math.pow(tl.b - dl.b, 2));\n\t\t\t\treturn {\n\t\t\t\t\tn: c.name,\n\t\t\t\t\th: c.hex,\n\t\t\t\t\tb: c.brand,\n\t\t\t\t\tdE: de\n\t\t\t\t}\n\t\t\t}).sort(function(a, b) {\n\t\t\t\treturn a.dE - b.dE\n\t\t\t});\n\t\t\trender(m.slice(0, 12))\n\t\t}\n\n\t\tfunction render(list) {\n\t\t\tml.innerHTML = list.map(function(x, i) {\n\t\t\t\tvar c = x.dE < 2 ? 'gd' : x.dE < 5 ? 'yd' : 'rd';\n\t\t\t\treturn '<div class=\"m\" onclick=\"init(\\'' + x.h + '\\')\"><div class=\"s\" style=\"background:' + x.h + '\"></div><div class=\"if\"><div class=\"n\">' + x.n + '</div><div class=\"bn\">' + x.b + '</div></div><div class=\"d\"><div class=\"dl\">ΔE</div><div class=\"dv ' + c + '\">' + x.dE.toFixed(1) + '</div></div>' + (i === 0 ? '<span class=\"bb\">Ближ.</span>' : '') + '</div>'\n\t\t\t}).join('')\n\t\t}\n\n\t\tfunction aL(d) {\n\t\t\tL = Math.max(0, Math.min(100, L + d));\n\t\t\tdraw()\n\t\t}\n\n\t\tfunction aC(d) {\n\t\t\tC = Math.max(0, C + d);\n\t\t\tdraw()\n\t\t}\n\n\t\tfunction aH(d) {\n\t\t\tH = (H + d + 360) % 360;\n\t\t\tdraw()\n\t\t}\n\t\tp.addEventListener('input', function() {\n\t\t\tinit(this.value)\n\t\t});\n\t\th.addEventListener('input', function() {\n\t\t\tif (/^#[0-9A-Fa-f]{6}$/.test(this.value)) init(this.value)\n\t\t});\n\t\tsL.addEventListener('input', function() {\n\t\t\tL = +this.value;\n\t\t\tdraw()\n\t\t});\n\t\tsC.addEventListener('input', function() {\n\t\t\tC = +this.value;\n\t\t\tdraw()\n\t\t});\n\t\tsH.addEventListener('input', function() {\n\t\t\tH = +this.value;\n\t\t\tdraw()\n\t\t});\n\t\tps.addEventListener('change', function() {\n\t\t\tloadPalette(this.value)\n\t\t});\n\t\tasync function loadPalettes() {\n\t\t\ttry {\n\t\t\t\tvar r = await fetch('/data/palettes-list.json');\n\t\t\t\tvar list = await r.json();\n\t\t\t\tps.innerHTML = list.map(function(p) {\n\t\t\t\t\treturn '<option value=\"' + p.file.replace('data/', '') + '\">' + p.name + '</option>'\n\t\t\t\t}).join('');\n\t\t\t\tloadPalette('benjamin-moore-color-preview.json')\n\t\t\t} catch (e) {\n\t\t\t\tps.innerHTML = '<option>Ошибка</option>'\n\t\t\t}\n\t\t}\n\t\tasync function loadPalette(file) {\n\t\t\ttry {\n\t\t\t\tvar r = await fetch('/data/' + file);\n\t\t\t\tvar data = await r.json();\n\t\t\t\tcolors = data.map(function(c) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tname: c.name || c.Name || '--',\n\t\t\t\t\t\thex: c.hex || c.Hex || '#000000',\n\t\t\t\t\t\tbrand: c.collection || c.Series || ''\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tdraw()\n\t\t\t} catch (e) {\n\t\t\t\tcolors = [];\n\t\t\t\tml.innerHTML = '<div class=\"lo\">Ошибка загрузки</div>'\n\t\t\t}\n\t\t}\n\t\tloadPalettes();\n\t\tinit('#7a92a3');\n\t</script>\n</p>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-14T22:02:55+03:00",
            "date_modified": "2026-03-21T15:32:03+03:00"
        },
        {
            "id": "https://пэйнт.рф/laboratoriya-cveta.html",
            "url": "https://пэйнт.рф/laboratoriya-cveta.html",
            "title": "Лаборатория цвета",
            "summary": "Здесь представлены вариации использования сервиса подбора цвета по цветовому кругу Поиск относительных цветов по интерьерным палитрам на CIE LCh Сервис 2 Сервис 3 Сервис 4&hellip;",
            "content_html": "<p>Здесь представлены вариации использования сервиса подбора цвета по цветовому кругу</p>\n<p><a href=\"https://пэйнт.рф/servis.html\">Поиск относительных цветов по интерьерным палитрам на CIE LCh</a></p>\n<p><a href=\"https://пэйнт.рф/servis2.html\">Сервис 2</a></p>\n<p><a href=\"https://пэйнт.рф/servis3.html\">Сервис 3</a></p>\n<p><a href=\"https://пэйнт.рф/servis4.html\">Сервис 4</a></p>\n<p><a href=\"https://пэйнт.рф/servis-5.html\">Сервис 5</a></p>\n<p><a href=\"https://пэйнт.рф/servis-7.html\">Сервис 7</a></p>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-14T12:45:06+03:00",
            "date_modified": "2026-03-21T13:37:58+03:00"
        },
        {
            "id": "https://пэйнт.рф/servis4.html",
            "url": "https://пэйнт.рф/servis4.html",
            "title": "Сервис4",
            "summary": "ChromaLab CIE LCh* · Интерьерные палитры · v2.1 Цветовое пространство LCh* Текущий цвет #C8916A L* 65 · C* 42 · h* 48° Lab: 65 /&hellip;",
            "content_html": "<header></header>\n<div class=\"app\">\n<div class=\"logo-service\">ChromaLab</div>\n<div class=\"logo-sub\">CIE LCh* · Интерьерные палитры · v2.1</div>\n<!-- ── LEFT PANEL ── -->\n<div class=\"panel-left\"><!-- Color Wheel -->\n<div>\n<div class=\"section-title\">Цветовое пространство LCh*</div>\n<div class=\"wheel-container\"><canvas id=\"colorWheel\"></canvas>\n<div id=\"wheelCursor\" class=\"wheel-cursor\"></div>\n</div>\n</div>\n<!-- Preview -->\n<div>\n<div class=\"section-title\">Текущий цвет</div>\n<div class=\"color-preview-block\">\n<div id=\"mainSwatch\" class=\"color-swatch-main\"></div>\n<div class=\"color-values\">\n<div id=\"hexDisplay\" class=\"color-hex\">#C8916A</div>\n<div id=\"lchDisplay\" class=\"color-lch-display\">L* 65 · C* 42 · h* 48°</div>\n<div id=\"labDisplay\" class=\"color-lch-display\">Lab: 65 / 28 / 31</div>\n</div>\n</div>\n</div>\n<!-- LCh Sliders -->\n<div>\n<div class=\"section-title\">Параметры LCh*</div>\n<div class=\"slider-group\">\n<div class=\"slider-row\">\n<div class=\"slider-label\"><span class=\"slider-name\"><span class=\"lch-letter\">L*</span> Светлота</span> <span id=\"valL\" class=\"slider-value L\">65</span></div>\n<input type=\"range\" id=\"sliderL\" min=\"0\" max=\"100\" value=\"65\"></div>\n<div class=\"slider-row\">\n<div class=\"slider-label\"><span class=\"slider-name\"><span class=\"lch-letter\">C*</span> Насыщенность</span> <span id=\"valC\" class=\"slider-value C\">42</span></div>\n<input type=\"range\" id=\"sliderC\" min=\"0\" max=\"150\" value=\"42\"></div>\n<div class=\"slider-row\">\n<div class=\"slider-label\"><span class=\"slider-name\"><span class=\"lch-letter\">h*</span> Оттенок</span> <span id=\"valH\" class=\"slider-value H\">48°</span></div>\n<input type=\"range\" id=\"sliderH\" min=\"0\" max=\"360\" value=\"48\"></div>\n</div>\n</div>\n<!-- HEX input -->\n<div>\n<div class=\"section-title\">Ввод HEX / Lab</div>\n<div class=\"hex-input-row\"><input type=\"text\" class=\"hex-input\" id=\"hexInput\" placeholder=\"#C8916A или 65,28,31\" maxlength=\"20\"> <button class=\"btn\">→</button></div>\n</div>\n<!-- Modifiers -->\n<div>\n<div class=\"section-title\">Быстрые модификаторы</div>\n<div class=\"modifiers\">\n<div class=\"mod-pill\" data-dl=\"10\">светлее</div>\n<div class=\"mod-pill\" data-dl=\"-10\">темнее</div>\n<div class=\"mod-pill\" data-dh=\"-20\">теплее</div>\n<div class=\"mod-pill\" data-dh=\"20\">холоднее</div>\n<div class=\"mod-pill\" data-dc=\"20\">насыщеннее</div>\n<div class=\"mod-pill\" data-dc=\"-20\">пастельнее</div>\n<div class=\"mod-pill\" data-dh=\"30\">зеленее</div>\n<div class=\"mod-pill\" data-dh=\"-30\">краснее</div>\n</div>\n</div>\n</div>\n<!-- ── RIGHT PANEL ── -->\n<div class=\"panel-right\">\n<div class=\"search-header\"><input type=\"text\" class=\"search-natural\" id=\"naturalSearch\" placeholder=\"Теплее и светлее, почти бежевый…\" onkeydown=\"if(event.key==='Enter') naturalSearch()\"> <button class=\"search-btn\">НАЙТИ</button></div>\n<div class=\"tabs\">\n<div class=\"tab active\">Похожие LCh</div>\n<div class=\"tab\">По брендам</div>\n<div class=\"tab\">Аналогичные</div>\n</div>\n<div id=\"resultsArea\" class=\"results-area\"><!-- filled by JS --></div>\n</div>\n</div>\n<!-- ── MODAL ── -->\n<div id=\"modalOverlay\" class=\"modal-overlay\">\n<div class=\"modal\">\n<div id=\"modalSwatch\" class=\"modal-swatch\"><button class=\"modal-close\">✕</button></div>\n<div class=\"modal-body\">\n<div id=\"modalName\" class=\"modal-name\">—</div>\n<div id=\"modalBrand\" class=\"modal-brand\">—</div>\n<div class=\"modal-specs\">\n<div class=\"spec-item\">\n<div class=\"spec-label\">HEX</div>\n<div id=\"mHex\" class=\"spec-val\">—</div>\n</div>\n<div class=\"spec-item\">\n<div class=\"spec-label\">ΔE (расстояние)</div>\n<div id=\"mDelta\" class=\"spec-val\">—</div>\n</div>\n<div class=\"spec-item\">\n<div class=\"spec-label\">L* · C* · h*</div>\n<div id=\"mLch\" class=\"spec-val\">—</div>\n</div>\n<div class=\"spec-item\">\n<div class=\"spec-label\">Lab</div>\n<div id=\"mLab\" class=\"spec-val\">—</div>\n</div>\n</div>\n<div class=\"modal-actions\">\n<div class=\"btn-outline\">⬅ Использовать как источник</div>\n<div class=\"btn-outline\">Скопировать HEX</div>\n</div>\n</div>\n</div>\n</div>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-14T12:29:06+03:00",
            "date_modified": "2026-03-14T18:13:00+03:00"
        },
        {
            "id": "https://пэйнт.рф/servis3.html",
            "url": "https://пэйнт.рф/servis3.html",
            "title": "Сервис3",
            "summary": "⭐️ Сервис интерьерных цветов Выберите цвет L (светлота)50 C (насыщенность)20 h (тон)210 L 50 a 0 b 0 Корректировка #7A92A3 Светлота50% Насыщенность20 Тон210° ☀️ Светлее&hellip;",
            "content_html": "<div class=\"container\">\n<h1>⭐️ Сервис интерьерных цветов</h1>\n<div class=\"grid\">\n<div class=\"panel\">\n<h3>Выберите цвет</h3>\n<div class=\"color-input\"><input type=\"color\" id=\"colorPicker\" value=\"#7a92a3\"> <input type=\"text\" id=\"hexInput\" value=\"#7A92A3\"></div>\n<div class=\"lch-box\">\n<div class=\"lch-row\">L (светлота)<span id=\"lVal\">50</span></div>\n<div class=\"bar\">\n<div id=\"lBar\" class=\"bar-fill\" style=\"width: 50%; background: linear-gradient(90deg,#000,#fff);\"></div>\n</div>\n<div class=\"lch-row\">C (насыщенность)<span id=\"cVal\">20</span></div>\n<div class=\"bar\">\n<div id=\"cBar\" class=\"bar-fill\" style=\"width: 20%; background: linear-gradient(90deg,#999,#000);\"></div>\n</div>\n<div class=\"lch-row\">h (тон)<span id=\"hVal\">210</span></div>\n<div class=\"bar\">\n<div id=\"hBar\" class=\"bar-fill\" style=\"width: 58%; background: linear-gradient(90deg,red,yellow,lime,cyan,blue,magenta,red);\"></div>\n</div>\n</div>\n<div class=\"lab-row\">\n<div class=\"lab-box\">\n<div class=\"lab-label\">L</div>\n<div id=\"labL\" class=\"lab-val\">50</div>\n</div>\n<div class=\"lab-box\">\n<div class=\"lab-label\">a</div>\n<div id=\"labA\" class=\"lab-val\">0</div>\n</div>\n<div class=\"lab-box\">\n<div class=\"lab-label\">b</div>\n<div id=\"labB\" class=\"lab-val\">0</div>\n</div>\n</div>\n</div>\n<div class=\"panel\">\n<h3>Корректировка</h3>\n<div id=\"preview\" style=\"background: #7a92a3;\"><span id=\"previewHex\">#7A92A3</span></div>\n<div class=\"slider-row\">\n<div class=\"slider-header\">Светлота<span id=\"sliderLVal\">50%</span></div>\n<input type=\"range\" id=\"sliderL\" min=\"0\" max=\"100\" value=\"50\"></div>\n<div class=\"slider-row\">\n<div class=\"slider-header\">Насыщенность<span id=\"sliderCVal\">20</span></div>\n<input type=\"range\" id=\"sliderC\" min=\"0\" max=\"100\" value=\"20\"></div>\n<div class=\"slider-row\">\n<div class=\"slider-header\">Тон<span id=\"sliderHVal\">210°</span></div>\n<input type=\"range\" id=\"sliderH\" min=\"0\" max=\"360\" value=\"210\"></div>\n<div class=\"btns\"><button>☀️ Светлее</button> <button>🌑 Темнее</button> <button>🌈 Насыщеннее</button> <button>🌫 Бледнее</button> <button style=\"background: #fff3e0; color: #e65100;\">🔥 Теплее</button> <button style=\"background: #e3f2fd; color: #1565c0;\">❄️ Холоднее</button></div>\n</div>\n<div class=\"panel\">\n<div class=\"results-title\">Ближайшие цвета</div>\n<div id=\"matches\" class=\"match-list\"></div>\n</div>\n</div>\n</div>\n<p>\n<script>\n        // Цветовые преобразования CIE LCh (без внешних библиотек)\n        \n        function hexToRgb(hex) {\n            var r = parseInt(hex.slice(1,3), 16);\n            var g = parseInt(hex.slice(3,5), 16);\n            var b = parseInt(hex.slice(5,7), 16);\n            return {r:r, g:g, b:b};\n        }\n        \n        function rgbToHex(r, g, b) {\n            var toHex = function(n) {\n                var h = Math.max(0, Math.min(255, Math.round(n))).toString(16);\n                return h.length === 1 ? '0' + h : h;\n            };\n            return '#' + toHex(r) + toHex(g) + toHex(b);\n        }\n        \n        function rgbToXyz(r, g, b) {\n            var r = r / 255, g = g / 255, b = b / 255;\n            r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;\n            g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;\n            b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;\n            var x = (r * 0.4124 + g * 0.3576 + b * 0.1805) * 100;\n            var y = (r * 0.2126 + g * 0.7152 + b * 0.0722) * 100;\n            var z = (r * 0.0193 + g * 0.1192 + b * 0.9505) * 100;\n            return {x:x, y:y, z:z};\n        }\n        \n        function xyzToLab(x, y, z) {\n            var refX = 95.047, refY = 100.000, refZ = 108.883;\n            x = x / refX; y = y / refY; z = z / refZ;\n            x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;\n            y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;\n            z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;\n            var L = (116 * y) - 16;\n            var a = 500 * (x - y);\n            var bVal = 200 * (y - z);\n            return {L:L, a:a, b:bVal};\n        }\n        \n        function labToXyz(L, a, b) {\n            var refX = 95.047, refY = 100.000, refZ = 108.883;\n            var y = (L + 16) / 116;\n            var x = a / 500 + y;\n            var z = y - b / 200;\n            var y3 = Math.pow(y, 3), x3 = Math.pow(x, 3), z3 = Math.pow(z, 3);\n            y = y3 > 0.008856 ? y3 : (y - 16/116) / 7.787;\n            x = x3 > 0.008856 ? x3 : (x - 16/116) / 7.787;\n            z = z3 > 0.008856 ? z3 : (z - 16/116) / 7.787;\n            return {x:x*refX, y:y*refY, z:z*refZ};\n        }\n        \n        function xyzToRgb(x, y, z) {\n            x = x / 100; y = y / 100; z = z / 100;\n            var r = x * 3.2406 + y * -1.5372 + z * -0.4986;\n            var g = x * -0.9689 + y * 1.8758 + z * 0.0415;\n            var b = x * 0.0557 + y * -0.2040 + z * 1.0570;\n            r = r > 0.0031308 ? 1.055 * Math.pow(r, 1/2.4) - 0.055 : 12.92 * r;\n            g = g > 0.0031308 ? 1.055 * Math.pow(g, 1/2.4) - 0.055 : 12.92 * g;\n            b = b > 0.0031308 ? 1.055 * Math.pow(b, 1/2.4) - 0.055 : 12.92 * b;\n            return {r:r*255, g:g*255, b:b*255};\n        }\n        \n        function labToLch(L, a, b) {\n            var C = Math.sqrt(a*a + b*b);\n            var h = Math.atan2(b, a) * 180 / Math.PI;\n            if (h < 0) h += 360;\n            return {L:L, C:C, h:h};\n        }\n        \n        function lchToLab(L, C, h) {\n            var hRad = h * Math.PI / 180;\n            var a = C * Math.cos(hRad);\n            var bVal = C * Math.sin(hRad);\n            return {L:L, a:a, b:bVal};\n        }\n        \n        function hexToLch(hex) {\n            var rgb = hexToRgb(hex);\n            var xyz = rgbToXyz(rgb.r, rgb.g, rgb.b);\n            var lab = xyzToLab(xyz.x, xyz.y, xyz.z);\n            return labToLch(lab.L, lab.a, lab.b);\n        }\n        \n        function lchToHex(L, C, h) {\n            var lab = lchToLab(L, C, h);\n            var xyz = labToXyz(lab.L, lab.a, lab.b);\n            var rgb = xyzToRgb(xyz.x, xyz.y, xyz.z);\n            return rgbToHex(rgb.r, rgb.g, rgb.b);\n        }\n\n        // База интерьерных цветов (загружается динамически)\n        var colors = [];\n\n        async function loadColors() {\n            try {\n                const response = await fetch('/data/palettes-list.json');\n                const palettes = await response.json();\n                \n                const results = [];\n                for (const p of palettes) {\n                    try {\n                        const res = await fetch(p.file);\n                        if (res.ok) {\n                            const data = await res.json();\n                            results.push(...data);\n                        }\n                    } catch (e) {}\n                }\n                \n                colors = results.map(c => ({\n                    n: c.name,\n                    h: c.hex,\n                    b: c.series || c.ColorFamily || c.collection?.split('/')[0] || ''\n                })).filter(c => c.h && /^#[0-9A-Fa-f]{6}$/.test(c.h));\n                \n                init(document.getElementById('colorPicker').value);\n            } catch (e) {\n                console.error('Ошибка загрузки:', e);\n            }\n        }\n\n        // Приложение\n        var L = 50, C = 20, H = 210;\n        \n        var p = document.getElementById('colorPicker');\n        var h = document.getElementById('hexInput');\n        var pre = document.getElementById('preview');\n        var preHex = document.getElementById('previewHex');\n        var sL = document.getElementById('sliderL');\n        var sC = document.getElementById('sliderC');\n        var sH = document.getElementById('sliderH');\n        var mat = document.getElementById('matches');\n\n        function init(hex) {\n            var lch = hexToLch(hex);\n            L = Math.max(0, Math.min(100, lch.L));\n            C = Math.max(0, lch.C);\n            H = lch.h;\n            draw();\n        }\n\n        function draw() {\n            var hex = lchToHex(L, C, H);\n            var rgb = hexToRgb(hex);\n            var xyz = rgbToXyz(rgb.r, rgb.g, rgb.b);\n            var lab = xyzToLab(xyz.x, xyz.y, xyz.z);\n            \n            pre.style.background = hex;\n            preHex.textContent = hex.toUpperCase();\n            h.value = hex.toUpperCase();\n            p.value = hex;\n            \n            sL.value = L;\n            sC.value = C;\n            sH.value = H;\n            \n            document.getElementById('lVal').textContent = Math.round(L);\n            document.getElementById('cVal').textContent = Math.round(C);\n            document.getElementById('hVal').textContent = Math.round(H);\n            document.getElementById('sliderLVal').textContent = Math.round(L) + '%';\n            document.getElementById('sliderCVal').textContent = Math.round(C);\n            document.getElementById('sliderHVal').textContent = Math.round(H) + '°';\n            document.getElementById('lBar').style.width = L + '%';\n            document.getElementById('cBar').style.width = Math.min(100, C) + '%';\n            document.getElementById('hBar').style.width = (H/360*100) + '%';\n            document.getElementById('labL').textContent = Math.round(lab.L);\n            document.getElementById('labA').textContent = Math.round(lab.a);\n            document.getElementById('labB').textContent = Math.round(lab.b);\n            \n            find(lab);\n        }\n\n        function find(targetLab) {\n            var list = colors.map(function(x) {\n                var dbRgb = hexToRgb(x.h);\n                var dbXyz = rgbToXyz(dbRgb.r, dbRgb.g, dbRgb.b);\n                var dbLab = xyzToLab(dbXyz.x, dbXyz.y, dbXyz.z);\n                var dE = Math.sqrt(\n                    Math.pow(targetLab.L - dbLab.L, 2) +\n                    Math.pow(targetLab.a - dbLab.a, 2) +\n                    Math.pow(targetLab.b - dbLab.b, 2)\n                );\n                return {n:x.n, h:x.h, b:x.b, dE:dE};\n            }).sort(function(a,b){return a.dE-b.dE});\n            \n            render(list.slice(0,8));\n        }\n\n        function render(list) {\n            mat.innerHTML = list.map(function(x,i) {\n                var cls = x.dE < 2 ? 'color:green' : x.dE < 5 ? 'color:orange' : 'color:red';\n                return '<div class=\"match\" onclick=\"init(\\''+x.h+'\\')\">'+\n                    '<div class=\"swatch\" style=\"background:'+x.h+'\"></div>'+\n                    '<div class=\"info\"><div class=\"name\">'+x.n+'</div><div class=\"brand\">'+x.b+'</div></div>'+\n                    '<div class=\"delta\"><div style=\"font-size:10px;color:#888\">ΔE</div><div class=\"delta-val\" style=\"'+cls+'\">'+x.dE.toFixed(1)+'</div></div>'+\n                    (i===0?'<span class=\"best\">Ближайший</span>':'')+\n                '</div>';\n            }).join('');\n        }\n\n        function adjL(d){ L=Math.max(0,Math.min(100,L+d)); draw(); }\n        function adjC(d){ C=Math.max(0,C+d); draw(); }\n        function adjH(d){ H=(H+d+360)%360; draw(); }\n\n        p.addEventListener('input',function(){init(this.value)});\n        h.addEventListener('input',function(){if(/^#[0-9A-Fa-f]{6}$/.test(this.value))init(this.value)});\n        sL.addEventListener('input',function(){L=+this.value;draw()});\n        sC.addEventListener('input',function(){C=+this.value;draw()});\n        sH.addEventListener('input',function(){H=+this.value;draw()});\n\n        init('#7a92a3');\n        loadColors();\n    </script>\n</p>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-14T12:28:12+03:00",
            "date_modified": "2026-03-21T15:31:56+03:00"
        },
        {
            "id": "https://пэйнт.рф/servis2.html",
            "url": "https://пэйнт.рф/servis2.html",
            "title": "Подбор интерьерных цветов",
            "summary": "Поиск интерьерных цветов (Мульти-загрузка) Подбор интерьерных цветов — важный этап при создании гармоничного дизайна помещения. Онлайн-сервис «Пэйнт.рф» позволяет быстро и точно подобрать нужный оттенок, сравнить&hellip;",
            "content_html": "<div class=\"container\">\n<h2>Поиск интерьерных цветов (Мульти-загрузка)</h2>\n<!-- КОНТЕЙНЕР ВИДЖЕТА -->\n<div id=\"lch-service-widget\"></div>\n</div>\n<p>\n<script>\n\t\t(function() {\n\t\t\t// === 1. ВСТРОЕННАЯ БИБЛИОТЕКА ЦВЕТА ===\n\t\t\tconst ColorMath = {\n\t\t\t\tisValidHex: (hex) => /^#([0-9A-F]{3}){1,2}$/i.test(hex),\n\t\t\t\thexToRgb: (hex) => {\n\t\t\t\t\thex = hex.replace('#', '');\n\t\t\t\t\tif (hex.length === 3) hex = hex.split('').map(c => c + c).join('');\n\t\t\t\t\tconst int = parseInt(hex, 16);\n\t\t\t\t\treturn [(int >> 16) & 255, (int >> 8) & 255, int & 255];\n\t\t\t\t},\n\t\t\t\trgbToLab: (r, g, b) => {\n\t\t\t\t\tlet [lr, lg, lb] = [r, g, b].map(v => {\n\t\t\t\t\t\tv /= 255;\n\t\t\t\t\t\treturn v > 0.04045 ? Math.pow((v + 0.055) / 1.055, 2.4) : v / 12.92;\n\t\t\t\t\t});\n\t\t\t\t\tlr *= 100;\n\t\t\t\t\tlg *= 100;\n\t\t\t\t\tlb *= 100;\n\t\t\t\t\tlet x = lr * 0.4124 + lg * 0.3576 + lb * 0.1805;\n\t\t\t\t\tlet y = lr * 0.2126 + lg * 0.7152 + lb * 0.0722;\n\t\t\t\t\tlet z = lr * 0.0193 + lg * 0.1192 + lb * 0.9505;\n\t\t\t\t\tx /= 95.047;\n\t\t\t\t\ty /= 100.000;\n\t\t\t\t\tz /= 108.883;\n\t\t\t\t\t[x, y, z] = [x, y, z].map(v => v > 0.008856 ? Math.pow(v, 1 / 3) : (7.787 * v) + (16 / 116));\n\t\t\t\t\treturn [(116 * y) - 16, 500 * (x - y), 200 * (y - z)];\n\t\t\t\t},\n\t\t\t\tlabToLch: (l, a, b) => {\n\t\t\t\t\tconst c = Math.sqrt(a * a + b * b);\n\t\t\t\t\tlet h = Math.atan2(b, a) * (180 / Math.PI);\n\t\t\t\t\tif (h < 0) h += 360;\n\t\t\t\t\treturn [l, c, h];\n\t\t\t\t},\n\t\t\t\thexToLch: (hex) => {\n\t\t\t\t\tif (!ColorMath.isValidHex(hex)) return null;\n\t\t\t\t\tconst [r, g, b] = ColorMath.hexToRgb(hex);\n\t\t\t\t\tconst [l, a, b_val] = ColorMath.rgbToLab(r, g, b);\n\t\t\t\t\treturn ColorMath.labToLch(l, a, b_val);\n\t\t\t\t},\n\t\t\t\tparseLCHString: (str) => {\n\t\t\t\t\tif (!str) return [0, 0, 0];\n\t\t\t\t\treturn str.split(',').map(s => parseFloat(s.trim()));\n\t\t\t\t},\n\t\t\t\tcalcDeltaE: (target, pLCH) => {\n\t\t\t\t\tconst dL = target.L - pLCH[0];\n\t\t\t\t\tconst dC = target.C - pLCH[1];\n\t\t\t\t\tlet dH = Math.abs(target.h - pLCH[2]);\n\t\t\t\t\tif (dH > 180) dH = 360 - dH;\n\t\t\t\t\treturn Math.sqrt((dL * 1.5) ** 2 + (dC * 1.2) ** 2 + (dH * 1.0) ** 2);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// === 2. КОНФИГУРАЦИЯ ===\n\t\t\tconst CONFIG = {\n\t\t\t\tindexUrl: '/data/palettes-list.json', // Файл-индекс со списком палитр\n\t\t\t\tdefaultHex: '#D25362'\n\t\t\t};\n\n\t\t\t// === 3. ИНИЦИАЛИЗАЦИЯ ===\n\t\t\tconst host = document.getElementById('lch-service-widget');\n\t\t\tif (!host) return;\n\n\t\t\tconst shadow = host.attachShadow({\n\t\t\t\tmode: 'open'\n\t\t\t});\n\t\t\tconst pfx = 'lcs-';\n\n\t\t\t// === 4. СТИЛИ ===\n\t\t\tconst styles = `\n    <style>\n        :host { all: initial; display: block; font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; color: #333; }\n        .${pfx}wrapper { background: #fff; border: 1px solid #e0e0e0; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }\n        .${pfx}grid { display: flex; flex-wrap: wrap; }\n        .${pfx}sidebar { flex: 0 0 320px; background: #f9fafb; padding: 20px; border-right: 1px solid #eee; min-width: 280px; }\n        .${pfx}content { flex: 1; padding: 20px; min-width: 300px; }\n        @media (max-width: 768px) { .${pfx}sidebar { flex: 0 0 100%; border-right: none; border-bottom: 1px solid #eee; } }\n        \n        .${pfx}color-preview { background: #fff; padding: 15px; border-radius: 8px; border: 1px solid #eee; text-align: center; margin-bottom: 20px; }\n        .${pfx}big-swatch { width: 100%; height: 90px; border-radius: 6px; margin-bottom: 10px; border: 1px solid rgba(0,0,0,0.1); }\n        .${pfx}hex-display { font-size: 20px; font-weight: 700; font-family: monospace; color: #2c3e50; }\n        .${pfx}lch-badges { display: flex; justify-content: center; gap: 8px; margin-top: 8px; }\n        .${pfx}badge { background: #eef2f7; color: #555; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }\n\n        .${pfx}input-group { margin-bottom: 15px; }\n        .${pfx}label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: #444; }\n        .${pfx}input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 14px; box-sizing: border-box; }\n        \n        .${pfx}btn { width: 100%; padding: 11px; background: #007bff; color: white; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: background 0.2s; font-size: 14px; }\n        .${pfx}btn:hover { background: #0056b3; }\n        .${pfx}btn-secondary { background: #6c757d; margin-top: 5px; }\n        .${pfx}btn-secondary:hover { background: #545b62; }\n        .${pfx}btn:disabled { background: #ccc; cursor: not-allowed; opacity: 0.7; }\n        .${pfx}btn-loading { position: relative; pointer-events: none; }\n        .${pfx}btn-loading::after { content: ''; position: absolute; left: 50%; top: 50%; width: 16px; height: 16px; margin: -8px 0 0 -8px; border: 2px solid #fff; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }\n\n        @keyframes spin { to { transform: rotate(360deg); } }\n\n        .${pfx}mod-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 15px 0; }\n        .${pfx}mod-btn { padding: 8px 2px; font-size: 11px; background: #fff; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; }\n        .${pfx}mod-btn:hover { background: #f0f0f0; }\n\n        .${pfx}slider-wrap { margin-bottom: 12px; }\n        .${pfx}slider-row { display: flex; justify-content: space-between; font-size: 12px; font-weight: 600; margin-bottom: 4px; color: #555; }\n        .${pfx}range { width: 100%; accent-color: #007bff; }\n\n        .${pfx}palette-box { border: 1px solid #ddd; border-radius: 6px; background: #fff; max-height: 250px; overflow-y: auto; margin-bottom: 10px; }\n        .${pfx}p-item { padding: 8px 10px; border-bottom: 1px solid #f0f0f0; font-size: 13px; display: flex; align-items: center; }\n        .${pfx}p-item:last-child { border-bottom: none; }\n        .${pfx}checkbox { margin-right: 8px; }\n        .${pfx}select-all { padding: 8px; background: #f1f3f5; font-size: 12px; font-weight: bold; text-align: center; cursor: pointer; border-bottom: 1px solid #ddd; }\n\n        .${pfx}tabs { display: flex; gap: 8px; margin-bottom: 20px; border-bottom: 1px solid #eee; padding-bottom: 10px; flex-wrap: wrap; }\n        .${pfx}tab { padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 600; cursor: pointer; background: #f1f3f5; color: #555; }\n        .${pfx}tab.active { background: #007bff; color: white; }\n        \n        .${pfx}results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 15px; }\n        .${pfx}card { border: 1px solid #eee; border-radius: 8px; overflow: hidden; background: #fff; cursor: pointer; transition: transform 0.2s; position: relative; }\n        .${pfx}card:hover { transform: translateY(-3px); box-shadow: 0 6px 12px rgba(0,0,0,0.1); }\n        .${pfx}card-swatch { height: 110px; width: 100%; }\n        .${pfx}delta-badge { position: absolute; top: 8px; right: 8px; background: rgba(0,0,0,0.75); color: #fff; font-size: 10px; padding: 3px 6px; border-radius: 4px; }\n        .${pfx}card-body { padding: 12px; }\n        .${pfx}card-name { font-weight: 700; font-size: 14px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n        .${pfx}card-series { font-size: 11px; color: #888; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n        .${pfx}card-meta { font-size: 10px; color: #aaa; font-family: monospace; background: #f8f9fa; padding: 3px; border-radius: 3px; text-align: center; }\n\n        .${pfx}status-msg { text-align: center; padding: 40px 20px; color: #666; }\n        .${pfx}error-msg { text-align: center; padding: 20px; color: #d9534f; background: #fdf7f7; border: 1px solid #f5c6cb; border-radius: 6px; margin-bottom: 15px; }\n        .${pfx}log-area { font-size: 11px; color: #666; margin-top: 10px; max-height: 100px; overflow-y: auto; background: #f9f9f9; padding: 5px; border-radius: 4px; }\n    </style>\n    `;\n\n\t\t\t// === 5. HTML СТРУКТУРА ===\n\t\t\tconst html = `\n        <div class=\"${pfx}wrapper\">\n            <div class=\"${pfx}grid\">\n                <div class=\"${pfx}sidebar\">\n                    <div class=\"${pfx}color-preview\">\n                        <div id=\"${pfx}swatch\" class=\"${pfx}big-swatch\" style=\"background: #cccccc;\"></div>\n                        <div id=\"${pfx}hexVal\" class=\"${pfx}hex-display\">#CCCCCC</div>\n                        <div id=\"${pfx}lchVals\" class=\"${pfx}lch-badges\">\n                            <span class=\"${pfx}badge\">L: -</span>\n                            <span class=\"${pfx}badge\">C: -</span>\n                            <span class=\"${pfx}badge\">h: -°</span>\n                        </div>\n                    </div>\n\n                    <div class=\"${pfx}input-group\">\n                        <label class=\"${pfx}label\">Исходный цвет (HEX)</label>\n                        <input type=\"text\" id=\"${pfx}inpHex\" class=\"${pfx}input\" value=\"${CONFIG.defaultHex}\">\n                        <button id=\"${pfx}btnApply\" class=\"${pfx}btn\">Применить цвет</button>\n                    </div>\n\n                    <div class=\"${pfx}input-group\">\n                        <label class=\"${pfx}label\">Модификаторы</label>\n                        <div class=\"${pfx}mod-grid\">\n                            <button class=\"${pfx}mod-btn\" data-type=\"L\" data-val=\"10\">Светлее</button>\n                            <button class=\"${pfx}mod-btn\" data-type=\"L\" data-val=\"-10\">Темнее</button>\n                            <button class=\"${pfx}mod-btn\" data-type=\"C\" data-val=\"10\">Насыщ.</button>\n                            <button class=\"${pfx}mod-btn\" data-type=\"C\" data-val=\"-10\">Пастель</button>\n                            <button class=\"${pfx}mod-btn\" data-type=\"H\" data-val=\"15\">Теплее</button>\n                            <button class=\"${pfx}mod-btn\" data-type=\"H\" data-val=\"-15\">Холодн.</button>\n                        </div>\n                        <div class=\"${pfx}slider-wrap\">\n                            <div class=\"${pfx}slider-row\"><span>L</span><span id=\"${pfx}lblL\">0</span></div>\n                            <input type=\"range\" id=\"${pfx}rngL\" class=\"${pfx}range\" min=\"-40\" max=\"40\" value=\"0\">\n                        </div>\n                        <div class=\"${pfx}slider-wrap\">\n                            <div class=\"${pfx}slider-row\"><span>C</span><span id=\"${pfx}lblC\">0</span></div>\n                            <input type=\"range\" id=\"${pfx}rngC\" class=\"${pfx}range\" min=\"-40\" max=\"40\" value=\"0\">\n                        </div>\n                        <div class=\"${pfx}slider-wrap\">\n                            <div class=\"${pfx}slider-row\"><span>h</span><span id=\"${pfx}lblH\">0°</span></div>\n                            <input type=\"range\" id=\"${pfx}rngH\" class=\"${pfx}range\" min=\"-90\" max=\"90\" value=\"0\">\n                        </div>\n                        <button id=\"${pfx}btnReset\" class=\"${pfx}btn ${pfx}btn-secondary\">Сброс</button>\n                    </div>\n\n                    <div class=\"${pfx}input-group\">\n                        <label class=\"${pfx}label\">Палитры (из индекса)</label>\n                        <div id=\"${pfx}selectAll\" class=\"${pfx}select-all\">Выбрать все / Снять все</div>\n                        <div id=\"${pfx}paletteList\" class=\"${pfx}palette-box\">\n                            <div style=\"padding:15px; text-align:center; color:#999;\">Загрузка списка палитр...</div>\n                        </div>\n                        <button id=\"${pfx}btnLoad\" class=\"${pfx}btn\" disabled>Загрузить выбранные палитры</button>\n                        <div id=\"${pfx}loadLog\" class=\"${pfx}log-area\"></div>\n                    </div>\n                </div>\n\n                <div class=\"${pfx}content\">\n                    <div class=\"${pfx}tabs\">\n                        <div class=\"${pfx}tab active\" data-mode=\"closest\">Ближайшие (ΔE)</div>\n                        <div class=\"${pfx}tab\" data-mode=\"byBrand\">Лучшие по брендам</div>\n                        <div class=\"${pfx}tab\" data-mode=\"related\">Родственные</div>\n                    </div>\n                    <div id=\"${pfx}results\" class=\"${pfx}results-grid\">\n                        <div class=\"${pfx}status-msg\">Выберите палитры и нажмите «Загрузить».</div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    `;\n\n\t\t\tshadow.innerHTML = styles + html;\n\n\t\t\t// === 6. ЛОГИКА ===\n\t\t\tlet paletteIndex = []; // Данные из palettes-list.json\n\t\t\tlet loadedColors = []; // Объединенный массив всех цветов\n\t\t\tlet baseColor = null;\n\t\t\tlet modifiers = {\n\t\t\t\tL: 0,\n\t\t\t\tC: 0,\n\t\t\t\tH: 0\n\t\t\t};\n\t\t\tlet searchMode = 'closest';\n\n\t\t\tconst $ = (id) => shadow.getElementById(id);\n\t\t\tconst els = {\n\t\t\t\tinpHex: $(`${pfx}inpHex`),\n\t\t\t\tbtnApply: $(`${pfx}btnApply`),\n\t\t\t\tswatch: $(`${pfx}swatch`),\n\t\t\t\thexVal: $(`${pfx}hexVal`),\n\t\t\t\tlchVals: $(`${pfx}lchVals`),\n\t\t\t\trngL: $(`${pfx}rngL`),\n\t\t\t\trngC: $(`${pfx}rngC`),\n\t\t\t\trngH: $(`${pfx}rngH`),\n\t\t\t\tlblL: $(`${pfx}lblL`),\n\t\t\t\tlblC: $(`${pfx}lblC`),\n\t\t\t\tlblH: $(`${pfx}lblH`),\n\t\t\t\tbtnReset: $(`${pfx}btnReset`),\n\t\t\t\tmodBtns: shadow.querySelectorAll(`.${pfx}mod-btn`),\n\t\t\t\tpaletteList: $(`${pfx}paletteList`),\n\t\t\t\tselectAll: $(`${pfx}selectAll`),\n\t\t\t\tbtnLoad: $(`${pfx}btnLoad`),\n\t\t\t\tresults: $(`${pfx}results`),\n\t\t\t\ttabs: shadow.querySelectorAll(`.${pfx}tab`),\n\t\t\t\tloadLog: $(`${pfx}loadLog`)\n\t\t\t};\n\n\t\t\tfunction log(msg) {\n\t\t\t\tconst div = document.createElement('div');\n\t\t\t\tdiv.textContent = msg;\n\t\t\t\tels.loadLog.appendChild(div);\n\t\t\t\tels.loadLog.scrollTop = els.loadLog.scrollHeight;\n\t\t\t}\n\n\t\t\tfunction updateBaseColorUI(hex) {\n\t\t\t\tif (!ColorMath.isValidHex(hex)) return;\n\t\t\t\tconst lch = ColorMath.hexToLch(hex);\n\t\t\t\tbaseColor = {\n\t\t\t\t\thex,\n\t\t\t\t\tL: lch[0],\n\t\t\t\t\tC: lch[1],\n\t\t\t\t\th: lch[2]\n\t\t\t\t};\n\t\t\t\tels.swatch.style.background = hex;\n\t\t\t\tels.hexVal.textContent = hex.toUpperCase();\n\t\t\t\tels.lchVals.innerHTML = `\n            <span class=\"${pfx}badge\">L: ${lch[0].toFixed(1)}</span>\n            <span class=\"${pfx}badge\">C: ${lch[1].toFixed(1)}</span>\n            <span class=\"${pfx}badge\">h: ${lch[2].toFixed(0)}°</span>\n        `;\n\t\t\t}\n\n\t\t\tfunction getTargetLCH() {\n\t\t\t\tif (!baseColor) return null;\n\t\t\t\tlet h = baseColor.h + modifiers.H;\n\t\t\t\twhile (h < 0) h += 360;\n\t\t\t\twhile (h >= 360) h -= 360;\n\t\t\t\treturn {\n\t\t\t\t\tL: baseColor.L + modifiers.L,\n\t\t\t\t\tC: Math.max(0, baseColor.C + modifiers.C),\n\t\t\t\t\th: h\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tfunction performSearch() {\n\t\t\t\tif (!baseColor || loadedColors.length === 0) return;\n\t\t\t\tconst target = getTargetLCH();\n\t\t\t\tlet results = [];\n\n\t\t\t\tif (searchMode === 'closest') {\n\t\t\t\t\tresults = loadedColors.map(item => {\n\t\t\t\t\t\tconst pLCH = ColorMath.parseLCHString(item.LCH);\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...item,\n\t\t\t\t\t\t\t_delta: ColorMath.calcDeltaE(target, pLCH),\n\t\t\t\t\t\t\t_pLCH: pLCH\n\t\t\t\t\t\t};\n\t\t\t\t\t}).sort((a, b) => a._delta - b._delta).slice(0, 15);\n\t\t\t\t} else if (searchMode === 'byBrand') {\n\t\t\t\t\tconst best = {};\n\t\t\t\t\tloadedColors.forEach(item => {\n\t\t\t\t\t\tconst pLCH = ColorMath.parseLCHString(item.LCH);\n\t\t\t\t\t\tconst delta = ColorMath.calcDeltaE(target, pLCH);\n\t\t\t\t\t\tconst key = item.collection || item.name;\n\t\t\t\t\t\tif (!best[key] || delta < best[key]._delta) {\n\t\t\t\t\t\t\tbest[key] = {\n\t\t\t\t\t\t\t\t...item,\n\t\t\t\t\t\t\t\t_delta: delta,\n\t\t\t\t\t\t\t\t_pLCH: pLCH\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\tresults = Object.values(best).sort((a, b) => a._delta - b._delta);\n\t\t\t\t} else if (searchMode === 'related') {\n\t\t\t\t\tresults = loadedColors.filter(item => {\n\t\t\t\t\t\tconst pLCH = ColorMath.parseLCHString(item.LCH);\n\t\t\t\t\t\tlet dH = Math.abs(pLCH[2] - target.h);\n\t\t\t\t\t\tif (dH > 180) dH = 360 - dH;\n\t\t\t\t\t\treturn dH < 25;\n\t\t\t\t\t}).map(item => {\n\t\t\t\t\t\tconst pLCH = ColorMath.parseLCHString(item.LCH);\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...item,\n\t\t\t\t\t\t\t_delta: ColorMath.calcDeltaE(target, pLCH),\n\t\t\t\t\t\t\t_pLCH: pLCH\n\t\t\t\t\t\t};\n\t\t\t\t\t}).sort((a, b) => Math.abs(a._pLCH[0] - target.L) - Math.abs(b._pLCH[0] - target.L)).slice(0, 15);\n\t\t\t\t}\n\n\t\t\t\trenderResults(results);\n\t\t\t}\n\n\t\t\tfunction renderResults(list) {\n\t\t\t\tels.results.innerHTML = '';\n\t\t\t\tif (!list.length) {\n\t\t\t\t\tels.results.innerHTML = `<div class=\"${pfx}status-msg\">Совпадений не найдено.</div>`;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlist.forEach(item => {\n\t\t\t\t\tconst card = document.createElement('div');\n\t\t\t\t\tcard.className = `${pfx}card`;\n\t\t\t\t\tconst deltaHtml = item._delta !== undefined ? `<div class=\"${pfx}delta-badge\">ΔE: ${item._delta.toFixed(1)}</div>` : '';\n\t\t\t\t\tconst series = item.series || item.collection || 'Palette';\n\n\t\t\t\t\tcard.innerHTML = `\n                <div class=\"${pfx}card-swatch\" style=\"background-color: ${item.hex}\">${deltaHtml}</div>\n                <div class=\"${pfx}card-body\">\n                    <div class=\"${pfx}card-name\" title=\"${item.name}\">${item.name}</div>\n                    <div class=\"${pfx}card-series\" title=\"${series}\">${series}</div>\n                    <div class=\"${pfx}card-meta\">${item.hex}</div>\n                </div>\n            `;\n\t\t\t\t\tcard.onclick = () => {\n\t\t\t\t\t\tels.inpHex.value = item.hex;\n\t\t\t\t\t\tupdateBaseColorUI(item.hex);\n\t\t\t\t\t\tresetModifiers();\n\t\t\t\t\t\tperformSearch();\n\t\t\t\t\t};\n\t\t\t\t\tels.results.appendChild(card);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tfunction resetModifiers() {\n\t\t\t\tmodifiers = {\n\t\t\t\t\tL: 0,\n\t\t\t\t\tC: 0,\n\t\t\t\t\tH: 0\n\t\t\t\t};\n\t\t\t\tels.rngL.value = 0;\n\t\t\t\tels.lblL.textContent = '0';\n\t\t\t\tels.rngC.value = 0;\n\t\t\t\tels.lblC.textContent = '0';\n\t\t\t\tels.rngH.value = 0;\n\t\t\t\tels.lblH.textContent = '0°';\n\t\t\t}\n\n\t\t\t// --- ЗАГРУЗКА ДАННЫХ ---\n\n\t\t\tasync function loadIndex() {\n\t\t\t\ttry {\n\t\t\t\t\tconst res = await fetch(CONFIG.indexUrl);\n\t\t\t\t\tif (!res.ok) throw new Error(`HTTP ${res.status}`);\n\t\t\t\t\tpaletteIndex = await res.json();\n\n\t\t\t\t\tif (!Array.isArray(paletteIndex)) throw new Error('Индекс не является массивом');\n\n\t\t\t\t\trenderPaletteList();\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconsole.error(e);\n\t\t\t\t\tels.paletteList.innerHTML = `<div class=\"${pfx}error-msg\">Ошибка загрузки индекса:<br>${CONFIG.indexUrl}<br>${e.message}</div>`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction renderPaletteList() {\n\t\t\t\tels.paletteList.innerHTML = '';\n\t\t\t\tif (paletteIndex.length === 0) {\n\t\t\t\t\tels.paletteList.innerHTML = '<div style=\"padding:10px;\">Список пуст</div>';\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tpaletteIndex.forEach((item, idx) => {\n\t\t\t\t\tconst name = item.name || item.collection || `Palette ${idx+1}`;\n\t\t\t\t\tconst div = document.createElement('div');\n\t\t\t\t\tdiv.className = `${pfx}p-item`;\n\t\t\t\t\tdiv.innerHTML = `\n                <input type=\"checkbox\" class=\"${pfx}checkbox\" value=\"${idx}\" checked>\n                <span>${name}</span>\n            `;\n\t\t\t\t\tels.paletteList.appendChild(div);\n\t\t\t\t});\n\t\t\t\tels.btnLoad.disabled = false;\n\t\t\t\tels.btnLoad.textContent = `Загрузить (${paletteIndex.length})`;\n\t\t\t}\n\n\t\t\tasync function loadSelectedPalettes() {\n\t\t\t\tconst checked = Array.from(shadow.querySelectorAll(`.${pfx}checkbox:checked`));\n\t\t\t\tif (checked.length === 0) return alert('Выберите хотя бы одну палитру');\n\n\t\t\t\tels.btnLoad.classList.add(`${pfx}btn-loading`);\n\t\t\t\tels.btnLoad.textContent = 'Загрузка...';\n\t\t\t\tels.loadLog.innerHTML = '';\n\t\t\t\tels.results.innerHTML = '<div class=\"${pfx}status-msg\">Загрузка файлов палитр...</div>';\n\n\t\t\t\tloadedColors = [];\n\t\t\t\tconst promises = [];\n\n\t\t\t\tchecked.forEach(chk => {\n\t\t\t\t\tconst idx = parseInt(chk.value);\n\t\t\t\t\tconst item = paletteIndex[idx];\n\t\t\t\t\tconst url = item.url || item.file; // Поддержка разных ключей\n\n\t\t\t\t\tif (!url) {\n\t\t\t\t\t\tlog(`⚠️ Нет URL для ${item.name}`);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst p = fetch(url)\n\t\t\t\t\t\t.then(res => {\n\t\t\t\t\t\t\tif (!res.ok) throw new Error(`HTTP ${res.status}`);\n\t\t\t\t\t\t\treturn res.json();\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.then(data => {\n\t\t\t\t\t\t\t// Ожидаем массив цветов. Если объект, пробуем найти массив внутри\n\t\t\t\t\t\t\tlet colors = Array.isArray(data) ? data : (data.colors || data.palettes || []);\n\n\t\t\t\t\t\t\t// Добавляем метку источника, если её нет\n\t\t\t\t\t\t\tcolors = colors.map(c => ({\n\t\t\t\t\t\t\t\t...c,\n\t\t\t\t\t\t\t\tcollection: c.collection || item.name || url\n\t\t\t\t\t\t\t}));\n\n\t\t\t\t\t\t\tloadedColors = [...loadedColors, ...colors];\n\t\t\t\t\t\t\tlog(`✅ Загружено: ${item.name} (${colors.length} цветов)`);\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch(err => {\n\t\t\t\t\t\t\tlog(`❌ Ошибка ${item.name}: ${err.message}`);\n\t\t\t\t\t\t});\n\n\t\t\t\t\tpromises.push(p);\n\t\t\t\t});\n\n\t\t\t\tawait Promise.all(promises);\n\n\t\t\t\tels.btnLoad.classList.remove(`${pfx}btn-loading`);\n\t\t\t\tels.btnLoad.textContent = `Загружено (${loadedColors.length} цветов)`;\n\n\t\t\t\tif (loadedColors.length === 0) {\n\t\t\t\t\tels.results.innerHTML = '<div class=\"${pfx}error-msg\">Не удалось загрузить ни одного цвета. Проверьте консоль и логи.</div>';\n\t\t\t\t} else {\n\t\t\t\t\tels.results.innerHTML = `<div class=\"${pfx}status-msg\">В базе <strong>${loadedColors.length}</strong> цветов.<br>Введите цвет для поиска.</div>`;\n\t\t\t\t\tif (baseColor) performSearch();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// --- СОБЫТИЯ ---\n\t\t\tels.btnApply.onclick = () => {\n\t\t\t\tconst v = els.inpHex.value.trim();\n\t\t\t\tif (ColorMath.isValidHex(v)) {\n\t\t\t\t\tupdateBaseColorUI(v);\n\t\t\t\t\tresetModifiers();\n\t\t\t\t\tperformSearch();\n\t\t\t\t} else alert('Неверный HEX');\n\t\t\t};\n\n\t\t\tels.btnReset.onclick = () => {\n\t\t\t\tresetModifiers();\n\t\t\t\tif (baseColor) performSearch();\n\t\t\t};\n\n\t\t\t[els.rngL, els.rngC, els.rngH].forEach(inp => {\n\t\t\t\tinp.oninput = (e) => {\n\t\t\t\t\tconst type = e.target.id.includes('L') ? 'L' : e.target.id.includes('C') ? 'C' : 'H';\n\t\t\t\t\tmodifiers[type] = parseInt(e.target.value);\n\t\t\t\t\tconst sign = modifiers[type] > 0 ? '+' : '';\n\t\t\t\t\tconst unit = type === 'H' ? '°' : '';\n\t\t\t\t\tif (type === 'L') els.lblL.textContent = sign + modifiers[type];\n\t\t\t\t\tif (type === 'C') els.lblC.textContent = sign + modifiers[type];\n\t\t\t\t\tif (type === 'H') els.lblH.textContent = sign + modifiers[type] + unit;\n\t\t\t\t\tif (baseColor) performSearch();\n\t\t\t\t};\n\t\t\t});\n\n\t\t\tels.modBtns.forEach(btn => {\n\t\t\t\tbtn.onclick = () => {\n\t\t\t\t\tconst t = btn.dataset.type;\n\t\t\t\t\tconst v = parseInt(btn.dataset.val);\n\t\t\t\t\tmodifiers[t] += v;\n\t\t\t\t\tif (t === 'L') modifiers.L = Math.max(-40, Math.min(40, modifiers.L));\n\t\t\t\t\tif (t === 'C') modifiers.C = Math.max(-40, Math.min(40, modifiers.C));\n\n\t\t\t\t\tels.rngL.value = modifiers.L;\n\t\t\t\t\tels.lblL.textContent = (modifiers.L > 0 ? '+' : '') + modifiers.L;\n\t\t\t\t\tels.rngC.value = modifiers.C;\n\t\t\t\t\tels.lblC.textContent = (modifiers.C > 0 ? '+' : '') + modifiers.C;\n\t\t\t\t\tels.rngH.value = modifiers.H;\n\t\t\t\t\tels.lblH.textContent = (modifiers.H > 0 ? '+' : '') + modifiers.H + '°';\n\n\t\t\t\t\tif (baseColor) performSearch();\n\t\t\t\t};\n\t\t\t});\n\n\t\t\tels.selectAll.onclick = () => {\n\t\t\t\tconst all = shadow.querySelectorAll(`.${pfx}checkbox`);\n\t\t\t\tconst state = !Array.from(all).every(c => c.checked);\n\t\t\t\tall.forEach(c => c.checked = state);\n\t\t\t};\n\n\t\t\tels.btnLoad.onclick = loadSelectedPalettes;\n\n\t\t\tels.tabs.forEach(tab => {\n\t\t\t\ttab.onclick = () => {\n\t\t\t\t\tels.tabs.forEach(t => t.classList.remove('active'));\n\t\t\t\t\ttab.classList.add('active');\n\t\t\t\t\tsearchMode = tab.dataset.mode;\n\t\t\t\t\tif (baseColor && loadedColors.length > 0) performSearch();\n\t\t\t\t};\n\t\t\t});\n\n\t\t\t// Старт\n\t\t\tupdateBaseColorUI(els.inpHex.value);\n\t\t\tloadIndex();\n\n\t\t})();\n\t</script>\n</p>\n<p><strong>Подбор интерьерных цветов</strong> — важный этап при создании гармоничного дизайна помещения. Онлайн-сервис «Пэйнт.рф» позволяет быстро и точно подобрать нужный оттенок, сравнить его с популярными палитрами и найти ближайшие аналоги среди известных производителей.</p>\n<p>С помощью инструмента вы можете загрузить исходный цвет в формате HEX и мгновенно получить подборку похожих оттенков из различных каталогов, включая RAL, Pantone, Sherwin-Williams, Dulux и другие. Это особенно удобно для дизайнеров интерьера, архитекторов и всех, кто занимается ремонтом или оформлением помещений. Сервис поддерживает мультизагрузку, что позволяет одновременно работать с несколькими цветами и ускоряет процесс подбора. Гибкие настройки дают возможность изменять параметры цвета — насыщенность, светлоту и тон — чтобы добиться максимально точного результата. Отдельное преимущество — поиск аналогов по разным брендам. Если у вас есть конкретный цвет, но он недоступен у нужного производителя, вы легко найдете максимально близкий вариант. Это помогает сэкономить время и избежать ошибок при выборе краски или отделочных материалов.</p>\n<p>Инструмент также позволяет фильтровать результаты по популярности, бренду и характеристикам цвета. Благодаря этому вы получаете не просто список оттенков, а удобную систему для профессиональной работы с цветом. <em>Онлайн-подбор цветов </em>на «Пэйнт.рф» — это современное решение для точного выбора оттенков, которое подходит как для профессионалов, так и для новичков. Используйте сервис, чтобы создать идеальную цветовую палитру для вашего интерьера.</p>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-14T12:27:57+03:00",
            "date_modified": "2026-03-21T19:18:43+03:00"
        },
        {
            "id": "https://пэйнт.рф/servis.html",
            "url": "https://пэйнт.рф/servis.html",
            "title": "Поиск относительных цветов по интерьерным палитрам на CIE LCh",
            "summary": "Relative Colors Поиск относительных цветов по интерьерным палитрам на CIE LCh Стандартные RGB и HEX плохо подходят для запроса \"похожий, но немного другой\". Здесь используется&hellip;",
            "content_html": "<div class=\"wrap\"><header class=\"topbar\">\n<div class=\"brand\">\n<div class=\"mark\"> </div>\n<div style=\"min-width: 0;\">\n<h1>Relative Colors</h1>\n<p>Поиск относительных цветов по интерьерным палитрам на CIE LCh</p>\n</div>\n</div>\n<div class=\"actions\"><button id=\"btnReload\" class=\"btn\" type=\"button\">Перезагрузить палитры</button> <button id=\"btnSearchNearest\" class=\"btn primary\" type=\"button\">Искать по HEX / Lab</button></div>\n</header>\n<section class=\"hero\">\n<div class=\"panel\">\n<div class=\"badge\">Теплее, холоднее, светлее, темнее - по-человечески</div>\n<h2>Найди цвет похожий, но чуть теплее и светлее.</h2>\n<p class=\"lead\">Стандартные RGB и HEX плохо подходят для запроса \"похожий, но немного другой\". Здесь используется <strong>CIE LCh</strong> - модель, которая ближе к тому, как глаз воспринимает светлоту, насыщенность и оттенок. Вы задаете понятный сдвиг, а сервис ищет совпадения в интерьерных палитрах.</p>\n<div class=\"chips\"><span class=\"chip\"><i></i> L*: 0-100</span> <span class=\"chip\"><i style=\"background: var(--accent2);\"></i> C*: насыщенность</span> <span class=\"chip\"><i style=\"background: var(--accent3);\"></i> h*: 0-360°</span></div>\n<div id=\"loadStatus\" class=\"status\">Палитры еще не загружены.</div>\n<div class=\"help\">Отдельный режим ищет ближайший интерьерный цвет по HEX или Lab. Это полезно, когда у вас уже есть исходный образец.</div>\n</div>\n<aside class=\"side\">\n<div class=\"mini\">\n<h3>Как это работает</h3>\n<ol class=\"steps\">\n<li>\n<div class=\"n\">1</div>\n<div><strong>Берем базовый цвет</strong>HEX или Lab → перевод в Lab/LCh</div>\n</li>\n<li>\n<div class=\"n\">2</div>\n<div><strong>Понимаем запрос</strong>Теплее, холоднее, светлее, темнее, насыщеннее</div>\n</li>\n<li>\n<div class=\"n\">3</div>\n<div><strong>Ищем в палитрах</strong>Сравниваем с готовыми интерьерными цветами</div>\n</li>\n</ol>\n</div>\n</aside>\n</section>\n<section class=\"grid\">\n<div class=\"card\">\n<h3>Относительный поиск</h3>\n<div class=\"tabs\" role=\"tablist\" aria-label=\"Режим поиска\"><button id=\"tabRelative\" class=\"tab\" role=\"tab\" type=\"button\" aria-selected=\"true\">Словами</button> <button id=\"tabNearest\" class=\"tab\" role=\"tab\" type=\"button\" aria-selected=\"false\">По HEX / Lab</button></div>\n<div id=\"relativePane\" class=\"stack\" style=\"margin-top: 14px;\">\n<div class=\"two\">\n<div class=\"field\"><label for=\"baseHex\">Базовый HEX</label><input id=\"baseHex\" value=\"#C8B39B\" autocomplete=\"off\" spellcheck=\"false\" type=\"text\">\n<div class=\"help\">Формат: <code>#RRGGBB</code>.</div>\n</div>\n<div class=\"field\"><label for=\"baseLab\">Или Lab</label><input id=\"baseLab\" placeholder=\"74, 2, 18\" autocomplete=\"off\" spellcheck=\"false\" type=\"text\">\n<div class=\"help\">Формат: <code>L, a, b</code>. Lab приоритетнее HEX.</div>\n</div>\n</div>\n<div class=\"field\"><label for=\"prompt\">Человеческий запрос</label> <textarea id=\"prompt\" placeholder=\"Например: чуть теплее и светлее, но спокойнее и менее насыщенно\"></textarea>\n<div class=\"help\">Поддерживаются слова: <code>теплее</code>, <code>холоднее</code>, <code>светлее</code>, <code>темнее</code>, <code>насыщеннее</code>, <code>приглушеннее</code>. Усилители: <code>чуть</code>, <code>слегка</code>, <code>немного</code>, <code>сильно</code>, <code>очень</code>.</div>\n</div>\n<div class=\"ranges\">\n<div class=\"range-row\">\n<div class=\"range-head\">Светлота<strong id=\"outDL\">0</strong></div>\n<input id=\"rL\" type=\"range\" min=\"-25\" max=\"25\" step=\"1\" value=\"0\"></div>\n<div class=\"range-row\">\n<div class=\"range-head\">Насыщенность<strong id=\"outDC\">0</strong></div>\n<input id=\"rC\" type=\"range\" min=\"-35\" max=\"35\" step=\"1\" value=\"0\"></div>\n<div class=\"range-row\">\n<div class=\"range-head\">Теплее / холоднее<strong id=\"outDH\">0°</strong></div>\n<input id=\"rH\" type=\"range\" min=\"-45\" max=\"45\" step=\"1\" value=\"0\"></div>\n</div>\n<div class=\"quick\"><button class=\"btn\" type=\"button\" data-nudge=\"L:+5\">Светлее</button> <button class=\"btn\" type=\"button\" data-nudge=\"L:-5\">Темнее</button> <button class=\"btn\" type=\"button\" data-nudge=\"H:+10\">Теплее</button> <button class=\"btn\" type=\"button\" data-nudge=\"H:-10\">Холоднее</button> <button class=\"btn\" type=\"button\" data-nudge=\"C:+8\">Насыщеннее</button> <button class=\"btn\" type=\"button\" data-nudge=\"C:-8\">Спокойнее</button> <button id=\"btnReset\" class=\"btn\" type=\"button\">Сброс</button></div>\n<div class=\"swatches\">\n<div class=\"swatch\">\n<div id=\"baseBar\" class=\"bar\"></div>\n<div class=\"meta\">\n<div class=\"meta-top\"><strong>База</strong><code id=\"baseOutHex\">#C8B39B</code></div>\n<div class=\"sub\">LCh: <code id=\"baseOutLch\">—</code></div>\n</div>\n</div>\n<div class=\"swatch\">\n<div id=\"targetBar\" class=\"bar\"></div>\n<div class=\"meta\">\n<div class=\"meta-top\"><strong>Цель</strong><code id=\"targetOutHex\">—</code></div>\n<div class=\"sub\">LCh: <code id=\"targetOutLch\">—</code></div>\n</div>\n</div>\n</div>\n<div id=\"relativeStatus\" class=\"status\">Задайте базовый цвет и описание сдвига, затем смотрите совпадения ниже.</div>\n</div>\n<div id=\"nearestPane\" class=\"stack hidden\" style=\"margin-top: 14px;\">\n<div class=\"two\">\n<div class=\"field\"><label for=\"nearestHex\">Искомый HEX</label><input id=\"nearestHex\" value=\"#C8B39B\" autocomplete=\"off\" spellcheck=\"false\" type=\"text\"></div>\n<div class=\"field\"><label for=\"nearestLab\">Или Lab</label><input id=\"nearestLab\" placeholder=\"74, 2, 18\" autocomplete=\"off\" spellcheck=\"false\" type=\"text\"></div>\n</div>\n<div class=\"help\">Этот режим не сдвигает исходный цвет, а ищет ближайшие варианты в базе палитр.</div>\n<div class=\"quick\"><button id=\"btnRunNearest\" class=\"btn primary\" type=\"button\">Найти ближайшие цвета</button></div>\n<div id=\"nearestStatus\" class=\"status\">Вставьте HEX или Lab и запустите поиск.</div>\n</div>\n</div>\n<div class=\"card\">\n<h3>Совпадения по палитрам</h3>\n<div class=\"help\">Показываются ближайшие цвета по загруженным интерьерным палитрам. Если локальные данные не найдены, включается демонстрационная база.</div>\n<div id=\"paletteList\" class=\"palette-list\"></div>\n</div>\n</section>\n<section class=\"grid\" style=\"padding-top: 18px;\">\n<div class=\"card\">\n<h3>Топ цветов</h3>\n<div class=\"help\">Ближайшие отдельные цвета по текущей цели.</div>\n<div id=\"colorResults\" class=\"results\"></div>\n</div>\n</section>\n</div>\n<p>\n<script>\n\t\t\"use strict\";\n\t\tconst DATA_ROOT = new URL(\"./data/\", location.href);\n\t\tconst ROOT_DATA_DIR = new URL(\"./data/data/\", location.href);\n\t\tconst DEMO_PALETTES = [{\n\t\t\t\tname: \"Nordic Warm Neutrals\",\n\t\t\t\tcolors: [\n\t\t\t\t\t[\"Linen Chalk\", \"#E8DDCA\"],\n\t\t\t\t\t[\"Oat Silk\", \"#DCC9AC\"],\n\t\t\t\t\t[\"Warm Putty\", \"#C8B39B\"],\n\t\t\t\t\t[\"Clay Mist\", \"#B89479\"],\n\t\t\t\t\t[\"Terracotta Dusk\", \"#A45F3B\"]\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"Stone & Plaster\",\n\t\t\t\tcolors: [\n\t\t\t\t\t[\"Bone White\", \"#EFE7D8\"],\n\t\t\t\t\t[\"Dune Beige\", \"#D5C2A7\"],\n\t\t\t\t\t[\"Greige Drift\", \"#BDB2A6\"],\n\t\t\t\t\t[\"Graphite Plaster\", \"#6D6A66\"],\n\t\t\t\t\t[\"Charcoal Mortar\", \"#3A3938\"]\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"Sage & Sea\",\n\t\t\t\tcolors: [\n\t\t\t\t\t[\"Pale Sage\", \"#C9D1C0\"],\n\t\t\t\t\t[\"Dusty Olive\", \"#8E9A7D\"],\n\t\t\t\t\t[\"Deep Moss\", \"#3F5141\"],\n\t\t\t\t\t[\"Salt Teal\", \"#8AB7B0\"],\n\t\t\t\t\t[\"Deep Teal\", \"#1F6F74\"]\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"Dusty Blues\",\n\t\t\t\tcolors: [\n\t\t\t\t\t[\"Powder Blue\", \"#B5C7D6\"],\n\t\t\t\t\t[\"Slate Blue\", \"#60748B\"],\n\t\t\t\t\t[\"Night Indigo\", \"#1B2A3A\"],\n\t\t\t\t\t[\"Blush Plaster\", \"#E1C6C1\"],\n\t\t\t\t\t[\"Muted Rose\", \"#C79C96\"]\n\t\t\t\t]\n\t\t\t}\n\t\t];\n\t\tconst $ = id => document.getElementById(id),\n\t\t\tclamp = (v, a, b) => Math.min(b, Math.max(a, v)),\n\t\t\tround = (v, p = 1) => Math.round(v * 10 ** p) / 10 ** p,\n\t\t\tuniq = list => [...new Set(list.filter(Boolean))];\n\n\t\tfunction parseHex(str) {\n\t\t\tconst m = /^#?([0-9a-fA-F]{6})$/.exec(String(str || \"\").trim());\n\t\t\tif (!m) return null;\n\t\t\tconst hex = \"#\" + m[1].toLowerCase();\n\t\t\treturn {\n\t\t\t\thex,\n\t\t\t\tr: parseInt(hex.slice(1, 3), 16),\n\t\t\t\tg: parseInt(hex.slice(3, 5), 16),\n\t\t\t\tb: parseInt(hex.slice(5, 7), 16)\n\t\t\t}\n\t\t}\n\n\t\tfunction hexFromRgb({\n\t\t\tr,\n\t\t\tg,\n\t\t\tb\n\t\t}) {\n\t\t\tconst to2 = n => clamp(Math.round(n), 0, 255).toString(16).padStart(2, \"0\");\n\t\t\treturn \"#\" + to2(r) + to2(g) + to2(b)\n\t\t}\n\n\t\tfunction srgbToLinear(v) {\n\t\t\tconst x = v / 255;\n\t\t\treturn x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)\n\t\t}\n\n\t\tfunction linearToSrgb01(x) {\n\t\t\tconst v = x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;\n\t\t\treturn clamp(v, 0, 1)\n\t\t}\n\t\tconst REF_X = 0.95047,\n\t\t\tREF_Y = 1,\n\t\t\tREF_Z = 1.08883\n\n\t\tfunction rgbToXyz({\n\t\t\tr,\n\t\t\tg,\n\t\t\tb\n\t\t}) {\n\t\t\tconst R = srgbToLinear(r),\n\t\t\t\tG = srgbToLinear(g),\n\t\t\t\tB = srgbToLinear(b);\n\t\t\treturn {\n\t\t\t\tX: R * 0.4124564 + G * 0.3575761 + B * 0.1804375,\n\t\t\t\tY: R * 0.2126729 + G * 0.7151522 + B * 0.072175,\n\t\t\t\tZ: R * 0.0193339 + G * 0.119192 + B * 0.9503041\n\t\t\t}\n\t\t}\n\n\t\tfunction xyzToRgb({\n\t\t\tX,\n\t\t\tY,\n\t\t\tZ\n\t\t}) {\n\t\t\tconst R = X * 3.2404542 + Y * -1.5371385 + Z * -0.4985314,\n\t\t\t\tG = X * -0.969266 + Y * 1.8760108 + Z * 0.041556,\n\t\t\t\tB = X * 0.0556434 + Y * -0.2040259 + Z * 1.0572252;\n\t\t\treturn {\n\t\t\t\tr: Math.round(linearToSrgb01(R) * 255),\n\t\t\t\tg: Math.round(linearToSrgb01(G) * 255),\n\t\t\t\tb: Math.round(linearToSrgb01(B) * 255)\n\t\t\t}\n\t\t}\n\n\t\tfunction fLab(t) {\n\t\t\treturn t > 0.008856451679 ? Math.cbrt(t) : (903.296296296 * t + 16) / 116\n\t\t}\n\n\t\tfunction finvLab(t) {\n\t\t\tconst t3 = t * t * t;\n\t\t\treturn t3 > 0.008856451679 ? t3 : (116 * t - 16) / 903.296296296\n\t\t}\n\n\t\tfunction xyzToLab({\n\t\t\tX,\n\t\t\tY,\n\t\t\tZ\n\t\t}) {\n\t\t\tconst x = fLab(X / REF_X),\n\t\t\t\ty = fLab(Y / REF_Y),\n\t\t\t\tz = fLab(Z / REF_Z);\n\t\t\treturn {\n\t\t\t\tL: 116 * y - 16,\n\t\t\t\ta: 500 * (x - y),\n\t\t\t\tb: 200 * (y - z)\n\t\t\t}\n\t\t}\n\n\t\tfunction labToXyz({\n\t\t\tL,\n\t\t\ta,\n\t\t\tb\n\t\t}) {\n\t\t\tconst y = (L + 16) / 116,\n\t\t\t\tx = y + a / 500,\n\t\t\t\tz = y - b / 200;\n\t\t\treturn {\n\t\t\t\tX: REF_X * finvLab(x),\n\t\t\t\tY: REF_Y * finvLab(y),\n\t\t\t\tZ: REF_Z * finvLab(z)\n\t\t\t}\n\t\t}\n\n\t\tfunction labToLch({\n\t\t\tL,\n\t\t\ta,\n\t\t\tb\n\t\t}) {\n\t\t\tconst C = Math.sqrt(a * a + b * b);\n\t\t\tlet h = Math.atan2(b, a) * 180 / Math.PI;\n\t\t\tif (h < 0) h += 360;\n\t\t\treturn {\n\t\t\t\tL,\n\t\t\t\tC,\n\t\t\t\th\n\t\t\t}\n\t\t}\n\n\t\tfunction lchToLab({\n\t\t\tL,\n\t\t\tC,\n\t\t\th\n\t\t}) {\n\t\t\tconst hr = h * Math.PI / 180;\n\t\t\treturn {\n\t\t\t\tL,\n\t\t\t\ta: C * Math.cos(hr),\n\t\t\t\tb: C * Math.sin(hr)\n\t\t\t}\n\t\t}\n\n\t\tfunction safeParseLab(str) {\n\t\t\tconst p = String(str || \"\").trim().split(/[\\s,]+/).filter(Boolean).map(Number);\n\t\t\tif (p.length < 3 || !p.slice(0, 3).every(Number.isFinite)) return null;\n\t\t\treturn {\n\t\t\t\tL: p[0],\n\t\t\t\ta: p[1],\n\t\t\t\tb: p[2]\n\t\t\t}\n\t\t}\n\t\tconst labDistance = (a, b) => Math.sqrt((a.L - b.L) ** 2 + (a.a - b.a) ** 2 + (a.b - b.b) ** 2),\n\t\t\tformatLch = l => `L* ${round(l.L,1)}  C* ${round(l.C,1)}  h* ${round(l.h,1)}°`;\n\n\t\tfunction escapeHtml(s) {\n\t\t\treturn String(s).replaceAll(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\")\n\t\t}\n\n\t\tfunction colorFromAny(v, fallback = \"\") {\n\t\t\tif (typeof v === \"string\") {\n\t\t\t\tconst rgb = parseHex(v);\n\t\t\t\tif (!rgb) return null;\n\t\t\t\tconst lab = xyzToLab(rgbToXyz(rgb));\n\t\t\t\treturn {\n\t\t\t\t\tname: fallback || v,\n\t\t\t\t\thex: rgb.hex,\n\t\t\t\t\trgb,\n\t\t\t\t\tlab,\n\t\t\t\t\tlch: labToLch(lab)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!v || typeof v !== \"object\") return null;\n\t\t\tconst rgb = parseHex(v.hex || v.color || v.value || v.code || v.rgbHex);\n\t\t\tif (!rgb) return null;\n\t\t\tconst lab = xyzToLab(rgbToXyz(rgb));\n\t\t\treturn {\n\t\t\t\tname: v.name || v.title || fallback || rgb.hex,\n\t\t\t\thex: rgb.hex,\n\t\t\t\trgb,\n\t\t\t\tlab,\n\t\t\t\tlch: labToLch(lab)\n\t\t\t}\n\t\t}\n\n\t\tfunction normalizePalettePayload(payload, fallbackName) {\n\t\t\tif (Array.isArray(payload)) return {\n\t\t\t\tname: fallbackName || \"Palette\",\n\t\t\t\tcolors: payload.map((item, i) => typeof item === \"string\" ? colorFromAny(item, `Color ${i+1}`) : colorFromAny(item, item?.name || `Color ${i+1}`)).filter(Boolean)\n\t\t\t};\n\t\t\tif (!payload || typeof payload !== \"object\") return {\n\t\t\t\tname: fallbackName || \"Palette\",\n\t\t\t\tcolors: []\n\t\t\t};\n\t\t\tconst raw = payload.colors || payload.items || payload.paints || payload.swatches || payload.palette || [];\n\t\t\treturn {\n\t\t\t\tname: payload.name || payload.title || fallbackName || \"Palette\",\n\t\t\t\tbrand: payload.brand || payload.collection || \"\",\n\t\t\t\tcolors: Array.isArray(raw) ? raw.map((item, i) => colorFromAny(item, item?.name || `Color ${i+1}`)).filter(Boolean) : []\n\t\t\t}\n\t\t}\n\t\tasync function fetchJson(url) {\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tcache: \"no-store\"\n\t\t\t});\n\t\t\tif (!res.ok) throw new Error(res.statusText);\n\t\t\treturn await res.json()\n\t\t}\n\n\t\tfunction normalizePaletteList(raw) {\n\t\t\tif (Array.isArray(raw)) return raw;\n\t\t\tif (raw && Array.isArray(raw.palettes)) return raw.palettes;\n\t\t\tif (raw && Array.isArray(raw.items)) return raw.items;\n\t\t\tif (raw && Array.isArray(raw.data)) return raw.data;\n\t\t\treturn []\n\t\t}\n\n\t\tfunction parsePaletteDescriptor(entry, i) {\n\t\t\tif (typeof entry === \"string\") return {\n\t\t\t\tname: entry.replace(/\\.json$/i, \"\"),\n\t\t\t\tfile: entry\n\t\t\t};\n\t\t\tif (!entry || typeof entry !== \"object\") return {\n\t\t\t\tname: `Palette ${i+1}`,\n\t\t\t\tfile: \"\"\n\t\t\t};\n\t\t\treturn {\n\t\t\t\tname: entry.name || entry.title || entry.slug || `Palette ${i+1}`,\n\t\t\t\tfile: entry.file || entry.path || entry.slug || entry.name || \"\",\n\t\t\t\traw: entry\n\t\t\t}\n\t\t}\n\n\t\tfunction normalizeEntryPath(raw) {\n\t\t\treturn String(raw || \"\").trim().replace(/^\\.?\\//, \"\").replace(/^data%2F/i, \"\").replace(/^data\\//i, \"\")\n\t\t}\n\n\t\tfunction candidateUrlsForEntry(entry) {\n\t\t\tconst raw = typeof entry === \"string\" ? entry : entry?.file || entry?.path || entry?.slug || entry?.name;\n\t\t\tif (!raw) return [];\n\t\t\tconst base = normalizeEntryPath(raw);\n\t\t\tconst stem = base.endsWith(\".json\") ? base : `${base}.json`;\n\t\t\treturn uniq([new URL(stem, DATA_ROOT).href, new URL(encodeURIComponent(stem), DATA_ROOT).href, new URL(base, DATA_ROOT).href, new URL(encodeURIComponent(base), DATA_ROOT).href])\n\t\t}\n\n\t\tfunction candidateListUrls() {\n\t\t\treturn uniq([new URL(\"palettes-list.json\", DATA_ROOT).href, new URL(\"palettes-list.json\", ROOT_DATA_DIR).href, new URL(\"data/palettes-list.json\", ROOT_DATA_DIR).href])\n\t\t}\n\t\tasync function loadPalettes() {\n\t\t\tlet rawList = null;\n\t\t\tfor (const url of candidateListUrls()) {\n\t\t\t\ttry {\n\t\t\t\t\trawList = await fetchJson(url);\n\t\t\t\t\tbreak\n\t\t\t\t} catch (e) {}\n\t\t\t}\n\t\t\tif (!rawList) return [];\n\t\t\tconst descriptors = normalizePaletteList(rawList).map(parsePaletteDescriptor);\n\t\t\tconst palettes = [];\n\t\t\tfor (const desc of descriptors) {\n\t\t\t\tif (desc.raw && (Array.isArray(desc.raw.colors) || Array.isArray(desc.raw.items) || Array.isArray(desc.raw.swatches))) {\n\t\t\t\t\tpalettes.push(normalizePalettePayload(desc.raw, desc.name));\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlet loaded = null;\n\t\t\t\tfor (const url of candidateUrlsForEntry(desc.file || desc.name)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tloaded = await fetchJson(url);\n\t\t\t\t\t\tbreak\n\t\t\t\t\t} catch (e) {}\n\t\t\t\t}\n\t\t\t\tif (loaded) palettes.push(normalizePalettePayload(loaded, desc.name))\n\t\t\t}\n\t\t\treturn palettes.filter(p => p.colors && p.colors.length)\n\t\t}\n\n\t\tfunction flattenColors(palettes) {\n\t\t\tconst items = [];\n\t\t\tfor (const palette of palettes)\n\t\t\t\tfor (const color of palette.colors) items.push({\n\t\t\t\t\tpalette: palette.name,\n\t\t\t\t\tbrand: palette.brand || \"\",\n\t\t\t\t\t...color\n\t\t\t\t});\n\t\t\treturn items\n\t\t}\n\n\t\tfunction parseNudgeText(text) {\n\t\t\tconst t = String(text || \"\").toLowerCase();\n\t\t\tlet scale = 1;\n\t\t\tif (/(чуть|слегка|немного)/.test(t)) scale = .5;\n\t\t\tif (/(сильно|очень)/.test(t)) scale = 1.5;\n\t\t\tlet dL = 0,\n\t\t\t\tdC = 0,\n\t\t\t\tdH = 0;\n\t\t\tif (/светлее/.test(t)) dL += 8 * scale;\n\t\t\tif (/темнее/.test(t)) dL -= 8 * scale;\n\t\t\tif (/теплее/.test(t)) dH += 10 * scale;\n\t\t\tif (/холоднее/.test(t)) dH -= 10 * scale;\n\t\t\tif (/(насыщеннее|ярче|сочнее)/.test(t)) dC += 8 * scale;\n\t\t\tif (/(приглушеннее|спокойнее|мягче|пыльнее)/.test(t)) dC -= 8 * scale;\n\t\t\treturn {\n\t\t\t\tdL,\n\t\t\t\tdC,\n\t\t\t\tdH\n\t\t\t}\n\t\t}\n\t\tconst state = {\n\t\t\tmode: \"relative\",\n\t\t\tdL: 0,\n\t\t\tdC: 0,\n\t\t\tdH: 0,\n\t\t\tpalettes: [],\n\t\t\tcolors: [],\n\t\t\tlastTarget: null\n\t\t}\n\n\t\tfunction getBaseLab() {\n\t\t\tconst fromLab = safeParseLab($(\"baseLab\").value);\n\t\t\tif (fromLab) return fromLab;\n\t\t\tconst fromHex = parseHex($(\"baseHex\").value);\n\t\t\treturn fromHex ? xyzToLab(rgbToXyz(fromHex)) : null\n\t\t}\n\n\t\tfunction targetFromRelative() {\n\t\t\tconst base = getBaseLab();\n\t\t\tif (!base) return null;\n\t\t\tconst baseLch = labToLch(base);\n\t\t\tconst targetLch = {\n\t\t\t\tL: clamp(baseLch.L + state.dL, 0, 100),\n\t\t\t\tC: Math.max(0, baseLch.C + state.dC),\n\t\t\t\th: (baseLch.h + state.dH + 360) % 360\n\t\t\t};\n\t\t\treturn {\n\t\t\t\tbase,\n\t\t\t\tbaseLch,\n\t\t\t\ttargetLch,\n\t\t\t\ttargetLab: lchToLab(targetLch)\n\t\t\t}\n\t\t}\n\n\t\tfunction targetFromNearest() {\n\t\t\tconst lab = safeParseLab($(\"nearestLab\").value);\n\t\t\tif (lab) return {\n\t\t\t\ttargetLab: lab,\n\t\t\t\tsource: \"Lab\"\n\t\t\t};\n\t\t\tconst hex = parseHex($(\"nearestHex\").value);\n\t\t\tif (hex) return {\n\t\t\t\ttargetLab: xyzToLab(rgbToXyz(hex)),\n\t\t\t\tsource: \"HEX\"\n\t\t\t};\n\t\t\treturn null\n\t\t}\n\n\t\tfunction setMode(mode) {\n\t\t\tstate.mode = mode;\n\t\t\t$(\"tabRelative\").setAttribute(\"aria-selected\", String(mode === \"relative\"));\n\t\t\t$(\"tabNearest\").setAttribute(\"aria-selected\", String(mode === \"nearest\"));\n\t\t\t$(\"relativePane\").classList.toggle(\"hidden\", mode !== \"relative\");\n\t\t\t$(\"nearestPane\").classList.toggle(\"hidden\", mode !== \"nearest\")\n\t\t}\n\n\t\tfunction nearestColor(color, target) {\n\t\t\treturn {\n\t\t\t\t...color,\n\t\t\t\tscore: labDistance(color.lab, target)\n\t\t\t}\n\t\t}\n\n\t\tfunction rankPalette(palette, target) {\n\t\t\tconst ranked = palette.colors.map(c => ({\n\t\t\t\t...c,\n\t\t\t\tscore: labDistance(c.lab, target)\n\t\t\t})).sort((a, b) => a.score - b.score);\n\t\t\treturn {\n\t\t\t\t...palette,\n\t\t\t\tbest: ranked[0] || null,\n\t\t\t\tranked,\n\t\t\t\tscore: ranked[0] ? ranked[0].score : Infinity\n\t\t\t}\n\t\t}\n\n\t\tfunction renderTopLists(targetLab) {\n\t\t\tconst ranked = state.colors.map(c => nearestColor(c, targetLab)).sort((a, b) => a.score - b.score);\n\t\t\tconst topColors = ranked.slice(0, 10);\n\t\t\tconst topPalettes = state.palettes.map(p => rankPalette(p, targetLab)).sort((a, b) => a.score - b.score).slice(0, 4);\n\t\t\tconst colorRoot = $(\"colorResults\");\n\t\t\tcolorRoot.innerHTML = \"\";\n\t\t\tif (!topColors.length) {\n\t\t\t\tcolorRoot.innerHTML = '<div class=\"status\">Нет цветов для показа. Проверьте загрузку данных или откройте страницу через сервер.</div>'\n\t\t\t} else\n\t\t\t\tfor (const item of topColors) {\n\t\t\t\t\tconst node = document.createElement(\"div\");\n\t\t\t\t\tnode.className = \"result\";\n\t\t\t\t\tnode.innerHTML = `<div class=\"sw\" style=\"background:${item.hex}\"></div><div class=\"name\"><b>${escapeHtml(item.name||item.hex)}</b><span>${escapeHtml(item.palette)}${item.brand?\" · \"+escapeHtml(item.brand):\"\"} · ${item.hex.toUpperCase()}</span></div><div class=\"score\">${round(item.score,1)}<small>${item.score<8?\"почти точное совпадение\":item.score<16?\"очень близко\":\"близко\"}</small></div>`;\n\t\t\t\t\tcolorRoot.appendChild(node)\n\t\t\t\t}\n\t\t\tconst paletteRoot = $(\"paletteList\");\n\t\t\tpaletteRoot.innerHTML = \"\";\n\t\t\tif (!topPalettes.length) {\n\t\t\t\tpaletteRoot.innerHTML = '<div class=\"status\">Палитры еще не загружены. Нажмите \"Перезагрузить палитры\" или проверьте файл <code>data/palettes-list.json</code>.</div>';\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor (const palette of topPalettes) {\n\t\t\t\tconst box = document.createElement(\"div\");\n\t\t\t\tbox.className = \"palette-card\";\n\t\t\t\tbox.innerHTML = `<div class=\"palette-top\"><b>${escapeHtml(palette.name)}</b><span>${palette.best?round(palette.best.score,1)+\" pts\":\"no data\"}</span></div><div class=\"swatch-row\"></div>`;\n\t\t\t\tconst row = box.querySelector(\".swatch-row\");\n\t\t\t\tfor (const color of palette.ranked.slice(0, 4)) {\n\t\t\t\t\tconst tile = document.createElement(\"div\");\n\t\t\t\t\ttile.className = \"tiny\";\n\t\t\t\t\ttile.innerHTML = `<div class=\"c\" style=\"background:${color.hex}\"></div><div class=\"t\">${escapeHtml(color.name||color.hex)}<br><span style=\"color:rgba(29,26,22,.62)\">${color.hex.toUpperCase()}</span></div>`;\n\t\t\t\t\trow.appendChild(tile)\n\t\t\t\t}\n\t\t\t\tpaletteRoot.appendChild(box)\n\t\t\t}\n\t\t}\n\n\t\tfunction renderRelative() {\n\t\t\tconst parsed = targetFromRelative();\n\t\t\tif (!parsed) {\n\t\t\t\t$(\"relativeStatus\").textContent = \"Не получилось распознать базовый цвет. Введите корректный HEX (#RRGGBB) или Lab (L, a, b).\";\n\t\t\t\t$(\"baseOutHex\").textContent = $(\"targetOutHex\").textContent = \"—\";\n\t\t\t\t$(\"baseOutLch\").textContent = $(\"targetOutLch\").textContent = \"—\";\n\t\t\t\t$(\"baseBar\").style.background = $(\"targetBar\").style.background = \"transparent\";\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst baseHex = hexFromRgb(xyzToRgb(labToXyz(parsed.base))),\n\t\t\t\ttargetHex = hexFromRgb(xyzToRgb(labToXyz(parsed.targetLab)));\n\t\t\t$(\"outDL\").textContent = (state.dL >= 0 ? \"+\" : \"\") + state.dL;\n\t\t\t$(\"outDC\").textContent = (state.dC >= 0 ? \"+\" : \"\") + state.dC;\n\t\t\t$(\"outDH\").textContent = (state.dH >= 0 ? \"+\" : \"\") + state.dH + \"°\";\n\t\t\t$(\"baseOutHex\").textContent = baseHex.toUpperCase();\n\t\t\t$(\"baseOutLch\").textContent = formatLch(parsed.baseLch);\n\t\t\t$(\"targetOutHex\").textContent = targetHex.toUpperCase();\n\t\t\t$(\"targetOutLch\").textContent = formatLch(labToLch(parsed.targetLab));\n\t\t\t$(\"baseBar\").style.background = baseHex;\n\t\t\t$(\"targetBar\").style.background = targetHex;\n\t\t\t$(\"relativeStatus\").textContent = \"Цель построена в CIE LCh. Теперь сервис сравнивает ее с загруженными интерьерными цветами.\";\n\t\t\tstate.lastTarget = parsed.targetLab;\n\t\t\trenderTopLists(parsed.targetLab)\n\t\t}\n\n\t\tfunction renderNearest() {\n\t\t\tconst parsed = targetFromNearest();\n\t\t\tif (!parsed) {\n\t\t\t\t$(\"nearestStatus\").textContent = \"Не получилось распознать HEX или Lab. Попробуйте формат #RRGGBB или L, a, b.\";\n\t\t\t\treturn\n\t\t\t}\n\t\t\t$(\"nearestStatus\").textContent = `Ищем ближайшие цвета к ${parsed.source}. Результат обновлен по текущей базе палитр.`;\n\t\t\tstate.lastTarget = parsed.targetLab;\n\t\t\trenderTopLists(parsed.targetLab)\n\t\t}\n\n\t\tfunction syncSlidersFromPrompt() {\n\t\t\tconst d = parseNudgeText($(\"prompt\").value);\n\t\t\tstate.dL = clamp(d.dL, -25, 25);\n\t\t\tstate.dC = clamp(d.dC, -35, 35);\n\t\t\tstate.dH = clamp(d.dH, -45, 45);\n\t\t\t$(\"rL\").value = String(state.dL);\n\t\t\t$(\"rC\").value = String(state.dC);\n\t\t\t$(\"rH\").value = String(state.dH);\n\t\t\trenderRelative()\n\t\t}\n\n\t\tfunction bind() {\n\t\t\t$(\"tabRelative\").addEventListener(\"click\", () => setMode(\"relative\"));\n\t\t\t$(\"tabNearest\").addEventListener(\"click\", () => setMode(\"nearest\"));\n\t\t\t$(\"btnSearchNearest\").addEventListener(\"click\", () => setMode(\"nearest\"));\n\t\t\t$(\"btnRunNearest\").addEventListener(\"click\", renderNearest);\n\t\t\t$(\"btnReload\").addEventListener(\"click\", () => bootstrap());\n\t\t\t$(\"baseHex\").addEventListener(\"input\", renderRelative);\n\t\t\t$(\"baseLab\").addEventListener(\"input\", renderRelative);\n\t\t\t$(\"nearestHex\").addEventListener(\"input\", renderNearest);\n\t\t\t$(\"nearestLab\").addEventListener(\"input\", renderNearest);\n\t\t\t$(\"prompt\").addEventListener(\"input\", syncSlidersFromPrompt);\n\t\t\t$(\"rL\").addEventListener(\"input\", e => {\n\t\t\t\tstate.dL = Number(e.target.value);\n\t\t\t\trenderRelative()\n\t\t\t});\n\t\t\t$(\"rC\").addEventListener(\"input\", e => {\n\t\t\t\tstate.dC = Number(e.target.value);\n\t\t\t\trenderRelative()\n\t\t\t});\n\t\t\t$(\"rH\").addEventListener(\"input\", e => {\n\t\t\t\tstate.dH = Number(e.target.value);\n\t\t\t\trenderRelative()\n\t\t\t});\n\t\t\tdocument.querySelectorAll(\"[data-nudge]\").forEach(btn => btn.addEventListener(\"click\", () => {\n\t\t\t\tconst m = /^(L|C|H):([+-]?\\d+)$/.exec(btn.getAttribute(\"data-nudge\") || \"\");\n\t\t\t\tif (!m) return;\n\t\t\t\tconst axis = m[1],\n\t\t\t\t\tdelta = Number(m[2]);\n\t\t\t\tif (axis === \"L\") state.dL = clamp(state.dL + delta, -25, 25);\n\t\t\t\tif (axis === \"C\") state.dC = clamp(state.dC + delta, -35, 35);\n\t\t\t\tif (axis === \"H\") state.dH = clamp(state.dH + delta, -45, 45);\n\t\t\t\t$(\"rL\").value = String(state.dL);\n\t\t\t\t$(\"rC\").value = String(state.dC);\n\t\t\t\t$(\"rH\").value = String(state.dH);\n\t\t\t\trenderRelative()\n\t\t\t}));\n\t\t\t$(\"btnReset\").addEventListener(\"click\", () => {\n\t\t\t\tstate.dL = 0;\n\t\t\t\tstate.dC = 0;\n\t\t\t\tstate.dH = 0;\n\t\t\t\t$(\"prompt\").value = \"\";\n\t\t\t\t$(\"rL\").value = \"0\";\n\t\t\t\t$(\"rC\").value = \"0\";\n\t\t\t\t$(\"rH\").value = \"0\";\n\t\t\t\trenderRelative()\n\t\t\t})\n\t\t}\n\t\tasync function bootstrap() {\n\t\t\t$(\"loadStatus\").textContent = \"Подключаюсь к /DB/colors.sqlite... Загружаю палитры  /DB/full-palitres.db3...\";\n\t\t\ttry {\n\t\t\t\tconst palettes = await loadPalettes();\n\t\t\t\tstate.palettes = palettes.filter(p => p.colors && p.colors.length);\n\t\t\t\tif (!state.palettes.length) throw new Error(\"no palettes\");\n\t\t\t\tstate.colors = flattenColors(state.palettes);\n\t\t\t\t$(\"loadStatus\").textContent = `Загружено палитр: ${state.palettes.length}. Цветов в базе: ${state.colors.length}.`;\n\t\t\t\trenderRelative()\n\t\t\t} catch (e) {\n\t\t\t\tstate.palettes = DEMO_PALETTES.map(p => normalizePalettePayload(p, p.name));\n\t\t\t\tstate.colors = flattenColors(state.palettes);\n\t\t\t\t$(\"loadStatus\").textContent = \"Не удалось прочитать локальные data-файлы, поэтому включена встроенная демонстрационная база.\";\n\t\t\t\trenderRelative()\n\t\t\t}\n\t\t}\n\t\tbind();\n\t\tbootstrap();\n\t</script>\n</p>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-12T11:29:05+03:00",
            "date_modified": "2026-04-02T21:10:25+03:00"
        },
        {
            "id": "https://пэйнт.рф/kartochka-tovara.html",
            "url": "https://пэйнт.рф/kartochka-tovara.html",
            "title": "Карточка товара",
            "summary": "Интерьерная краска Premium Моющаяся акриловая краска для стен и потолков. Высокая укрывистость, устойчивость к истиранию, подходит для жилых и коммерческих помещений. 1 × 60 000&hellip;",
            "content_html": "<div class=\"paint-card\"><!-- Картинка -->\n<div class=\"paint-image\"><img loading=\"lazy\" src=\"/media/website/samplepaint.png\" alt=\"Интерьерная краска Premium\" sizes=\"(max-width: 1920px) 100vw, 1920px\" srcset=\"/media/website/responsive/samplepaint-xs.png 640w ,/media/website/responsive/samplepaint-sm.png 768w ,/media/website/responsive/samplepaint-md.png 1024w ,/media/website/responsive/samplepaint-lg.png 1366w ,/media/website/responsive/samplepaint-xl.png 1600w ,/media/website/responsive/samplepaint-2xl.png 1920w\"></div>\n<!-- Информация -->\n<div class=\"paint-info\">\n<h3 class=\"paint-title\">Интерьерная краска Premium</h3>\n<p class=\"paint-description\">Моющаяся акриловая краска для стен и потолков. Высокая укрывистость, устойчивость к истиранию, подходит для жилых и коммерческих помещений.</p>\n<!-- Выбранный цвет -->\n<div class=\"color-selected\"><span class=\"color-label\">Выбранный цвет:</span> <span id=\"selectedColorName\" class=\"color-name\">Не выбран</span> <span id=\"selectedColorPreview\" class=\"color-dot\"></span></div>\n<!-- Количество и кнопки -->\n<div class=\"paint-controls\">\n<div class=\"qty-block\"><span class=\"quantity-label\">КОЛИЧЕСТВО:</span> <input type=\"number\" id=\"paintQty\" class=\"quantity-input\" value=\"1\" min=\"1\"></div>\n<div class=\"paint-buttons\"><button id=\"selectColorBtn\" class=\"btn-secondary\">ВЫБРАТЬ ЦВЕТ</button> <button id=\"buyWithColorBtn\" class=\"btn-primary\">КУПИТЬ</button></div>\n</div>\n</div>\n</div>\n<!-- Попап выбора цвета -->\n<div id=\"colorPopupOverlay\" class=\"popup-overlay\"></div>\n<div id=\"colorPopup\" class=\"popup popup-large\"><button id=\"closeColorPopup\" class=\"popup-close\">×</button>\n<h2 class=\"popup-title\">Выберите цвет</h2>\n<div class=\"color-search\"><input type=\"text\" id=\"colorSearch\" placeholder=\"Поиск палитры...\"></div>\n<div id=\"colorGrid\" class=\"color-grid\"></div>\n<button id=\"confirmColorBtn\" class=\"popup-button\" disabled=\"disabled\">Подтвердить выбор</button></div>\n<!-- Попап покупки -->\n<div id=\"buyPopupOverlay\" class=\"popup-overlay\"></div>\n<div id=\"buyPopup\" class=\"popup\"><button id=\"closeBuyPopup\" class=\"popup-close\">×</button>\n<h2 class=\"popup-title\">Оформление заказа</h2>\n<p id=\"buyPopupPrice\" class=\"popup-subtitle\">1 × 60 000 ₽</p>\n<div class=\"selected-color-info\"><span class=\"color-label\">Выбранный цвет:</span> <span id=\"buySelectedColorName\" class=\"color-name\">Не выбран</span> <span id=\"buySelectedColorPreview\" class=\"color-dot\"></span></div>\n<form id=\"buyForm\"><input type=\"text\" id=\"buyName\" placeholder=\"Ваше имя\" required=\"\"> <input type=\"tel\" id=\"buyPhone\" placeholder=\"+7 (999) 123-45-67\" required=\"\"> <input type=\"hidden\" id=\"buySelectedColorId\"> <input type=\"hidden\" id=\"buySelectedColorNameHidden\"> <input type=\"hidden\" id=\"buyQuantity\"> <button class=\"popup-button\" type=\"submit\">Отправить заказ</button>\n<p class=\"popup-agreement\">Нажимая кнопку, вы соглашаетесь с обработкой персональных данных</p>\n</form>\n<div id=\"buySuccessMessage\" class=\"popup-success\">\n<div class=\"success-icon\">✓</div>\n<h3>Заказ отправлен!</h3>\n<p>Мы свяжемся с вами</p>\n</div>\n</div>\n<p>\n<script src=\"/js/tovar.js\"></script>\n</p>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-03T17:38:34+03:00",
            "date_modified": "2026-03-14T15:49:49+03:00"
        },
        {
            "id": "https://пэйнт.рф/color-details.html",
            "url": "https://пэйнт.рф/color-details.html",
            "title": "Карточка цвета со слайдером",
            "summary": "Гостинная 1 Описание первого слайда Описание второго слайда Описание третьего слайда Описание четвертого слайда ← Назад к палитре",
            "content_html": "<div id=\"colorCard\" class=\"color-card\">\n<div class=\"color-preview-container\">\n<div id=\"colorPreview\" class=\"color-preview\">\n<div class=\"slider-container\">\n<div class=\"slider\">\n<div class=\"slide\"><img loading=\"lazy\" src=\"https://kraski-matus.ru/media/color-slider/slide1.png\" alt=\"Гостинная\" data-is-external-image=\"true\">\n<div class=\"slide-content\">\n<h3>Гостинная 1</h3>\n<p>Описание первого слайда</p>\n</div>\n</div>\n<div class=\"slide\"><img loading=\"lazy\" src=\"https://kraski-matus.ru/media/color-slider/slide2.png\" alt=\"Кухня\" data-is-external-image=\"true\">\n<div class=\"slide-content\">\n<h3>Кухня</h3>\n<p>Описание второго слайда</p>\n</div>\n</div>\n<div class=\"slide\"><img loading=\"lazy\" src=\"https://kraski-matus.ru/media/color-slider/slide3.png\" alt=\"Гостинная 2\" data-is-external-image=\"true\">\n<div class=\"slide-content\">\n<h3>Гостинная 2</h3>\n<p>Описание третьего слайда</p>\n</div>\n</div>\n<div class=\"slide\"><img loading=\"lazy\" src=\"https://kraski-matus.ru/media/color-slider/slide4.png\" alt=\"Ванная комната\" data-is-external-image=\"true\">\n<div class=\"slide-content\">\n<h3>Ванная комната</h3>\n<p>Описание четвертого слайда</p>\n</div>\n</div>\n</div>\n<div class=\"navigation\">\n<div class=\"nav-dot\"> </div>\n<div class=\"nav-dot\"> </div>\n<div class=\"nav-dot\"> </div>\n<div class=\"nav-dot\"> </div>\n</div>\n</div>\n<div class=\"color-header\">\n<div class=\"color-info\">\n<h1 id=\"colorName\" class=\"color-name\"><span id=\"colorNameText\"></span></h1>\n<h2><span id=\"paletteInfo\" class=\"palette-info\"></span></h2>\n<div class=\"color-id\">ID: <span id=\"colorId\"></span></div>\n<div id=\"colorSeries\"></div>\n<div class=\"color-meta\">\n<div class=\"meta-row\">\n<div class=\"meta-label\">HEX:</div>\n<div id=\"colorHex\" class=\"meta-value\"></div>\n</div>\n<div class=\"meta-row\">\n<div class=\"meta-label\">RGB:</div>\n<div id=\"colorRgb\" class=\"meta-value\"></div>\n</div>\n<div class=\"meta-row\">\n<div class=\"meta-label\">LRV:</div>\n<div id=\"colorLrv\" class=\"meta-value\"></div>\n</div>\n<div class=\"meta-row\">\n<div class=\"meta-label\">LAB:</div>\n<div id=\"colorLab\" class=\"meta-value\"></div>\n</div>\n<div class=\"meta-row\">\n<div class=\"meta-label\">LCH:</div>\n<div id=\"colorLch\" class=\"meta-value\"></div>\n</div>\n<div class=\"meta-row\">\n<div class=\"meta-label\">Цветовая семья:</div>\n<div id=\"colorFamily\" class=\"meta-value\"></div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n<div class=\"similar-colors\">\n<h3>Похожие цвета</h3>\n<div id=\"similarColorsList\" class=\"similar-colors-list\"></div>\n</div>\n</div>\n<p><a href=\"javascript:history.back()\" class=\"back-link\">← Назад к палитре</a></p>\n<p> </p>",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-02T17:38:47+03:00",
            "date_modified": "2026-03-14T18:17:19+03:00"
        },
        {
            "id": "https://пэйнт.рф/palette.html",
            "url": "https://пэйнт.рф/palette.html",
            "title": "Палитра цветов",
            "summary": "ЦВЕТОВЫЕ ПАЛИТРЫ ДЛЯ ВАШЕГО БИЗНЕСА Палитра цветов Загрузка... (function(m,e,t,r,i,k,a){ m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); for (var j = 0; j",
            "content_html": "<h1>ЦВЕТОВЫЕ ПАЛИТРЫ<br>ДЛЯ ВАШЕГО БИЗНЕСА</h1>\n<div class=\"palette-container\">\n<h1 id=\"paletteTitle\">Палитра цветов</h1>\n<div id=\"colorCards\" class=\"color-cards\" style=\"display: grid; grid-template-columns: repeat(auto-fill,minmax(120px,1fr)); gap: 1rem; font-size: 0.7rem; text-align: center;\">\n<p>Загрузка...</p>\n</div>\n</div>\n<p>\n<script>\n\n// Обновление мета-тегов\nfunction updateMetaTags(paletteName) {\n    document.title = `Палитра цветов ${paletteName} | Matus Paint`;\n\n    let metaDescription = document.querySelector('meta[name=\"description\"]');\n    if (!metaDescription) {\n        metaDescription = document.createElement('meta');\n        metaDescription.name = \"description\";\n        document.head.appendChild(metaDescription);\n    }\n\n    metaDescription.content =\n        `Каталог цветов ${paletteName} с HEX-кодами и описаниями. Полная профессиональная палитра.`;\n}\n\n// Формирование URL цвета\nfunction getColorDetailsUrl(color) {\n    return `color-details.html?collection=${encodeURIComponent(color.collection)}&id=${encodeURIComponent(color.id)}`;\n}\n\nconst urlParams = new URLSearchParams(window.location.search);\nconst collectionName = urlParams.get('collection');\n\nif (!collectionName) {\n    document.getElementById('colorCards').innerHTML =\n        `<p style=\"color:red;\">Ошибка: не указана палитра.</p>`;\n} else {\n\n    const formattedPaletteName = collectionName\n        .replace(/-/g, ' ')\n        .replace(/\\b\\w/g, l => l.toUpperCase());\n\n    document.getElementById('paletteTitle').textContent =\n        `Палитра цветов — ${formattedPaletteName}`;\n\n    updateMetaTags(formattedPaletteName);\n\n    fetch(`/data/${collectionName}.json`)\n        .then(response => {\n            if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);\n            return response.json();\n        })\n        .then(colors => {\n\n            if (!colors || !colors.length)\n                throw new Error('Палитра пуста или данные некорректны.');\n\n            const container = document.getElementById('colorCards');\n            container.innerHTML = '';\n\n            colors.forEach(color => {\n\n                const card = document.createElement('a');\n                card.href = getColorDetailsUrl(color);\n                card.className = 'color-card';\n                card.style.textDecoration = 'none';\n                card.style.color = 'inherit';\n\n                card.innerHTML = `\n                    <div style=\"\n                        background:${color.hex};\n                        height:60px;\n                        margin-bottom:0.5rem;\n                        border:1px solid #eee;\">\n                    </div>\n                    <div>\n                        <div style=\"font-weight:bold;\">${color.name}</div>\n                        ${color.series ? `<div style=\"color:#666;\">${color.series}</div>` : ''}\n                    </div>\n                `;\n\n                container.appendChild(card);\n            });\n\n        })\n        .catch(error => {\n            document.getElementById('colorCards').innerHTML =\n                `<p style=\"color:red;\">\n                    Ошибка загрузки: ${error.message}<br>\n                    <a href=\"/data/${collectionName}.json\" target=\"_blank\">\n                        Проверить JSON\n                    </a>\n                </p>`;\n        });\n}\n</script>\n<script>\n        lucide.createIcons();\n\n        function toggleMenu() {\n            document.getElementById('navLinks').classList.toggle('active');\n        }\n\n        // Анимация счетчиков\n        const counters = document.querySelectorAll('.counter');\n        const speed = 200;\n\n        const startCounters = () => {\n            counters.forEach(counter => {\n                const updateCount = () => {\n                    const target = +counter.getAttribute('data-target');\n                    const count = +counter.innerText;\n                    const inc = target / speed;\n\n                    if (count < target) {\n                        counter.innerText = Math.ceil(count + inc);\n                        setTimeout(updateCount, 15);\n                    } else {\n                        counter.innerText = target + (target === 170 ? '+' : '+');\n                    }\n                };\n                updateCount();\n            });\n        };\n\n        // Запуск анимации при загрузке\n        window.addEventListener('DOMContentLoaded', startCounters);\n    </script>\n<script>\n    // Универсальная функция переключения\n    function toggleMenu() {\n        const nav = document.getElementById('navLinks');\n        const burger = document.getElementById('burgerBtn');\n        \n        nav.classList.toggle('active');\n        burger.classList.toggle('active');\n        \n        // Блокируем скролл основной страницы при открытом меню\n        if (nav.classList.contains('active')) {\n            document.body.style.overflow = 'hidden';\n        } else {\n            document.body.style.overflow = 'initial';\n        }\n    }\n\n    // Функция закрытия (для кликов по ссылкам)\n    function closeMenu() {\n        const nav = document.getElementById('navLinks');\n        const burger = document.getElementById('burgerBtn');\n        \n        nav.classList.remove('active');\n        burger.classList.remove('active');\n        document.body.style.overflow = 'initial';\n    }\n\n    // Закрытие при клике вне меню (на область контента)\n    window.addEventListener('click', (e) => {\n        const nav = document.getElementById('navLinks');\n        const burger = document.getElementById('burgerBtn');\n        if (nav.classList.contains('active') && !nav.contains(e.target) && !burger.contains(e.target)) {\n            closeMenu();\n        }\n    });\n</script>\n</p>\n<!-- Yandex.Metrika counter -->\n<p>\n<script type=\"text/javascript\">\n    (function(m,e,t,r,i,k,a){\n        m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};\n        m[i].l=1*new Date();\n        for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}\n        k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)\n    })(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=107033938', 'ym');\n\n    ym(107033938, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:\"dataLayer\", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});\n</script>\n</p>\n<noscript><div><img loading=\"lazy\" src=\"https://mc.yandex.ru/watch/107033938\" style=\"position:absolute; left:-9999px;\" alt=\"\"  data-is-external-image=\"true\"></div></noscript><!-- /Yandex.Metrika counter -->",
            "author": {
                "name": "пэйнт.рф"
            },
            "tags": [
            ],
            "date_published": "2026-03-02T17:34:02+03:00",
            "date_modified": "2026-03-02T17:36:23+03:00"
        }
    ]
}
