Files
LEDMatrix/web_interface/static/v3/app.css
T
ChuckandClaude Opus 5 521f81272f feat(element-style): visibility, alignment and scale per element
Completes the customization vocabulary: hide an element, align it, and
resize a logo, alongside the font/size/colour/offset that already existed.
All three per mode.

They resolve to "change nothing" until the user asks for something -- True,
None and 1.0 -- rather than to whatever the schema declares. That is the
same invariant the font fields keep: a caller that honours them still
renders an untouched config exactly as it did before they existed. A
schema default therefore does not count as a choice, which matters because
the save flow writes that default into config either way.

scale sits in the layout block with the offsets rather than in the element
block, because it is geometry: a logo has a scale and no font. The widget's
columns come from the schema, so a logo row shows visibility, offsets and
scale and no empty font cell.

Two bugs found by the tests rather than by reading:

- A nullable enum needs null in its enum list, not just in its type. The
  mode copy of `align` defaulted to null and then failed its own schema, so
  a plugin declaring any enum field with modes could not save at all. Six
  tests failed on this before any of them reached what they were testing.
- defaults_from_schema only ever extracted font/font_size/text_color, so
  the schema defaults for the new fields were invisible to the resolver and
  a declared default read as a user choice.

Widget: the table scrolls horizontally and pins the element-name column.
Nine columns do not fit the config panel, and clipping them hid the offsets
entirely while scrolling them made every row anonymous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 17:54:23 -04:00

1497 lines
42 KiB
CSS

