A module that sees something a tech must act on now (a timer overrun, a meter past its limit, timecode gone) raises an alert on a channel, 1–6, and every dashboard listening to that channel flashes: the menu bar — brought out for the duration if it was hidden — and a frame round the screen alternate between the level's two colours, with the alert's text bold across the bar. Low fades slowly, medium faster, critical snaps with no fade and floods the board behind the tiles too, in a muted cut that shows faintly through the tile panels without costing their content any contrast. Server: the hub. One alert per channel (a new one replaces the last), timed out server-side (1–600 s, 15 by default, 0 = held until cleared), broadcast over the shell stream as `alert` / `alert-clear`; GET /api/alerts for a dashboard that opens or reconnects mid-alert; POST /api/alerts and DELETE /api/alerts/<n>, open like a layout save. Modules raise from their server entry with shell.alert() / shell.clearAlert() or from a tile with moduleApi.alert() / moduleApi.clearAlert(); a module's alerts end when it stops. Colours live as alerts.colors in the data directory's proddash.json. Dashboard: the Layouts menu gains a folded Alerts row with six numbered toggles; the channel list saves with the layout, local and named (the server keeps it in a saved layout). Admin: a folded Alerts section with two colour pickers per level, a swatch running each level's cadence, and a Test row. Docs: docs/ALERTS.md (the feature), a "Raising alerts" section in the module guide, README. Tools: tools/alert.js raises/clears/lists from the command line; tools/alerts.test.js drives it all end to end. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
651 lines
19 KiB
CSS
651 lines
19 KiB
CSS
/* ProdDash admin page — on top of the shell's style.css */
|
|
|
|
body { overflow: auto; }
|
|
|
|
.admin-tag {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
padding: 2px 6px;
|
|
margin-left: 6px;
|
|
vertical-align: 2px;
|
|
}
|
|
|
|
#admin-main {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
width: min(720px, 100%);
|
|
margin: 0 auto;
|
|
padding: 18px 16px 60px;
|
|
}
|
|
|
|
.admin-h1 {
|
|
font-size: 16px;
|
|
margin: 22px 0 6px;
|
|
}
|
|
|
|
.admin-copy {
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.admin-storage { margin-top: -6px; font-size: 12px; }
|
|
.admin-storage code {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
font-size: 11.5px;
|
|
color: var(--text);
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
padding: 1px 5px;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.admin-card {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 14px 16px;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.admin-card h2 { font-size: 14px; margin-bottom: 8px; }
|
|
|
|
.login-row { display: flex; gap: 8px; }
|
|
.login-row input {
|
|
flex: 1;
|
|
padding: 8px 10px;
|
|
font-size: 14px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 7px;
|
|
}
|
|
.login-row input:focus { outline: none; border-color: var(--accent); }
|
|
|
|
.module-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.module-head .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }
|
|
.module-head .dot.ok { background: var(--accent); box-shadow: 0 0 5px var(--accent); }
|
|
.module-head .dot.connecting { background: var(--warn); }
|
|
.module-head .dot.error { background: var(--danger); }
|
|
|
|
.module-name { font-size: 14px; font-weight: 700; }
|
|
.module-version { color: var(--muted); font-size: 11px; }
|
|
.module-head .spacer { flex: 1; }
|
|
|
|
.module-desc { color: var(--muted); font-size: 12px; margin: 6px 0 2px; }
|
|
|
|
.module-health {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
margin-top: 4px;
|
|
min-height: 15px;
|
|
}
|
|
.module-health.error { color: var(--danger); }
|
|
|
|
/* a module card's own status line ("polling timers on 10.3.11.20:1025",
|
|
"no ProPresenter host configured"): a readout, so it sits in a bordered
|
|
monospace box a shade lighter than the card; ok / connecting / error
|
|
show as the text (and border) colour, the box itself never changes */
|
|
.module-status {
|
|
display: inline-block;
|
|
max-width: 100%;
|
|
margin-top: 8px;
|
|
padding: 4px 8px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
background: color-mix(in srgb, var(--text) 5%, var(--panel));
|
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
font-size: 11.5px;
|
|
line-height: 1.45;
|
|
color: var(--muted);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.module-status:empty { display: none; }
|
|
.module-status.ok { color: var(--text); }
|
|
.module-status.connecting {
|
|
color: var(--warn);
|
|
border-color: color-mix(in srgb, var(--warn) 40%, var(--border));
|
|
}
|
|
.module-status.error {
|
|
color: var(--danger);
|
|
border-color: color-mix(in srgb, var(--danger) 40%, var(--border));
|
|
}
|
|
|
|
.module-mount-error {
|
|
margin-top: 8px;
|
|
padding: 8px 10px;
|
|
border: 1px solid var(--danger);
|
|
border-radius: 8px;
|
|
color: var(--danger);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.module-form { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; }
|
|
.module-form .field { max-width: 420px; }
|
|
|
|
/* a multi-line string (manifest type `text`): the same box as the other
|
|
inputs (those rules live in the shell's style.css), a few lines tall */
|
|
.field textarea {
|
|
padding: 7px 9px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
line-height: 1.4;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 7px;
|
|
resize: vertical;
|
|
min-height: 72px;
|
|
}
|
|
.field textarea:focus { outline: none; border-color: var(--accent); }
|
|
|
|
/* endpoint fields: every module's upstream is entered the same way */
|
|
.field .endpoint-row { display: flex; align-items: center; gap: 7px; }
|
|
.endpoint-row .ep-host { flex: 1; min-width: 0; }
|
|
.endpoint-row .ep-port { width: 92px; flex: none; }
|
|
.endpoint-row .ep-sep { color: var(--muted); font-weight: 600; }
|
|
/* several picks from a list (manifest type `multiselect`) */
|
|
.field.multiselect-field { cursor: default; }
|
|
.field.multiselect-field .multiselect { display: flex; flex-direction: column; gap: 6px; }
|
|
.multiselect-row { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
|
.multiselect-empty { color: var(--muted); font-size: 12px; }
|
|
|
|
.module-form-empty { color: var(--muted); font-size: 12px; margin-top: 10px; }
|
|
|
|
/* labeled config subsections (manifest field `group`) */
|
|
.field-group {
|
|
margin: 14px 0 4px;
|
|
padding: 12px 14px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 9px;
|
|
}
|
|
.field-group-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
position: relative; /* the ⓘ popover hangs off this row */
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--muted);
|
|
margin-bottom: 10px;
|
|
}
|
|
.field-group-name { min-width: 0; }
|
|
.field-group .field:last-child { margin-bottom: 0; }
|
|
|
|
/* group help (configGroups[name].help): an ⓘ beside the title. Hover or
|
|
focus peeks at the text; a click pins it open until the next click. */
|
|
.group-info { display: inline-flex; align-items: center; }
|
|
.group-info-btn {
|
|
width: 16px;
|
|
height: 16px;
|
|
padding: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px solid var(--muted);
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
color: var(--muted);
|
|
font: italic 700 10px/1 ui-serif, Georgia, "Times New Roman", serif;
|
|
text-transform: none;
|
|
letter-spacing: 0;
|
|
cursor: help;
|
|
}
|
|
.group-info-btn:hover,
|
|
.group-info.is-open .group-info-btn { color: var(--text); border-color: var(--text); }
|
|
.group-info-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
.group-info-pop {
|
|
display: none;
|
|
position: absolute;
|
|
left: 0;
|
|
top: calc(100% + 6px);
|
|
z-index: 5;
|
|
max-width: min(440px, 100%);
|
|
padding: 8px 10px;
|
|
background: color-mix(in srgb, var(--text) 6%, var(--panel));
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
color: var(--text);
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
line-height: 1.5;
|
|
letter-spacing: normal;
|
|
text-transform: none;
|
|
white-space: normal;
|
|
cursor: auto;
|
|
}
|
|
/* keeps the popover open while the pointer crosses the gap down to it */
|
|
.group-info-pop::before { content: ""; position: absolute; left: 0; right: 0; top: -8px; height: 8px; }
|
|
.group-info:hover .group-info-pop,
|
|
.group-info-btn:focus-visible + .group-info-pop,
|
|
.group-info.is-open .group-info-pop { display: block; }
|
|
|
|
/* group toggle (configGroups[name].toggle): the named switch sits at the
|
|
right of the title row as the group's on/off; the body dims while it is
|
|
off but stays editable */
|
|
.field-group-toggle {
|
|
margin-left: auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex: none;
|
|
}
|
|
.field-group-toggle .field {
|
|
margin: 0;
|
|
max-width: none;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
letter-spacing: normal;
|
|
text-transform: none;
|
|
color: var(--text);
|
|
}
|
|
.field-group-toggle .field.switch-field { flex-direction: row-reverse; gap: 8px; }
|
|
.field-group-toggle .field.switch-field .switch-label { font-size: 12px; }
|
|
.field-group-toggle .field.check { flex-direction: row-reverse; gap: 8px; }
|
|
.field-group.is-off > .field-group-body { opacity: 0.45; transition: opacity 0.15s; }
|
|
.field-group.has-toggle > .field-group-body { transition: opacity 0.15s; }
|
|
|
|
/* collapsible groups (manifest configGroups[name].collapsed) */
|
|
details.field-group > summary.field-group-title {
|
|
cursor: pointer;
|
|
list-style: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
margin-bottom: 0;
|
|
user-select: none;
|
|
}
|
|
details.field-group > summary.field-group-title::-webkit-details-marker { display: none; }
|
|
details.field-group > summary.field-group-title::before {
|
|
content: "";
|
|
width: 0; height: 0;
|
|
border-left: 5px solid var(--muted);
|
|
border-top: 4px solid transparent;
|
|
border-bottom: 4px solid transparent;
|
|
transition: transform 0.15s;
|
|
}
|
|
details.field-group[open] > summary.field-group-title::before { transform: rotate(90deg); }
|
|
details.field-group[open] > summary.field-group-title { margin-bottom: 10px; }
|
|
details.field-group > summary.field-group-title:hover { color: var(--text); }
|
|
|
|
/* multi-column groups (configGroups[name].columns): a neat checklist grid */
|
|
.field-group-body.is-columns {
|
|
display: grid;
|
|
grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
|
|
gap: 4px 16px;
|
|
}
|
|
.field-group-body.is-columns .field { margin-bottom: 4px; max-width: none; }
|
|
@media (max-width: 560px) { .field-group-body.is-columns { grid-template-columns: 1fr 1fr; } }
|
|
|
|
/* one line of guidance under a control (field `help`) */
|
|
.field-help {
|
|
display: block;
|
|
font-size: 11.5px;
|
|
line-height: 1.45;
|
|
color: var(--muted);
|
|
margin-top: 2px;
|
|
}
|
|
.field-help code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
|
|
/* showWhen: a hidden dependent field must actually collapse — `.field`'s
|
|
own `display:flex` (an author rule) otherwise outranks the [hidden] UA
|
|
default and the field stays visible. */
|
|
.module-form .field[hidden] { display: none; }
|
|
|
|
/* a boolean rendered as a toggle (manifest type `switch`) */
|
|
.field.switch-field {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
max-width: none;
|
|
}
|
|
.field.switch-field .switch-label { font-size: 13px; }
|
|
|
|
/* the card's one action row: status on the left, Configure / Apply on the right */
|
|
.module-actions {
|
|
display: flex;
|
|
align-items: flex-end; /* the button stays bottom-right when a readout wraps */
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
}
|
|
.module-name.is-toggle { cursor: pointer; }
|
|
.module-name.is-toggle:hover,
|
|
.module-name.is-toggle:focus-visible { color: var(--accent); outline: none; }
|
|
.module-actions .admin-status { flex: 1; margin-top: 0; }
|
|
.module-actions .module-status { flex: 0 1 auto; min-width: 0; margin-top: 0; }
|
|
.module-actions .btn { flex: none; margin-left: auto; }
|
|
|
|
.admin-status { min-height: 16px; font-size: 12px; color: var(--muted); margin-top: 8px; }
|
|
.admin-status.error { color: var(--danger); }
|
|
.admin-status.ok { color: var(--accent); }
|
|
|
|
/* enable/disable switch */
|
|
.switch {
|
|
position: relative;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
user-select: none;
|
|
}
|
|
.switch input { position: absolute; opacity: 0; pointer-events: none; }
|
|
.switch .track {
|
|
width: 34px;
|
|
height: 19px;
|
|
border-radius: 999px;
|
|
background: var(--border);
|
|
position: relative;
|
|
transition: background 0.2s;
|
|
flex: none;
|
|
}
|
|
.switch .track::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 2px; left: 2px;
|
|
width: 15px; height: 15px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
transition: transform 0.2s, background 0.2s;
|
|
}
|
|
.switch input:checked + .track { background: var(--accent); }
|
|
.switch input:checked + .track::after { transform: translateX(15px); background: var(--bg); }
|
|
|
|
.layout-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 9px 12px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.layout-row .name { flex: 1; font-size: 13px; font-weight: 600; }
|
|
.layout-row .meta { color: var(--muted); font-size: 11px; }
|
|
.layouts-empty { color: var(--muted); font-size: 13px; }
|
|
|
|
/* ── version, update banner, catalog ───────────────────────────────── */
|
|
|
|
.admin-version {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
margin-left: 4px;
|
|
vertical-align: 2px;
|
|
}
|
|
|
|
.update-card {
|
|
border-color: color-mix(in srgb, var(--warn) 55%, var(--border));
|
|
background: color-mix(in srgb, var(--warn) 7%, var(--panel));
|
|
margin-top: 18px;
|
|
}
|
|
.update-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.update-head strong { font-size: 14px; }
|
|
.update-head .spacer { flex: 1; }
|
|
.update-card .admin-copy { margin: 8px 0 0; }
|
|
|
|
.btn.small { padding: 4px 9px; font-size: 12px; }
|
|
.btn.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); }
|
|
.btn.danger:hover { background: color-mix(in srgb, var(--danger) 14%, var(--bg)); }
|
|
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
a.btn { text-decoration: none; }
|
|
|
|
.module-source {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 1px 5px;
|
|
}
|
|
.module-update-slot:empty { display: none; }
|
|
|
|
.catalog-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.catalog-toolbar .admin-status { margin-top: 0; }
|
|
|
|
.catalog-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 12px 14px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.catalog-main { flex: 1; min-width: 0; }
|
|
.catalog-pick { margin: 5px 10px 0 0; flex: none; }
|
|
.catalog-title { display: flex; align-items: center; gap: 8px; }
|
|
.catalog-row .module-desc { margin: 4px 0 2px; }
|
|
.catalog-row .module-health { margin-top: 2px; }
|
|
|
|
/* ── theme ──────────────────────────────────────────────────────────── */
|
|
|
|
/* one swatch per preset: a miniature dashboard (page, a tile on it, the
|
|
accent as a status dot) and the name; the current one wears the accent */
|
|
/* ── a collapsible admin section ─────────────────────────────────────
|
|
The pattern for any section of this page that folds. The markup is a
|
|
<details class="admin-fold"> holding the section's contents, headed by a
|
|
<summary class="admin-h1 admin-fold-title">: the title keeps the same
|
|
type as every other section heading — a <summary> would otherwise come
|
|
out at the normal weight — and the caret follows the words, so a folded
|
|
section still reads as a heading first. The whole row is the hit target. */
|
|
details.admin-fold > summary.admin-fold-title {
|
|
cursor: pointer;
|
|
list-style: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 700; /* matches h1.admin-h1; summary defaults to normal */
|
|
user-select: none;
|
|
}
|
|
details.admin-fold > summary.admin-fold-title::-webkit-details-marker { display: none; }
|
|
/* after the title, not before it — the heading reads first, the caret says
|
|
there is more behind it */
|
|
details.admin-fold > summary.admin-fold-title::after {
|
|
content: "";
|
|
width: 0; height: 0;
|
|
border-left: 6px solid var(--muted);
|
|
border-top: 5px solid transparent;
|
|
border-bottom: 5px solid transparent;
|
|
transition: transform 0.15s;
|
|
}
|
|
details.admin-fold[open] > summary.admin-fold-title::after { transform: rotate(90deg); }
|
|
details.admin-fold > summary.admin-fold-title:hover { color: var(--accent); }
|
|
|
|
/* Alerts: a row per level — a swatch running that level's cadence between
|
|
the two chosen colours (the keyframes are the shell's, in style.css), the
|
|
level's name, and its two pickers — then the test row. */
|
|
.alert-levels {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-bottom: 14px;
|
|
}
|
|
.alert-level {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.alert-preview {
|
|
flex: none;
|
|
width: 56px;
|
|
height: 28px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
background: var(--alert-a, var(--border));
|
|
animation: alert-flash var(--alert-period, 3s) var(--alert-ease, ease-in-out) infinite;
|
|
}
|
|
.alert-preview.low { --alert-period: 3.2s; --alert-ease: ease-in-out; }
|
|
.alert-preview.medium { --alert-period: 1.1s; --alert-ease: ease-in-out; }
|
|
.alert-preview.critical { --alert-period: 0.6s; --alert-ease: steps(1, end); }
|
|
.alert-level-name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
}
|
|
.alert-level-name strong { font-size: 13px; }
|
|
.alert-level-name span { font-size: 12px; color: var(--muted); }
|
|
.alert-level input[type="color"] {
|
|
flex: none;
|
|
width: 44px;
|
|
height: 26px;
|
|
padding: 2px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
background: var(--bg);
|
|
cursor: pointer;
|
|
}
|
|
.alert-level input[type="color"]:focus { outline: none; border-color: var(--accent); }
|
|
.alert-test {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
padding-top: 12px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.alert-test-label { font-size: 13px; color: var(--muted); }
|
|
.alert-test-select {
|
|
height: 26px;
|
|
padding: 0 6px;
|
|
font: inherit;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
}
|
|
.alert-test-select:focus { outline: none; border-color: var(--accent); }
|
|
|
|
.theme-swatches {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
|
|
gap: 10px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.theme-swatch {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
padding: 8px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
color: var(--text);
|
|
font: inherit;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.theme-swatch:hover { border-color: var(--muted); }
|
|
.theme-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
.theme-swatch.is-current { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
.theme-preview {
|
|
position: relative;
|
|
display: block;
|
|
height: 46px;
|
|
border-radius: 6px;
|
|
background: var(--sw-bg);
|
|
border: 1px solid color-mix(in srgb, var(--sw-panel) 60%, var(--sw-accent));
|
|
overflow: hidden;
|
|
}
|
|
.theme-preview::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 8px; right: 8px; top: 10px; bottom: 8px;
|
|
border-radius: 4px;
|
|
background: var(--sw-panel);
|
|
border: 1px solid color-mix(in srgb, var(--sw-panel) 75%, var(--sw-accent));
|
|
}
|
|
.theme-preview::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 14px; top: 16px;
|
|
width: 8px; height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--sw-accent);
|
|
box-shadow: 0 0 6px var(--sw-accent);
|
|
}
|
|
.theme-name { display: flex; align-items: center; gap: 6px; }
|
|
.theme-swatch.is-current .theme-name::after {
|
|
content: "current";
|
|
margin-left: auto;
|
|
font-size: 9.5px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
color: var(--accent);
|
|
}
|
|
#theme-status { margin-top: 4px; }
|
|
|
|
/* ── the admin PIN ── */
|
|
|
|
/* Digits, evenly spaced, so six of them read as a PIN rather than a word. */
|
|
.pin-input {
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: 0.35em;
|
|
text-align: center;
|
|
max-width: 9em;
|
|
}
|
|
.pin-form {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
gap: 12px;
|
|
}
|
|
.pin-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
}
|
|
.pin-field[hidden] { display: none; }
|
|
.pin-field input {
|
|
padding: 8px 10px;
|
|
font-size: 14px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 7px;
|
|
}
|
|
.pin-field input:focus { outline: none; border-color: var(--accent); }
|
|
.pin-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-left: auto;
|
|
}
|