openvibers/openvibe.bot Commits

Overlay controls back in their corners, at their own size (#401)

OP OpenVibers committed 2 days ago · 35a290b901 · 4 files, +121 −73

Maticus, 11-11: the on-screen controls "come in all weird and show only the top
left corner of the virtual joystick; the previous size and position was perfect".

- The locked-control badge (#391) made the overlay stick and slider
  position: relative, dropping them from their corners to the frame top. Nothing
  repositions a control now; the full panel badge sits on the stick.
- Controls v2 (#393) styled every #panel stick, the overlay one too, at the full
  panel size inside its 132 px box. Its rules are #panel:not(.bot-surface) now:
  the overlay and the one row keep the look they had.
- The lock was a data: image, refused by the panel pages default-src self (a bare
  blue dot): it is /panel/lock.svg now.

Changes

ddiff --git a/public/lock.svg b/public/lock.svg
nnew file mode 100644
iindex 0000000..49bebe6
---- /dev/null
++++ b/public/lock.svg
@@@ -0,0 +1 @@
++<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.6" stroke-linecap="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>
ddiff --git a/public/panel.css b/public/panel.css
iindex f7ebfa0..784f508 100644
---- a/public/panel.css
++++ b/public/panel.css
@@@ -671,11 +671,14 @@ body.bot-mode-compact .bot-compact .bot-notice { color: var(--warn); background:
controls) and says so with a small lock at its corner. */
#panel[data-role="watcher"] [data-command] input:disabled,
#panel[data-role="watcher"] [data-command] button:disabled { pointer-events: none; }
--#panel[data-role="watcher"]:has([data-drive]:not([hidden])) :is(.bot-stick, .bot-slider, .widget-drive .drive-pad) { cursor: pointer; position: relative; }
--#panel[data-role="watcher"]:has([data-drive]:not([hidden])) :is(.bot-stick, .widget-drive .drive-pad)::after {
++#panel[data-role="watcher"]:has([data-drive]:not([hidden])) :is(.bot-stick, .bot-slider, .widget-drive .drive-pad) { cursor: pointer; }
++/* Nothing here moves a control: the overlay's stick and slider are placed (absolute), and making them relative dropped
++ them out of their corners to the frame's top (Maticus, 11-11: "shows only the top left corner of the virtual
++ joystick"). The lock sits on the stick itself (relative already), clear of the pad's arrangement icon. */
++#panel[data-role="watcher"]:has([data-drive]:not([hidden])) :is(.bot-stick, .widget-drive .drive-pad > .stick)::after {
content: ""; position: absolute; right: 6%; top: 6%; width: 18px; height: 18px; border-radius: 50%; pointer-events: none;
background: color-mix(in srgb, var(--fill-accent) 85%, transparent) center / 11px 11px no-repeat
-- url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.6' stroke-linecap='round'%3E%3Crect x='5' y='11' width='14' height='9' rx='2'/%3E%3Cpath d='M8 11V8a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E");
++ url("/panel/lock.svg");
}
/* ── Controls v2 (ask 368, 2026-11-11: the owner circled the pad, the Stop, the head slider and the readouts:
@@@ -683,11 +686,11 @@ body.bot-mode-compact .bot-compact .bot-notice { color: var(--warn); background:
framed anywhere: the stick has its four marks and a knob that lights when it drives; Stop is a ringed red disc
beside it (on the hand this person chose); a slider says its value with − and + to step it; battery and latency
are slim readout rows. The pad's arrangement menu (the sliders icon) keeps the hand, the size and the readouts. ─── */
--#panel .widget-drive { position: relative; }
--#panel .drive-pad { grid-template-columns: auto auto; justify-content: center; align-items: center; gap: 0.75rem 1.4rem; }
--#panel .drive-pad > :not(.stick):not(.stop) { grid-column: 1 / -1; }
--#panel[data-hand="left"] .drive-pad .stop { order: -1; }
--#panel .stick {
++#panel:not(.bot-surface) .widget-drive { position: relative; }
++#panel:not(.bot-surface) .drive-pad { grid-template-columns: auto auto; justify-content: center; align-items: center; gap: 0.75rem 1.4rem; }
++#panel:not(.bot-surface) .drive-pad > :not(.stick):not(.stop) { grid-column: 1 / -1; }
++#panel:not(.bot-surface)[data-hand="left"] .drive-pad .stop { order: -1; }
++#panel:not(.bot-surface) .stick {
--stick: clamp(8.5rem, 44vw, 12.5rem);
width: var(--stick);
background:
@@@ -696,12 +699,12 @@ body.bot-mode-compact .bot-compact .bot-notice { color: var(--warn); background:
box-shadow: inset 0 2px 14px rgba(0, 0, 0, 0.55), inset 0 -1px 0 rgba(255, 255, 255, 0.04), 0 1px 0 rgba(255, 255, 255, 0.05);
transition: box-shadow 0.18s ease, border-color 0.18s ease;
}
--#panel[data-pad="s"] .stick { --stick: clamp(7rem, 34vw, 9.5rem); }
--#panel[data-pad="l"] .stick { --stick: clamp(10rem, 58vw, 15rem); }
--#panel .stick-ring { inset: 22%; border: 1px solid color-mix(in srgb, var(--line) 70%, transparent); }
--#panel .stick-cross { inset: 30%; opacity: 0.35; }
--#panel .stick-dirs { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; fill: color-mix(in srgb, var(--text) 32%, transparent); transition: fill 0.18s ease; }
--#panel .stick-knob {
++#panel:not(.bot-surface)[data-pad="s"] .stick { --stick: clamp(7rem, 34vw, 9.5rem); }
++#panel:not(.bot-surface)[data-pad="l"] .stick { --stick: clamp(10rem, 58vw, 15rem); }
++#panel:not(.bot-surface) .stick-ring { inset: 22%; border: 1px solid color-mix(in srgb, var(--line) 70%, transparent); }
++#panel:not(.bot-surface) .stick-cross { inset: 30%; opacity: 0.35; }
++#panel:not(.bot-surface) .stick-dirs { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; fill: color-mix(in srgb, var(--text) 32%, transparent); transition: fill 0.18s ease; }
++#panel:not(.bot-surface) .stick-knob {
width: 38%;
background:
radial-gradient(circle at 36% 28%, rgba(255, 255, 255, 0.28), transparent 42%),
@@@ -709,12 +712,12 @@ body.bot-mode-compact .bot-compact .bot-notice { color: var(--warn); background:
border: 1px solid color-mix(in srgb, var(--line) 50%, var(--text) 30%);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
--#panel .stick.held, #panel .stick.active { border-color: color-mix(in srgb, var(--accent) 60%, var(--line)); box-shadow: inset 0 2px 14px rgba(0, 0, 0, 0.5), 0 0 0 4px color-mix(in srgb, var(--accent) 16%, transparent); }
--#panel .stick.held .stick-dirs, #panel .stick.active .stick-dirs { fill: color-mix(in srgb, var(--accent) 70%, var(--text)); }
--#panel .stick.active .stick-knob { background: radial-gradient(circle at 36% 28%, rgba(255, 255, 255, 0.4), transparent 42%), radial-gradient(circle at 50% 55%, #6ea8ff, var(--accent) 75%); border-color: #9cc2ff; box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 22%, transparent), 0 10px 22px rgba(0, 0, 0, 0.45); }
--#panel .stick[aria-disabled="true"] { opacity: 0.5; filter: saturate(0.4); }
++#panel:not(.bot-surface) .stick.held, #panel:not(.bot-surface) .stick.active { border-color: color-mix(in srgb, var(--accent) 60%, var(--line)); box-shadow: inset 0 2px 14px rgba(0, 0, 0, 0.5), 0 0 0 4px color-mix(in srgb, var(--accent) 16%, transparent); }
++#panel:not(.bot-surface) .stick.held .stick-dirs, #panel:not(.bot-surface) .stick.active .stick-dirs { fill: color-mix(in srgb, var(--accent) 70%, var(--text)); }
++#panel:not(.bot-surface) .stick.active .stick-knob { background: radial-gradient(circle at 36% 28%, rgba(255, 255, 255, 0.4), transparent 42%), radial-gradient(circle at 50% 55%, #6ea8ff, var(--accent) 75%); border-color: #9cc2ff; box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 22%, transparent), 0 10px 22px rgba(0, 0, 0, 0.45); }
++#panel:not(.bot-surface) .stick[aria-disabled="true"] { opacity: 0.5; filter: saturate(0.4); }
--#panel .drive-pad .stop {
++#panel:not(.bot-surface) .drive-pad .stop {
width: calc(var(--stick, 11rem) * 0.46); height: calc(var(--stick, 11rem) * 0.46); min-width: 4.25rem; min-height: 4.25rem; max-width: 6rem; max-height: 6rem;
padding: 0; border-radius: 50%; display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.2rem;
font-size: 0.78rem; font-weight: 700; letter-spacing: 0.02em; color: var(--text);
@@@ -723,69 +726,74 @@ body.bot-mode-compact .bot-compact .bot-notice { color: var(--warn); background:
box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 10%, transparent), 0 6px 14px rgba(0, 0, 0, 0.35);
transition: transform 0.12s ease, background 0.15s ease, box-shadow 0.15s ease;
}
--#panel .drive-pad .stop .icon { width: 1.35em; height: 1.35em; fill: var(--danger); stroke: none; }
--#panel .drive-pad .stop:hover:not(:disabled) { background: radial-gradient(circle at 50% 35%, color-mix(in srgb, var(--fill-danger) 36%, var(--surface)), color-mix(in srgb, var(--fill-danger) 18%, var(--bg))); }
--#panel .drive-pad .stop:active:not(:disabled) { transform: scale(0.94); }
--#panel .drive-pad .stop:disabled { opacity: 0.5; filter: saturate(0.5); }
++#panel:not(.bot-surface) .drive-pad .stop .icon { width: 1.35em; height: 1.35em; fill: var(--danger); stroke: none; }
++#panel:not(.bot-surface) .drive-pad .stop:hover:not(:disabled) { background: radial-gradient(circle at 50% 35%, color-mix(in srgb, var(--fill-danger) 36%, var(--surface)), color-mix(in srgb, var(--fill-danger) 18%, var(--bg))); }
++#panel:not(.bot-surface) .drive-pad .stop:active:not(:disabled) { transform: scale(0.94); }
++#panel:not(.bot-surface) .drive-pad .stop:disabled { opacity: 0.5; filter: saturate(0.5); }
/* The arrangement menu: a quiet icon in the drive card's corner, its choices as segmented rows. */
--#panel .pad-prefs-btn { position: absolute; top: 0.45rem; right: 0.45rem; min-width: 36px; min-height: 36px; width: 36px; height: 36px; padding: 0; border-radius: 10px; display: grid; place-items: center; background: transparent; border-color: transparent; color: var(--muted); }
--#panel .pad-prefs-btn:hover, #panel .pad-prefs-btn[aria-expanded="true"] { color: var(--text); background: var(--raised); }
--#panel .pad-prefs-btn .icon { width: 1.15rem; height: 1.15rem; }
--#panel .pad-prefs { position: absolute; top: 2.9rem; right: 0.45rem; z-index: 5; display: grid; gap: 0.6rem; width: min(17rem, calc(100% - 0.9rem)); padding: 0.75rem; border-radius: 14px; background: var(--surface); border: 1px solid var(--line); box-shadow: 0 16px 36px -12px rgba(0, 0, 0, 0.6); }
--#panel .pad-prefs[hidden] { display: none; }
--#panel .pad-pref { display: grid; gap: 0.3rem; }
--#panel .pad-pref-l { font-size: 0.72rem; font-weight: 600; color: var(--muted); }
--#panel .pad-pref-opts { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 3px; padding: 3px; border-radius: 10px; background: var(--bg); }
--#panel .pad-pref label { position: relative; }
--#panel .pad-pref input { position: absolute; inset: 0; opacity: 0; margin: 0; width: 100%; height: 100%; min-height: 0; cursor: pointer; }
--#panel .pad-pref span { display: grid; place-items: center; min-height: 34px; border-radius: 8px; font-size: 0.8rem; font-weight: 600; color: var(--muted); }
--#panel .pad-pref input:checked + span { background: var(--raised); color: var(--text); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05), 0 2px 6px rgba(0, 0, 0, 0.25); }
--#panel .pad-pref input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
++#panel:not(.bot-surface) .pad-prefs-btn { position: absolute; top: 0.45rem; right: 0.45rem; min-width: 36px; min-height: 36px; width: 36px; height: 36px; padding: 0; border-radius: 10px; display: grid; place-items: center; background: transparent; border-color: transparent; color: var(--muted); }
++#panel:not(.bot-surface) .pad-prefs-btn:hover, #panel:not(.bot-surface) .pad-prefs-btn[aria-expanded="true"] { color: var(--text); background: var(--raised); }
++#panel:not(.bot-surface) .pad-prefs-btn .icon { width: 1.15rem; height: 1.15rem; }
++#panel:not(.bot-surface) .pad-prefs { position: absolute; top: 2.9rem; right: 0.45rem; z-index: 5; display: grid; gap: 0.6rem; width: min(17rem, calc(100% - 0.9rem)); padding: 0.75rem; border-radius: 14px; background: var(--surface); border: 1px solid var(--line); box-shadow: 0 16px 36px -12px rgba(0, 0, 0, 0.6); }
++#panel:not(.bot-surface) .pad-prefs[hidden] { display: none; }
++#panel:not(.bot-surface) .pad-pref { display: grid; gap: 0.3rem; }
++#panel:not(.bot-surface) .pad-pref-l { font-size: 0.72rem; font-weight: 600; color: var(--muted); }
++#panel:not(.bot-surface) .pad-pref-opts { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 3px; padding: 3px; border-radius: 10px; background: var(--bg); }
++#panel:not(.bot-surface) .pad-pref label { position: relative; }
++#panel:not(.bot-surface) .pad-pref input { position: absolute; inset: 0; opacity: 0; margin: 0; width: 100%; height: 100%; min-height: 0; cursor: pointer; }
++#panel:not(.bot-surface) .pad-pref span { display: grid; place-items: center; min-height: 34px; border-radius: 8px; font-size: 0.8rem; font-weight: 600; color: var(--muted); }
++#panel:not(.bot-surface) .pad-pref input:checked + span { background: var(--raised); color: var(--text); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05), 0 2px 6px rgba(0, 0, 0, 0.25); }
++#panel:not(.bot-surface) .pad-pref input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
/* A slider: its name and value on one line, − the track + on the next; the track fills to the value. */
--#panel .control.slider { display: grid; gap: 0.4rem; align-items: stretch; }
--#panel .slider-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; margin: 0; }
--#panel .slider-head .control-name { min-width: 0; color: var(--text); font-weight: 600; font-size: 0.9rem; }
--#panel .control-value { font-variant-numeric: tabular-nums; font-size: 0.85rem; color: var(--muted); }
--#panel .slider-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 0.5rem; }
--#panel .slider-row .step { width: 40px; height: 40px; min-width: 40px; min-height: 40px; padding: 0; border-radius: 50%; display: grid; place-items: center; }
--#panel .slider-row .step .icon { width: 1.1rem; height: 1.1rem; }
--#panel .slider-row input[type="range"] {
++#panel:not(.bot-surface) .control.slider { display: grid; gap: 0.4rem; align-items: stretch; }
++#panel:not(.bot-surface) .slider-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; margin: 0; }
++#panel:not(.bot-surface) .slider-head .control-name { min-width: 0; color: var(--text); font-weight: 600; font-size: 0.9rem; }
++#panel:not(.bot-surface) .control-value { font-variant-numeric: tabular-nums; font-size: 0.85rem; color: var(--muted); }
++#panel:not(.bot-surface) .slider-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 0.5rem; }
++#panel:not(.bot-surface) .slider-row .step { width: 40px; height: 40px; min-width: 40px; min-height: 40px; padding: 0; border-radius: 50%; display: grid; place-items: center; }
++#panel:not(.bot-surface) .slider-row .step .icon { width: 1.1rem; height: 1.1rem; }
++#panel:not(.bot-surface) .slider-row input[type="range"] {
-webkit-appearance: none; appearance: none; min-width: 0; height: 28px; min-height: 0; margin: 0; background: transparent; cursor: pointer;
}
--#panel .slider-row input[type="range"]::-webkit-slider-runnable-track { height: 8px; border-radius: 999px; background: linear-gradient(90deg, var(--accent) 0 var(--p, 50%), color-mix(in srgb, var(--line) 80%, var(--text) 10%) var(--p, 50%) 100%); }
--#panel .slider-row input[type="range"]::-moz-range-track { height: 8px; border-radius: 999px; background: color-mix(in srgb, var(--line) 80%, var(--text) 10%); }
--#panel .slider-row input[type="range"]::-moz-range-progress { height: 8px; border-radius: 999px; background: var(--accent); }
--#panel .slider-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; margin-top: -8px; border-radius: 50%; background: #fff; border: 0; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 35%, transparent), 0 2px 6px rgba(0, 0, 0, 0.4); transition: box-shadow 0.15s ease; }
--#panel .slider-row input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; border-radius: 50%; background: #fff; border: 0; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 35%, transparent), 0 2px 6px rgba(0, 0, 0, 0.4); }
--#panel .slider-row input[type="range"]:active::-webkit-slider-thumb { box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 30%, transparent), 0 2px 6px rgba(0, 0, 0, 0.4); }
--#panel .slider-row input[type="range"]:disabled { opacity: 0.45; cursor: not-allowed; }
++#panel:not(.bot-surface) .slider-row input[type="range"]::-webkit-slider-runnable-track { height: 8px; border-radius: 999px; background: linear-gradient(90deg, var(--accent) 0 var(--p, 50%), color-mix(in srgb, var(--line) 80%, var(--text) 10%) var(--p, 50%) 100%); }
++#panel:not(.bot-surface) .slider-row input[type="range"]::-moz-range-track { height: 8px; border-radius: 999px; background: color-mix(in srgb, var(--line) 80%, var(--text) 10%); }
++#panel:not(.bot-surface) .slider-row input[type="range"]::-moz-range-progress { height: 8px; border-radius: 999px; background: var(--accent); }
++#panel:not(.bot-surface) .slider-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; margin-top: -8px; border-radius: 50%; background: #fff; border: 0; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 35%, transparent), 0 2px 6px rgba(0, 0, 0, 0.4); transition: box-shadow 0.15s ease; }
++#panel:not(.bot-surface) .slider-row input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; border-radius: 50%; background: #fff; border: 0; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 35%, transparent), 0 2px 6px rgba(0, 0, 0, 0.4); }
++#panel:not(.bot-surface) .slider-row input[type="range"]:active::-webkit-slider-thumb { box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 30%, transparent), 0 2px 6px rgba(0, 0, 0, 0.4); }
++#panel:not(.bot-surface) .slider-row input[type="range"]:disabled { opacity: 0.45; cursor: not-allowed; }
/* Readouts: battery and latency as slim rows (the heading is the row's label), folded when this person folds them. */
--#panel .widget-battery { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 0.75rem; padding: 0.6rem 0.9rem; }
--#panel .widget-battery > h2 { margin: 0; }
--#panel .battery { gap: 0.6rem; }
--#panel .battery meter { height: 0.55rem; border-radius: 999px; border: 0; background: color-mix(in srgb, var(--line) 80%, var(--text) 8%); }
--#panel .battery meter::-webkit-meter-bar { height: 0.55rem; border: 0; border-radius: 999px; background: color-mix(in srgb, var(--line) 80%, var(--text) 8%); }
--#panel .battery meter::-webkit-meter-optimum-value { border-radius: 999px; background: var(--ok); }
--#panel .battery meter::-webkit-meter-suboptimum-value { border-radius: 999px; background: var(--warn); }
--#panel .battery meter::-webkit-meter-even-less-good-value { border-radius: 999px; background: var(--danger); }
--#panel .battery .readout { font-size: 0.95rem; min-width: 3.2ch; text-align: right; }
--#panel .widget-latency { padding: 0.6rem 0.9rem 0.75rem; }
--#panel .widget-latency > h2 { margin-bottom: 0.45rem; }
--#panel .stats { gap: 0.4rem; }
--#panel .stat { gap: 0.1rem; padding: 0.4rem 0.55rem; border-radius: 10px; border-color: transparent; background: color-mix(in srgb, var(--bg) 70%, var(--surface)); }
--#panel .stat .readout { font-size: 1rem; }
--#panel .stat-label { font-size: 0.68rem; line-height: 1.2; }
--#panel .readout:is(:empty) { color: var(--muted); }
--#panel[data-readouts="off"] :is(.widget-battery, .widget-latency, .widget-telemetry) { display: none; }
++#panel:not(.bot-surface) .widget-battery { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 0.75rem; padding: 0.6rem 0.9rem; }
++#panel:not(.bot-surface) .widget-battery > h2 { margin: 0; }
++#panel:not(.bot-surface) .battery { gap: 0.6rem; }
++#panel:not(.bot-surface) .battery meter { height: 0.55rem; border-radius: 999px; border: 0; background: color-mix(in srgb, var(--line) 80%, var(--text) 8%); }
++#panel:not(.bot-surface) .battery meter::-webkit-meter-bar { height: 0.55rem; border: 0; border-radius: 999px; background: color-mix(in srgb, var(--line) 80%, var(--text) 8%); }
++#panel:not(.bot-surface) .battery meter::-webkit-meter-optimum-value { border-radius: 999px; background: var(--ok); }
++#panel:not(.bot-surface) .battery meter::-webkit-meter-suboptimum-value { border-radius: 999px; background: var(--warn); }
++#panel:not(.bot-surface) .battery meter::-webkit-meter-even-less-good-value { border-radius: 999px; background: var(--danger); }
++#panel:not(.bot-surface) .battery .readout { font-size: 0.95rem; min-width: 3.2ch; text-align: right; }
++#panel:not(.bot-surface) .widget-latency { padding: 0.6rem 0.9rem 0.75rem; }
++#panel:not(.bot-surface) .widget-latency > h2 { margin-bottom: 0.45rem; }
++#panel:not(.bot-surface) .stats { gap: 0.4rem; }
++#panel:not(.bot-surface) .stat { gap: 0.1rem; padding: 0.4rem 0.55rem; border-radius: 10px; border-color: transparent; background: color-mix(in srgb, var(--bg) 70%, var(--surface)); }
++#panel:not(.bot-surface) .stat .readout { font-size: 1rem; }
++#panel:not(.bot-surface) .stat-label { font-size: 0.68rem; line-height: 1.2; }
++#panel:not(.bot-surface) .readout:is(:empty) { color: var(--muted); }
++#panel:not(.bot-surface)[data-readouts="off"] :is(.widget-battery, .widget-latency, .widget-telemetry) { display: none; }
/* The offline notice: a calm line, not an alarm. */
--#panel .notice[data-device-fault], #panel .notice[data-notice] { display: flex; align-items: center; gap: 0.5rem; font-size: 0.88rem; line-height: 1.35; }
--#panel .notice[hidden] { display: none; }
++#panel:not(.bot-surface) .notice[data-device-fault], #panel:not(.bot-surface) .notice[data-notice] { display: flex; align-items: center; gap: 0.5rem; font-size: 0.88rem; line-height: 1.35; }
++#panel:not(.bot-surface) .notice[hidden] { display: none; }
--@media (prefers-reduced-motion: reduce) { #panel .stick, #panel .drive-pad .stop, #panel .stick-dirs { transition: none; } }
--#panel .pad-prefs { text-align: left; }
++@media (prefers-reduced-motion: reduce) { #panel:not(.bot-surface) .stick, #panel:not(.bot-surface) .drive-pad .stop, #panel:not(.bot-surface) .stick-dirs { transition: none; } }
++#panel:not(.bot-surface) .pad-prefs { text-align: left; }
/* A wide panel lays the cards in columns: each card is as tall as what it holds (the battery was a tall empty box). */
--#panel .widgets { align-items: start; }
++#panel:not(.bot-surface) .widgets { align-items: start; }
++
++/* The overlay and the one-row controls keep the look they had before Controls v2 (Maticus, 11-11: "the previous size
++ and position was perfect"): Controls v2 is the full panel's (#panel:not(.bot-surface) above); their stick draws its
++ dots, not the v2 direction marks. */
++.bot-surface .stick-dirs { display: none; }
ddiff --git a/server/web/routes.js b/server/web/routes.js
iindex e3809c7..edf2a80 100644
---- a/server/web/routes.js
++++ b/server/web/routes.js
@@@ -229,6 +229,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
const asset = (file, type) => (req, res) => { res.setHeader('Cache-Control', cache.assetHeaders(req.path, { hashed: false })); res.type(type).sendFile(path.join(PUBLIC, file)); };
r.get('/panel/panel.js', asset('panel.js', 'application/javascript'));
r.get('/panel/panel.css', asset('panel.css', 'text/css'));
++ r.get('/panel/lock.svg', asset('lock.svg', 'image/svg+xml')); // the locked-control badge (a data: image is refused by default-src 'self')
r.get('/panel/embed-frame.js', asset('embed-frame.js', 'application/javascript'));
r.get('/panel/screen-share.js', asset('screen-share.js', 'application/javascript'));
r.get('/panel/drive.js', asset('drive.js', 'application/javascript'));
ddiff --git a/test/overlay-controls-css.test.js b/test/overlay-controls-css.test.js
nnew file mode 100644
iindex 0000000..980c758
---- /dev/null
++++ b/test/overlay-controls-css.test.js
@@@ -0,0 +1,38 @@
++'use strict';
++// The controls over a stream keep their places and their look (Maticus, 2026-11-11: "the on screen controls look
++// janky all of the sudden… shows only the top left corner of the virtual joystick. the previous size and position was
++// perfect"). Two rules broke them: the locked-control badge made the overlay's stick and slider position: relative
++// (#391), which dropped them from their corners to the frame's top; and Controls v2 (#393) styled every #panel stick,
++// the overlay's too, at the full panel's size inside its 132 px box. The badge's lock was also a data: image, which the
++// panel pages' default-src 'self' refuses (a bare blue dot).
++// node test/overlay-controls-css.test.js
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'panel.css'), 'utf8');
++const rules = [...css.replace(/\/\*[\s\S]*?\*\//g, '').matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({ sel: m[1].trim(), body: m[2] }));
++
++// 1. Nothing outside the overlay's own placement rules gives its stick or slider a position.
++const placed = rules.filter((r) => /\.bot-(stick|slider)\b(?![\w-])/.test(r.sel) && /(^|;)\s*position\s*:/.test(r.body));
++for (const r of placed) {
++ const value = (r.body.match(/position\s*:\s*([a-z]+)/) || [])[1];
++ assert.ok(['absolute', 'static'].includes(value), `the overlay's stick and slider stay placed: "${r.sel}" sets position: ${value}`);
++}
++
++// 2. Controls v2 is the full panel's: every #panel selector after its heading leaves the overlay and the one row out.
++const v2 = css.slice(css.indexOf('/* ── Controls v2'));
++assert.ok(v2.length > 100, 'the Controls v2 block is there');
++const v2rules = [...v2.replace(/\/\*[\s\S]*?\*\//g, '').matchAll(/([^{}]+)\{[^{}]*\}/g)].map((m) => m[1]);
++const loose = v2rules.flatMap((sel) => sel.split(',')).map((s) => s.trim()).filter((s) => /#panel(?!:not\(\.bot-surface\))/.test(s));
++assert.deepStrictEqual(loose, [], 'Controls v2 selectors are scoped to #panel:not(.bot-surface)');
++assert.ok(/\.bot-surface \.stick-dirs\s*\{\s*display:\s*none/.test(css), 'the overlay stick draws its dots, not the v2 marks');
++
++// 3. No data: images: the panel pages run under default-src 'self'.
++assert.ok(!/url\(\s*["']?data:/.test(css), 'no data: URL in panel.css (default-src \'self\' refuses it)');
++assert.ok(css.includes('url("/panel/lock.svg")'), 'the lock badge is a same-origin file');
++const routes = fs.readFileSync(path.join(__dirname, '..', 'server', 'web', 'routes.js'), 'utf8');
++assert.ok(routes.includes("r.get('/panel/lock.svg', asset('lock.svg', 'image/svg+xml'))"), 'and the panel serves it');
++assert.ok(fs.existsSync(path.join(__dirname, '..', 'public', 'lock.svg')), 'the file exists');
++
++console.log('overlay controls css: all checks passed');