FOSFENO/web/panel/panel.css
hacklab 61543e1987 MOTORES: pastillas configurables y destino (salida o capa)
Las pestanas de capa de la barra superior no valian: mezclaban navegar por el
panel con elegir sobre que se trabaja, y sacaban del panel de directo. Se
quitan y todo pasa a MOTORES, con dos decisiones separadas:

  DONDE  -> fila 'Donde se ve': SALIDA o una capa del mapping (+ crea una).
  QUE    -> las pastillas: combinaciones guardadas por el usuario.

Pinchar una pastilla la manda al destino elegido: con SALIDA cambia el motor
global, con una capa cambia SOLO esa capa y el resto sigue igual. Con una capa
elegida sus ajustes salen en MOTORES (la tarjeta de propiedades se mueve, no
se duplica: dos formularios para lo mismo se desincronizan a la primera).

Las pastillas son configurables: motor + preset + ajustes y su nombre, con
'Configurar pastillas'. Persisten en data/slots.json (gitignorado, es de cada
instalacion). De fabrica vienen los cinco motores de siempre, asi que quien
actualice se encuentra lo mismo que tenia. Hydra, Mezcla y projectM nativo
solo pueden ir a la salida, y al intentar mandarlos a una capa se dice.

Comprobado en Chromium por CDP, no con --dump-dom: ese fotografia el DOM al
cargar, antes de que llegue el estado por socket, y daba falsos "esta vacio".
Verificado que disparar sobre una capa deja el motor global intacto, que la
tarjeta de propiedades va y vuelve de su sitio, y que las pastillas nuevas se
guardan en disco.

De paso, dos fallos que aparecieron al probarlo:
- Restaurar la vista guardada corria al principio del fichero y llamaba a
  codigo que usa variables declaradas con let mas abajo: ReferenceError que
  se llevaba por delante el resto del script (socket incluido) y dejaba el
  panel en blanco. Ahora se restaura al final.
- #card-destino no estaba en la lista de 'order' del CSS, asi que caia a 0 y
  se colaba encima de las pastillas pese a ir despues en el HTML.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 16:47:01 +02:00

451 lines
16 KiB
CSS

