feat: criado repositório jhonny-editor no Gitea

criado repositório jhonny-editor no Gitea
adicionado script admin/deploy.command com commit automático
atualizado admin/DEV-NOTES.md com template limpo

Resumo:
- 48 arquivos alterados
- 26 novos
- 19 modificados
- 3 removidos

 22 files changed, 658 insertions(+), 568 deletions(-)

Arquivos:
  - AGENTS.md
  - admin/DEV-NOTES.md
  - admin/OpenCut.command
  - admin/commit.command
  - admin/deploy.command
  - admin/update.command
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/cep-plugin/styles.css
  - code/engine/ARQUITETURA.md
  - code/engine/arquitetura/README.md
  - code/engine/gerar_relatorio_timeline.py
  - code/engine/integracoes/apple_speech/apple_speech_transcriber.swift
  - code/engine/integracoes/apple_speech/provider_de_transcricao_apple.py
  - code/engine/integracoes/midia/__init__.py
  - code/engine/integracoes/whisper/provider_de_transcricao_local.py
  - code/engine/scanner/__init__.py
  - code/engine/scanner/coordenacao/__init__.py
  - code/engine/scanner/descoberta/__init__.py
  - code/engine/scanner/modelos.py
  - code/engine/scanner/transcricao_da_timeline.py
  - code/src/tools/discovery.ts
  - :memory:.ses
  - admin/Jhonny.command
  - admin/inativos/OpenCut.command
  - admin/inativos/update.command
  - code/docs/glossario-analise-emocional.md
  - code/docs/levantamento-apple-vision-e-apple-intelligence.md
  - code/docs/levantamento-ferramentas-analise-visual-local.md
  - code/engine/arquitetura/biblioteca-inteligente-de-videos.md
  - code/engine/executar_scanner.py
  - code/engine/integracoes/huggingface/
  - code/engine/integracoes/midia/extracao_de_metadados.py
  - code/engine/integracoes/visual/
  - code/engine/requirements-visual.txt
  - code/engine/scanner/configuracao_visual.py
  - code/engine/scanner/descoberta/descoberta_de_arquivos.py
  - code/engine/scanner/metadados.py
  - code/engine/scanner/relatorio_visual.py
  - code/engine/scanner/retakes/
  - code/engine/scanner/visual.py
  - code/engine/testes/test_analise_visual_local.py
  - code/engine/testes/test_arquivos_e_metadados.py
  - code/engine/testes/test_provider_de_transcricao_apple.py
  - code/relatorios/analise-brools/
  - code/relatorios/analise-visual/
  - code/relatorios/audio/arquivos/
  - code/relatorios/transcricao-timeline.md
