The server now records exactly what was last sent to the device on
every device-facing render (/frame/image, /frame/advance, /frame/back,
and the global hold actions), persisted as Frame.last_displayed_image/
_at and served back via GET /api/frames/{id}/now-displaying. The
header thumbnail is split into that frozen "now displaying" snapshot
and the existing live "up next" re-render, with an arrow between them
-- so editing a layout shows the change immediately on the right while
the left stays exactly what's actually on the panel until the device's
next real wake.
869 lines
26 KiB
CSS
869 lines
26 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; }
|
|
|
|
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; }
|
|
|
|
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; }
|
|
}
|