Files
espresso_frame/server/app/static/theme.css
T
tfaour 83994aab7b
Build and push server image / test (push) Successful in 42s
Build and push server image / build-and-push (push) Successful in 2m35s
Build and push server image / deploy (push) Successful in 51s
Add an admin-only server log viewer to the web UI
The root logger previously had no handler at all, so every module's
logger.info() call (user creation, claims, password resets, ...) was
silently dropped, not just unviewable. Adds a RotatingFileHandler
writing into the existing /data volume so log content also survives
container restarts/redeploys, plus /admin/logs (tail + line-count
picker + full-file download) alongside the existing Users & Frames
admin page.
2026-07-28 03:13:10 +00:00

917 lines
28 KiB
CSS

:root {
--bg: #f5f6f8;
--surface: #ffffff;
--surface-alt: #f0f1f4;
--border: #e3e5e9;
--text: #16181d;
--text-muted: #666d7a;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--focus-ring: rgba(37, 99, 235, 0.35);
--success-bg: #dcfce7;
--success-text: #166534;
--danger-bg: #fee2e2;
--danger-text: #991b1b;
--warn-bg: #fef9c3;
--warn-text: #854d0e;
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
--shadow-hover: 0 8px 20px rgba(16, 24, 40, 0.10);
--overlay: rgba(0, 0, 0, 0.55);
--overlay-hover: rgba(153, 27, 27, 0.85);
--color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0d1016;
--surface: #161a22;
--surface-alt: #1d222b;
--border: #2a2f3a;
--text: #e8eaed;
--text-muted: #9aa2b1;
--accent: #4c8dff;
--accent-hover: #6ea1ff;
--focus-ring: rgba(76, 141, 255, 0.4);
--success-bg: rgba(34, 197, 94, 0.16);
--success-text: #4ade80;
--danger-bg: rgba(239, 68, 68, 0.16);
--danger-text: #f87171;
--warn-bg: rgba(234, 179, 8, 0.16);
--warn-text: #fbbf24;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 2px 10px rgba(0, 0, 0, 0.35);
--shadow-hover: 0 10px 24px rgba(0, 0, 0, 0.45);
--overlay: rgba(0, 0, 0, 0.55);
--overlay-hover: rgba(248, 113, 113, 0.35);
--color-scheme: dark;
}
}
:root[data-theme="dark"] {
--bg: #0d1016;
--surface: #161a22;
--surface-alt: #1d222b;
--border: #2a2f3a;
--text: #e8eaed;
--text-muted: #9aa2b1;
--accent: #4c8dff;
--accent-hover: #6ea1ff;
--focus-ring: rgba(76, 141, 255, 0.4);
--success-bg: rgba(34, 197, 94, 0.16);
--success-text: #4ade80;
--danger-bg: rgba(239, 68, 68, 0.16);
--danger-text: #f87171;
--warn-bg: rgba(234, 179, 8, 0.16);
--warn-text: #fbbf24;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 2px 10px rgba(0, 0, 0, 0.35);
--shadow-hover: 0 10px 24px rgba(0, 0, 0, 0.45);
--overlay: rgba(0, 0, 0, 0.55);
--overlay-hover: rgba(248, 113, 113, 0.35);
--color-scheme: dark;
}
* { box-sizing: border-box; }
html {
color-scheme: var(--color-scheme);
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased;
transition: background-color .15s ease, color .15s ease;
}
.page {
max-width: 1080px;
margin: 0 auto;
padding: 28px 20px 72px;
}
.page.page-narrow {
max-width: 420px;
padding-top: 88px;
}
.topbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
}
.brand { display: flex; align-items: flex-start; gap: 12px; }
.brand-mark {
font-size: 26px;
line-height: 1;
margin-top: 2px;
}
h1 { font-size: 21px; margin: 0; letter-spacing: -0.01em; font-weight: 700; }
p.sub { color: var(--text-muted); font-size: 13.5px; margin: 5px 0 0; line-height: 1.5; }
.icon-btn {
flex: none;
width: 38px;
height: 38px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
box-shadow: var(--shadow);
transition: background-color .15s ease, border-color .15s ease, transform .1s ease;
}
.icon-btn:hover { background: var(--surface-alt); }
.icon-btn:active { transform: scale(0.94); }
.topbar-actions { display: flex; align-items: center; gap: 14px; }
.topnav { display: flex; align-items: center; gap: 14px; font-size: 13.5px; }
.topnav a { color: var(--text-muted); text-decoration: none; }
.topnav a:hover { color: var(--text); }
.inline-form { display: inline; margin: 0; }
button.linklike {
background: none;
border: none;
padding: 0;
margin: 0;
color: var(--text-muted);
font-size: 13.5px;
font-weight: 400;
cursor: pointer;
box-shadow: none;
}
button.linklike:hover { color: var(--text); background: none; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.admin-table th { text-align: left; color: var(--text-muted); font-weight: 600; padding: 6px 8px 6px 0; border-bottom: 1px solid var(--border); }
.admin-table td { padding: 8px 8px 8px 0; border-bottom: 1px solid var(--border); vertical-align: top; }
.admin-actions form { margin: 4px 0 0; }
.admin-actions details summary { cursor: pointer; color: var(--text-muted); font-size: 13px; }
.admin-actions input[type="password"] { margin-top: 6px; }
.admin-frame { border-bottom: 1px solid var(--border); padding: 10px 0; }
.admin-frame:last-child { border-bottom: none; }
.admin-inline-form { display: flex; gap: 8px; align-items: center; margin-top: 6px; }
.admin-inline-form input[type="text"] { margin-top: 0; flex: 1; }
.log-view-controls { display: flex; gap: 10px; align-items: center; margin: 10px 0; font-size: 13px; }
.log-view-controls a:not(.btn-inline) { color: var(--text-muted); }
.log-view-controls a.active { color: var(--accent); font-weight: 600; }
.log-view {
background: var(--surface-alt);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
max-height: 65vh;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
font-family: ui-monospace, "SF Mono", Consolas, monospace;
font-size: 12.5px;
line-height: 1.5;
color: var(--text);
}
h2.card-title, summary.card-title {
font-size: 14.5px;
font-weight: 650;
margin: 0 0 14px;
letter-spacing: 0.01em;
text-transform: uppercase;
color: var(--text-muted);
}
summary.card-title { cursor: pointer; margin-bottom: 0; }
details.card[open] summary.card-title { margin-bottom: 14px; }
details.card .sub { margin-top: 8px; }
.card-title-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
.card-title-row .card-title { margin-bottom: 0; }
.palette-table-wrap { overflow-x: auto; margin-top: 14px; }
.palette-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.palette-table th {
text-align: left;
color: var(--text-muted);
font-weight: 600;
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.02em;
padding: 0 8px 8px 0;
}
.palette-table td { padding: 4px 8px 4px 0; vertical-align: middle; }
.palette-table input { margin-top: 0; }
.palette-swatch-preview {
display: block;
width: 22px;
height: 22px;
border-radius: 6px;
border: 1px solid var(--border);
}
.palette-table input.palette-hex {
width: 92px;
font-family: ui-monospace, "SF Mono", Consolas, monospace;
text-transform: lowercase;
}
.palette-table input.palette-rgb {
width: 58px;
}
.slider-value {
float: right;
font-weight: 400;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
input[type="color"] {
width: 44px;
height: 34px;
padding: 2px;
cursor: pointer;
}
input[type="range"] {
width: 100%;
margin-top: 8px;
accent-color: var(--accent);
padding: 0;
background: none;
}
.preview-compare {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
margin-top: 14px;
}
.preview-img {
width: 100%;
display: block;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--surface-alt);
min-height: 100px;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 20px 22px 22px;
box-shadow: var(--shadow);
}
.card + .card { margin-top: 20px; }
/* Installed as a standalone app, the boxed-card look reads as "still a
website" -- flatten page-level cards into the page background so it
feels native. Cards inside the widget dialog keep their box: they're
grouping subsections of one form, not top-level page furniture. */
@media (display-mode: standalone) {
.card {
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
padding: 20px 0;
}
.card + .card {
margin-top: 4px;
padding-top: 24px;
border-top: 1px solid var(--border);
}
#widget-dialog-body .card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 20px 22px 22px;
}
#widget-dialog-body .card + .card {
margin-top: 20px;
padding-top: 22px;
border-top: none;
}
}
label { display: block; margin-top: 16px; font-size: 13px; font-weight: 600; color: var(--text); }
label:first-child { margin-top: 0; }
input, select {
width: 100%;
padding: 9px 10px;
box-sizing: border-box;
margin-top: 5px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
background: var(--bg);
color: var(--text);
font-family: inherit;
transition: border-color .15s ease, box-shadow .15s ease;
}
input:focus, select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--focus-ring);
}
.saved-layout-add { display: flex; align-items: center; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
.saved-layout-add input { width: auto; flex: 1 1 200px; margin-top: 0; }
.saved-layout-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.saved-layout-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface-alt);
flex-wrap: wrap;
}
.saved-layout-name { display: flex; align-items: center; gap: 8px; min-width: 0; }
.saved-layout-name > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saved-layout-badge {
font-size: 11px;
padding: 2px 7px;
border-radius: 999px;
background: var(--warn-bg);
color: var(--warn-text);
white-space: nowrap;
}
.saved-layout-controls { display: flex; align-items: center; gap: 2px; flex: none; }
.saved-layout-controls .btn-inline { margin: 0; }
.saved-layout-rename-input { width: auto; flex: 1 1 160px; margin-top: 0; }
.checkbox-row { display: flex; align-items: center; gap: 8px; margin-top: 16px; }
.checkbox-row input { width: auto; margin-top: 0; }
.checkbox-row label { margin-top: 0; font-weight: normal; }
.richtext-toolbar { display: flex; align-items: center; gap: 4px; margin-top: 12px; flex-wrap: wrap; }
.richtext-btn {
width: auto;
min-width: 32px;
padding: 5px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface-alt);
color: var(--text);
font-size: 14px;
cursor: pointer;
}
.richtext-btn:hover { background: var(--surface); }
.richtext-toolbar-sep { width: 1px; align-self: stretch; background: var(--border); margin: 0 4px; }
.richtext-color-label {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0;
font-weight: normal;
font-size: 14px;
color: var(--text-muted);
cursor: pointer;
}
.richtext-color-label input[type="color"] {
width: 22px;
height: 22px;
padding: 0;
margin-top: 0;
}
.richtext-editor {
margin-top: 8px;
min-height: 90px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-alt);
color: var(--text);
font-size: 15px;
line-height: 1.5;
}
.richtext-editor:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.calendar-user-list { list-style: none; margin: 12px 0 0; padding: 0; }
.calendar-user-list > li { margin-top: 14px; }
.calendar-user-list > li:first-child { margin-top: 0; }
.calendar-user-name { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
.calendar-row { flex-wrap: wrap; }
.calendar-color-picker { display: inline-flex; align-items: center; gap: 5px; margin-left: 8px; }
.border-color-picker { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
.color-swatch {
width: 20px; height: 20px; padding: 0; margin: 0;
border: 2px solid var(--border); border-radius: 5px;
box-shadow: none; cursor: pointer;
}
.color-swatch.selected { border-color: var(--text); box-shadow: 0 0 0 1.5px var(--text); }
.color-swatch-auto {
width: auto; height: 20px; padding: 0 6px; font-size: 10px; font-weight: 600;
color: var(--text-muted); background: var(--surface-alt);
}
.color-swatch-auto.selected { color: var(--text); }
button {
margin-top: 20px;
padding: 10px 16px;
border: none;
border-radius: 8px;
background: var(--accent);
color: white;
cursor: pointer;
font-size: 14px;
font-weight: 600;
font-family: inherit;
transition: background-color .15s ease, transform .1s ease;
}
button:hover { background: var(--accent-hover); }
button:active { transform: translateY(1px); }
button:disabled { opacity: 0.5; cursor: default; pointer-events: none; }
button.btn-inline {
margin-top: 0;
padding: 3px 10px;
font-size: 12px;
vertical-align: middle;
}
button.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
margin-right: 8px;
}
button.secondary:hover { background: var(--surface-alt); }
.status { margin-top: 16px; padding: 10px 12px; border-radius: 8px; font-size: 14px; }
.status.ok { background: var(--success-bg); color: var(--success-text); }
.status.err { background: var(--danger-bg); color: var(--danger-text); }
.info-box {
margin-bottom: 20px;
padding: 12px 14px;
border-radius: 10px;
font-size: 13px;
background: var(--surface-alt);
color: var(--text-muted);
border: 1px solid var(--border);
}
.info-box.warn { background: var(--warn-bg); color: var(--warn-text); border-color: transparent; }
#widget-canvas-wrap {
/* Height is set in px by frame_layout.js (measured wrap width * rows/cols)
-- not CSS aspect-ratio, which isn't supported on every mobile browser
this app gets viewed from, and percentage heights on the widget boxes
below would silently collapse to 0 against an indeterminate-height
ancestor if it weren't. Box positions/sizes are likewise set in px by
JS, not CSS percentages, for the same cross-browser reason. */
width: 100%;
max-width: 640px;
box-sizing: border-box;
border: 1px solid var(--border);
border-radius: 10px;
background-color: var(--surface-alt);
background-repeat: repeat;
}
#widget-canvas { position: relative; width: 100%; height: 100%; }
.widget-box {
position: absolute;
box-sizing: border-box;
border: 2px solid var(--accent);
background: var(--surface-alt); /* fallback for browsers without color-mix() support */
background: color-mix(in srgb, var(--accent) 14%, var(--surface));
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
touch-action: none;
cursor: grab;
user-select: none;
overflow: hidden;
}
.widget-box.dragging { cursor: grabbing; box-shadow: var(--shadow-hover); z-index: 2; }
.widget-box-label {
font-size: 13px;
font-weight: 600;
color: var(--text);
pointer-events: none;
}
.widget-box-remove, .widget-box-settings {
position: absolute;
top: 4px;
width: 20px;
height: 20px;
padding: 0;
margin: 0;
line-height: 1;
border-radius: 50%;
border: none;
background: var(--overlay);
color: #fff;
font-size: 12px;
cursor: pointer;
}
.widget-box-remove { right: 4px; }
.widget-box-settings { right: 28px; }
.widget-box-remove:hover, .widget-box-settings:hover { background: var(--overlay-hover); }
.widget-box-lock-badge {
position: absolute;
bottom: 4px;
left: 4px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--overlay);
color: #fff;
font-size: 11px;
line-height: 20px;
text-align: center;
pointer-events: none; /* passive indicator, not a control -- toggled from the widget's own dialog */
}
.widget-box-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
touch-action: none;
border-right: 3px solid var(--accent);
border-bottom: 3px solid var(--accent);
border-bottom-right-radius: 4px;
}
#widget-dialog {
position: fixed;
margin: auto;
width: min(680px, calc(100vw - 32px));
max-height: min(720px, calc(100vh - 64px));
padding: 24px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
color: var(--text);
box-shadow: var(--shadow-hover);
/* #widget-dialog-body scrolls on its own (min-height: 0 is what lets a
flex child actually shrink/scroll instead of forcing the dialog
past max-height) so .dialog-result stays pinned as a visible footer
regardless of scroll position -- otherwise a save message can land
off-screen below a long form with no visible feedback at all. */
display: flex;
flex-direction: column;
}
#widget-dialog::backdrop { background: var(--overlay); }
#widget-dialog-close {
position: absolute;
top: 14px;
right: 14px;
width: 30px;
height: 30px;
font-size: 18px;
z-index: 1;
}
.dialog-title { margin: 0 40px 16px 0; font-size: 18px; }
#widget-dialog-body { overflow-y: auto; min-height: 0; }
#widget-dialog-body .card { box-shadow: none; }
#widget-dialog-body .card:first-child { margin-top: 0; }
.dialog-result { flex: none; }
.dialog-result:not(:empty) { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }
code {
background: var(--surface-alt);
color: var(--text);
padding: 2px 5px;
border-radius: 4px;
font-size: 0.92em;
}
.info-box code, .info-box.warn code { background: rgba(127, 127, 127, 0.18); color: inherit; }
.thumb { width: 160px; max-width: 100%; border-radius: 8px; display: block; }
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; margin-top: 8px; }
.photo-card {
position: relative; cursor: grab; border-radius: 8px; overflow: hidden; aspect-ratio: 1;
background: var(--surface-alt); border: 1px solid var(--border);
box-shadow: var(--shadow);
transition: box-shadow .15s ease, transform .15s ease;
/* pan-y (not none): lets a normal touch-scroll of the page work
when you touch a card without meaning to drag it. Dragging on
touch instead requires a brief hold first (see the JS below),
which switches this to "none" for the rest of that touch --
only once we're sure it's a deliberate drag, not a scroll. */
touch-action: pan-y;
/* Without this, a press-and-drag gesture also triggers the
browser's native text/content selection (the blue highlight) --
distracting, and on some browsers it fights the pointer-based
drag tracking below closely enough to break it outright. */
user-select: none;
-webkit-user-select: none;
}
.photo-card:hover { box-shadow: var(--shadow-hover); transform: translateY(-1px); }
.photo-card:active { cursor: grabbing; }
.photo-card.drag-armed {
box-shadow: 0 0 0 3px var(--focus-ring) inset;
transform: scale(0.97);
}
/* No transform transition here -- the JS drives transform on every
pointermove to track the finger 1:1, and the .15s base transition
would otherwise make it visibly lag behind a fast swipe. */
.photo-card.dragging {
z-index: 20;
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.35);
transition: box-shadow .15s ease;
pointer-events: none; /* so elementFromPoint hits the card underneath, not this one */
cursor: grabbing;
}
.photo-card.drag-over { outline: 3px solid var(--accent); outline-offset: -3px; }
.photo-card img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.photo-card .badge { position: absolute; top: 6px; left: 6px; background: var(--overlay); color: white; font-size: 10px; padding: 2px 6px; border-radius: 4px; }
.photo-card .remove-btn, .thumb-wrap .remove-btn {
position: absolute; top: 6px; right: 6px; width: 22px; height: 22px; margin: 0; padding: 0;
line-height: 20px; text-align: center; font-size: 13px; border-radius: 50%;
background: var(--overlay); color: white;
}
.photo-card .remove-btn:hover, .thumb-wrap .remove-btn:hover { background: var(--overlay-hover); }
.photo-card .show-next {
position: absolute; bottom: 6px; left: 6px; right: 6px; margin: 0; padding: 5px 0;
font-size: 11px; text-align: center; background: rgba(37, 99, 235, 0.85); border-radius: 4px;
}
.thumb-wrap { position: relative; display: inline-block; }
.layout {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
gap: 20px;
align-items: start;
}
.main-col, .side-col { display: flex; flex-direction: column; }
@media (max-width: 860px) {
/* minmax(0, 1fr), not bare 1fr -- bare 1fr is minmax(auto, 1fr), whose
"auto" minimum lets the track (and everything in it) grow to fit its
widest descendant's min-content size instead of actually shrinking
to the viewport, causing page-wide horizontal overflow/scroll on
narrow screens. Same fix the desktop rule above already applies. */
.layout { grid-template-columns: minmax(0, 1fr); }
}
/* ------------------------------------------------------------------ */
/* App shell: left sidebar (frame list + account nav) + main content. */
/* Used by app_base.html for all logged-in pages; the narrow auth/ */
/* manage pages keep the simple centered .page layout above. */
/* ------------------------------------------------------------------ */
.shell { display: flex; min-height: 100vh; }
.sidebar {
width: 248px;
flex: none;
display: flex;
flex-direction: column;
background: var(--surface);
border-right: 1px solid var(--border);
padding: 20px 14px 16px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.sidebar .brand { display: flex; align-items: center; gap: 10px; padding: 0 8px 18px; }
.sidebar .brand h1 { font-size: 17px; }
.sidebar-section {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
padding: 14px 8px 6px;
}
.sidebar a.nav-item, .sidebar .nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border-radius: 8px;
color: var(--text);
text-decoration: none;
font-size: 14px;
transition: background-color .12s ease;
}
.sidebar a.nav-item:hover { background: var(--surface-alt); }
.sidebar a.nav-item.active { background: var(--surface-alt); font-weight: 600; }
.nav-item .frame-dot {
width: 8px; height: 8px; border-radius: 50%; flex: none;
background: var(--text-muted); opacity: 0.5;
}
.nav-item.online .frame-dot { background: #22c55e; opacity: 1; }
.nav-item .nav-sub { margin-left: auto; font-size: 11.5px; color: var(--text-muted); }
.sidebar-footer { margin-top: auto; padding-top: 14px; border-top: 1px solid var(--border); }
.sidebar-footer .nav-item { color: var(--text-muted); font-size: 13.5px; }
.sidebar-footer form { margin: 0; }
.sidebar-footer button.linklike {
display: block; width: 100%; text-align: left; padding: 9px 10px; border-radius: 8px;
}
.sidebar-footer button.linklike:hover { background: var(--surface-alt); }
.main {
flex: 1;
min-width: 0;
padding: 24px 28px 72px;
max-width: 1160px;
}
.page-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
flex-wrap: wrap;
}
.page-head h1 { font-size: 21px; margin: 0; letter-spacing: -0.01em; font-weight: 700; }
.page-head .head-actions { display: flex; align-items: center; gap: 10px; }
.tabs {
display: flex;
gap: 4px;
margin-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.tabs a {
padding: 9px 14px;
font-size: 14px;
color: var(--text-muted);
text-decoration: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color .12s ease;
}
.tabs a:hover { color: var(--text); }
.tabs a.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tabs a.tab-disabled { opacity: 0.45; }
.tabs a.tab-disabled:hover { opacity: 0.7; }
.frame-name-view { display: inline-flex; align-items: center; gap: 6px; }
.frame-name-pencil {
background: none;
border: none;
box-shadow: none;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 4px;
margin: 0;
opacity: 0.55;
color: var(--text);
transition: opacity .12s ease, background-color .12s ease;
}
.frame-name-pencil:hover { opacity: 1; background: var(--surface-alt); border-radius: 6px; }
.frame-name-edit { display: inline-flex; align-items: center; gap: 6px; }
.frame-name-edit input {
width: auto;
margin-top: 0;
padding: 5px 8px;
font-size: 15px;
font-weight: 700;
}
.frame-name-edit button { margin-top: 0; }
.frame-preview-pair {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 12px;
vertical-align: middle;
}
.frame-preview-arrow {
color: var(--text-muted);
font-size: 16px;
line-height: 1;
}
.frame-preview-thumb {
height: 44px;
width: auto;
max-width: 130px;
object-fit: contain;
vertical-align: middle;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface-alt);
cursor: pointer;
transition: opacity .12s ease;
}
.frame-preview-thumb:hover { opacity: 0.8; }
.frame-preview-thumb-empty {
opacity: 0.3;
cursor: default;
width: 60px;
font-size: 0; /* no src yet -- suppresses the browser's fallback alt-text render */
}
.frame-preview-thumb-empty:hover { opacity: 0.3; }
.frame-preview-dialog {
position: fixed;
margin: auto;
padding: 0;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
box-shadow: var(--shadow-hover);
line-height: 0; /* avoid a baseline gap under the image */
}
.frame-preview-dialog::backdrop { background: var(--overlay); }
.frame-preview-dialog img {
display: block;
max-width: min(90vw, 900px);
max-height: min(85vh, 900px);
width: auto;
height: auto;
border-radius: 14px;
cursor: pointer;
}
#frame-preview-dialog-close {
position: absolute;
top: 14px;
right: 14px;
width: 30px;
height: 30px;
font-size: 18px;
z-index: 1;
}
.control-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 18px;
padding: 10px 14px;
border-radius: 10px;
font-size: 13.5px;
background: var(--warn-bg);
color: var(--warn-text);
}
.control-banner button { margin: 0; }
/* Always-visible device summary, sitting between the page title and the
tabs (see _device_status_bar.html) -- a compact horizontal row rather
than a full .card, since it has to fit above the tabs on every frame
page without pushing content down. */
.device-status-bar {
padding: 10px 16px;
margin-bottom: 18px;
}
.device-status-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
column-gap: 26px;
row-gap: 6px;
}
.device-status-row .sub { margin: 0; }
.device-stat {
font-size: 13px;
color: var(--text-muted);
white-space: nowrap;
}
.device-stat strong { color: var(--text); font-weight: 600; }
.device-stat.alert, .device-stat.alert strong { color: var(--danger-text); }
@media (max-width: 860px) {
.device-status-row { column-gap: 16px; }
}
/* Mobile: sidebar off-canvas, hamburger in a slim top bar. */
.mobile-bar { display: none; }
.sidebar-backdrop { display: none; }
@media (max-width: 860px) {
.mobile-bar {
display: flex;
align-items: center;
gap: 12px;
position: sticky;
top: 0;
z-index: 30;
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 10px 14px;
}
.mobile-bar .brand { display: flex; align-items: center; gap: 8px; }
.mobile-bar h1 { font-size: 16px; margin: 0; }
.mobile-bar .icon-btn { width: 34px; height: 34px; box-shadow: none; }
.mobile-bar .spacer { flex: 1; }
.shell { display: block; }
.sidebar {
position: fixed;
left: 0; top: 0; bottom: 0;
z-index: 50;
height: 100vh;
transform: translateX(-105%);
transition: transform .2s ease;
box-shadow: var(--shadow-hover);
}
.shell.sidebar-open .sidebar { transform: translateX(0); }
.sidebar-backdrop {
position: fixed; inset: 0; z-index: 40;
background: var(--overlay);
opacity: 0; pointer-events: none;
transition: opacity .2s ease;
}
.shell.sidebar-open .sidebar-backdrop { display: block; opacity: 1; pointer-events: auto; }
.sidebar-backdrop { display: block; }
.main { padding: 18px 14px 64px; }
}