This commit is contained in:
João Henrique
2026-09-08 16:13:27 -04:00
parent b541f502ba
commit b9bf3b2863
76 changed files with 16147 additions and 322 deletions
+41
View File
@@ -25,6 +25,9 @@
<button class="tab-button active" id="tabBtnSilence" role="tab" aria-selected="true" aria-controls="tabSilence" onclick="switchTab('silence')" type="button">
<span class="tab-icon" aria-hidden="true">✂</span>Editar vídeo
</button>
<button class="tab-button" id="tabBtnScanner" role="tab" aria-selected="false" aria-controls="tabScanner" onclick="switchTab('scanner')" type="button">
<span class="tab-icon" aria-hidden="true">◉</span>Scanner
</button>
<button class="tab-button" id="tabBtnBridge" role="tab" aria-selected="false" aria-controls="tabBridge" onclick="switchTab('bridge')" type="button">
<span class="tab-icon" aria-hidden="true">⇄</span>Conexão
</button>
@@ -218,6 +221,44 @@
</div>
<!-- ============================== SCANNER =============================== -->
<div class="tab-panel" id="tabScanner" role="tabpanel" aria-labelledby="tabBtnScanner" hidden>
<p class="tab-intro">Escolha o que será lido no arquivo original. O Scanner usa os tempos da timeline, mas não exporta frames pelo Premiere.</p>
<section class="card">
<div class="card-head"><h2>Amostragem e faixas</h2></div>
<label class="field-label" for="scannerInterval">Frames de vídeo</label>
<select id="scannerInterval" class="select-field"><option value="0.2">Precisa — 5 frames por segundo</option><option value="1" selected>Equilibrada — 1 frame por segundo</option><option value="5">Econômica — 1 frame a cada 5 segundos</option></select>
<p class="field-help">Use a amostragem precisa somente no trecho que será reenquadrado.</p>
<button class="button button-ghost" id="btnScannerDetectTracks" onclick="scannerDetectTracks()" type="button">Detectar faixas da sequência</button>
<p class="field-help" id="scannerTracksHelp">Abra a sequência desejada e detecte as faixas para selecioná-las.</p>
<div class="scanner-track-grid"><div><span class="field-label">Faixas de vídeo</span><div class="scanner-track-list" id="scannerVideoTracks"></div></div><div><span class="field-label">Faixas de áudio</span><div class="scanner-track-list" id="scannerAudioTracks"></div></div></div>
</section>
<section class="card"><div class="card-head"><h2>Transcrição</h2><span class="card-kicker" id="scannerModelCount">—</span></div>
<div class="field-grid"><div><label class="field-label" for="scannerTranscribeModel">Modelo instalado</label><select id="scannerTranscribeModel" class="select-field"></select></div><div><label class="field-label" for="scannerTranscribeLanguage">Idioma</label><select id="scannerTranscribeLanguage" class="select-field"></select></div></div>
<label class="checkbox-field"><input id="scannerDiarization" type="checkbox"><span>Identificar locutores após transcrever</span></label>
<div class="hf-token-status" id="scannerHfStatus" data-state="unset"><span class="check-dot" aria-hidden="true"></span><span id="scannerHfStatusText">Hugging Face não configurado</span><button class="link-button" type="button" onclick="switchTab('settings')">Configurar</button></div>
<p class="field-help">A lista inclui somente modelos encontrados nas pastas locais configuradas. O token do Hugging Face é usado apenas na diarização; nunca é salvo no perfil nem no relatório.</p>
</section>
<div class="scanner-options-grid"><section class="card"><div class="card-head"><h2>O que detectar</h2></div>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="faces" checked><span>Rostos e qualidade facial</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="pessoas" checked><span>Pessoas</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="pose" checked><span>Pose e mãos</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="ocr" checked><span>Texto e códigos</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="categorias" checked><span>Objetos e categorias</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="estetica" checked><span>Estética, horizonte e geometria</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="segmentacao_de_pessoas" checked><span>Segmentação de pessoas</span></label>
</section>
<section class="card"><div class="card-head"><h2>Leitura temporal</h2></div>
<label class="checkbox-field"><input id="scannerScenes" type="checkbox" checked><span>Agrupar cenas</span></label>
<label class="checkbox-field"><input id="scannerContinuity" type="checkbox" checked><span>Medir continuidade e cortes</span></label>
<label class="checkbox-field"><input id="scannerReframe" type="checkbox"><span>Guardar geometria para reenquadramento</span></label>
<label class="checkbox-field"><input id="scannerInterpret" type="checkbox" checked><span>Descrever a cena com Foundation Models</span></label>
<div class="button-row"><button class="button button-ghost" onclick="saveScannerProfile()" type="button">Salvar perfil</button><button class="button button-primary" id="btnScannerProcess" onclick="scannerProcess()" type="button" disabled>Processar</button></div>
<p class="field-help" id="scannerProfileStatus">Perfil padrão pronto.</p>
<div class="callout callout-info" id="scannerReportCard" hidden><strong>Relatório pronto</strong><p id="scannerReportPath"></p><div class="button-row"><button class="button button-ghost" onclick="scannerOpenReport()" type="button">Abrir JSON</button><button class="button button-ghost" onclick="scannerCopyReport()" type="button">Copiar conteúdo</button></div></div>
</section></div>
</div>
<!-- ============================== CONEXÃO ============================== -->
<div class="tab-panel" id="tabBridge" role="tabpanel" aria-labelledby="tabBtnBridge" hidden>
+109
View File
@@ -148,6 +148,7 @@ function stepState(n) {
function switchTab(name) {
var tabs = [
{ key: "silence", panel: "tabSilence", btn: "tabBtnSilence" },
{ key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" },
{ key: "bridge", panel: "tabBridge", btn: "tabBtnBridge" },
{ key: "models", panel: "tabModels", btn: "tabBtnModels" },
{ key: "settings", panel: "tabSettings", btn: "tabBtnSettings" },
@@ -164,8 +165,116 @@ function switchTab(name) {
if (name === "models") renderModelList();
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); }
if (name === "settings") renderEditorPersonalities();
if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); }
}
// ---- Perfil do Scanner -----------------------------------------------------
// O painel e o Engine compartilham este contrato versionado. O perfil não
// contém caminhos de mídia; estes continuam sendo resolvidos pelo MCP.
var SCANNER_PROFILE_KEY = "mcp_scanner_profile_v1";
var SCANNER_ALL_RESOURCES = ["faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "codigos", "categorias", "estetica", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas"];
var scannerTimeline = null;
var scannerReportPath = null;
var SCANNER_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_scanner.py";
// `path` e `os` são carregados mais abaixo, junto com o runtime Node do CEP.
// Calcular isso aqui interrompia todo o script antes da inicialização do painel.
function scannerOutputRoot() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); }
function scannerSelectedTracks(kind) { return Array.prototype.slice.call(document.querySelectorAll("[data-scanner-track='" + kind + "']:checked")).map(function (item) { return Number(item.value); }).sort(function (a, b) { return a - b; }); }
function scannerRenderTracks(kind, tracks, selected) {
var target = document.getElementById(kind === "video" ? "scannerVideoTracks" : "scannerAudioTracks");
target.innerHTML = "";
if (!tracks.length) { target.textContent = "Nenhuma faixa encontrada."; return; }
tracks.forEach(function (track) { var label = document.createElement("label"); label.className = "checkbox-field"; var input = document.createElement("input"); input.type = "checkbox"; input.setAttribute("data-scanner-track", kind); input.value = track.index; input.checked = selected.indexOf(track.index) >= 0; label.appendChild(input); label.appendChild(document.createTextNode(" " + track.name + " · " + track.numClips + " clipe(s)")); target.appendChild(label); });
}
function scannerProfileFromScreen() {
var enabled = Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]:checked")).map(function (item) { return item.getAttribute("data-scanner-resource"); });
function include(source, extras) { if (enabled.indexOf(source) >= 0) extras.forEach(function (item) { if (enabled.indexOf(item) < 0) enabled.push(item); }); }
include("faces", ["qualidade_facial"]); include("pose", ["maos"]); include("ocr", ["codigos"]); include("estetica", ["horizonte", "retangulos", "contornos"]);
var modelo = document.getElementById("scannerTranscribeModel").value;
var opcao = document.getElementById("scannerTranscribeModel").selectedOptions[0];
return { versao: 1, intervalo_em_segundos: Number(document.getElementById("scannerInterval").value), faixas_de_video: scannerSelectedTracks("video"), faixas_de_audio: scannerSelectedTracks("audio"), recursos_vision: SCANNER_ALL_RESOURCES.filter(function (item) { return enabled.indexOf(item) >= 0; }), detectar_cenas: document.getElementById("scannerScenes").checked, analisar_continuidade: document.getElementById("scannerContinuity").checked, preparar_reenquadramento: document.getElementById("scannerReframe").checked, interpretar_cena: document.getElementById("scannerInterpret").checked, transcricao: { modelo: modelo, caminho_modelo: opcao && opcao.dataset.path || "", idioma: document.getElementById("scannerTranscribeLanguage").value, diarizacao: document.getElementById("scannerDiarization").checked, usar_hugging_face: !!loadHfToken() } };
}
function saveScannerProfile() {
try { var profile = scannerProfileFromScreen(); localStorage.setItem(SCANNER_PROFILE_KEY, JSON.stringify(profile)); document.getElementById("scannerProfileStatus").textContent = "Perfil salvo: " + profile.intervalo_em_segundos + " s por frame; " + profile.faixas_de_video.length + " faixa(s) de vídeo."; showToast("ok", "Perfil do Scanner salvo."); } catch (error) { showToast("err", error.message || String(error)); }
}
function loadScannerProfile() {
var saved; try { saved = JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY) || "null"); } catch (_) { saved = null; }
if (!saved) return;
document.getElementById("scannerInterval").value = String(saved.intervalo_em_segundos || 1);
if (scannerTimeline) { scannerRenderTracks("video", scannerTimeline.videoTracks, saved.faixas_de_video || []); scannerRenderTracks("audio", scannerTimeline.audioTracks, saved.faixas_de_audio || []); }
var resources = saved.recursos_vision || SCANNER_ALL_RESOURCES;
Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]")).forEach(function (item) { item.checked = resources.indexOf(item.getAttribute("data-scanner-resource")) >= 0; });
document.getElementById("scannerScenes").checked = saved.detectar_cenas !== false; document.getElementById("scannerContinuity").checked = saved.analisar_continuidade !== false; document.getElementById("scannerReframe").checked = !!saved.preparar_reenquadramento; document.getElementById("scannerInterpret").checked = saved.interpretar_cena !== false;
if (saved.transcricao) { document.getElementById("scannerTranscribeModel").value = saved.transcricao.modelo || ""; document.getElementById("scannerTranscribeLanguage").value = saved.transcricao.idioma || loadLanguage(); document.getElementById("scannerDiarization").checked = !!saved.transcricao.diarizacao && !!loadHfToken(); }
}
function renderScannerTranscriptionOptions() {
var modelos=document.getElementById("scannerTranscribeModel"), idiomas=document.getElementById("scannerTranscribeLanguage"); if (!modelos || !idiomas) return;
var atual=modelos.value; modelos.innerHTML=""; var disponiveis=discoverPortugueseModelsOnMerongo();
if (!disponiveis.length) { var vazio=document.createElement("option"); vazio.value=""; vazio.textContent="Nenhum modelo com português encontrado no Merongo"; modelos.appendChild(vazio); } else { disponiveis.forEach(function(item){var option=document.createElement("option");option.value=item.nome;option.dataset.path=item.caminho;option.textContent=item.nome+" · "+formatMB(item.tamanho);modelos.appendChild(option);}); if (atual) modelos.value=atual; }
idiomas.innerHTML=""; LANGUAGE_CATALOG.forEach(function(item){var option=document.createElement("option");option.value=item.value;option.textContent=item.label;idiomas.appendChild(option);}); idiomas.value=loadLanguage(); document.getElementById("scannerModelCount").textContent=disponiveis.length+" instalado(s)"; renderScannerHuggingFaceStatus();
}
function renderScannerHuggingFaceStatus() {
var status = document.getElementById("scannerHfStatus");
var text = document.getElementById("scannerHfStatusText");
var diarization = document.getElementById("scannerDiarization");
if (!status || !text || !diarization) return;
var available = !!loadHfToken();
status.setAttribute("data-state", available ? "valid" : "unset");
text.textContent = available ? "Hugging Face disponível para identificar locutores" : "Configure o token do Hugging Face para identificar locutores";
diarization.disabled = !available;
if (!available) diarization.checked = false;
}
var MERONGO_MODEL_ROOTS = ["/Volumes/Merongo/SISTEMAS/WHISPERX/whisper/models", "/Volumes/Merongo/SISTEMAS/MODELOSIA/hf-cache", "/Volumes/Merongo/Applications/WhisperX/huggingface/hub", "/Volumes/Merongo/Applications/Trasncritor", "/Volumes/Merongo/SISTEMAS/AUX"];
function discoverPortugueseModelsOnMerongo() {
var encontrados = {}, roots = [effectiveModelsPath()].concat(MERONGO_MODEL_ROOTS);
function walk(dir, profundidade) { if (profundidade > 5) return; var entries; try { entries=fs.readdirSync(dir,{withFileTypes:true}); } catch (_) { return; } entries.forEach(function(entry) { if (!entry.isDirectory() || entry.name === ".locks" || entry.name === ".git") return; var full=path.join(dir,entry.name); if (fs.existsSync(path.join(full,"model.bin")) && modelSupportsPortuguese(full)) { var nome=modelNameFromPath(full); if (!encontrados[nome]) encontrados[nome]={nome:nome,caminho:full,tamanho:folderSizeMB(full)}; return; } walk(full,profundidade+1); }); }
roots.filter(function(root,index){return root && roots.indexOf(root)===index;}).forEach(function(root){walk(root,0);});
return Object.keys(encontrados).sort().map(function(nome){return encontrados[nome];});
}
function modelSupportsPortuguese(folder) { try { var tokenizer=path.join(folder,"tokenizer.json"); if (!fs.existsSync(tokenizer)) return false; return fs.readFileSync(tokenizer,"utf-8").indexOf("<|pt|>") >= 0; } catch (_) { return false; } }
function modelNameFromPath(folder) { var value=String(folder).replace(/.*faster-whisper-/i,"").replace(/[\\/].*$/,""); return value || path.basename(folder); }
function scannerDetectTracks() {
setBusy("btnScannerDetectTracks", true); document.getElementById("scannerTracksHelp").textContent = "Lendo as faixas da sequência ativa…";
cs.evalScript("(function(){try{var s=app.project.activeSequence;if(!s)return JSON.stringify({ok:false,error:'Nenhuma sequência ativa.'});function tracks(c,a){for(var i=0;i<c.numTracks;i++){var t=c[i],clips=[];for(var j=0;j<t.clips.numItems;j++){var x=t.clips[j],p=x.projectItem;clips.push({nodeId:x.nodeId,name:x.name,start:Number(x.start.seconds),end:Number(x.end.seconds),inPoint:Number(x.inPoint.seconds),outPoint:Number(x.outPoint.seconds),duration:Number(x.duration.seconds),sourceFile:p&&p.getMediaPath?p.getMediaPath():'',sourceOffline:p&&p.isOffline?p.isOffline():false});}a.push({index:i,name:t.name||('Faixa '+(i+1)),numClips:t.clips.numItems,clips:clips});}}var v=[],a=[];tracks(s.videoTracks,v);tracks(s.audioTracks,a);return JSON.stringify({ok:true,id:s.sequenceID,name:s.name,end:Number(s.end.seconds),videoTracks:v,audioTracks:a});}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())", function(raw) { setBusy("btnScannerDetectTracks", false); var data; try { data=JSON.parse(raw); } catch (_) { data={ok:false,error:"Resposta inválida do Premiere."}; } if (!data.ok) { document.getElementById("scannerTracksHelp").textContent=data.error; showToast("err",data.error); return; } scannerTimeline=data; var saved; try { saved=JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY)||"null")||{}; } catch (_) { saved={}; } scannerRenderTracks("video",data.videoTracks,saved.faixas_de_video||data.videoTracks.map(function(t){return t.index;})); scannerRenderTracks("audio",data.audioTracks,saved.faixas_de_audio||[]); document.getElementById("scannerTracksHelp").textContent=data.name+": "+data.videoTracks.length+" faixa(s) de vídeo e "+data.audioTracks.length+" de áudio detectadas."; document.getElementById("btnScannerProcess").disabled=false; });
}
function scannerProcess() {
if (!scannerTimeline) { showToast("err", "Primeiro detecte as faixas da sequência."); return; }
var profile; try { profile=scannerProfileFromScreen(); } catch (error) { showToast("err", error.message); return; }
if (!profile.faixas_de_video.length && !profile.faixas_de_audio.length) { showToast("err", "Selecione ao menos uma faixa."); return; }
saveScannerProfile(); var outputRoot=scannerOutputRoot(); ensureDir(outputRoot);
var stamp=new Date().toISOString().replace(/[:.]/g,"-");
var input=path.join(outputRoot,"pedido-"+stamp+".json");
scannerReportPath=path.join(outputRoot,"relatorio-"+stamp+".json");
fs.writeFileSync(input,JSON.stringify({perfil:profile,timeline:scannerTimeline},null,2),"utf-8");
setBusy("btnScannerProcess",true); taskStart("Scanner", "Preparando análise", null);
var spawnOptions={cwd:"/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code",stdio:["ignore","pipe","pipe"]};
var huggingFaceEnvironment=hfSpawnOptions(); if(huggingFaceEnvironment&&huggingFaceEnvironment.env) spawnOptions.env=huggingFaceEnvironment.env;
var child=childProcess.spawn("python3",[SCANNER_ENGINE_SCRIPT,input,scannerReportPath],spawnOptions);
var stderr="", stdout="";
child.stdout.on("data",function(chunk) {
stdout += chunk.toString(); var lines=stdout.split("\n"); stdout=lines.pop();
lines.forEach(function(line) { try { var event=JSON.parse(line); if (event.evento === "progresso") taskUpdate({stage:event.etapa, detail:event.clipe || "", pct:event.percentual}); if (event.evento === "concluido" && event.arquivo) scannerReportPath=event.arquivo; } catch (_) {} });
});
child.stderr.on("data",function(chunk){stderr+=chunk.toString();});
child.on("error",function(error){setBusy("btnScannerProcess",false);taskEnd(false,"Scanner indisponível");showToast("err",error.message);});
child.on("close",function(code){setBusy("btnScannerProcess",false);if(code!==0||!fs.existsSync(scannerReportPath)){taskEnd(false,"Scanner falhou");showToast("err",stderr||"Não foi possível gerar o relatório.");return;}taskEnd(true,"Relatório JSON gerado");document.getElementById("scannerReportPath").textContent=scannerReportPath;document.getElementById("scannerReportCard").hidden=false;showToast("ok","Scanner concluído.");});
}
function scannerOpenReport() { if (!scannerReportPath) return; childProcess.spawn("open",[scannerReportPath],{detached:true,stdio:"ignore"}).unref(); }
function scannerCopyReport() { if (!scannerReportPath) return; try { var copy=childProcess.spawn("pbcopy"); copy.stdin.end(fs.readFileSync(scannerReportPath,"utf-8")); showToast("ok","Conteúdo do relatório copiado."); } catch (error) { showToast("err",error.message); } }
// Mostra na aba de edição o idioma configurado, para não precisar ir até
// Modelos só para conferir com que idioma a transcrição vai rodar.
function syncLanguageInfo() {
+6
View File
@@ -145,6 +145,11 @@ h1, h2, h3 { margin: 0; }
.tab-button.active .tab-icon { color: var(--violet); opacity: 1; }
.tab-panel[hidden] { display: none; }
.tab-intro { margin: 0 0 12px; color: var(--text-muted); font-size: 10px; }
.scanner-track-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 10px; }
.scanner-track-list { min-height: 34px; max-height: 150px; overflow-y: auto; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-deep); padding: 5px; }
.scanner-track-list .checkbox-field { margin: 2px 0; padding: 5px; border: 0; background: none; }
.scanner-options-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-items: start; }
.scanner-options-grid .card { margin: 0 0 10px; }
/* --------------------- Painel de progresso (task dock) ------------------- */
.task-dock {
@@ -584,6 +589,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
.tab-button { font-size: 0; gap: 0; padding: 10px 2px; }
.tab-icon { font-size: 14px; }
.field-grid { grid-template-columns: minmax(0, 1fr); }
.scanner-track-grid, .scanner-options-grid { grid-template-columns: minmax(0, 1fr); }
.status-panel { grid-template-columns: 38px minmax(0, 1fr); }
.command-stat { grid-column: 2; padding: 8px 0 0; text-align: left; }
.command-stat strong, .command-stat span { display: inline; }