Spaces:
Running on Zero
Running on Zero
Start kiosk capture and rotate scene automatically
Browse files
src/demo/infer_single_image.py
CHANGED
|
@@ -838,7 +838,7 @@ def _resolve_checkpoint_path(checkpoint: str | Path) -> Path:
|
|
| 838 |
|
| 839 |
|
| 840 |
def patch_supersplat_html_auto_rotate(html_path: Path) -> None:
|
| 841 |
-
"""
|
| 842 |
|
| 843 |
Args:
|
| 844 |
html_path: Generated SuperSplat HTML path.
|
|
@@ -848,14 +848,18 @@ def patch_supersplat_html_auto_rotate(html_path: Path) -> None:
|
|
| 848 |
"""
|
| 849 |
source = html_path.read_text(encoding="utf-8")
|
| 850 |
old = "noanim: url.searchParams.has('noanim'),"
|
| 851 |
-
new = "noanim:
|
| 852 |
if new in source:
|
| 853 |
return
|
| 854 |
-
if old
|
|
|
|
|
|
|
|
|
|
|
|
|
| 855 |
raise RuntimeError(
|
| 856 |
-
"Could not
|
| 857 |
)
|
| 858 |
-
html_path.write_text(source
|
| 859 |
|
| 860 |
|
| 861 |
def patch_supersplat_html_viewer_bridge(
|
|
|
|
| 838 |
|
| 839 |
|
| 840 |
def patch_supersplat_html_auto_rotate(html_path: Path) -> None:
|
| 841 |
+
"""Pause generated camera animation unless the viewer opts into rotation.
|
| 842 |
|
| 843 |
Args:
|
| 844 |
html_path: Generated SuperSplat HTML path.
|
|
|
|
| 848 |
"""
|
| 849 |
source = html_path.read_text(encoding="utf-8")
|
| 850 |
old = "noanim: url.searchParams.has('noanim'),"
|
| 851 |
+
new = "noanim: !url.searchParams.has('autorotate'),"
|
| 852 |
if new in source:
|
| 853 |
return
|
| 854 |
+
if old in source:
|
| 855 |
+
source = source.replace(old, new, 1)
|
| 856 |
+
elif "noanim: true," in source:
|
| 857 |
+
source = source.replace("noanim: true,", new, 1)
|
| 858 |
+
else:
|
| 859 |
raise RuntimeError(
|
| 860 |
+
"Could not configure SuperSplat camera rotation; the viewer bundle changed."
|
| 861 |
)
|
| 862 |
+
html_path.write_text(source, encoding="utf-8")
|
| 863 |
|
| 864 |
|
| 865 |
def patch_supersplat_html_viewer_bridge(
|
src/demo/server_frontend/app.js
CHANGED
|
@@ -32,6 +32,7 @@ const htmlDownload = document.getElementById("htmlDownload");
|
|
| 32 |
const kioskCooldown = document.getElementById("kioskCooldown");
|
| 33 |
const kioskCooldownTime = document.getElementById("kioskCooldownTime");
|
| 34 |
const kioskStartButton = document.getElementById("kioskStartButton");
|
|
|
|
| 35 |
const urlParams = new URLSearchParams(window.location.search);
|
| 36 |
const kioskIntervalMinutes = Math.min(60, Math.max(1, Number.parseInt(urlParams.get("interval") || "3", 10) || 3));
|
| 37 |
document.getElementById("kioskIntervalLabel").textContent = `${kioskIntervalMinutes} ${kioskIntervalMinutes === 1 ? "minute" : "minutes"}`;
|
|
@@ -60,8 +61,16 @@ const cameraFormats = {
|
|
| 60 |
const KIOSK_COOLDOWN_DEFAULT_SECONDS = 180;
|
| 61 |
const KIOSK_COOLDOWN_MAX_SECONDS = 3600;
|
| 62 |
const KIOSK_COOLDOWN_STORAGE_KEY = "infinisplat:kiosk-cooldown-until";
|
|
|
|
| 63 |
const KIOSK_ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
| 64 |
const kioskMode = KIOSK_ENABLED_VALUES.has((urlParams.get("kiosk") || "").toLowerCase());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
let cooldownUntil = 0;
|
| 66 |
let cooldownTotalMs = 0;
|
| 67 |
let cooldownTicker = null;
|
|
@@ -360,7 +369,7 @@ async function refreshCameraDevices(activeDeviceId = "") {
|
|
| 360 |
const devices = (await navigator.mediaDevices.enumerateDevices())
|
| 361 |
.filter((device) => device.kind === "videoinput");
|
| 362 |
const availableIds = new Set(devices.map((device) => device.deviceId));
|
| 363 |
-
if (selectedCameraDeviceId && !availableIds.has(selectedCameraDeviceId)) {
|
| 364 |
selectedCameraDeviceId = "";
|
| 365 |
}
|
| 366 |
|
|
@@ -379,6 +388,21 @@ async function refreshCameraDevices(activeDeviceId = "") {
|
|
| 379 |
const preferredId = selectedCameraDeviceId || activeDeviceId;
|
| 380 |
cameraSelect.value = availableIds.has(preferredId) ? preferredId : "";
|
| 381 |
cameraSelect.disabled = running || devices.length < 2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
}
|
| 383 |
|
| 384 |
async function switchCamera() {
|
|
@@ -447,6 +471,11 @@ async function startCamera() {
|
|
| 447 |
console.error(error);
|
| 448 |
if (error?.name === "NotFoundError" || error?.name === "OverconstrainedError") {
|
| 449 |
selectedCameraDeviceId = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
}
|
| 451 |
stopCamera();
|
| 452 |
if (kioskMode && cameraButton.dataset.kioskStarted === "true") {
|
|
@@ -569,6 +598,19 @@ kioskStartButton.addEventListener("click", () => {
|
|
| 569 |
startCamera();
|
| 570 |
});
|
| 571 |
cameraSelect.addEventListener("change", switchCamera);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
cancelCameraButton.addEventListener("click", stopCamera);
|
| 573 |
captureButton.addEventListener("click", capturePhoto);
|
| 574 |
for (const input of cameraFormatInputs) input.addEventListener("change", updateCameraFrame);
|
|
@@ -602,6 +644,8 @@ navigator.mediaDevices?.addEventListener?.("devicechange", () => {
|
|
| 602 |
if (cameraStream) {
|
| 603 |
refreshCameraDevices(cameraStream.getVideoTracks()[0]?.getSettings?.().deviceId || "")
|
| 604 |
.catch((error) => console.warn("Could not refresh the camera list.", error));
|
|
|
|
|
|
|
| 605 |
}
|
| 606 |
});
|
| 607 |
|
|
@@ -707,7 +751,7 @@ async function runPipeline() {
|
|
| 707 |
);
|
| 708 |
if (!viewer?.url) throw new Error("The viewer export returned no file.");
|
| 709 |
setViewerState("loading", "Initializing 3D viewer", "Loading scene data and waiting for the first rendered frame");
|
| 710 |
-
viewerFrame.src = `${viewer.url}?v=${Date.now()}`;
|
| 711 |
reconstructed = true;
|
| 712 |
if (kioskMode) document.documentElement.dataset.kioskState = "showcase";
|
| 713 |
|
|
@@ -727,6 +771,11 @@ async function runPipeline() {
|
|
| 727 |
showToast(errorMessage(error), true);
|
| 728 |
} finally {
|
| 729 |
if (reconstructed) beginCooldown();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
setRunning(false);
|
| 731 |
}
|
| 732 |
}
|
|
@@ -737,6 +786,7 @@ async function connect() {
|
|
| 737 |
try {
|
| 738 |
client = await Client.connect(window.location.origin);
|
| 739 |
reconstructButton.disabled = !selectedFile || inputsLocked();
|
|
|
|
| 740 |
const template = resultValue(await client.predict("/viewer_template", {}));
|
| 741 |
if (template?.url) viewerPreloader.src = template.url;
|
| 742 |
} catch (error) {
|
|
@@ -750,6 +800,9 @@ if (kioskMode) {
|
|
| 750 |
document.documentElement.dataset.kioskState = "welcome";
|
| 751 |
cameraButton.dataset.kioskStarted = "true";
|
| 752 |
cameraButton.textContent = "Start 3D playground";
|
|
|
|
|
|
|
|
|
|
| 753 |
const deadline = storedCooldownDeadline();
|
| 754 |
if (Number.isFinite(deadline) && deadline > Date.now()) {
|
| 755 |
beginCooldown(kioskCooldownMs || deadline - Date.now(), deadline);
|
|
|
|
| 32 |
const kioskCooldown = document.getElementById("kioskCooldown");
|
| 33 |
const kioskCooldownTime = document.getElementById("kioskCooldownTime");
|
| 34 |
const kioskStartButton = document.getElementById("kioskStartButton");
|
| 35 |
+
const kioskCameraSelect = document.getElementById("kioskCameraSelect");
|
| 36 |
const urlParams = new URLSearchParams(window.location.search);
|
| 37 |
const kioskIntervalMinutes = Math.min(60, Math.max(1, Number.parseInt(urlParams.get("interval") || "3", 10) || 3));
|
| 38 |
document.getElementById("kioskIntervalLabel").textContent = `${kioskIntervalMinutes} ${kioskIntervalMinutes === 1 ? "minute" : "minutes"}`;
|
|
|
|
| 61 |
const KIOSK_COOLDOWN_DEFAULT_SECONDS = 180;
|
| 62 |
const KIOSK_COOLDOWN_MAX_SECONDS = 3600;
|
| 63 |
const KIOSK_COOLDOWN_STORAGE_KEY = "infinisplat:kiosk-cooldown-until";
|
| 64 |
+
const KIOSK_CAMERA_STORAGE_KEY = "infinisplat:kiosk-camera-device";
|
| 65 |
const KIOSK_ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
| 66 |
const kioskMode = KIOSK_ENABLED_VALUES.has((urlParams.get("kiosk") || "").toLowerCase());
|
| 67 |
+
if (kioskMode) {
|
| 68 |
+
try {
|
| 69 |
+
selectedCameraDeviceId = window.localStorage.getItem(KIOSK_CAMERA_STORAGE_KEY) || "";
|
| 70 |
+
} catch (error) {
|
| 71 |
+
console.warn("The kiosk camera selection could not be read from storage.", error);
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
let cooldownUntil = 0;
|
| 75 |
let cooldownTotalMs = 0;
|
| 76 |
let cooldownTicker = null;
|
|
|
|
| 369 |
const devices = (await navigator.mediaDevices.enumerateDevices())
|
| 370 |
.filter((device) => device.kind === "videoinput");
|
| 371 |
const availableIds = new Set(devices.map((device) => device.deviceId));
|
| 372 |
+
if (selectedCameraDeviceId && devices.length && !availableIds.has(selectedCameraDeviceId)) {
|
| 373 |
selectedCameraDeviceId = "";
|
| 374 |
}
|
| 375 |
|
|
|
|
| 388 |
const preferredId = selectedCameraDeviceId || activeDeviceId;
|
| 389 |
cameraSelect.value = availableIds.has(preferredId) ? preferredId : "";
|
| 390 |
cameraSelect.disabled = running || devices.length < 2;
|
| 391 |
+
|
| 392 |
+
kioskCameraSelect.replaceChildren();
|
| 393 |
+
const kioskAutomaticOption = document.createElement("option");
|
| 394 |
+
kioskAutomaticOption.value = "";
|
| 395 |
+
kioskAutomaticOption.textContent = "Automatic camera";
|
| 396 |
+
kioskCameraSelect.append(kioskAutomaticOption);
|
| 397 |
+
devices.forEach((device, index) => {
|
| 398 |
+
const option = document.createElement("option");
|
| 399 |
+
option.value = device.deviceId;
|
| 400 |
+
option.textContent = device.label || `Camera ${index + 1}`;
|
| 401 |
+
kioskCameraSelect.append(option);
|
| 402 |
+
});
|
| 403 |
+
const kioskPreferredId = selectedCameraDeviceId || activeDeviceId;
|
| 404 |
+
kioskCameraSelect.value = availableIds.has(kioskPreferredId) ? kioskPreferredId : "";
|
| 405 |
+
kioskCameraSelect.disabled = devices.length < 2 || cameraStream !== null;
|
| 406 |
}
|
| 407 |
|
| 408 |
async function switchCamera() {
|
|
|
|
| 471 |
console.error(error);
|
| 472 |
if (error?.name === "NotFoundError" || error?.name === "OverconstrainedError") {
|
| 473 |
selectedCameraDeviceId = "";
|
| 474 |
+
try {
|
| 475 |
+
window.localStorage.removeItem(KIOSK_CAMERA_STORAGE_KEY);
|
| 476 |
+
} catch (storageError) {
|
| 477 |
+
console.warn("The unavailable kiosk camera selection could not be cleared.", storageError);
|
| 478 |
+
}
|
| 479 |
}
|
| 480 |
stopCamera();
|
| 481 |
if (kioskMode && cameraButton.dataset.kioskStarted === "true") {
|
|
|
|
| 598 |
startCamera();
|
| 599 |
});
|
| 600 |
cameraSelect.addEventListener("change", switchCamera);
|
| 601 |
+
kioskCameraSelect.addEventListener("change", () => {
|
| 602 |
+
selectedCameraDeviceId = kioskCameraSelect.value;
|
| 603 |
+
cameraSelect.value = selectedCameraDeviceId;
|
| 604 |
+
try {
|
| 605 |
+
if (selectedCameraDeviceId) {
|
| 606 |
+
window.localStorage.setItem(KIOSK_CAMERA_STORAGE_KEY, selectedCameraDeviceId);
|
| 607 |
+
} else {
|
| 608 |
+
window.localStorage.removeItem(KIOSK_CAMERA_STORAGE_KEY);
|
| 609 |
+
}
|
| 610 |
+
} catch (error) {
|
| 611 |
+
console.warn("The kiosk camera selection could not be saved.", error);
|
| 612 |
+
}
|
| 613 |
+
});
|
| 614 |
cancelCameraButton.addEventListener("click", stopCamera);
|
| 615 |
captureButton.addEventListener("click", capturePhoto);
|
| 616 |
for (const input of cameraFormatInputs) input.addEventListener("change", updateCameraFrame);
|
|
|
|
| 644 |
if (cameraStream) {
|
| 645 |
refreshCameraDevices(cameraStream.getVideoTracks()[0]?.getSettings?.().deviceId || "")
|
| 646 |
.catch((error) => console.warn("Could not refresh the camera list.", error));
|
| 647 |
+
} else if (kioskMode) {
|
| 648 |
+
refreshCameraDevices().catch((error) => console.warn("Could not refresh kiosk cameras.", error));
|
| 649 |
}
|
| 650 |
});
|
| 651 |
|
|
|
|
| 751 |
);
|
| 752 |
if (!viewer?.url) throw new Error("The viewer export returned no file.");
|
| 753 |
setViewerState("loading", "Initializing 3D viewer", "Loading scene data and waiting for the first rendered frame");
|
| 754 |
+
viewerFrame.src = `${viewer.url}?v=${Date.now()}${kioskMode ? "&autorotate" : ""}`;
|
| 755 |
reconstructed = true;
|
| 756 |
if (kioskMode) document.documentElement.dataset.kioskState = "showcase";
|
| 757 |
|
|
|
|
| 771 |
showToast(errorMessage(error), true);
|
| 772 |
} finally {
|
| 773 |
if (reconstructed) beginCooldown();
|
| 774 |
+
else if (kioskMode && cameraButton.dataset.kioskStarted === "true") {
|
| 775 |
+
document.documentElement.dataset.kioskState = "showcase";
|
| 776 |
+
window.clearTimeout(kioskCaptureTimer);
|
| 777 |
+
kioskCaptureTimer = window.setTimeout(startCamera, 15000);
|
| 778 |
+
}
|
| 779 |
setRunning(false);
|
| 780 |
}
|
| 781 |
}
|
|
|
|
| 786 |
try {
|
| 787 |
client = await Client.connect(window.location.origin);
|
| 788 |
reconstructButton.disabled = !selectedFile || inputsLocked();
|
| 789 |
+
if (kioskMode && selectedFile && !running) window.setTimeout(runPipeline, 0);
|
| 790 |
const template = resultValue(await client.predict("/viewer_template", {}));
|
| 791 |
if (template?.url) viewerPreloader.src = template.url;
|
| 792 |
} catch (error) {
|
|
|
|
| 800 |
document.documentElement.dataset.kioskState = "welcome";
|
| 801 |
cameraButton.dataset.kioskStarted = "true";
|
| 802 |
cameraButton.textContent = "Start 3D playground";
|
| 803 |
+
refreshCameraDevices().catch((error) => {
|
| 804 |
+
console.warn("Could not list kiosk cameras.", error);
|
| 805 |
+
});
|
| 806 |
const deadline = storedCooldownDeadline();
|
| 807 |
if (Number.isFinite(deadline) && deadline > Date.now()) {
|
| 808 |
beginCooldown(kioskCooldownMs || deadline - Date.now(), deadline);
|
src/demo/server_frontend/index.html
CHANGED
|
@@ -17,13 +17,14 @@
|
|
| 17 |
document.documentElement.dataset.kiosk = "on";
|
| 18 |
}
|
| 19 |
</script>
|
| 20 |
-
<link rel="stylesheet" href="/infinisplat/assets/styles.css?v=
|
| 21 |
</head>
|
| 22 |
<body>
|
| 23 |
<main class="app-shell">
|
| 24 |
<section class="kiosk-welcome" aria-label="Fubar Labs 3D Printing Playground">
|
| 25 |
<div><span class="kiosk-eyebrow">FUBAR LABS · NEW JERSEY MAKER SPACE</span><h2>Make something <em>impossible.</em></h2>
|
| 26 |
-
<p>
|
|
|
|
| 27 |
<button class="kiosk-start" id="kioskStartButton" type="button">Start the 3D playground <span aria-hidden="true">↗</span></button></div>
|
| 28 |
<div class="kiosk-instructions"><span class="kiosk-live-dot"></span><span>Live 3D showcase</span><small>New scene every <b id="kioskIntervalLabel">3 minutes</b></small></div>
|
| 29 |
</section>
|
|
@@ -153,6 +154,6 @@
|
|
| 153 |
</main>
|
| 154 |
|
| 155 |
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
| 156 |
-
<script type="module" src="/infinisplat/assets/app.js?v=
|
| 157 |
</body>
|
| 158 |
</html>
|
|
|
|
| 17 |
document.documentElement.dataset.kiosk = "on";
|
| 18 |
}
|
| 19 |
</script>
|
| 20 |
+
<link rel="stylesheet" href="/infinisplat/assets/styles.css?v=11">
|
| 21 |
</head>
|
| 22 |
<body>
|
| 23 |
<main class="app-shell">
|
| 24 |
<section class="kiosk-welcome" aria-label="Fubar Labs 3D Printing Playground">
|
| 25 |
<div><span class="kiosk-eyebrow">FUBAR LABS · NEW JERSEY MAKER SPACE</span><h2>Make something <em>impossible.</em></h2>
|
| 26 |
+
<p>Choose a camera and start the 3D Printing Playground. It captures automatically, then brings every new scene to life with a rotating 3D view.</p>
|
| 27 |
+
<label class="kiosk-camera-picker" for="kioskCameraSelect"><span>CAMERA</span><select id="kioskCameraSelect" aria-label="Select kiosk camera"><option value="">Choose a camera…</option></select></label>
|
| 28 |
<button class="kiosk-start" id="kioskStartButton" type="button">Start the 3D playground <span aria-hidden="true">↗</span></button></div>
|
| 29 |
<div class="kiosk-instructions"><span class="kiosk-live-dot"></span><span>Live 3D showcase</span><small>New scene every <b id="kioskIntervalLabel">3 minutes</b></small></div>
|
| 30 |
</section>
|
|
|
|
| 154 |
</main>
|
| 155 |
|
| 156 |
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
| 157 |
+
<script type="module" src="/infinisplat/assets/app.js?v=13"></script>
|
| 158 |
</body>
|
| 159 |
</html>
|
src/demo/server_frontend/styles.css
CHANGED
|
@@ -567,6 +567,7 @@ button, input { font: inherit; }
|
|
| 567 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .source-panel { display: block; position: fixed; right: 24px; bottom: 24px; z-index: 10; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
|
| 568 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome { min-height: 54vh; align-items: center; justify-content: center; text-align: center; }
|
| 569 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome > div:first-child { max-width: 800px; }
|
|
|
|
| 570 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-instructions { display: none; }
|
| 571 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .workspace { display: none; }
|
| 572 |
:root[data-kiosk="on"][data-kiosk-state="processing"] .kiosk-welcome { display: none; }
|
|
@@ -574,6 +575,9 @@ button, input { font: inherit; }
|
|
| 574 |
.kiosk-welcome h2 { margin: 6px 0; font-size: clamp(1.8rem, 4vw, 3.8rem); letter-spacing: -.045em; line-height: 1; }
|
| 575 |
.kiosk-welcome h2 em { color: #b8f34a; font-style: normal; }
|
| 576 |
.kiosk-welcome p { max-width: 650px; margin: 9px 0 0; color: #b0beb6; font-size: clamp(.8rem, 1.15vw, 1rem); }
|
|
|
|
|
|
|
|
|
|
| 577 |
.kiosk-instructions { display: grid; grid-template-columns: auto auto; align-items: center; gap: 4px 9px; flex-shrink: 0; padding: 12px 15px; border: 1px solid #35453b; border-radius: 9px; background: #121a16; color: #e3eee6; font-size: .82rem; font-weight: 700; }
|
| 578 |
.kiosk-instructions small { grid-column: 2; color: #9eaea4; font-size: .7rem; font-weight: 500; }
|
| 579 |
.kiosk-instructions b { color: #b8f34a; font-weight: 700; }
|
|
|
|
| 567 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .source-panel { display: block; position: fixed; right: 24px; bottom: 24px; z-index: 10; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
|
| 568 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome { min-height: 54vh; align-items: center; justify-content: center; text-align: center; }
|
| 569 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome > div:first-child { max-width: 800px; }
|
| 570 |
+
:root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-welcome p { margin-inline: auto; }
|
| 571 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .kiosk-instructions { display: none; }
|
| 572 |
:root[data-kiosk="on"][data-kiosk-state="welcome"] .workspace { display: none; }
|
| 573 |
:root[data-kiosk="on"][data-kiosk-state="processing"] .kiosk-welcome { display: none; }
|
|
|
|
| 575 |
.kiosk-welcome h2 { margin: 6px 0; font-size: clamp(1.8rem, 4vw, 3.8rem); letter-spacing: -.045em; line-height: 1; }
|
| 576 |
.kiosk-welcome h2 em { color: #b8f34a; font-style: normal; }
|
| 577 |
.kiosk-welcome p { max-width: 650px; margin: 9px 0 0; color: #b0beb6; font-size: clamp(.8rem, 1.15vw, 1rem); }
|
| 578 |
+
.kiosk-camera-picker { display: grid; grid-template-columns: auto minmax(180px, 280px); align-items: center; justify-content: center; gap: 12px; margin: 20px auto 0; color: #b8f34a; font-size: .68rem; font-weight: 850; letter-spacing: .15em; text-align: left; }
|
| 579 |
+
.kiosk-camera-picker select { min-height: 44px; padding: 0 38px 0 13px; border: 1px solid #536756; border-radius: 6px; background: #111a14; color: #f1f7f2; font: inherit; font-size: .82rem; font-weight: 600; letter-spacing: 0; }
|
| 580 |
+
.kiosk-camera-picker select:disabled { opacity: .6; }
|
| 581 |
.kiosk-instructions { display: grid; grid-template-columns: auto auto; align-items: center; gap: 4px 9px; flex-shrink: 0; padding: 12px 15px; border: 1px solid #35453b; border-radius: 9px; background: #121a16; color: #e3eee6; font-size: .82rem; font-weight: 700; }
|
| 582 |
.kiosk-instructions small { grid-column: 2; color: #9eaea4; font-size: .7rem; font-weight: 500; }
|
| 583 |
.kiosk-instructions b { color: #b8f34a; font-weight: 700; }
|