diff --git a/src/static/help.js b/src/static/help.js index d91996c..9656ed3 100644 --- a/src/static/help.js +++ b/src/static/help.js @@ -3,6 +3,8 @@ document.addEventListener('DOMContentLoaded', () => { const helpBtn = document.getElementById('help-btn'); const helpModal = document.getElementById('help-modal'); const helpCloseBtn = document.getElementById('help-close-btn'); + const mainMenuBtn = document.getElementById('main-menu-btn'); + const mainMenuDropdown = document.getElementById('main-menu-dropdown'); if (helpBtn && helpModal) { helpBtn.addEventListener('click', () => { @@ -24,6 +26,32 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Mobile main menu: forward clicks to existing header buttons + if (mainMenuBtn && mainMenuDropdown) { + mainMenuBtn.addEventListener('click', () => { + mainMenuDropdown.classList.toggle('open'); + }); + + mainMenuDropdown.addEventListener('click', (event) => { + const target = event.target; + if (target && target.matches('button[data-target]')) { + const id = target.getAttribute('data-target'); + const realBtn = document.getElementById(id); + if (realBtn) { + realBtn.click(); + } + mainMenuDropdown.classList.remove('open'); + } + }); + + // Close menu when clicking outside + document.addEventListener('click', (event) => { + if (!mainMenuDropdown.contains(event.target) && event.target !== mainMenuBtn) { + mainMenuDropdown.classList.remove('open'); + } + }); + } + // Settings modal wiring (reusing existing settings endpoints). const settingsButton = document.getElementById('settings-btn'); const settingsModal = document.getElementById('settings-modal'); @@ -145,11 +173,16 @@ document.addEventListener('DOMContentLoaded', () => { if (stationForm) { stationForm.addEventListener('submit', async (e) => { e.preventDefault(); + const ssid = (document.getElementById('station-ssid').value || '').trim(); + if (!ssid) { + showSettingsMessage('SSID is required', 'error'); + return; + } const formData = { - ssid: document.getElementById('station-ssid').value, - password: document.getElementById('station-password').value, - ip: document.getElementById('station-ip').value || null, - gateway: document.getElementById('station-gateway').value || null, + ssid, + password: document.getElementById('station-password').value || '', + ip: (document.getElementById('station-ip').value || '').trim() || null, + gateway: (document.getElementById('station-gateway').value || '').trim() || null, }; try { const response = await fetch('/settings/wifi/station', { @@ -157,7 +190,12 @@ document.addEventListener('DOMContentLoaded', () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData), }); - const result = await response.json(); + let result = {}; + try { + result = await response.json(); + } catch (_) { + result = { error: response.status === 400 ? 'Bad request (check SSID and connection)' : 'Request failed' }; + } if (response.ok) { showSettingsMessage('WiFi station connected successfully!', 'success'); setTimeout(loadStationStatus, 1000); diff --git a/src/static/presets.js b/src/static/presets.js index fbdee33..31387c0 100644 --- a/src/static/presets.js +++ b/src/static/presets.js @@ -684,18 +684,13 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // Handle "Add Preset" button in tab area (dynamically loaded) - document.addEventListener('click', async (e) => { - if (e.target && e.target.id === 'preset-add-btn-tab') { - await showAddPresetToTabModal(); + const showAddPresetToTabModal = async (optionalTabId) => { + let tabId = optionalTabId; + if (!tabId) { + // Get current tab ID from the presets section + const leftPanel = document.querySelector('.presets-section[data-tab-id]'); + tabId = leftPanel ? leftPanel.dataset.tabId : null; } - }); - - const showAddPresetToTabModal = async () => { - // Get current tab ID from the left-panel - const leftPanel = document.querySelector('.presets-section[data-tab-id]'); - let tabId = leftPanel ? leftPanel.dataset.tabId : null; - if (!tabId) { // Fallback: try to get from URL const pathParts = window.location.pathname.split('/'); @@ -704,7 +699,6 @@ document.addEventListener('DOMContentLoaded', () => { tabId = pathParts[tabIndex + 1]; } } - if (!tabId) { alert('Could not determine current tab.'); return; @@ -761,13 +755,12 @@ document.addEventListener('DOMContentLoaded', () => { const listContainer = document.getElementById('add-preset-list'); const presetNames = Object.keys(allPresets); - if (presetNames.length === 0) { - listContainer.innerHTML = '

No presets available. Create a preset first.

'; + const availableToAdd = presetNames.filter(presetId => !currentTabPresets.includes(presetId)); + if (availableToAdd.length === 0) { + listContainer.innerHTML = '

No presets to add. All presets are already in this tab, or create a preset first.

'; } else { - presetNames.forEach(presetId => { + availableToAdd.forEach(presetId => { const preset = allPresets[presetId]; - const isInCurrentTab = currentTabPresets.includes(presetId); - const row = document.createElement('div'); row.className = 'profiles-row'; @@ -779,31 +772,17 @@ document.addEventListener('DOMContentLoaded', () => { details.style.fontSize = '0.85em'; details.textContent = preset.pattern || '-'; - const actionButton = document.createElement('button'); - if (isInCurrentTab) { - // Already in this tab: allow removing from this tab - actionButton.className = 'btn btn-danger btn-small'; - actionButton.textContent = 'Remove'; - actionButton.addEventListener('click', async (e) => { - e.stopPropagation(); - if (confirm(`Remove preset "${preset.name || presetId}" from this tab?`)) { - await removePresetFromTab(tabId, presetId); - modal.remove(); - } - }); - } else { - // Not yet in this tab: allow adding (even if used in other tabs) - actionButton.className = 'btn btn-primary btn-small'; - actionButton.textContent = 'Add'; - actionButton.addEventListener('click', async () => { - await addPresetToTab(presetId, tabId); - modal.remove(); - }); - } + const addButton = document.createElement('button'); + addButton.className = 'btn btn-primary btn-small'; + addButton.textContent = 'Add'; + addButton.addEventListener('click', async () => { + await addPresetToTab(presetId, tabId); + modal.remove(); + }); row.appendChild(label); row.appendChild(details); - row.appendChild(actionButton); + row.appendChild(addButton); listContainer.appendChild(row); }); } @@ -824,6 +803,9 @@ document.addEventListener('DOMContentLoaded', () => { alert('Failed to load presets.'); } }; + try { + window.showAddPresetToTabModal = showAddPresetToTabModal; + } catch (e) {} const addPresetToTab = async (presetId, tabId) => { if (!tabId) { @@ -906,6 +888,9 @@ document.addEventListener('DOMContentLoaded', () => { alert('Failed to add preset to tab.'); } }; + try { + window.addPresetToTab = addPresetToTab; + } catch (e) {} if (presetEditorCloseButton) { presetEditorCloseButton.addEventListener('click', closeEditor); } @@ -1126,7 +1111,8 @@ document.addEventListener('DOMContentLoaded', () => { // Build an ESPNow preset message for a single preset and optionally include a select // for the given device names, then send it via WebSocket. -const sendPresetViaEspNow = (presetId, preset, deviceNames) => { +// saveToDevice defaults to true. +const sendPresetViaEspNow = (presetId, preset, deviceNames, saveToDevice = true) => { try { const colors = Array.isArray(preset.colors) && preset.colors.length ? preset.colors @@ -1152,6 +1138,10 @@ const sendPresetViaEspNow = (presetId, preset, deviceNames) => { }, }, }; + if (saveToDevice) { + // Instruct led-driver to save this preset when received. + message.save = true; + } // Optionally include a select section for specific devices if (Array.isArray(deviceNames) && deviceNames.length > 0) { @@ -1225,11 +1215,6 @@ const ensurePresetContextMenu = () => { cursor: pointer; font-size: 0.9rem; `; - if (action === 'remove') { - // Visually emphasize and align remove to the right - item.style.textAlign = 'right'; - item.style.color = '#ff8080'; - } item.addEventListener('mouseover', () => { item.style.backgroundColor = '#3a3a3a'; }); @@ -1240,20 +1225,17 @@ const ensurePresetContextMenu = () => { }; addItem('Edit preset…', 'edit'); - addItem('Remove', 'remove'); menu.addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action]'); if (!btn || !presetContextTarget) { return; } - const { tabId, presetId } = presetContextTarget; + const { presetId } = presetContextTarget; const action = btn.dataset.action; hidePresetContextMenu(); if (action === 'edit') { await editPresetFromTab(presetId); - } else if (action === 'remove') { - await removePresetFromTab(tabId, presetId); } }); @@ -1361,24 +1343,24 @@ const savePresetGrid = async (tabId, presetGrid) => { } }; -// Function to get drop target in 2D grid +// Function to get drop target: the cell that contains the cursor (or closest if in a gap) const getDropTarget = (container, x, y) => { const draggableElements = [...container.querySelectorAll('.draggable-preset:not(.dragging)')]; - - return draggableElements.reduce((closest, child) => { + // First try: find the element whose rect contains the cursor + const containing = draggableElements.find((child) => { const box = child.getBoundingClientRect(); - const centerX = box.left + box.width / 2; - const centerY = box.top + box.height / 2; - const distanceX = Math.abs(x - centerX); - const distanceY = Math.abs(y - centerY); - const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); - - if (distance < closest.distance) { - return { distance: distance, element: child }; - } else { - return closest; - } - }, { distance: Infinity }).element; + return x >= box.left && x <= box.right && y >= box.top && y <= box.bottom; + }); + if (containing) return containing; + // Fallback: closest element by distance to center + const closest = draggableElements.reduce((best, child) => { + const box = child.getBoundingClientRect(); + const cx = box.left + box.width / 2; + const cy = box.top + box.height / 2; + const d = Math.hypot(x - cx, y - cy); + return d < best.distance ? { distance: d, element: child } : best; + }, { distance: Infinity }); + return closest.element; }; // Function to render presets for a specific tab in 2D grid @@ -1426,17 +1408,8 @@ const renderTabPresets = async (tabId) => { const dropTarget = getDropTarget(presetsList, e.clientX, e.clientY); if (dropTarget && dropTarget !== dragging) { - // Insert before or after based on position - const rect = dropTarget.getBoundingClientRect(); - const draggingRect = dragging.getBoundingClientRect(); - - if (e.clientX < rect.left + rect.width / 2) { - // Insert before - presetsList.insertBefore(dragging, dropTarget); - } else { - // Insert after - presetsList.insertBefore(dragging, dropTarget.nextSibling); - } + // Insert before drop target so the dragged item takes that cell's position + presetsList.insertBefore(dragging, dropTarget); } }); @@ -1477,7 +1450,7 @@ const renderTabPresets = async (tabId) => { const empty = document.createElement('p'); empty.className = 'muted-text'; empty.style.gridColumn = '1 / -1'; // Span all columns - empty.textContent = 'No presets added to this tab. Click "Add Preset" to add one.'; + empty.textContent = 'No presets added to this tab. Open the tab\'s Edit menu and click "Add Preset" to add one.'; presetsList.appendChild(empty); } else { flatPresets.forEach((presetId) => { @@ -1496,97 +1469,67 @@ const renderTabPresets = async (tabId) => { }; const createPresetButton = (presetId, preset, tabId, isSelected = false) => { - // Create wrapper div for button and edit button - const wrapper = document.createElement('div'); - wrapper.style.cssText = 'display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;'; - wrapper.draggable = true; - wrapper.dataset.presetId = presetId; - wrapper.classList.add('draggable-preset'); - - // Create preset button const button = document.createElement('button'); - button.className = 'pattern-button'; - button.style.flex = '1'; + button.className = 'pattern-button draggable-preset'; + button.draggable = true; + button.dataset.presetId = presetId; if (isSelected) { button.classList.add('active'); } - button.dataset.presetId = presetId; - - const presetInfo = document.createElement('div'); - presetInfo.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start; width: 100%;'; - + + const colors = Array.isArray(preset.colors) ? preset.colors.filter(c => c) : []; + const isRainbow = (preset.pattern || '').toLowerCase() === 'rainbow'; + const barColors = isRainbow + ? ['#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#8F00FF'] + : colors; + if (barColors.length > 0) { + const n = barColors.length; + const stops = barColors.flatMap((c, i) => { + const start = (100 * i / n).toFixed(2); + const end = (100 * (i + 1) / n).toFixed(2); + return [`${c} ${start}%`, `${c} ${end}%`]; + }).join(', '); + button.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), linear-gradient(to right, ${stops})`; + } + const presetNameLabel = document.createElement('span'); presetNameLabel.textContent = preset.name || presetId; presetNameLabel.style.fontWeight = 'bold'; - presetNameLabel.style.marginBottom = '0.25rem'; - - const presetDetails = document.createElement('span'); - presetDetails.style.fontSize = '0.85em'; - presetDetails.style.color = '#aaa'; - const colors = Array.isArray(preset.colors) ? preset.colors : []; - presetDetails.textContent = `${preset.pattern || '-'} • ${colors.length} color${colors.length !== 1 ? 's' : ''}`; - - presetInfo.appendChild(presetNameLabel); - presetInfo.appendChild(presetDetails); - button.appendChild(presetInfo); - - // Left-click selects preset, right-click opens editor - button.addEventListener('click', (e) => { - if (isDraggingPreset) { - return; - } + presetNameLabel.className = 'pattern-button-label'; + button.appendChild(presetNameLabel); - // Remove active class from all presets in this tab + button.addEventListener('click', (e) => { + if (isDraggingPreset) return; const presetsList = document.getElementById('presets-list-tab'); if (presetsList) { - presetsList.querySelectorAll('.pattern-button').forEach(btn => { - btn.classList.remove('active'); - }); + presetsList.querySelectorAll('.pattern-button').forEach(btn => btn.classList.remove('active')); } - - // Add active class to clicked preset button.classList.add('active'); - - // Store selected preset for this tab selectedPresets[tabId] = presetId; - - // Build and send a select message via WebSocket for all device names in this tab. const section = button.closest('.presets-section'); sendSelectForCurrentTabDevices(presetId, section); }); button.addEventListener('contextmenu', async (e) => { e.preventDefault(); - if (isDraggingPreset) { - return; - } - // Right-click: directly open the preset editor using data we already have + if (isDraggingPreset) return; await editPresetFromTab(presetId, tabId, preset); }); - - wrapper.appendChild(button); - - // Add drag event handlers - wrapper.addEventListener('dragstart', (e) => { + + button.addEventListener('dragstart', (e) => { isDraggingPreset = true; - wrapper.classList.add('dragging'); + button.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', presetId); }); - - wrapper.addEventListener('dragend', (e) => { - wrapper.classList.remove('dragging'); - // Remove any drag-over classes from siblings - document.querySelectorAll('.draggable-preset').forEach(el => { - el.classList.remove('drag-over'); - }); - // Reset dragging flag after a short delay to allow click event to check it - setTimeout(() => { - isDraggingPreset = false; - }, 100); + + button.addEventListener('dragend', (e) => { + button.classList.remove('dragging'); + document.querySelectorAll('.draggable-preset').forEach(el => el.classList.remove('drag-over')); + setTimeout(() => { isDraggingPreset = false; }, 100); }); - - return wrapper; + + return button; }; const editPresetFromTab = async (presetId, tabId, existingPreset) => { @@ -1685,6 +1628,9 @@ const removePresetFromTab = async (tabId, presetId) => { alert('Failed to remove preset from tab.'); } }; +try { + window.removePresetFromTab = removePresetFromTab; +} catch (e) {} // Listen for HTMX swaps to render presets document.body.addEventListener('htmx:afterSwap', (event) => { diff --git a/src/static/profiles.js b/src/static/profiles.js index 33e7b82..6f2a61d 100644 --- a/src/static/profiles.js +++ b/src/static/profiles.js @@ -73,6 +73,70 @@ document.addEventListener("DOMContentLoaded", () => { } }); + const cloneButton = document.createElement("button"); + cloneButton.className = "btn btn-secondary btn-small"; + cloneButton.textContent = "Clone"; + cloneButton.addEventListener("click", async () => { + const baseName = (profile && profile.name) || profileId; + const suggested = `${baseName}`; + const name = prompt("New profile name:", suggested); + if (name === null) { + return; + } + const trimmed = String(name).trim(); + if (!trimmed) { + alert("Profile name cannot be empty."); + return; + } + try { + const response = await fetch(`/profiles/${profileId}/clone`, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ name: trimmed }), + }); + if (!response.ok) { + throw new Error("Failed to clone profile"); + } + const data = await response.json().catch(() => null); + let newProfileId = null; + if (data && typeof data === "object") { + if (data.id) { + newProfileId = String(data.id); + } else { + const ids = Object.keys(data); + if (ids.length > 0) { + newProfileId = String(ids[0]); + } + } + } + if (newProfileId) { + await fetch(`/profiles/${newProfileId}/apply`, { + method: "POST", + headers: { Accept: "application/json" }, + }); + } + document.cookie = "current_tab=; path=/; max-age=0"; + await loadProfiles(); + if (typeof window.loadTabs === "function") { + await window.loadTabs(); + } + if (typeof window.loadTabsModal === "function") { + await window.loadTabsModal(); + } + const tabContent = document.getElementById("tab-content"); + if (tabContent) { + tabContent.innerHTML = ` +
+ Select a tab to get started +
+ `; + } + } catch (error) { + console.error("Clone profile failed:", error); + alert("Failed to clone profile."); + } + }); + const deleteButton = document.createElement("button"); deleteButton.className = "btn btn-danger btn-small"; deleteButton.textContent = "Delete"; @@ -98,6 +162,7 @@ document.addEventListener("DOMContentLoaded", () => { row.appendChild(label); row.appendChild(applyButton); + row.appendChild(cloneButton); row.appendChild(deleteButton); profilesList.appendChild(row); }); @@ -150,8 +215,44 @@ document.addEventListener("DOMContentLoaded", () => { if (!response.ok) { throw new Error("Failed to create profile"); } + const data = await response.json().catch(() => null); + let newProfileId = null; + if (data && typeof data === "object") { + if (data.id) { + newProfileId = String(data.id); + } else { + const ids = Object.keys(data); + if (ids.length > 0) { + newProfileId = String(ids[0]); + } + } + } + + if (newProfileId) { + await fetch(`/profiles/${newProfileId}/apply`, { + method: "POST", + headers: { Accept: "application/json" }, + }); + } + newProfileInput.value = ""; + // Clear current tab and refresh the UI so the new profile starts empty. + document.cookie = "current_tab=; path=/; max-age=0"; await loadProfiles(); + if (typeof window.loadTabs === "function") { + await window.loadTabs(); + } + if (typeof window.loadTabsModal === "function") { + await window.loadTabsModal(); + } + const tabContent = document.getElementById("tab-content"); + if (tabContent) { + tabContent.innerHTML = ` +
+ Select a tab to get started +
+ `; + } } catch (error) { console.error("Create profile failed:", error); alert("Failed to create profile."); diff --git a/src/static/style.css b/src/static/style.css index cf7c2e8..ae8d541 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -20,25 +20,65 @@ body { header { background-color: #1a1a1a; - padding: 1rem 2rem; + padding: 0.75rem 1rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #4a4a4a; + gap: 0.75rem; } header h1 { - font-size: 1.5rem; + font-size: 1.35rem; font-weight: 600; } .header-actions { display: flex; gap: 0.5rem; + flex-wrap: wrap; + justify-content: flex-end; +} + +.header-menu-mobile { + display: none; + position: relative; +} + +.main-menu-dropdown { + position: absolute; + top: 100%; + right: 0; + background-color: #1a1a1a; + border: 1px solid #4a4a4a; + border-radius: 4px; + padding: 0.25rem 0; + display: none; + min-width: 160px; + z-index: 1100; +} + +.main-menu-dropdown.open { + display: block; +} + +.main-menu-dropdown button { + width: 100%; + background: none; + border: none; + color: white; + text-align: left; + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + cursor: pointer; +} + +.main-menu-dropdown button:hover { + background-color: #333; } .btn { - padding: 0.5rem 1rem; + padding: 0.45rem 0.9rem; border: none; border-radius: 4px; cursor: pointer; @@ -87,15 +127,22 @@ header h1 { } .tabs-container { - background-color: #1a1a1a; - border-bottom: 2px solid #4a4a4a; - padding: 0.5rem 1rem; + background-color: transparent; + padding: 0.5rem 0; + flex: 1; + min-width: 0; + align-self: stretch; + display: flex; + align-items: center; } .tabs-list { display: flex; gap: 0.5rem; overflow-x: auto; + padding-bottom: 0.25rem; + flex: 1; + min-width: 0; } .tab-button { @@ -122,10 +169,28 @@ header h1 { .tab-content { flex: 1; display: block; - overflow: auto; + overflow-y: auto; + overflow-x: hidden; padding: 0.5rem 1rem 1rem; } +.presets-toolbar { + align-items: center; +} + +.tab-brightness-group { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0.25rem; + margin-left: auto; +} + +.tab-brightness-group label { + white-space: nowrap; + font-size: 0.85rem; +} + .left-panel { flex: 0 0 50%; display: flex; @@ -355,6 +420,33 @@ header h1 { font-size: 1.1rem; } +/* Make the presets area fill available vertical space; no border around presets */ +.presets-section { + display: flex; + flex-direction: column; + height: 100%; + min-width: 0; + overflow-x: hidden; + border: none; + background-color: transparent; + padding: 0; +} + +/* Tab preset selecting area: 3 columns, vertical scroll only */ +#presets-list-tab { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-auto-rows: 5rem; + column-gap: 0.3rem; + row-gap: 0.3rem; + align-content: start; + width: 100%; +} + /* Settings modal layout */ .settings-section { background-color: #1a1a1a; @@ -478,21 +570,38 @@ header h1 { } .presets-list { - display: grid; - grid-template-columns: repeat(3, 1fr); + display: flex; + flex-wrap: wrap; gap: 0.75rem; + width: 100%; } .pattern-button { - padding: 0.75rem; + height: 5rem; + padding: 0 0.5rem; background-color: #3a3a3a; color: white; - border: none; + border: 3px solid #000; border-radius: 4px; cursor: pointer; - font-size: 0.9rem; + font-size: 0.85rem; text-align: left; transition: background-color 0.2s; + line-height: 1; + display: flex; + align-items: center; + overflow: hidden; + box-shadow: none; + outline: none; + position: relative; +} + +/* Preset select buttons inside the tab grid */ +#presets-list-tab .pattern-button { + display: flex; +} +.pattern-button .pattern-button-label { + text-shadow: 0 0 2px rgba(0,0,0,0.8), 0 1px 2px rgba(0,0,0,0.6); } .pattern-button:hover { @@ -502,10 +611,28 @@ header h1 { .pattern-button.active { background-color: #6a5acd; color: white; + border-color: #ffffff; +} +.pattern-button.active[style*="background-image"] { + background-color: transparent; +} + +.pattern-button.active::after { + content: ''; + position: absolute; + inset: -3px; + border-radius: 7px; + padding: 3px; + pointer-events: none; + background: #ffffff; + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + mask-composite: exclude; } .pattern-button.default-preset { - border: 2px solid #6a5acd; + /* No border; active state shows selection */ } .color-palette { @@ -604,7 +731,7 @@ header h1 { background-color: #2e2e2e; padding: 2rem; border-radius: 8px; - min-width: 400px; + min-width: 320px; max-width: 500px; } @@ -661,3 +788,269 @@ header h1 { background: #5a5a5a; } +/* Mobile-friendly layout */ +@media (max-width: 800px) { + header { + flex-direction: row; + align-items: center; + gap: 0.25rem; + } + + header h1 { + font-size: 1.1rem; + } /* On mobile, hide header buttons; all actions (including Tabs) are in the Menu dropdown */ + .header-actions { + display: none; + } + + .header-menu-mobile { + display: block; + margin-top: 0; + margin-left: auto; + } + + .btn { + font-size: 0.8rem; + padding: 0.4rem 0.7rem; + } + + .tabs-container { + padding: 0.5rem 0; + border-bottom: none; + } + + .tab-content { + padding: 0.5rem; + } + + .left-panel { + flex: 1; + border-right: none; + padding-right: 0; + } + + .right-panel { + padding-left: 0; + margin-top: 1rem; + } + + /* Hide the "Presets for ..." heading to save space on mobile */ + .presets-section h3 { + display: none; + } + + .modal-content { + min-width: 280px; + max-width: 95vw; + padding: 1.25rem; + } + + .form-row { + grid-template-columns: 1fr; + } +} + +/* Styles moved from inline +