/* FOSFENO :: Panel de control
Tema verde acido y negro, con acentos en naranja neon.
Movil: una columna. Pantalla ancha: dos columnas. */
@font-face {
font-family: "Xirod";
src: url("/panel/fonts/xirod.otf") format("opentype");
font-display: swap;
}
:root {
--bg: #060a06;
--card: #0d120c;
--line: #243016;
--green: #b4ff00; /* verde acido */
--green-d: #86bd00;
--orange: #ff7a00; /* naranja acido */
--orange-n: #ff9d2a; /* naranja neon */
--ink: #050705; /* negro: texto sobre superficies brillantes */
--txt: #cfe8ad; /* texto claro sobre fondo oscuro */
--dim: #6f8a52;
--red: #ff3b2f;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
/* El atributo 'hidden' debe ocultar siempre, incluso sobre display:flex */
[hidden] { display: none !important; }
body {
background: var(--bg);
color: var(--txt);
font-family: "Segoe UI", Roboto, system-ui, sans-serif;
-webkit-tap-highlight-color: transparent;
padding-bottom: 48px;
}
/* --- Barra superior fija: titulo + pestanas + avisos --- */
.topbar {
position: sticky; top: 0; z-index: 10; background: var(--bg);
}
/* --- Cabecera con el titulo centrado y las pestanas a su derecha --- */
header {
position: relative;
display: flex; align-items: center; justify-content: center;
flex-wrap: wrap; gap: 10px 20px;
padding: 14px 46px; border-bottom: 2px solid var(--green-d);
background: var(--bg);
}
header h1 {
font-family: "Xirod", "Arial Black", sans-serif;
font-size: 32px; font-weight: 400; letter-spacing: 0.09em;
color: var(--green); text-shadow: 0 0 18px rgba(180, 255, 0, 0.65);
}
#conn {
position: absolute; right: 20px; top: 50%; transform: translateY(-50%);
}
/* --- Pestanas MOTORES / MAPPING --- */
.tabs { display: flex; gap: 8px; }
.tab {
position: relative; cursor: pointer;
font-family: "Xirod", "Arial Black", sans-serif;
font-size: 12px; font-weight: 400; letter-spacing: 0.07em;
padding: 10px 15px; border-radius: 10px;
background: #161d10; border: 2px solid var(--line); color: var(--dim);
}
.tab.active {
border-color: var(--orange); color: var(--orange-n);
box-shadow: 0 0 12px rgba(255, 122, 0, 0.35);
}
/* Punto verde en la pestana MAPPING cuando el mapping esta activado, para
saber desde MOTORES que la salida se esta deformando. */
.tabdot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: var(--green); box-shadow: 0 0 8px var(--green);
margin-left: 7px; vertical-align: middle;
}
/* En movil el titulo baja de tamano para que las pestanas quepan al lado */
@media (max-width: 560px) {
header { padding: 12px 34px; gap: 8px 12px; }
header h1 { font-size: 23px; }
.tab { padding: 9px 11px; font-size: 11px; }
#conn { right: 12px; }
}
.dot { width: 13px; height: 13px; border-radius: 50%; display: inline-block; }
.dot.on { background: var(--green); box-shadow: 0 0 9px var(--green); }
.dot.off { background: var(--red); box-shadow: 0 0 9px var(--red); }
/* --- Disposicion: movil en una columna --- */
main {
max-width: 560px; margin: 0 auto;
padding: 16px; display: flex; flex-direction: column; gap: 14px;
}
.column { display: contents; } /* en movil, las columnas no existen */
/* Orden de las tarjetas cuando estan todas en una sola columna (movil).
Toda tarjeta de <main> tiene que estar en esta lista: la que falte se
queda en order 0 y se cuela LA PRIMERA, por mucho que en el HTML este
abajo. Primero las pastillas (lo que se toca en directo) y justo despues
el destino, que es su pareja: que lanzas / a donde va. */
#card-power { order: 1; }
#card-engines { order: 2; }
#card-destino { order: 3; }
#card-audio { order: 4; }
#card-sens { order: 5; }
#ctl-butterchurn, #ctl-editor, #ctl-mixer, #ctl-projectm { order: 6; }
#card-now { order: 7; }
#card-sys { order: 8; }
#net-foot { order: 9; }
/* --- Disposicion: pantalla ancha (portatil) en dos columnas --- */
@media (min-width: 880px) {
main {
width: 94%; max-width: 1600px; flex-direction: row;
align-items: flex-start; gap: 18px;
}
.column {
display: flex; flex-direction: column; gap: 14px; flex: 1 1 0;
}
}
/* --- Vistas (pestanas) ---
MOTORES ensena todo menos las tarjetas de mapping, y al reves. Salvo la
tarjeta de propiedades de capa, que MOTORES se lleva prestada cuando estas
editando una capa (ver colocarPropiedades en panel.js). */
body[data-view="motores"] [data-vista="mapping"] { display: none !important; }
body[data-view="motores"] #destino-props #card-propiedades {
display: block !important;
}
body[data-view="mapping"] main > .column > :not([data-vista="mapping"]) {
display: none !important;
}
body[data-view="mapping"] main {
flex-direction: column; width: 100%; max-width: 900px;
}
body[data-view="mapping"] .column { display: contents; }
/* En pantalla ancha el mapping se reparte: la previsualizacion grande a la
izquierda (que es donde se trabaja) y las capas con sus propiedades a la
derecha, sin tener que bajar y subir todo el rato. */
@media (min-width: 1000px) {
body[data-view="mapping"] main {
display: grid; width: 96%; max-width: 1700px;
grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
grid-template-areas: "previa capas" "previa props" "previa .";
align-items: start; align-content: start; gap: 16px;
}
body[data-view="mapping"] #card-mapping { grid-area: previa; }
body[data-view="mapping"] #card-capas { grid-area: capas; }
body[data-view="mapping"] #card-propiedades { grid-area: props; }
/* La previsualizacion manda: que ocupe todo el alto util disponible */
body[data-view="mapping"] #map-preview { aspect-ratio: 16 / 9; }
}
/* --- Tarjetas --- */
.card {
background: var(--card); border: 1px solid var(--line);
border-radius: 16px; padding: 16px;
display: flex; flex-direction: column; gap: 12px;
}
.card.center { align-items: center; }
/* Cabecera de tarjeta: titulo centrado, boton de info a la derecha.
El hueco lateral evita que un titulo largo se meta debajo del boton. */
.cardhead {
position: relative; width: 100%;
display: flex; align-items: center; justify-content: center;
min-height: 30px; padding: 0 34px;
}
/* Titulo de cada tarjeta: mas grande y en la fuente Xirod */
.cardhead .label {
font-family: "Xirod", "Arial Black", sans-serif;
font-size: 16px; font-weight: 400; letter-spacing: 0.04em;
}
.label { color: var(--green); font-size: 13px; text-transform: uppercase;
letter-spacing: 0.09em; font-weight: 700; }
.value { color: var(--orange-n); font-weight: 800; }
.hint { color: var(--dim); font-size: 12px; line-height: 1.45; }
.row { display: flex; align-items: center; justify-content: space-between;
gap: 10px; flex-wrap: wrap; }
.row .cmd { flex: 1; }
/* En una fila de etiqueta + desplegable, el desplegable no baja de un ancho
usable: si no cabe, la fila parte en dos lineas en vez de estrujarlo. */
.row .label { flex: 1 1 auto; }
.row select.inline { flex: 0 1 auto; min-width: 148px; max-width: 100%; }
/* --- Boton de informacion (circular naranja, esquina de la tarjeta) --- */
.info {
position: absolute; right: 0; top: 50%; transform: translateY(-50%);
width: 28px; height: 28px; border-radius: 50%;
background: var(--orange); color: var(--ink); border: none;
font-weight: 900; font-style: italic; font-size: 15px; cursor: pointer;
box-shadow: 0 0 10px rgba(255, 122, 0, 0.5);
}
.info:active { background: var(--orange-n); }
/* --- Boton de encendido (circular grande) --- */
.power-btn {
width: 128px; height: 128px; border-radius: 50%;
border: none; cursor: pointer; font-weight: 900; font-size: 26px;
letter-spacing: 0.05em;
background: var(--green); color: var(--ink);
box-shadow: 0 0 26px rgba(180, 255, 0, 0.55);
}
.power-btn.off {
background: var(--card); color: var(--dim);
border: 3px solid var(--line); box-shadow: none;
}
/* --- Selector de motor (botones circulares) --- */
.engines {
display: flex; flex-wrap: wrap; gap: 12px; justify-content: center;
}
.engine {
width: 96px; aspect-ratio: 1 / 1; border-radius: 50%;
display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 2px; cursor: pointer;
background: var(--card); border: 3px solid var(--line); color: var(--txt);
}
/* En movil, algo mas pequenos para que entren TRES por fila: con 96 px se
quedaban en un 2+2+1 descuadrado (flex parte la linea antes de encoger). */
@media (max-width: 560px) {
.engines { gap: 10px; }
.engine { width: 86px; }
.engine strong { font-size: 12px; }
}
.engine strong { font-size: 13px; }
.engine small { font-size: 9px; color: var(--dim); }
.engine.active {
background: var(--green); color: var(--ink); border-color: var(--orange);
box-shadow: 0 0 20px rgba(180, 255, 0, 0.6);
}
.engine.active small { color: var(--ink); }
/* En modo configurar, la pastilla abierta se marca distinto de la que esta
sonando: naranja = "la estas editando", verde = "es la que se ve". */
.engine.editando {
border-color: var(--orange); border-style: dashed;
box-shadow: 0 0 16px rgba(255, 157, 42, 0.5);
}
.engine.nueva { border-style: dashed; color: var(--dim); }
.engine.nueva strong { font-size: 22px; }
/* --- Fila de destinos (salida / capas) ---
Deliberadamente distintas de las pestanas de arriba: aquellas cambian de
pantalla, estas eligen a donde va lo que dispares. */
.subtabs {
display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
margin-bottom: 4px;
}
.subtab {
padding: 8px 14px; border-radius: 999px; cursor: pointer;
background: #161d10; border: 2px solid var(--line); color: var(--dim);
font-weight: 700; font-size: 12px; letter-spacing: 0.08em;
max-width: 14ch; overflow: hidden; text-overflow: ellipsis;
white-space: nowrap;
}
.subtab.active {
background: var(--orange); color: var(--ink); border-color: var(--orange);
}
.subtab.nueva { border-style: dashed; }
#destino-props { margin-top: 12px; }
/* Prestada de MAPPING: dentro de MOTORES no lleva su propio marco, que
quedaria como una tarjeta dentro de otra */
#destino-props #card-propiedades {
background: none; border: none; padding: 0; border-radius: 0;
}
.cmd.del { color: #ff8a7a; }
/* --- Botones de comando --- */
.cmd, .sysbtn {
padding: 13px; border-radius: 11px; cursor: pointer;
background: #161d10; border: 1px solid var(--line); color: var(--txt);
font-weight: 700; font-size: 14px;
}
.cmd:active, .sysbtn:active { background: #202a16; }
.cmd.accent { background: var(--green); color: var(--ink); border: none; }
/* --- Selectores y sliders --- */
select {
width: 100%; padding: 12px; border-radius: 11px;
background: #161d10; color: var(--txt); border: 1px solid var(--line);
font-size: 14px;
}
select.inline { width: auto; padding: 8px 10px; }
input[type=range] { width: 100%; accent-color: var(--green); height: 30px; }
.check { display: flex; align-items: center; gap: 10px; font-size: 14px;
color: var(--txt); }
.check input { width: 22px; height: 22px; accent-color: var(--orange); }
/* --- Botones segmentados --- */
.seg { display: flex; gap: 6px; }
.segbtn {
flex: 1; padding: 11px 6px; border-radius: 10px; cursor: pointer;
background: #161d10; border: 2px solid var(--line); color: var(--dim);
font-weight: 700; font-size: 13px;
}
.segbtn.active {
border-color: var(--orange); color: var(--orange-n);
box-shadow: 0 0 12px rgba(255, 122, 0, 0.35);
}
/* --- BPM --- */
.bpm {
font-size: 30px; font-weight: 900; color: var(--orange-n);
text-shadow: 0 0 14px rgba(255, 157, 42, 0.6);
font-variant-numeric: tabular-nums;
}
/* --- Sliders del mezclador --- */
.slider { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
.slider .head {
display: flex; justify-content: space-between; font-size: 13px;
color: var(--dim);
}
.slider .head b { color: var(--green); font-weight: 800; }
/* --- Editor CodeMirror --- */
.CodeMirror {
height: 220px; border-radius: 11px; border: 1px solid var(--green-d);
font-size: 13px; font-family: "Fira Mono", Consolas, monospace;
}
/* --- Tarjeta de estado --- */
#card-now { text-align: center; }
.now { font-size: 15px; color: var(--orange-n); word-break: break-word; }
/* --- Sistema --- */
.sys { flex-direction: row; }
.sys .sysbtn { flex: 1; }
.sysbtn.danger { color: var(--red); border-color: var(--red); }
/* --- Pie con la direccion del panel --- */
.netfoot {
text-align: center; color: var(--dim);
font-size: 12px; font-family: monospace; padding: 4px 0 8px;
}
/* --- Banda de avisos (dentro de la barra superior fija) --- */
.notif {
display: flex; align-items: center; gap: 10px;
padding: 12px 16px; font-size: 14px; font-weight: 600;
}
.notif.lv-info { background: var(--green); color: var(--ink); }
.notif.lv-warn { background: var(--orange); color: var(--ink); }
.notif.lv-error { background: var(--red); color: #fff; }
.notif span { flex: 1; }
.notif button {
background: transparent; border: none; color: inherit;
font-size: 22px; font-weight: 900; cursor: pointer; line-height: 1;
}
/* --- Ventana de informacion --- */
.modal {
position: fixed; inset: 0; z-index: 50;
background: rgba(3, 6, 3, 0.88);
display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal-box {
background: var(--card); border: 2px solid var(--green);
border-radius: 16px; padding: 22px; max-width: 460px; width: 100%;
max-height: 80vh; overflow-y: auto;
display: flex; flex-direction: column; gap: 14px;
box-shadow: 0 0 30px rgba(180, 255, 0, 0.3);
}
.modal-box h2 {
font-family: "Xirod", "Arial Black", sans-serif; font-weight: 400;
color: var(--green); font-size: 16px; letter-spacing: 0.06em;
text-align: center;
}
.modal-box p { color: var(--txt); font-size: 14px; line-height: 1.5; }
/* --- Bloques agrupados dentro de una tarjeta ---
Un grupo de controles que aparece o desaparece junto (por ejemplo los de
la camara o los del fondo Butter en el mezclador). */
.stack { display: flex; flex-direction: column; gap: 12px; }
/* Controles que ahora mismo no pintan nada: siguen visibles pero apagados,
asi se ve que existen sin que despisten. */
.dim { opacity: 0.42; }
/* Resumen del mezclador: la linea que dice que va a salir por el proyector */
.hint.resumen {
border-left: 3px solid var(--orange); padding: 6px 0 6px 10px;
color: var(--txt); font-size: 13px;
}
/* --- Previsualizacion del mapping --- */
#map-preview {
width: 100%; aspect-ratio: 16 / 9; display: block; margin: 6px 0;
background: #0a0a0a; border: 1px solid var(--line); border-radius: 8px;
touch-action: none; cursor: crosshair;
}
/* --- Lista de capas del mapping ---
Una fila por capa: nombre + fuente a la izquierda, botones a la derecha.
La fila entera selecciona; los botones no la seleccionan. */
#map-list { display: flex; flex-direction: column; gap: 8px; }
.capa {
display: flex; align-items: center; gap: 8px; cursor: pointer;
padding: 9px 10px; border-radius: 11px;
background: #161d10; border: 2px solid var(--line);
}
.capa.sel { border-color: var(--orange); box-shadow: 0 0 12px rgba(255,122,0,0.3); }
.capa.oculta { opacity: 0.45; }
.capa .txt { flex: 1; min-width: 0; }
.capa .nom {
display: block; color: var(--txt); font-weight: 700; font-size: 14px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.capa .src {
display: block; color: var(--dim); font-size: 11px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.capa.mascara .nom { color: #ff6b6b; }
.capa button {
flex: 0 0 auto; width: 34px; height: 34px; border-radius: 9px; cursor: pointer;
background: #0d120c; border: 1px solid var(--line); color: var(--txt);
font-size: 14px; font-weight: 700; line-height: 1; padding: 0;
}
.capa button:disabled { opacity: 0.3; }
.capa .del { color: var(--red); }
/* Malla: los +/- son botones pequenos y van juntos, no barras a lo ancho */
.grupo { display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto; }
#map-sub-dec, #map-sub-inc {
flex: 0 0 auto; width: 46px; padding: 10px 0; font-size: 17px;
}
#map-sub-val { min-width: 40px; text-align: center; }
/* Campo de texto (nombre de la capa) */
.txt[type="text"] {
flex: 1; padding: 10px; border-radius: 10px; font-size: 14px;
background: #161d10; color: var(--txt); border: 1px solid var(--line);
}
/* Favoritos de projectM: lista de visuales para pinchar en directo */
#pm-fav-list {
display: flex; flex-direction: column; gap: 8px;
max-height: 220px; overflow-y: auto;
}
#pm-fav-list .favplay {
text-align: left; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}
#pm-fav-list .favdel { flex: 0 0 auto; }