// 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 = `
${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);
}
})();