/* LED Matrix v3 Custom Styles */
/* Modern, clean design with utility classes */
/* CSS Custom Properties for Theme Colors */
:root {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-secondary: #059669;
--color-secondary-hover: #047857;
--color-accent: #7c3aed;
--color-accent-hover: #6d28d9;
--color-background: #f9fafb;
--color-surface: #ffffff;
--color-text-primary: #111827;
--color-text-secondary: #374151;
--color-text-tertiary: #4b5563;
--color-border: #e5e7eb;
--color-border-light: #f3f4f6;
--color-success: #059669;
--color-success-bg: #d1fae5;
--color-error: #dc2626;
--color-error-bg: #fee2e2;
--color-warning: #d97706;
--color-warning-bg: #fef3c7;
--color-info: #2563eb;
--color-info-bg: #dbeafe;
--color-purple-bg: #f3e8ff;
--color-purple-text: #6b21a8;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 5%);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 10%), 0 1px 2px 0 rgb(0 0 0 / 6%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 10%), 0 2px 4px -1px rgb(0 0 0 / 6%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 10%), 0 4px 6px -2px rgb(0 0 0 / 5%);
}
/* Dark Theme Overrides */
[data-theme="dark"] {
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-secondary: #10b981;
--color-secondary-hover: #34d399;
--color-accent: #8b5cf6;
--color-accent-hover: #a78bfa;
--color-background: #111827;
--color-surface: #1f2937;
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-tertiary: #9ca3af;
--color-border: #374151;
--color-border-light: #1f2937;
--color-success: #34d399;
--color-success-bg: #064e3b;
--color-error: #f87171;
--color-error-bg: #7f1d1d;
--color-warning: #fbbf24;
--color-warning-bg: #78350f;
--color-info: #60a5fa;
--color-info-bg: #1e3a5f;
--color-purple-bg: #2e1065;
--color-purple-text: #c4b5fd;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 30%);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 40%), 0 1px 2px 0 rgb(0 0 0 / 30%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 40%), 0 2px 4px -1px rgb(0 0 0 / 30%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 40%), 0 4px 6px -2px rgb(0 0 0 / 30%);
}
/* Dark Mode - Utility Class Overrides */
[data-theme="dark"] .bg-white { background-color: var(--color-surface); }
[data-theme="dark"] .bg-gray-50 { background-color: var(--color-background); }
[data-theme="dark"] .bg-gray-100 { background-color: var(--color-surface); }
[data-theme="dark"] .bg-gray-200 { background-color: var(--color-border); }
[data-theme="dark"] .text-gray-900 { color: var(--color-text-primary); }
[data-theme="dark"] .text-gray-600 { color: var(--color-text-tertiary); }
[data-theme="dark"] .text-gray-500 { color: var(--color-text-tertiary); }
[data-theme="dark"] .text-gray-400 { color: var(--color-text-tertiary); }
[data-theme="dark"] .text-green-600 { color: var(--color-success); }
[data-theme="dark"] .text-red-600 { color: var(--color-error); }
[data-theme="dark"] .border-gray-200 { border-color: var(--color-border); }
[data-theme="dark"] .border-gray-300 { border-color: #4b5563; }
[data-theme="dark"] .hover\:bg-gray-50:hover { background-color: var(--color-border); }
[data-theme="dark"] .hover\:bg-gray-200:hover { background-color: #4b5563; }
[data-theme="dark"] .hover\:text-gray-700:hover { color: #e5e7eb; }
[data-theme="dark"] .hover\:border-gray-300:hover { border-color: #6b7280; }
[data-theme="dark"] .bg-red-100 { background-color: #450a0a; }
[data-theme="dark"] .bg-yellow-100 { background-color: #422006; }
[data-theme="dark"] .bg-green-100 { background-color: #022c22; }
[data-theme="dark"] .text-red-700 { color: #fca5a5; }
[data-theme="dark"] .hover\:bg-red-200:hover { background-color: #7f1d1d; }
/* Base styles */
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: var(--color-text-primary);
background-color: var(--color-background);
}
/* Utility classes */
.bg-gray-50 { background-color: #f9fafb; }
.bg-gray-100 { background-color: #f3f4f6; }
.bg-white { background-color: #ffffff; }
.bg-gray-800 { background-color: #1f2937; }
.bg-gray-900 { background-color: #111827; }
.bg-green-500 { background-color: #10b981; }
.bg-red-500 { background-color: #ef4444; }
.bg-blue-500 { background-color: #3b82f6; }
.bg-yellow-500 { background-color: #f59e0b; }
.bg-green-600 { background-color: #059669; }
.bg-red-600 { background-color: #dc2626; }
.bg-blue-600 { background-color: #2563eb; }
.bg-yellow-600 { background-color: #d97706; }
.bg-gray-200 { background-color: #e5e7eb; }
/* Log level row backgrounds (subtle tints on dark log container) */
.log-level-error { background-color: rgb(127 29 29 / 10%); }
.log-level-warning { background-color: rgb(120 53 15 / 10%); }
.log-level-debug { background-color: rgb(31 41 55 / 30%); }
.text-gray-100 { color: #f3f4f6; }
.text-gray-200 { color: #e5e7eb; }
.text-gray-300 { color: #d1d5db; }
.text-gray-900 { color: #111827; }
.text-gray-600 { color: #374151; }
.text-gray-500 { color: #4b5563; }
.text-gray-400 { color: #6b7280; }
.text-red-300 { color: #fca5a5; }
.text-yellow-300 { color: #fcd34d; }
.text-white { color: #ffffff; }
.text-green-600 { color: #059669; }
.text-red-600 { color: #dc2626; }
/* Status badge chips (e.g. tools.html's dirty/clean and power-status badges) */
.bg-red-100 { background-color: #fee2e2; }
.bg-yellow-100 { background-color: #fef9c3; }
.bg-green-100 { background-color: #dcfce7; }
.text-red-800 { color: #991b1b; }
.text-yellow-800 { color: #854d0e; }
.text-green-800 { color: #166534; }
.border-gray-200 { border-color: #e5e7eb; }
.border-gray-300 { border-color: #d1d5db; }
.border-gray-700 { border-color: #374151; }
.border-transparent { border-color: transparent; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-md { border-radius: 0.375rem; }
.rounded-full { border-radius: 9999px; }
.rounded { border-radius: 0.25rem; }
.shadow { box-shadow: var(--shadow); }
.shadow-sm { box-shadow: var(--shadow-sm); }
.shadow-md { box-shadow: var(--shadow-md); }
.shadow-lg { box-shadow: var(--shadow-lg); }
.p-6 { padding: 1.5rem; }
.p-4 { padding: 1rem; }
.p-2 { padding: 0.5rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.pb-4 { padding-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-8 { margin-bottom: 2rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-4 { margin-top: 1rem; }
.mr-2 { margin-right: 0.5rem; }
.ml-3 { margin-left: 0.75rem; }
.w-full { width: 100%; }
.w-0 { width: 0; }
.w-2 { width: 0.5rem; }
.w-4 { width: 1rem; }
.h-2 { height: 0.5rem; }
.h-4 { height: 1rem; }
.h-10 { height: 2.5rem; }
.h-16 { height: 4rem; }
.h-24 { height: 6rem; }
.h-32 { height: 8rem; }
.h-96 { height: 24rem; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.flex-wrap { flex-wrap: wrap; }
.flex-shrink-0 { flex-shrink: 0; }
.flex-1 { flex: 1; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.space-x-1 > * + * { margin-left: 0.25rem; }
.space-x-2 > * + * { margin-left: 0.5rem; }
.space-x-4 > * + * { margin-left: 1rem; }
.space-y-1 > * + * { margin-top: 0.25rem; }
.space-y-1\.5 > * + * { margin-top: 0.375rem; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.space-y-4 > * + * { margin-top: 1rem; }
.space-y-6 > * + * { margin-top: 1.5rem; }
.space-y-8 > * + * { margin-top: 2rem; }
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.gap-1\.5 { gap: 0.375rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.gap-x-2 { column-gap: 0.5rem; }
.gap-x-3 { column-gap: 0.75rem; }
.gap-x-4 { column-gap: 1rem; }
.gap-x-6 { column-gap: 1.5rem; }
.gap-x-8 { column-gap: 2rem; }
.gap-y-2 { row-gap: 0.5rem; }
/* Enhanced Typography */
.text-xs { font-size: 0.75rem; line-height: 1.4; }
.text-sm { font-size: 0.875rem; line-height: 1.5; }
.text-base { font-size: 1rem; line-height: 1.5; }
.text-lg { font-size: 1.125rem; line-height: 1.75; }
.text-xl { font-size: 1.25rem; line-height: 1.75; }
.text-2xl { font-size: 1.5rem; line-height: 2; }
.text-4xl { font-size: 2.25rem; line-height: 2.5; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
/* Headings with improved hierarchy */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.4;
color: var(--color-text-primary);
/* Improved line-height from 1.3 to 1.4 for better readability */
}
h1 { font-size: 1.875rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }
h4 { font-size: 1.125rem; }
.border-b { border-bottom-width: 1px; }
.border-b-2 { border-bottom-width: 2px; }
.relative { position: relative; }
.fixed { position: fixed; }
.absolute { position: absolute; }
.z-50 { z-index: 50; }
.top-4 { top: 1rem; }
.right-4 { right: 1rem; }
.max-w-7xl { max-width: 56rem; }
.max-w-full { max-width: 100%; }
.mx-auto { margin-left: auto; margin-right: auto; }
.overflow-x-auto { overflow-x: auto; }
.overflow-hidden { overflow: hidden; }
.aspect-video { aspect-ratio: 16 / 9; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.transition { transition-property: transform, opacity, color, border-color; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
/* Optimized: Replaced 'all' with specific properties to avoid animating expensive properties */
/* Removed .duration-300 - not used anywhere */
.hover\:bg-green-700:hover { background-color: #047857; }
.hover\:bg-red-700:hover { background-color: #b91c1c; }
.hover\:bg-gray-50:hover { background-color: #f9fafb; }
.hover\:bg-gray-800:hover { background-color: #1f2937; }
.hover\:bg-yellow-700:hover { background-color: #b45309; }
.hover\:text-gray-700:hover { color: #374151; }
.hover\:border-gray-300:hover { border-color: #d1d5db; }
.focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; }
.focus\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-shadow, 0 0 #0000);
}
/* Optimized: Split complex selector onto multiple lines for readability */
.focus\:ring-blue-500:focus { --tw-ring-color: #3b82f6; }
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
}
/* Smooth transitions for all interactive elements */
/* Optimized: Only transition properties that don't trigger expensive repaints */
a, button, input, select, textarea {
transition: color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
/* Removed background-color transition - can trigger repaints, use opacity or border-color instead */
}
/* Custom scrollbar for webkit browsers */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Logs container specific scrollbar */
#logs-container::-webkit-scrollbar {
width: 10px;
}
#logs-container::-webkit-scrollbar-track {
background: #374151;
}
#logs-container::-webkit-scrollbar-thumb {
background: #6b7280;
border-radius: 5px;
}
#logs-container::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* Smooth scrolling for logs container */
#logs-container {
scroll-behavior: smooth;
position: relative;
overflow-y: auto !important;
}
/* Ensure logs content doesn't cause overflow issues */
.log-entry {
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
box-sizing: border-box;
}
/* Ensure proper containment of logs */
#logs-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
#logs-empty {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
}
#logs-display {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
}
.logs-content {
height: 100%;
overflow-y: auto;
padding: 0;
}
/* Logs container responsive height - simplified for better scrolling */
@media (max-width: 768px) {
#logs-container {
height: 400px !important;
min-height: 300px !important;
}
}
@media (max-width: 640px) {
#logs-container {
height: 350px !important;
min-height: 250px !important;
}
}
/* Responsive breakpoints */
@media (min-width: 640px) {
.sm\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.sm\:block { display: block; }
.sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sm\:text-sm { font-size: 0.875rem; line-height: 1.25rem; }
}
@media (min-width: 768px) {
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.md\:flex { display: flex; }
.md\:hidden { display: none; }
.md\:block { display: block; }
.md\:w-auto { width: auto; }
}
@media (min-width: 1024px) {
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.lg\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.lg\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
.lg\:gap-x-3 { column-gap: 0.75rem; }
.lg\:gap-x-6 { column-gap: 1.5rem; }
.lg\:block { display: block; }
.lg\:flex { display: flex; }
.lg\:w-64 { width: 16rem; }
}
@media (min-width: 1280px) {
.xl\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.xl\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.xl\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.xl\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.xl\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.xl\:grid-cols-7 { grid-template-columns: repeat(7, minmax(0, 1fr)); }
.xl\:grid-cols-8 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
.xl\:px-12 { padding-left: 3rem; padding-right: 3rem; }
.xl\:space-x-6 > * + * { margin-left: 1.5rem; }
.xl\:gap-x-4 { column-gap: 1rem; }
.xl\:gap-x-8 { column-gap: 2rem; }
}
@media (min-width: 1536px) {
.2xl\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.2xl\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.2xl\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.2xl\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.2xl\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.2xl\:grid-cols-7 { grid-template-columns: repeat(7, minmax(0, 1fr)); }
.2xl\:grid-cols-8 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
.2xl\:grid-cols-9 { grid-template-columns: repeat(9, minmax(0, 1fr)); }
.2xl\:grid-cols-10 { grid-template-columns: repeat(10, minmax(0, 1fr)); }
.2xl\:px-16 { padding-left: 4rem; padding-right: 4rem; }
.2xl\:space-x-8 > * + * { margin-left: 2rem; }
}
/* ===== Mobile navigation drawer =====
Below md the #site-nav wrapper becomes an off-canvas drawer; at md and up
none of these rules apply and the nav renders exactly as before. */
@media (max-width: 767.98px) {
.site-nav {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 60;
width: min(85vw, 320px);
background-color: var(--color-surface);
border-right: 1px solid var(--color-border);
transform: translateX(-100%);
transition: transform 0.25s ease;
overflow-y: auto;
padding: 1rem;
-webkit-overflow-scrolling: touch;
}
.site-nav.open {
transform: translateX(0);
box-shadow: 0 0 24px rgba(0, 0, 0, 0.25);
}
/* Tabs become full-width rows with >=44px touch targets */
.site-nav .nav-tab {
display: flex;
width: 100%;
align-items: center;
gap: 0.5rem;
text-align: left;
padding: 0.75rem 1rem;
min-height: 44px;
}
.site-nav nav.-mb-px {
display: block;
}
.nav-backdrop {
position: fixed;
inset: 0;
z-index: 55;
background-color: rgba(0, 0, 0, 0.4);
}
/* Header widgets relocated into the drawer (see placeHeaderWidgets in
app.js). The originals carry `hidden`/breakpoint classes tuned for the
header, so re-enable them explicitly in the drawer context. */
#drawer-widgets #settings-search-wrap {
display: block !important;
margin-bottom: 1rem;
}
#drawer-widgets #settings-search-wrap input {
width: 100%;
}
#drawer-widgets #settings-search-results {
position: static;
width: 100%;
max-height: 50vh;
margin-top: 0.25rem;
}
#drawer-widgets #system-stats {
display: flex !important;
justify-content: space-between;
margin-bottom: 1rem;
}
/* Larger touch targets inside horizontally scrolling tables */
.overflow-x-auto table button {
min-width: 44px;
min-height: 44px;
}
.overflow-x-auto table input:not([type="checkbox"]),
.overflow-x-auto table select {
min-height: 40px;
}
.overflow-x-auto table input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
}
}
@media (min-width: 768px) {
/* Hard guards: even if mobileNavOpen was left true when the viewport
crossed the breakpoint, the drawer/backdrop must render as plain
in-flow nav on desktop. */
.site-nav {
position: static;
transform: none;
width: auto;
padding: 0;
border-right: none;
box-shadow: none;
background-color: transparent;
overflow-y: visible;
}
.nav-backdrop {
display: none !important;
}
#drawer-widgets {
display: none;
}
}
/* Mobile modal sizing: every .modal-content dialog fits the viewport with
internal scrolling instead of overflowing it. */
@media (max-width: 640px) {
.modal-content {
width: 95vw !important;
max-width: 95vw !important;
max-height: 90vh;
overflow-y: auto;
}
}
/* Edge-fade hint that a container scrolls horizontally (pure CSS,
Lea Verou scrolling-shadows technique — backgrounds sit behind content). */
.overflow-x-auto {
background:
linear-gradient(90deg, var(--color-surface) 30%, rgba(255, 255, 255, 0)) left / 24px 100%,
linear-gradient(270deg, var(--color-surface) 30%, rgba(255, 255, 255, 0)) right / 24px 100%,
radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0)) left / 12px 100%,
radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0)) right / 12px 100%;
background-repeat: no-repeat;
background-attachment: local, local, scroll, scroll;
}
/* HTMX loading states */
.htmx-request .loading {
display: inline-block;
}
.htmx-request .btn-text {
opacity: 0.5;
}
/* Enhanced Button styles */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
line-height: 1.5;
text-decoration: none;
transition: transform 0.15s ease, opacity 0.15s ease;
cursor: pointer;
border: none;
/* Removed ::before pseudo-element animation for better performance */
}
.btn:hover {
transform: translateY(-1px);
opacity: 0.9;
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn:disabled:hover {
transform: none;
}
.btn:focus {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 3px rgb(37 99 235 / 30%);
}
/* Global button text contrast fix: Ensure buttons with white backgrounds have dark text */
button.bg-white {
color: #111827 !important; /* text-gray-900 equivalent - ensures good contrast on white background */
}
/* Form styles */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-secondary);
margin-bottom: 0.25rem;
}
.form-control {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background-color: #ffffff;
color: #111827; /* text-gray-900 - ensure dark text on white background */
font-size: 0.875rem;
line-height: 1.25rem;
transition: border-color 0.15s ease-in-out;
/* Removed box-shadow transition - using border-color only for better performance */
}
.form-control:focus {
border-color: var(--color-primary);
/* Using outline instead of box-shadow for focus state (better performance) */
outline: 2px solid rgb(37 99 235 / 20%);
outline-offset: 2px;
}
.form-control:disabled {
background-color: #f9fafb;
opacity: 0.6;
cursor: not-allowed;
}
/* Enhanced Card styles */
.card {
background-color: var(--color-surface);
border-radius: 0.5rem;
box-shadow: var(--shadow);
overflow: hidden;
transition: transform 0.15s ease;
contain: layout style paint;
/* Removed box-shadow transition for better performance - using transform only (GPU accelerated) */
/* Added CSS containment for better performance isolation */
}
.card:hover {
transform: translateY(-2px);
}
/* Plugin Card Styles */
.plugin-card {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 1.25rem;
transition: transform 0.15s ease, border-color 0.15s ease;
cursor: pointer;
position: relative;
contain: layout style paint;
/* Simplified transitions - using only transform and border-color (cheaper than box-shadow) */
/* Added CSS containment for better performance isolation */
}
.plugin-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
opacity: 0;
transition: opacity 0.15s ease;
}
.plugin-card:hover {
transform: translateY(-2px);
border-color: var(--color-primary);
/* Removed box-shadow transition for better performance */
}
.plugin-card:hover::before {
opacity: 1;
}
.plugin-card:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.plugin-card:active {
transform: translateY(0);
}
/* Status indicators */
.status-indicator {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.status-indicator.success {
background-color: #dcfce7;
color: #166534;
}
.status-indicator.error {
background-color: #fef2f2;
color: #991b1b;
}
.status-indicator.warning {
background-color: #fffbeb;
color: #92400e;
}
.status-indicator.info {
background-color: var(--color-info-bg);
color: var(--color-info);
}
/* Badge Styles */
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
line-height: 1.4;
}
.badge-success {
background-color: var(--color-success-bg);
color: var(--color-success);
}
.badge-error {
background-color: var(--color-error-bg);
color: var(--color-error);
}
.badge-warning {
background-color: var(--color-warning-bg);
color: var(--color-warning);
}
.badge-info {
background-color: var(--color-info-bg);
color: var(--color-info);
}
.badge-accent {
background-color: var(--color-purple-bg);
color: var(--color-purple-text);
}
/* Filter Pill Toggle States */
.filter-pill,
.category-filter-pill {
cursor: pointer;
user-select: none;
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}
.filter-pill[data-active="true"],
.category-filter-pill[data-active="true"] {
background-color: var(--color-info-bg);
border-color: var(--color-info);
color: var(--color-info);
font-weight: 600;
}
.filter-pill[data-active="true"]:hover,
.category-filter-pill[data-active="true"]:hover {
opacity: 0.85;
}
.category-filter-pill[data-active="true"] {
box-shadow: 0 0 0 1px var(--color-info);
}
/* Section Headers with Subtle Gradients */
.section-header {
background: linear-gradient(135deg, rgb(255 255 255 / 90%) 0%, rgb(249 250 251 / 90%) 100%);
border-bottom: 1px solid var(--color-border);
padding: 1rem 0;
margin-bottom: 1.5rem;
}
/* Enhanced Empty States */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--color-text-tertiary);
}
.empty-state-icon {
font-size: 3rem;
color: var(--color-text-tertiary);
opacity: 0.5;
margin-bottom: 1rem;
}
/* Enhanced Loading Skeleton */
.skeleton {
background-color: #f0f0f0;
border-radius: 0.375rem;
animation: skeletonPulse 1.5s ease-in-out infinite;
/* Simplified from gradient animation to opacity pulse for better performance */
}
@keyframes skeletonPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Enhanced Modal Styling */
.modal-backdrop {
background-color: rgb(0 0 0 / 50%);
/* Removed backdrop-filter: blur() for better performance on Raspberry Pi */
transition: opacity 0.2s ease;
}
.modal-content {
background: var(--color-surface);
border-radius: 0.75rem;
box-shadow: var(--shadow-lg);
animation: modalSlideIn 0.2s ease;
contain: layout style paint;
/* Added CSS containment for better performance isolation */
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ============================================================================ */
/* Settings tooltips (help_tip macro + tooltips.js) */
/* ============================================================================ */
/* The (i) info trigger placed next to a setting label. */
.help-tip {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
margin-left: 0.375rem;
padding: 0;
border: none;
background: transparent;
color: var(--color-text-tertiary);
font-size: 0.8125rem;
line-height: 1;
cursor: help;
vertical-align: middle;
border-radius: 9999px;
transition: color 0.12s ease;
}
.help-tip:hover,
.help-tip:focus-visible {
color: var(--color-primary);
}
.help-tip:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Singleton tooltip panel appended to <body> by tooltips.js. */
#ledm-tooltip {
position: fixed;
z-index: 1000;
max-width: 20rem;
padding: 0.5rem 0.75rem;
background: var(--color-surface);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
box-shadow: var(--shadow-lg);
font-size: 0.8125rem;
line-height: 1.45;
white-space: pre-line; /* renders authored "\n" line breaks */
pointer-events: none; /* never steals hover/click from the page */
animation: tooltipFade 0.12s ease;
}
#ledm-tooltip[hidden] {
display: none;
}
@keyframes tooltipFade {
from { opacity: 0; }
to { opacity: 1; }
}
/* ============================================================================ */
/* Settings search — global header dropdown + per-tab filter */
/* ============================================================================ */
#settings-search-results {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
box-shadow: var(--shadow-lg);
z-index: 50;
padding: 0.25rem;
max-height: min(60vh, 24rem);
overflow-y: auto;
}
.ssr-group {
padding: 0.375rem 0.625rem 0.25rem;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-tertiary);
position: sticky;
top: 0;
background: var(--color-surface);
}
.ssr-option {
display: flex;
flex-direction: column;
width: 100%;
text-align: left;
padding: 0.4rem 0.625rem;
border: none;
background: transparent;
border-radius: 0.375rem;
cursor: pointer;
color: var(--color-text-primary);
}
.ssr-option:hover,
.ssr-option.active {
background: var(--color-info-bg);
}
.ssr-label {
font-size: 0.875rem;
font-weight: 500;
}
.ssr-help {
font-size: 0.75rem;
color: var(--color-text-tertiary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.ssr-empty {
padding: 0.75rem 0.625rem;
font-size: 0.8125rem;
color: var(--color-text-tertiary);
}
/* Flash highlight applied to a field after search navigation. */
@keyframes settingFlash {
0% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); }
25% { box-shadow: 0 0 0 3px var(--color-primary); }
100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); }
}
.setting-flash {
animation: settingFlash 1.4s ease;
border-radius: 0.5rem;
}
@media (prefers-reduced-motion: reduce) {
#ledm-tooltip { animation: none; }
.setting-flash {
animation: none;
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
}
/* Removed .divider and .divider-light - not used anywhere */
/* Enhanced Spacing Utilities - Only unique classes not in main utility section */
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-6 { margin-top: 1.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.p-3 { padding: 0.75rem; }
.p-5 { padding: 1.25rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-5 { padding-left: 1.25rem; padding-right: 1.25rem; }
.py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
/* Removed duplicates: mt-4, mb-2, mb-4, mb-6, mb-8, p-4, p-6, px-4, py-2, py-3 (already defined above) */
/* Additional Utility Classes */
.min-w-0 { min-width: 0; }
.leading-relaxed { line-height: 1.625; }
/* Enhanced Navigation Tab Styles */
.nav-tab {
position: relative;
display: inline-flex;
align-items: center;
padding: 0.5rem 0.25rem;
border-bottom-width: 2px;
border-bottom-style: solid;
border-bottom-color: transparent;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.5;
color: #374151; /* text-gray-700 for better readability */
white-space: nowrap;
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
cursor: pointer;
background-color: transparent;
border-top: none;
border-left: none;
border-right: none;
}
.nav-tab i {
transition: color 0.15s ease;
margin-right: 0.5rem;
}
/* Inactive state - improved contrast */
.nav-tab:not(.nav-tab-active) {
color: #374151; /* text-gray-700 */
}
.nav-tab:not(.nav-tab-active) i {
color: #374151; /* text-gray-700 */
}
/* Hover state - enhanced visibility */
.nav-tab:not(.nav-tab-active):hover {
color: #111827; /* text-gray-900 */
background-color: #f3f4f6; /* bg-gray-100 */
border-bottom-color: #d1d5db; /* border-gray-300 */
}
.nav-tab:not(.nav-tab-active):hover i {
color: #111827; /* text-gray-900 */
}
/* Active state - prominent with gradient background */
.nav-tab-active {
color: #1d4ed8; /* text-blue-700 */
font-weight: 600;
border-bottom-width: 3px;
border-bottom-color: #2563eb; /* border-blue-600 */
background: linear-gradient(135deg, rgb(37 99 235 / 10%) 0%, rgb(59 130 246 / 5%) 100%);
box-shadow: 0 2px 4px rgb(37 99 235 / 10%);
}
.nav-tab-active i {
color: #1d4ed8; /* text-blue-700 */
}
/* Responsive padding adjustments */
@media (min-width: 1024px) {
.nav-tab {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
}
@media (min-width: 1280px) {
.nav-tab {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}
/* ==========================================================
Dark Mode - Component Overrides
========================================================== */
/* Header */
[data-theme="dark"] header {
background-color: var(--color-surface);
border-color: var(--color-border);
}
/* Form controls */
[data-theme="dark"] .form-control {
background-color: var(--color-border);
color: var(--color-text-primary);
border-color: #4b5563;
}
[data-theme="dark"] .form-control:focus {
border-color: var(--color-primary);
outline-color: rgb(59 130 246 / 30%);
}
[data-theme="dark"] .form-control:disabled {
background-color: var(--color-surface);
}
/* Buttons with bg-white */
[data-theme="dark"] button.bg-white {
background-color: var(--color-border) !important;
color: var(--color-text-primary) !important;
}
/* Nav tabs */
[data-theme="dark"] .nav-tab:not(.nav-tab-active) {
color: var(--color-text-secondary);
}
[data-theme="dark"] .nav-tab:not(.nav-tab-active) i {
color: var(--color-text-secondary);
}
[data-theme="dark"] .nav-tab:not(.nav-tab-active):hover {
color: var(--color-text-primary);
background-color: var(--color-border);
border-bottom-color: #6b7280;
}
[data-theme="dark"] .nav-tab:not(.nav-tab-active):hover i {
color: var(--color-text-primary);
}
[data-theme="dark"] .nav-tab-active {
color: #93c5fd;
border-bottom-color: #3b82f6;
background: linear-gradient(135deg, rgb(59 130 246 / 15%) 0%, rgb(59 130 246 / 5%) 100%);
}
[data-theme="dark"] .nav-tab-active i {
color: #93c5fd;
}
/* Section header */
[data-theme="dark"] .section-header {
background: linear-gradient(135deg, rgb(31 41 55 / 90%) 0%, rgb(17 24 39 / 90%) 100%);
}
/* Status indicators */
[data-theme="dark"] .status-indicator.success {
background-color: #064e3b;
color: #6ee7b7;
}
[data-theme="dark"] .status-indicator.error {
background-color: #7f1d1d;
color: #fca5a5;
}
[data-theme="dark"] .status-indicator.warning {
background-color: #78350f;
color: #fcd34d;
}
/* Skeleton loading */
[data-theme="dark"] .skeleton {
background-color: var(--color-border);
}
/* Scrollbar */
[data-theme="dark"] ::-webkit-scrollbar-track {
background: var(--color-surface);
}
[data-theme="dark"] ::-webkit-scrollbar-thumb {
background: #4b5563;
}
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Log panel - slightly deeper than surface for contrast */
[data-theme="dark"] #logs-container {
background-color: #0f1623;
border-color: var(--color-border);
}
[data-theme="dark"] #logs-display,
[data-theme="dark"] #logs-loading,
[data-theme="dark"] #logs-empty {
background-color: #0f1623;
}
/* Warning/info boxes used in various partials */
[data-theme="dark"] .bg-yellow-50 { background-color: #422006; }
[data-theme="dark"] .border-yellow-200 { border-color: #78350f; }
[data-theme="dark"] .text-yellow-800 { color: #fcd34d; }
[data-theme="dark"] .text-yellow-700 { color: #fbbf24; }
[data-theme="dark"] .text-yellow-600 { color: #fbbf24; }
[data-theme="dark"] .bg-blue-50 { background-color: #1e3a5f; }
[data-theme="dark"] .border-blue-200 { border-color: #1e40af; }
[data-theme="dark"] .text-blue-800 { color: #93c5fd; }
[data-theme="dark"] .text-blue-700 { color: #93c5fd; }
[data-theme="dark"] .bg-red-50 { background-color: #450a0a; }
[data-theme="dark"] .border-red-200 { border-color: #7f1d1d; }
[data-theme="dark"] .text-red-800 { color: #fca5a5; }
[data-theme="dark"] .text-red-700 { color: #fca5a5; }
[data-theme="dark"] .bg-green-50 { background-color: #022c22; }
[data-theme="dark"] .border-green-200 { border-color: #064e3b; }
[data-theme="dark"] .text-green-800 { color: #6ee7b7; }
[data-theme="dark"] .text-green-700 { color: #6ee7b7; }
/* Theme toggle button */
.theme-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
border: none;
background: transparent;
color: #4b5563;
}
.theme-toggle-btn:hover {
opacity: 0.8;
}
[data-theme="dark"] .theme-toggle-btn {
color: #fbbf24;
}
/* Update available banner */
.update-banner {
background-color: #eff6ff;
border-color: #bfdbfe;
color: #1e40af;
}
.update-banner-action {
background-color: #3b82f6;
color: #fff;
}
.update-banner-action:hover {
background-color: #2563eb;
}
.update-banner-dismiss {
color: #1e40af;
opacity: 0.6;
}
.update-banner-dismiss:hover {
opacity: 1;
}
[data-theme="dark"] .update-banner {
background-color: #1e293b;
border-color: #334155;
color: #93c5fd;
}
[data-theme="dark"] .update-banner-action {
background-color: #2563eb;
}
[data-theme="dark"] .update-banner-action:hover {
background-color: #3b82f6;
}
[data-theme="dark"] .update-banner-dismiss {
color: #93c5fd;
}
/* Installed-plugin "Update" button when a newer version is published.
A gentle pulsing ring draws the eye without being noisy. */
.plugin-update-available {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.6);
animation: plugin-update-pulse 2s ease-out infinite;
}
@keyframes plugin-update-pulse {
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.55); }
70% { box-shadow: 0 0 0 6px rgba(59, 130, 246, 0); }
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
}
@media (prefers-reduced-motion: reduce) {
.plugin-update-available { animation: none; }
}
/* Under-voltage / throttling warning banner */
.power-warning-banner {
background-color: #fef2f2;
border-color: #fecaca;
color: #991b1b;
}
.power-warning-banner-dismiss {
color: #991b1b;
opacity: 0.6;
}
.power-warning-banner-dismiss:hover {
opacity: 1;
}
[data-theme="dark"] .power-warning-banner {
background-color: #450a0a;
border-color: #7f1d1d;
color: #fca5a5;
}
[data-theme="dark"] .power-warning-banner-dismiss {
color: #fca5a5;
}
/* ===== Floating live preview (all tabs except Overview) ===== */
.floating-preview {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 70;
background-color: #111827;
border: 1px solid #374151;
border-radius: 0.5rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
overflow: hidden;
/* Desktop: draggable resize handle (bottom-left visually, since the
panel is anchored to the right). Touch devices use the size button. */
resize: both;
min-width: 160px;
max-width: 90vw;
}
.floating-preview img {
background-color: #000;
}
.floating-preview-toggle {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 70;
width: 44px;
height: 44px;
border-radius: 9999px;
background-color: var(--color-primary);
color: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
align-items: center;
justify-content: center;
}
@media (max-width: 640px) {
/* Whatever size is chosen, never wider than the phone viewport */
.floating-preview {
max-width: calc(100vw - 2rem);
}
}
/* ---- Style editor widget -------------------------------------------- */
/* One row per display element: label, font, size, colour, X, Y. The grid
is declared once here so the header and the rows cannot drift apart. */
/* The plugin config panel is narrower than nine columns of controls, and a
clipped table hides the offsets entirely -- scroll the table rather than
crushing the controls or dropping columns. */
.style-editor {
overflow-x: auto;
}
.style-editor-table {
min-width: max-content;
}
/* Scrolled right, the element names would otherwise scroll away and the
row you are editing becomes anonymous. */
.style-editor-label,
.style-editor-head > :first-child {
position: sticky;
left: 0;
z-index: 1;
background-color: #ffffff;
}
.style-editor-head > :first-child {
background-color: #f9fafb;
}
.style-editor-row {
/* Set by the widget: the column count depends on what the plugin
declares, so the track list cannot be fixed here. */
grid-template-columns: var(--style-editor-columns,
minmax(7rem, 1.4fr) minmax(8rem, 2fr) 5rem 4.5rem 5rem 5rem);
}
.style-editor-check {
width: 1rem;
height: 1rem;
}
.style-editor-row .form-control {
padding: 0.2rem 0.4rem;
height: auto;
}
.style-editor-row input[type="number"] {
width: 100%;
}
.style-editor-row input[type="number"]:disabled {
background-color: #f3f4f6;
color: #9ca3af;
cursor: not-allowed;
}
.style-editor-colour {
width: 2rem;
height: 1.6rem;
padding: 0;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
background: none;
cursor: pointer;
}
.style-editor-tab {
border: 1px solid #d1d5db;
background-color: #f9fafb;
color: #374151;
}
.style-editor-tab.is-active {
background-color: #2563eb;
border-color: #2563eb;
color: #ffffff;
}
.style-editor-panel[hidden] {
display: none;
}
@media (max-width: 768px) {
/* Stack rather than squeeze: six columns on a phone is unreadable. */
.style-editor-row {
grid-template-columns: 1fr 1fr;
}
.style-editor-head {
display: none;
}
.style-editor-label {
grid-column: 1 / -1;
font-weight: 600;
}
}