// pg4-debug.js - Modo debug e calibração visual para pg4 // Renderiza o campo KDE real sobre a matriz + marcadores de picos + painel de sliders. // Independente de pg4.js: reimplementa o pipeline com parâmetros mutáveis. (function () { 'use strict'; // ========================================================================= // CONFIG INICIAL // ========================================================================= const DEBUG_CONFIG = { bandwidth: 10, massaMin: 0.15, // Massa mínima para um sinal virar pG primário dominancia: 0.50, // Fração da massa do primário exigida de secundários nitidezMin: 0.25, // Destaque do pico em relação ao vale (ex-proeminência) densidadeMin: 0.40, // Densidade mínima relativa (% do máximo) para contar como sinal maxPGs: 3, minSep: 18 }; const GRID_SIZE = 100; const DEBUG_CANVAS_SIZE = 490; const DEBUG_CANVAS_TOP = '43%'; const DEBUG_CANVAS_LEFT = '50%'; const DEBUG_CANVAS_TRANSFORM = 'translate(-50%, -50%)'; // ========================================================================= // ESTADO // ========================================================================= const debugState = { active: false, pontos: [], grid: null, maxGlobal: 0, maxEscalaVisual: 1, picosMarcados: [], pGsFinais: [], canvas: null, panel: null, showAtratores: false }; // ========================================================================= // BOOTSTRAP // ========================================================================= console.log('pg4-debug: script carregado (atalho: Cmd+Alt+Ctrl+B)'); function init() { injectStyles(); registrarAtalho(); } function registrarAtalho() { // Atalho: Cmd+Alt+Ctrl+B (Mac) ou Win+Alt+Ctrl+B (Windows/Linux) document.addEventListener('keydown', (e) => { // Checagem robusta: metaKey (Cmd/Win) + altKey + ctrlKey + tecla B if (e.metaKey && e.altKey && e.ctrlKey && (e.key === 'b' || e.key === 'B' || e.code === 'KeyB')) { e.preventDefault(); e.stopPropagation(); toggleDebug(); } }); console.log('pg4-debug: atalho Cmd+Alt+Ctrl+B registrado'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // ========================================================================= // ABRIR / FECHAR // ========================================================================= function toggleDebug() { if (debugState.active) { fecharDebug(); } else { abrirDebug(); } } function abrirDebug() { const hidden = document.getElementById('hiddenCoordinates'); if (!hidden) { alert("pg4-debug: campo 'hiddenCoordinates' não encontrado."); return; } debugState.pontos = extrairPontos(hidden.value); if (debugState.pontos.length < 5) { alert('pg4-debug: pontos insuficientes para análise (mínimo 5).'); return; } criarCanvas(); criarPainel(); rodarAnalise(); debugState.active = true; } function fecharDebug() { const antigoCanvas = document.getElementById('pg4DebugCanvas'); const antigoPainel = document.getElementById('pg4DebugPanel'); if (antigoCanvas) antigoCanvas.remove(); if (antigoPainel) antigoPainel.remove(); debugState.canvas = null; debugState.panel = null; debugState.active = false; } function criarCanvas() { const matrix = document.getElementById('matrix'); if (!matrix) return; const antigo = document.getElementById('pg4DebugCanvas'); if (antigo) antigo.remove(); const canvas = document.createElement('canvas'); canvas.id = 'pg4DebugCanvas'; canvas.width = DEBUG_CANVAS_SIZE; canvas.height = DEBUG_CANVAS_SIZE; canvas.style.position = 'absolute'; canvas.style.top = DEBUG_CANVAS_TOP; canvas.style.left = DEBUG_CANVAS_LEFT; canvas.style.transform = DEBUG_CANVAS_TRANSFORM; canvas.style.pointerEvents = 'none'; canvas.style.zIndex = '50'; matrix.appendChild(canvas); debugState.canvas = canvas; } // ========================================================================= // PAINEL DE CONTROLES // ========================================================================= function criarPainel() { const antigo = document.getElementById('pg4DebugPanel'); if (antigo) antigo.remove(); const panel = document.createElement('div'); panel.id = 'pg4DebugPanel'; panel.innerHTML = `
pg4 · calibração KDE
${sliderHtml('bw', 'BANDWIDTH', 5, 20, 0.5, DEBUG_CONFIG.bandwidth, 'Largura do kernel gaussiano.')} ${sliderHtml('ma', 'MASSA', 0.05, 0.40, 0.01, DEBUG_CONFIG.massaMin, 'Massa mínima do pG primário (% de respondentes no raio).')} ${sliderHtml('do', 'DOMINÂNCIA', 0.20, 0.90, 0.05, DEBUG_CONFIG.dominancia, 'Fração da massa do primário exigida de secundários.')} ${sliderHtml('ni', 'NITIDEZ', 0.05, 0.60, 0.05, DEBUG_CONFIG.nitidezMin, 'Destaque do pico em relação ao vale circundante.')} ${sliderHtml('de', 'DENSIDADE', 0.10, 0.80, 0.05, DEBUG_CONFIG.densidadeMin, 'Densidade mínima relativa (% do pico máximo). Filtra sinais isolados.')}
sinal aprovado como pG
sinal candidato (filtrado)
sinal sem nitidez ou densidade
`; document.body.appendChild(panel); debugState.panel = panel; panel.querySelector('.pg4d-close').addEventListener('click', fecharDebug); panel.querySelector('#pg4d-copy').addEventListener('click', copiarConfig); // Toggle de atratores const chkAtratores = document.getElementById('pg4d-atratores'); chkAtratores.checked = debugState.showAtratores; chkAtratores.addEventListener('change', (e) => { debugState.showAtratores = e.target.checked; renderizar(); }); ['bw', 'ma', 'do', 'ni', 'de'].forEach(k => { const input = document.getElementById(`pg4d-${k}`); const val = document.getElementById(`pg4d-${k}-val`); input.addEventListener('input', () => { val.textContent = input.value; onParamChange(); }); }); } function sliderHtml(key, label, min, max, step, val, tip) { return ` `; } function onParamChange() { const bwNew = parseFloat(document.getElementById('pg4d-bw').value); const maNew = parseFloat(document.getElementById('pg4d-ma').value); const doNew = parseFloat(document.getElementById('pg4d-do').value); const niNew = parseFloat(document.getElementById('pg4d-ni').value); const deNew = parseFloat(document.getElementById('pg4d-de').value); const bwChanged = bwNew !== DEBUG_CONFIG.bandwidth; DEBUG_CONFIG.bandwidth = bwNew; DEBUG_CONFIG.massaMin = maNew; DEBUG_CONFIG.dominancia = doNew; DEBUG_CONFIG.nitidezMin = niNew; DEBUG_CONFIG.densidadeMin = deNew; if (bwChanged) { // BANDWIDTH afeta tudo: recalcula grid, picos, métricas rodarAnalise(); } else { // Filtros mudaram: reusa grid mas recalcula seleção debugState.picosMarcados = enriquecer( detectarPicos(debugState.grid, DEBUG_CONFIG.bandwidth), debugState.pontos, debugState.grid, DEBUG_CONFIG.bandwidth ); const sel = selecionar(debugState.picosMarcados, DEBUG_CONFIG); debugState.picosMarcados = sel.marcados; debugState.pGsFinais = sel.selecionados; renderizar(); atualizarStats(); } } // ========================================================================= // PIPELINE // ========================================================================= function rodarAnalise() { debugState.grid = construirGrid(debugState.pontos, DEBUG_CONFIG.bandwidth); debugState.maxGlobal = maximoGlobal(debugState.grid); // Escala visual = maxGlobal (pura escala relativa, lógica original). // Consistência visual entre tamanhos de amostra: o pico sempre fica no topo. // O "vermelhão em área grande" de antes é resolvido na tabela de cores, // que concentra o vermelho apenas nos pixels do pico real. debugState.maxEscalaVisual = debugState.maxGlobal || 1; const picos = detectarPicos(debugState.grid, DEBUG_CONFIG.bandwidth); const enriquecidos = enriquecer(picos, debugState.pontos, debugState.grid, DEBUG_CONFIG.bandwidth); const sel = selecionar(enriquecidos, DEBUG_CONFIG); debugState.picosMarcados = sel.marcados; debugState.pGsFinais = sel.selecionados; renderizar(); atualizarStats(); } function extrairPontos(input) { return input.split('\n').map(linha => { if (/^[a-zA-Z]+[a-zA-Z0-9_-]*-/.test(linha)) return null; const pura = linha.split('>')[0].split('(r=')[0]; const partes = pura.split(','); if (partes.length === 4 && partes.every(p => !isNaN(parseFloat(p)))) { return { x: parseFloat(partes[3]), y: parseFloat(partes[1]) }; } return null; }).filter(p => p !== null); } function construirGrid(pontos, bw) { const grid = Array.from({ length: GRID_SIZE }, () => new Float64Array(GRID_SIZE)); const h2 = 2 * bw * bw; const raio = Math.ceil(bw * 3); pontos.forEach(p => { const xMin = Math.max(0, Math.floor(p.x - raio)); const xMax = Math.min(GRID_SIZE - 1, Math.ceil(p.x + raio)); const yMin = Math.max(0, Math.floor(p.y - raio)); const yMax = Math.min(GRID_SIZE - 1, Math.ceil(p.y + raio)); for (let i = xMin; i <= xMax; i++) { for (let j = yMin; j <= yMax; j++) { const dx = i - p.x; const dy = j - p.y; grid[i][j] += Math.exp(-(dx * dx + dy * dy) / h2); } } }); return grid; } function maximoGlobal(grid) { let max = 0; for (let i = 0; i < GRID_SIZE; i++) { for (let j = 0; j < GRID_SIZE; j++) { if (grid[i][j] > max) max = grid[i][j]; } } return max; } function detectarPicos(grid, bw) { const picos = []; const raio = Math.max(2, Math.floor(bw / 2)); const threshold = maximoGlobal(grid) * 0.05; for (let i = raio; i < GRID_SIZE - raio; i++) { for (let j = raio; j < GRID_SIZE - raio; j++) { const v = grid[i][j]; if (v < threshold) continue; let ehMax = true; for (let di = -raio; di <= raio && ehMax; di++) { for (let dj = -raio; dj <= raio && ehMax; dj++) { if (di === 0 && dj === 0) continue; if (grid[i + di][j + dj] > v) ehMax = false; } } if (ehMax) picos.push({ x: i, y: j, densidade: v }); } } return picos; } function enriquecer(picos, pontos, grid, bw) { const total = pontos.length; const raioMassa = bw * 1.5; const maxGlobal = debugState.maxGlobal || 1; return picos.map(pico => { const massa = pontos.filter(p => Math.sqrt((p.x - pico.x) ** 2 + (p.y - pico.y) ** 2) <= raioMassa ).length / total; const raio = Math.ceil(bw * 2); let vale = pico.densidade; for (let di = -raio; di <= raio; di++) { for (let dj = -raio; dj <= raio; dj++) { const i = pico.x + di; const j = pico.y + dj; if (i < 0 || i >= GRID_SIZE || j < 0 || j >= GRID_SIZE) continue; if (di * di + dj * dj > raio * raio) continue; if (grid[i][j] < vale) vale = grid[i][j]; } } const nitidez = (pico.densidade - vale) / pico.densidade; const densidadeRel = pico.densidade / maxGlobal; return { ...pico, massa, nitidez, densidadeRel }; }); } function selecionar(picos, cfg) { const marcados = picos.map(p => ({ ...p, status: null, rank: null })); // Portão 1: densidade relativa (elimina sinais isolados com poucos pontos) marcados.forEach(p => { if (p.densidadeRel < cfg.densidadeMin) p.status = 'densidade_baixa'; }); // Portão 2: nitidez (saliências que não são picos reais) marcados.forEach(p => { if (p.status) return; if (p.nitidez < cfg.nitidezMin) p.status = 'sem_nitidez'; }); // CRÍTICO: ordenação por DENSIDADE (não por massa) // Dois clusters com mesma massa podem ter densidade diferente; // o mais compacto sempre tem densidade maior e vira pG primário. const candidatos = marcados .filter(p => !p.status) .sort((a, b) => b.densidadeRel - a.densidadeRel); if (candidatos.length === 0) { return { selecionados: [], marcados }; } // Portão 3: massa mínima (existência) if (candidatos[0].massa < cfg.massaMin) { candidatos.forEach(p => { p.status = 'primario_fraco'; }); return { selecionados: [], marcados }; } const selecionados = []; const massaPrim = candidatos[0].massa; candidatos[0].status = 'aprovado'; candidatos[0].rank = 0; selecionados.push(candidatos[0]); for (let i = 1; i < candidatos.length; i++) { const c = candidatos[i]; if (selecionados.length >= cfg.maxPGs) { c.status = 'teto'; continue; } // Portão 4: dominância relativa if (c.massa < massaPrim * cfg.dominancia) { c.status = 'dominancia'; continue; } // Portão 5: separação mínima const longe = selecionados.every(s => Math.sqrt((s.x - c.x) ** 2 + (s.y - c.y) ** 2) >= cfg.minSep ); if (!longe) { c.status = 'separacao'; continue; } c.status = 'aprovado'; c.rank = selecionados.length; selecionados.push(c); } return { selecionados, marcados }; } // ========================================================================= // RENDERIZAÇÃO // ========================================================================= function renderizar() { if (!debugState.canvas) return; const ctx = debugState.canvas.getContext('2d'); const W = debugState.canvas.width; const H = debugState.canvas.height; ctx.clearRect(0, 0, W, H); const grid = debugState.grid; const maxV = debugState.maxEscalaVisual || 1; // escala absoluta, não relativa const cellW = W / GRID_SIZE; const cellH = H / GRID_SIZE; for (let i = 0; i < GRID_SIZE; i++) { for (let j = 0; j < GRID_SIZE; j++) { const v = Math.min(1, grid[i][j] / maxV); // clamp em 1 if (v < 0.05) continue; const cx = (1 - i / 100) * W; const cy = (j / 100) * H; ctx.fillStyle = corParaDensidade(v); ctx.fillRect(cx - cellW / 2, cy - cellH / 2, cellW + 1, cellH + 1); } } // Atratores (linhas dos pGs até pontos contribuintes) - por cima do heatmap if (debugState.showAtratores) { desenharAtratores(ctx, W, H); } // Marcadores de sinais - por cima de tudo debugState.picosMarcados.forEach(pico => { const px = (1 - pico.x / 100) * W; const py = (pico.y / 100) * H; desenharMarcador(ctx, px, py, pico); }); } function desenharAtratores(ctx, W, H) { const pGs = debugState.pGsFinais; if (pGs.length === 0) return; const bw = DEBUG_CONFIG.bandwidth; const raioMassa = bw * 1.5; const pontos = debugState.pontos; pGs.forEach(pg => { const pgX = (1 - pg.x / 100) * W; const pgY = (pg.y / 100) * H; // Linhas: dos pontos contribuintes até o pG pontos.forEach(p => { const dist = Math.sqrt((p.x - pg.x) ** 2 + (p.y - pg.y) ** 2); if (dist > raioMassa) return; const pX = (1 - p.x / 100) * W; const pY = (p.y / 100) * H; // Proximidade: 1 no centro, 0 na borda do raio const proximidade = 1 - (dist / raioMassa); const alpha = (0.2 + 0.6 * proximidade).toFixed(2); const lineWidth = 0.4 + 1.6 * proximidade; ctx.beginPath(); ctx.moveTo(pgX, pgY); ctx.lineTo(pX, pY); ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`; ctx.lineWidth = lineWidth; ctx.stroke(); // Bolinha no ponto de origem ctx.beginPath(); ctx.arc(pX, pY, 2.5 + 1.5 * proximidade, 0, Math.PI * 2); ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; ctx.fill(); }); // Núcleo brilhante no centro do pG ctx.beginPath(); ctx.arc(pgX, pgY, 4, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; ctx.fill(); }); } function desenharMarcador(ctx, x, y, sinal) { let cor, raio, fill; switch (sinal.status) { case 'aprovado': cor = '#00ff88'; raio = 13; fill = true; break; case 'sem_nitidez': case 'densidade_baixa': cor = '#888'; raio = 5; fill = false; break; default: cor = '#ffcc00'; raio = 9; fill = false; } ctx.beginPath(); ctx.arc(x, y, raio, 0, Math.PI * 2); ctx.strokeStyle = cor; ctx.lineWidth = 2.5; ctx.stroke(); if (fill) { ctx.fillStyle = cor; ctx.beginPath(); ctx.arc(x, y, 3.5, 0, Math.PI * 2); ctx.fill(); } // Três métricas: massa · nitidez · densidade (todas em %) const label = `M${(sinal.massa * 100).toFixed(0)} · N${(sinal.nitidez * 100).toFixed(0)} · D${(sinal.densidadeRel * 100).toFixed(0)}`; ctx.font = 'bold 10px Arial'; const textW = ctx.measureText(label).width; ctx.fillStyle = 'rgba(0,0,0,0.75)'; ctx.fillRect(x + raio + 3, y - 7, textW + 8, 13); ctx.fillStyle = cor; ctx.fillText(label, x + raio + 7, y + 3); } function corParaDensidade(v) { // Stops ajustados para concentrar o vermelho no extremo superior. // Azul/verde ocupam mais espaço da escala (densidades baixas a médias), // vermelho só aparece mesmo perto do pico real (v > 95%). const stops = [ [0.00, [30, 50, 140, 0]], [0.20, [30, 100, 200, 70]], // azul até 20% [0.45, [0, 200, 220, 125]], // ciano em 45% [0.65, [100, 220, 100, 155]], // verde em 65% [0.82, [255, 200, 50, 175]], // amarelo em 82% [0.93, [255, 110, 80, 195]], // laranja em 93% [0.98, [255, 40, 90, 215]], // vermelho em 98% [1.00, [255, 0, 102, 225]] // vermelho pleno só no pico ]; for (let i = 1; i < stops.length; i++) { if (v <= stops[i][0]) { const [v0, c0] = stops[i - 1]; const [v1, c1] = stops[i]; const t = (v - v0) / (v1 - v0); const r = Math.round(c0[0] + t * (c1[0] - c0[0])); const g = Math.round(c0[1] + t * (c1[1] - c0[1])); const b = Math.round(c0[2] + t * (c1[2] - c0[2])); const a = (c0[3] + t * (c1[3] - c0[3])) / 255; return `rgba(${r},${g},${b},${a.toFixed(2)})`; } } const c = stops[stops.length - 1][1]; return `rgba(${c[0]},${c[1]},${c[2]},${c[3] / 255})`; } // ========================================================================= // ESTATÍSTICAS // ========================================================================= function atualizarStats() { const stats = document.getElementById('pg4d-stats'); if (!stats) return; const pontos = debugState.pontos.length; const sinais = debugState.picosMarcados.length; const pGs = debugState.pGsFinais.length; let html = `
Pontos: ${pontos}
Sinais detectados: ${sinais}
pGs aprovados: ${pGs}
`; if (pGs === 0 && sinais > 0) { html += `
Nenhum pG aprovado (dispersão ou filtros estritos).
`; } html += `
`; // Ordena por densidade (mesmo critério do selecionar) const sorted = [...debugState.picosMarcados].sort((a, b) => b.densidadeRel - a.densidadeRel); sorted.forEach((s, i) => { const cor = s.status === 'aprovado' ? '#00ff88' : (s.status === 'sem_nitidez' || s.status === 'densidade_baixa') ? '#888' : '#ffcc00'; const rotuloStatus = traduzirStatus(s.status); html += `
sinal ${i + 1} @ (${s.x}, ${s.y})
Massa: ${(s.massa * 100).toFixed(1)}%
Nitidez: ${(s.nitidez * 100).toFixed(0)}%
Densidade: ${(s.densidadeRel * 100).toFixed(0)}%
${rotuloStatus}
`; }); stats.innerHTML = html; } function traduzirStatus(s) { return { aprovado: '✓ aprovado como pG', densidade_baixa: `✗ densidade < ${(DEBUG_CONFIG.densidadeMin * 100).toFixed(0)}% (sinal isolado)`, sem_nitidez: `✗ nitidez < ${(DEBUG_CONFIG.nitidezMin * 100).toFixed(0)}%`, primario_fraco: `✗ massa < ${(DEBUG_CONFIG.massaMin * 100).toFixed(0)}% (dispersão)`, dominancia: `✗ massa < ${(DEBUG_CONFIG.dominancia * 100).toFixed(0)}% do primário`, separacao: '✗ próximo demais de outro pG', teto: `✗ já há ${DEBUG_CONFIG.maxPGs} pGs` }[s] || '?'; } // ========================================================================= // AÇÕES // ========================================================================= function copiarConfig() { const config = `// Cole em pg4.js substituindo as constantes: const BANDWIDTH = ${DEBUG_CONFIG.bandwidth}; const MASSA_MIN = ${DEBUG_CONFIG.massaMin}; const DOMINANCIA = ${DEBUG_CONFIG.dominancia}; const NITIDEZ_MIN = ${DEBUG_CONFIG.nitidezMin}; const DENSIDADE_MIN = ${DEBUG_CONFIG.densidadeMin};`; navigator.clipboard.writeText(config).then(() => { const btn = document.getElementById('pg4d-copy'); const orig = btn.textContent; btn.textContent = 'Copiado!'; setTimeout(() => { btn.textContent = orig; }, 1500); }).catch(() => { prompt('Copie manualmente:', config); }); } // ========================================================================= // ESTILOS // ========================================================================= function injectStyles() { const style = document.createElement('style'); style.textContent = ` #pg4DebugPanel { position: fixed; top: 60px; right: 20px; width: 290px; max-height: calc(100vh - 80px); overflow-y: auto; background: rgba(25, 18, 60, 0.96); border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 12px; padding: 14px; color: white; font-family: Arial, sans-serif; font-size: 11px; z-index: 300; backdrop-filter: blur(10px); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); } .pg4d-header { display: flex; justify-content: space-between; align-items: center; font-weight: bold; font-size: 12px; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.12); text-transform: uppercase; letter-spacing: 0.5px; } .pg4d-close { background: transparent; border: none; color: white; cursor: pointer; font-size: 20px; padding: 0 6px; line-height: 1; } .pg4d-close:hover { color: #ff0066; } .pg4d-sliders { margin-bottom: 6px; } .pg4d-slider { display: block; margin-bottom: 12px; cursor: help; } .pg4d-slider-top { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 10px; } .pg4d-label { color: rgba(255, 255, 255, 0.85); letter-spacing: 0.5px; } .pg4d-val { color: #ff0066; font-weight: bold; font-family: monospace; } .pg4d-slider input[type=range] { width: 100%; height: 4px; background: rgba(255, 255, 255, 0.12); outline: none; -webkit-appearance: none; border-radius: 2px; } .pg4d-slider input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: #ff0066; border-radius: 50%; cursor: pointer; } .pg4d-slider input[type=range]::-moz-range-thumb { width: 14px; height: 14px; background: #ff0066; border-radius: 50%; cursor: pointer; border: none; } .pg4d-stats { padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.12); } .pg4d-row { margin-bottom: 4px; font-size: 11px; } .pg4d-warn { color: #ffcc00; font-style: italic; margin-top: 6px; } .pg4d-divider { height: 1px; background: rgba(255,255,255,0.1); margin: 10px 0; } .pg4d-peak { background: rgba(255, 255, 255, 0.04); padding: 7px 9px; margin-bottom: 6px; border-radius: 5px; border-left: 3px solid #888; font-size: 10px; line-height: 1.4; } .pg4d-peak-head { font-weight: bold; margin-bottom: 2px; color: rgba(255,255,255,0.9); } .pg4d-status { margin-top: 3px; opacity: 0.75; font-size: 9.5px; } .pg4d-legend { margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.12); font-size: 10px; } .pg4d-legend > div { margin: 4px 0; display: flex; align-items: center; } .pg4d-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 8px; flex-shrink: 0; } .pg4d-dot-pg { background: #00ff88; border: 2px solid #00ff88; } .pg4d-dot-cand { border: 2px solid #ffcc00; } .pg4d-dot-weak { border: 2px solid #888; } .pg4d-toggles { margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.12); } .pg4d-toggle { display: flex; align-items: center; cursor: pointer; font-size: 11px; padding: 4px 0; } .pg4d-toggle input[type="checkbox"] { margin-right: 8px; accent-color: #ff0066; cursor: pointer; width: 14px; height: 14px; } .pg4d-toggle span { letter-spacing: 0.3px; } .pg4d-footer { margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.12); } .pg4d-action { background: rgba(255, 0, 102, 0.15); border: 1px solid #ff0066; color: #ff0066; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 10px; width: 100%; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; transition: background 0.2s; } .pg4d-action:hover { background: rgba(255, 0, 102, 0.3); } `; document.head.appendChild(style); } })();