Files
immich-folio/app/admin/styles/redesign.css
2026-10-02 18:14:23 +02:00

1470 lines
32 KiB
CSS

/* ── Admin redesign, stage 1 (docs/admin-ux-concept.md) ──────────
Loaded last from admin.css, so it overrides the earlier sheets rather than
rewriting them. Stage 1: the sidebar shell, the overview page, the site's
typography and a calmer use of the accent. */
/* ── Tokens ────────────────────────────────────────────────── */
.admin-layout,
.admin-listbox-popup {
/* The site's own fonts (set on <html> by the root layout from the preset).
The fallbacks are what the admin used before. */
--admin-font: var(
--font-sans,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
system-ui,
sans-serif
);
--admin-font-label: var(--font-caption, ui-monospace, 'SF Mono', Menlo, monospace);
--admin-bg: #0c0c0c;
--admin-bg-raised: #141414;
--admin-bg-elevated: #1b1b1b;
--admin-bg-input: #101010;
--admin-border: #242424;
--admin-border-hover: #3a3a3a;
--admin-sidebar-bg: #090909;
--admin-radius: 4px;
--admin-sidebar-width: 232px;
/* The one outer content column every workspace screen shares. Pages,
Overview and Diagnostics used to end at three different right edges
(QA A-18). Up to a 1592px window every screen simply fills the
workspace; wider, they all stop here instead of each at its own cap.
Text inside keeps its own narrower measure (the 70ch details). Only the
split editors (journal, content pages) break out of it. */
--admin-content-width: 1280px;
--admin-workspace-pad-x: 2.5rem;
font-family: var(--admin-font);
}
[data-theme='light'] .admin-layout,
[data-theme='light'] .admin-listbox-popup {
--admin-bg: #f6f6f5;
--admin-bg-raised: #ffffff;
--admin-bg-elevated: #efefee;
--admin-bg-input: #ffffff;
--admin-border: #e2e2e0;
--admin-border-hover: #c8c8c5;
--admin-sidebar-bg: #fbfbfa;
}
.admin-listbox-popup {
font-family: var(--admin-font);
}
/* ── Shell: sidebar + workspace ────────────────────────────── */
.admin-app {
display: grid;
grid-template-columns: var(--admin-sidebar-width) minmax(0, 1fr);
flex-direction: unset;
min-height: 100vh;
}
.admin-sidebar {
position: sticky;
top: 0;
height: 100vh;
display: flex;
flex-direction: column;
gap: 1.25rem;
padding: 1.25rem 0.75rem 1rem;
background: var(--admin-sidebar-bg);
border-right: 1px solid var(--admin-border);
overflow-y: auto;
z-index: 50;
}
.admin-brand {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.25rem 0.6rem 0.75rem;
color: var(--admin-text);
text-decoration: none;
}
/* The square marker the site uses for kickers and the header dot. */
.admin-brand-mark {
width: 10px;
height: 10px;
background: var(--admin-accent);
flex-shrink: 0;
}
.admin-brand-text {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.admin-brand-name {
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.admin-brand-sub {
font-family: var(--admin-font-label);
font-size: 0.65rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--admin-text-muted);
}
.admin-nav {
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
}
.admin-nav-group {
display: flex;
flex-direction: column;
gap: 1px;
}
.admin-nav-label {
padding: 0 0.6rem 0.35rem;
font-family: var(--admin-font-label);
font-size: 0.62rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--admin-text-muted);
}
.admin-nav-item {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.5rem 0.6rem;
border-radius: var(--admin-radius);
color: var(--admin-text-secondary);
font-size: 0.85rem;
text-decoration: none;
transition:
color 0.15s ease,
background 0.15s ease;
}
.admin-nav-item:hover {
color: var(--admin-text);
background: var(--admin-bg-raised);
}
.admin-nav-item.active {
color: var(--admin-text);
background: var(--admin-bg-elevated);
}
/* One of the accent's three jobs: where you are. */
.admin-nav-item.active::before {
content: '';
position: absolute;
left: -0.75rem;
top: 50%;
width: 3px;
height: 18px;
transform: translateY(-50%);
background: var(--admin-accent);
}
.admin-nav-icon {
display: inline-flex;
color: var(--admin-text-muted);
}
.admin-nav-item.active .admin-nav-icon {
color: var(--admin-text);
}
.admin-nav-text {
flex: 1;
}
.admin-nav-count {
min-width: 1.35rem;
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: var(--admin-accent);
color: var(--admin-on-accent);
font-family: var(--admin-font-label);
font-size: 0.68rem;
text-align: center;
}
.admin-nav-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--admin-error);
}
.admin-sidebar-foot {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding-top: 0.75rem;
border-top: 1px solid var(--admin-border);
}
.admin-sidebar .status-indicator-container {
display: block;
margin: 0;
}
.admin-sidebar .sidebar-status {
width: 100%;
justify-content: flex-start;
}
/* In the sidebar foot the dropdown opens upwards, as wide as the foot. The
sidebar scrolls (overflow-y: auto), which clips sideways too: a fixed 280px
panel lost its values past the 232px column. */
.admin-sidebar .status-dropdown {
top: auto;
bottom: 100%;
left: 0;
right: 0;
margin: 0 0 0.5rem;
width: auto;
border-radius: var(--admin-radius);
}
.admin-sidebar .status-item {
gap: 0.75rem;
}
.admin-sidebar .status-val {
text-align: right;
}
/* The collapsed sidebar does not clip (overflow: visible below), so the panel
keeps its width and opens out of the 64px column. */
.admin-app.is-collapsed .admin-sidebar .status-dropdown {
right: auto;
width: 280px;
}
.admin-sidebar-actions {
display: flex;
flex-direction: column;
gap: 1px;
}
.admin-side-action {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
padding: 0.45rem 0.6rem;
border: none;
border-radius: var(--admin-radius);
background: none;
color: var(--admin-text-muted);
font: inherit;
font-size: 0.8rem;
text-align: left;
text-decoration: none;
cursor: pointer;
}
.admin-side-action:hover:not(:disabled) {
color: var(--admin-text);
background: var(--admin-bg-raised);
}
.admin-side-action:disabled {
opacity: 0.5;
cursor: wait;
}
.admin-workspace {
max-width: calc(var(--admin-content-width) + 2 * var(--admin-workspace-pad-x));
padding: 2.25rem var(--admin-workspace-pad-x) 6rem;
margin: 0;
}
/* The save bar centres on the workspace, not on the window. */
.admin-app ~ .floating-save-bar,
.admin-app .floating-save-bar {
left: calc(50% + var(--admin-sidebar-width) / 2);
}
@media (max-width: 860px) {
.admin-app {
grid-template-columns: 1fr;
}
.admin-sidebar {
position: static;
height: auto;
border-right: none;
border-bottom: 1px solid var(--admin-border);
}
.admin-nav {
flex-direction: row;
flex-wrap: wrap;
gap: 0.25rem 1rem;
}
.admin-nav-item.active::before {
display: none;
}
.admin-workspace {
padding: 1.5rem 1rem 6rem;
}
.admin-app .floating-save-bar {
left: 50%;
}
}
/* ── Calmer accent ─────────────────────────────────────────── */
/* A switched-on card keeps a hairline; its switch carries the state. A thick
accent frame on every active card turned Studio Modern's settings into a
wall of red and took the colour's meaning with it. */
.admin-layout .admin-toggle-card.active {
border-color: var(--admin-border-hover);
box-shadow: none;
}
.admin-layout .admin-toggle-card,
.admin-layout .settings-group,
.admin-layout .analytics-panel,
.admin-layout .metric-card,
.admin-layout .messages-list {
border-radius: var(--admin-radius);
}
/* Section and field labels in the site's caption voice. */
.admin-layout .admin-field > label,
.admin-layout .settings-group-title {
font-family: var(--admin-font-label);
letter-spacing: 0.12em;
}
/* Panel headings are headings, not captions: the caption font made the
analytics titles a mixed-case line of monospace (QA A-18). They read like
the settings section headings; monospace stays with the paths. */
.admin-layout .settings-section-header h3,
.admin-layout .analytics-header h2,
.admin-layout .analytics-section-title h3 {
font-family: var(--admin-font);
font-weight: 500;
letter-spacing: -0.01em;
}
/* Section icons stay neutral; the accent is not decoration. */
.admin-layout .settings-section-header h3 svg,
.admin-layout .analytics-header h2 svg,
.admin-layout .analytics-section-title h3 svg {
color: var(--admin-text-muted);
}
/* ── Overview ──────────────────────────────────────────────── */
.admin-overview {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.overview-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.overview-kicker {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0 0 0.6rem;
font-family: var(--admin-font-label);
font-size: 0.7rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--admin-text-muted);
}
.overview-kicker::before {
content: '';
width: 7px;
height: 7px;
background: var(--admin-accent);
}
.overview-title {
margin: 0;
font-size: clamp(1.8rem, 3vw, 2.4rem);
font-weight: 500;
letter-spacing: -0.02em;
}
.overview-sub {
margin: 0.4rem 0 0;
color: var(--admin-text-secondary);
font-size: 0.9rem;
}
.overview-section-label {
margin: 0 0 0.9rem;
font-family: var(--admin-font-label);
font-size: 0.68rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--admin-text-muted);
}
.overview-attention {
display: flex;
flex-direction: column;
border: 1px solid var(--admin-border);
border-radius: var(--admin-radius);
background: var(--admin-bg-raised);
}
.overview-attention-item {
display: flex;
align-items: center;
gap: 0.9rem;
padding: 0.95rem 1.1rem;
color: var(--admin-text);
text-decoration: none;
font-size: 0.9rem;
}
.overview-attention-item + .overview-attention-item {
border-top: 1px solid var(--admin-border);
}
.overview-attention-item:hover {
background: var(--admin-bg-elevated);
}
.overview-attention-marker {
width: 8px;
height: 8px;
flex-shrink: 0;
}
.overview-attention-marker.accent {
background: var(--admin-accent);
}
.overview-attention-marker.warn {
background: var(--admin-warning);
}
.overview-attention-marker.error {
background: var(--admin-error);
}
.overview-attention-text {
flex: 1;
}
.overview-attention-detail {
display: block;
max-width: 70ch;
margin-top: 0.15rem;
color: var(--admin-text-muted);
font-size: 0.8rem;
}
.overview-attention-go {
font-family: var(--admin-font-label);
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--admin-text-muted);
}
.overview-calm {
margin: 0;
padding: 1rem 1.1rem;
border: 1px dashed var(--admin-border);
border-radius: var(--admin-radius);
color: var(--admin-text-muted);
font-size: 0.88rem;
}
.overview-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1px;
border: 1px solid var(--admin-border);
border-radius: var(--admin-radius);
background: var(--admin-border);
overflow: hidden;
}
.overview-stat {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 1.2rem 1.25rem 1.1rem;
background: var(--admin-bg-raised);
color: inherit;
text-decoration: none;
}
.overview-stat:hover {
background: var(--admin-bg-elevated);
}
.overview-stat-value {
font-size: 2rem;
font-weight: 500;
letter-spacing: -0.02em;
line-height: 1;
}
.overview-stat-label {
font-family: var(--admin-font-label);
font-size: 0.66rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--admin-text-muted);
}
.overview-stat-note {
color: var(--admin-text-secondary);
font-size: 0.78rem;
}
.overview-actions {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
/* Same rule for the metadata master card: its switch says it is on. */
.admin-layout .metadata-card.active {
border-color: var(--admin-border-hover);
box-shadow: none;
}
/* A chosen option (preset, frame, layout…) is a place, so it keeps the accent,
but as a hairline rather than a doubled frame. */
.admin-layout .preset-card.active,
.admin-layout .preset-btn.active {
box-shadow: none;
}
/* Page builder section icons: neutral, like the settings headers. */
.admin-layout .builder-section-header h2 svg {
color: var(--admin-text-muted);
}
/* "Disabled" is a state, not an error. The badge sets its red inline, hence
!important; stage 2 moves the colour into the stylesheet (after #711). */
.admin-layout .subpage-badge-protected[title='Page is disabled'] {
background: rgba(0, 0, 0, 0.72) !important;
color: var(--admin-text-secondary) !important;
border: 1px solid var(--admin-border-hover);
}
/* Every screen starts at the same left edge (stage 2 adds the shared page header). */
.admin-layout .diag-page {
margin-left: 0;
}
/* ── Page header (stage 2) ─────────────────────────────────── */
.page-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 1rem 2rem;
flex-wrap: wrap;
margin: 0 0 2rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--admin-border);
}
.page-header-kicker {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0 0 0.55rem;
font-family: var(--admin-font-label);
font-size: 0.68rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--admin-text-muted);
}
.page-header-kicker::before {
content: '';
width: 7px;
height: 7px;
background: var(--admin-accent);
}
.page-header-title {
display: flex;
align-items: center;
gap: 0.6rem;
margin: 0;
font-size: clamp(1.5rem, 2.4vw, 1.9rem);
font-weight: 500;
letter-spacing: -0.02em;
}
.page-header-sub {
margin: 0.35rem 0 0;
max-width: 62ch;
color: var(--admin-text-secondary);
font-size: 0.88rem;
line-height: 1.5;
}
.page-header-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* The journal list is an ordinary screen and gets the workspace column; only
the editor (split view) needs the full width journal-studio.css gives it. */
.admin-app .admin-main:has(> .journal-studio) {
max-width: calc(var(--admin-content-width) + 2 * var(--admin-workspace-pad-x));
padding: 2.25rem var(--admin-workspace-pad-x) 6rem;
}
@media (max-width: 860px) {
.admin-app .admin-main:has(> .journal-studio) {
padding: 1.5rem 1rem 6rem;
}
}
/* ── Confirm dialog (stage 3) ──────────────────────────────── */
.confirm-backdrop {
position: fixed;
inset: 0;
z-index: 1200;
display: grid;
place-items: center;
padding: 1rem;
background: var(--admin-overlay);
}
.confirm-card {
width: min(420px, 100%);
padding: 1.4rem 1.5rem 1.2rem;
border: 1px solid var(--admin-border-hover);
border-radius: var(--admin-radius);
background: var(--admin-bg-raised);
color: var(--admin-text);
font-family: var(--admin-font);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
}
.confirm-title {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
}
.confirm-message {
margin: 0.5rem 0 0;
color: var(--admin-text-secondary);
font-size: 0.88rem;
line-height: 1.5;
}
.confirm-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1.4rem;
}
.admin-btn.confirm-danger {
background: var(--admin-error);
border-color: var(--admin-error);
color: #fff;
}
/* A caption over a group of buttons or a self-labelled switch: looks like a
field label, but is not a <label>, since there is no single control for it
to name (#694 §4). */
.admin-layout .admin-field-label {
display: block;
margin-bottom: 0.4rem;
font-family: var(--admin-font-label);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--admin-text-secondary);
}
/* ── Page builder: structure list + panel (stage 4) ────────── */
.pb-split {
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
gap: 1.75rem;
align-items: start;
}
.pb-tree {
position: sticky;
top: 1rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
max-height: calc(100vh - 2rem);
overflow-y: auto;
padding-right: 0.25rem;
}
.pb-tree .builder-search-container {
margin: 0;
padding: 0;
background: none;
border: none;
}
.pb-tree .builder-search-wrapper {
max-width: none;
}
.pb-group {
display: flex;
flex-direction: column;
gap: 1px;
}
.pb-group-head {
display: flex;
justify-content: space-between;
padding: 0 0.5rem 0.4rem;
font-family: var(--admin-font-label);
font-size: 0.62rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--admin-text-muted);
}
.pb-row {
display: flex;
align-items: center;
border-radius: var(--admin-radius);
}
.pb-row-grip {
display: inline-flex;
padding: 0 0.1rem 0 0.25rem;
color: var(--admin-text-muted);
opacity: 0.45;
cursor: grab;
}
.pb-row:hover .pb-row-grip {
opacity: 1;
}
.pb-row-main {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
flex: 1;
min-width: 0;
padding: 0.4rem 0.5rem;
border: none;
border-radius: var(--admin-radius);
background: none;
color: var(--admin-text-secondary);
font: inherit;
font-size: 0.86rem;
text-align: left;
cursor: pointer;
}
.pb-row-solo {
width: 100%;
}
.pb-row-main:hover {
background: var(--admin-bg-raised);
color: var(--admin-text);
}
.pb-row.active .pb-row-main,
.pb-row-main.active {
background: var(--admin-bg-elevated);
color: var(--admin-text);
box-shadow: inset 2px 0 0 var(--admin-accent);
}
.pb-row.is-off .pb-row-name {
color: var(--admin-text-muted);
}
.pb-row-thumb {
display: inline-grid;
place-items: center;
flex: none;
width: 36px;
height: 26px;
overflow: hidden;
border-radius: 2px;
background: var(--admin-bg-elevated);
color: var(--admin-text-muted);
}
.pb-row-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.pb-row-name {
display: flex;
align-items: center;
gap: 0.35rem;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pb-row-count,
.pb-row-state {
font-family: var(--admin-font-label);
font-size: 0.68rem;
color: var(--admin-text-muted);
}
.pb-row-state {
padding: 0 0.35rem;
border: 1px solid var(--admin-border-hover);
border-radius: 2px;
font-size: 0.6rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
/* Content pages in the structure list (#722): a document icon and a PAGE tag
tell them apart from subpages at a glance. */
.pb-row-tag {
padding: 0 0.35rem;
border-radius: 2px;
background: var(--admin-bg-elevated);
font-family: var(--admin-font-label);
font-size: 0.58rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--admin-text-secondary);
}
.pb-offmenu {
min-height: 2.25rem;
border-radius: var(--admin-radius);
}
.pb-offmenu.is-over {
outline: 1px dashed var(--admin-border-hover);
}
.pb-offmenu .empty-hint {
margin: 0;
padding: 0.45rem 0.6rem;
font-size: 0.75rem;
}
.page-panel {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 720px;
}
.page-panel .builder-section-header {
margin-bottom: 0;
}
.page-panel textarea {
width: 100%;
resize: vertical;
}
.page-panel-switches {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.page-panel-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.new-page-card {
width: min(440px, 100%);
}
.new-page-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.pb-add-row {
display: flex;
gap: 0.35rem;
}
.pb-add-row .pb-add {
flex: 1;
}
.pb-add {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.35rem;
padding: 0.45rem 0.6rem;
border: 1px dashed var(--admin-border);
border-radius: var(--admin-radius);
background: none;
color: var(--admin-text-muted);
font: inherit;
font-size: 0.82rem;
cursor: pointer;
}
.pb-add:hover {
border-color: var(--admin-border-hover);
color: var(--admin-text);
}
.pb-panel {
min-width: 0;
}
.pb-panel .builder-section:first-child {
margin-top: 0;
}
/* The subpage sheet, in place: no overlay chrome, full panel height. */
.subpage-drawer-container--inline {
max-width: none;
max-height: none;
/* The overlay sheet is 100dvh tall on a phone (subpage-sheet.css). In place,
that height clipped everything past the first screen — album list, grid
settings, Delete — and the footer slid over the password field. The
sheet grows with its content and the page scrolls. */
height: auto;
border-radius: var(--admin-radius);
box-shadow: none;
animation: none;
}
/* Out of the overlay, the page itself scrolls. The body keeps no scroller of
its own: with `overflow-y: auto` it still became one (a sideways overflow
is enough), and `overscroll-behavior: contain` then swallowed every wheel
turn over the panel instead of handing it to the page. */
.subpage-drawer-container--inline .subpage-drawer-body {
overflow: visible;
overscroll-behavior: auto;
}
@media (max-width: 1100px) {
.pb-split {
grid-template-columns: 1fr;
}
.pb-tree {
position: static;
max-height: none;
}
/* Picking a row scrolls the stacked panel up (PageBuilder revealPanel). */
.pb-panel {
scroll-margin-top: 1rem;
}
}
/* A custom album title is marked by weight, not by the accent: red italics
on every renamed album read as an error. */
.admin-layout .album-tile-name.custom-title {
color: var(--admin-text);
font-style: normal;
font-weight: 600;
}
/* ── Collapsible sidebar ───────────────────────────────────── */
.admin-sidebar {
position: sticky;
transition: width 0.18s ease;
}
.admin-app {
transition: grid-template-columns 0.18s ease;
}
.admin-sidebar-toggle {
position: absolute;
top: 1.35rem;
right: 0.6rem;
display: inline-grid;
place-items: center;
width: 26px;
height: 26px;
border: 1px solid var(--admin-border);
border-radius: var(--admin-radius);
background: var(--admin-bg-raised);
color: var(--admin-text-muted);
cursor: pointer;
z-index: 2;
}
.admin-sidebar-toggle:hover {
color: var(--admin-text);
border-color: var(--admin-border-hover);
}
.admin-app.is-collapsed {
--admin-sidebar-width: 64px;
}
.admin-app.is-collapsed .admin-sidebar {
padding-left: 0.6rem;
padding-right: 0.6rem;
align-items: center;
/* The name overlays sit outside the 64px column. */
overflow: visible;
}
.admin-app.is-collapsed .admin-sidebar-toggle {
position: static;
order: -1;
}
.admin-app.is-collapsed .admin-brand {
justify-content: center;
padding: 0.25rem 0 0.5rem;
}
.admin-app.is-collapsed .admin-brand-text,
.admin-app.is-collapsed .admin-nav-text,
.admin-app.is-collapsed .admin-side-label,
.admin-app.is-collapsed .status-text {
display: none;
}
/* Group labels become a hairline between groups. */
.admin-app.is-collapsed .admin-nav-label {
display: block;
height: 1px;
margin: 0 0.35rem 0.35rem;
padding: 0;
overflow: hidden;
font-size: 0;
background: var(--admin-border);
}
.admin-app.is-collapsed .admin-nav,
.admin-app.is-collapsed .admin-sidebar-foot,
.admin-app.is-collapsed .admin-sidebar-actions {
width: 100%;
align-items: center;
}
.admin-app.is-collapsed .admin-nav-item,
.admin-app.is-collapsed .admin-side-action {
justify-content: center;
width: 40px;
height: 36px;
padding: 0;
}
.admin-app.is-collapsed .admin-nav-item.active::before {
left: -0.6rem;
}
/* The unread count shrinks to a dot on the icon. */
.admin-app.is-collapsed .admin-nav-count {
position: absolute;
top: 6px;
right: 6px;
min-width: 0;
width: 8px;
height: 8px;
padding: 0;
font-size: 0;
}
.admin-app.is-collapsed .admin-nav-dot {
position: absolute;
top: 7px;
right: 7px;
}
.admin-app.is-collapsed .sidebar-status {
justify-content: center;
width: 40px;
padding: 0;
}
.admin-app.is-collapsed .status-indicator-container {
display: flex;
justify-content: center;
}
/* Name overlays: on hover and on keyboard focus, beside the icon. */
.admin-app.is-collapsed [data-tip] {
position: relative;
}
.admin-app.is-collapsed [data-tip]:hover::after,
.admin-app.is-collapsed [data-tip]:focus-visible::after {
content: attr(data-tip);
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
padding: 0.35rem 0.6rem;
border: 1px solid var(--admin-border-hover);
border-radius: var(--admin-radius);
background: var(--admin-bg-elevated);
color: var(--admin-text);
font-family: var(--admin-font);
font-size: 0.8rem;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
white-space: nowrap;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
pointer-events: none;
z-index: 200;
}
@media (max-width: 860px) {
.admin-sidebar-toggle {
display: none;
}
}
/* ── Status pill switches ──────────────────────────────────── */
/* Every switch reads as a pill that says what it is: a dot plus ON or OFF.
"On" is the success green, never the accent — a preset may make the accent
red, and a switched-on setting must not look like an error. The markup is
unchanged (`.switch-toggle` > `.switch-slider`); the slider becomes the dot
and the word comes from CSS, hidden from assistive technology because the
button already carries `aria-pressed`. */
.admin-layout .switch-toggle,
.admin-layout .switch-toggle--sm {
width: auto;
min-width: 62px;
height: 24px;
padding: 0 10px;
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--admin-border-hover);
border-radius: 999px;
background: transparent;
color: var(--admin-text-muted);
font-family: var(--admin-font-label);
font-size: 0.66rem;
letter-spacing: 0.1em;
text-transform: uppercase;
transition:
color 0.2s ease,
border-color 0.2s ease,
background-color 0.2s ease;
}
.admin-layout .switch-toggle--sm {
min-width: 56px;
height: 22px;
padding: 0 8px;
}
.admin-layout .switch-toggle::after {
content: 'Off' / '';
}
.admin-layout .switch-toggle .switch-slider,
.admin-layout .switch-toggle--sm .switch-slider {
position: static;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--admin-border-hover);
box-shadow: none;
transform: none;
transition:
background-color 0.2s ease,
box-shadow 0.2s ease;
}
.admin-layout .switch-toggle.on {
color: var(--admin-success);
border-color: var(--admin-success);
background: color-mix(in srgb, var(--admin-success) 12%, transparent);
}
.admin-layout .switch-toggle.on::after {
content: 'On' / '';
}
.admin-layout .switch-toggle.on .switch-slider {
background: var(--admin-success);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--admin-success) 22%, transparent);
transform: none;
}
/* Inert: dashed, no dot colour — not an off switch someone could turn on. */
.admin-layout .setting-row--disabled .switch-toggle {
border-style: dashed;
background: transparent;
opacity: 0.6;
}
.admin-layout .setting-row--disabled .switch-slider {
background: var(--admin-text-muted);
}
@media (prefers-reduced-motion: reduce) {
.admin-layout .switch-toggle,
.admin-layout .switch-slider {
transition: none;
}
}
/* ── Feature lists ─────────────────────────────────────────── */
/* Dense rows grouped by meaning replace the grid of equal cards: the group
heading carries the weight, each switch is one line. */
.admin-layout .feature-list {
border-top: 1px solid var(--admin-border);
}
.admin-layout .feature-row {
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--admin-border);
}
.admin-layout .feature-row-main {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
padding: 11px 4px;
background: none;
border: 0;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.admin-layout .feature-row-main:focus-visible {
outline: var(--admin-focus-ring);
outline-offset: 2px;
border-radius: var(--admin-radius);
}
.admin-layout .feature-row-icon {
display: inline-flex;
color: var(--admin-text-muted);
flex-shrink: 0;
}
.admin-layout .feature-row-info {
flex: 1;
min-width: 0;
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
}
.admin-layout .feature-row-title {
font-size: 0.875rem;
font-weight: 500;
color: var(--admin-text);
}
.admin-layout .feature-row-desc {
font-size: 0.78rem;
color: var(--admin-text-muted);
}
.admin-layout .feature-row-main:hover .feature-row-title {
color: var(--admin-text);
text-decoration: underline;
text-decoration-color: var(--admin-border-hover);
text-underline-offset: 3px;
}
.admin-layout .feature-row-link {
flex-shrink: 0;
width: 84px;
text-align: right;
font-size: 0.75rem;
color: var(--admin-text-secondary);
text-decoration: none;
padding: 4px 2px;
}
.admin-layout .feature-row-link:hover {
color: var(--admin-text);
}
.admin-layout .settings-group .feature-list + .metadata-card,
.admin-layout .settings-group .metadata-card + .feature-list {
margin-top: 12px;
}
@media (max-width: 640px) {
.admin-layout .feature-row {
flex-wrap: wrap;
gap: 0 12px;
}
.admin-layout .feature-row-link {
width: auto;
padding: 0 0 10px 31px;
}
.admin-layout .feature-row-link:empty {
display: none;
}
}
/* ── Phone header (#732) ───────────────────────────────────── */
/* On a phone the sidebar used to unfold above the content — nav, status,
and four actions — and filled most of an iPhone screen. It is now a
header row with the brand and a menu button; the rest opens on demand. */
.admin-menu-toggle {
display: none;
}
@media (max-width: 860px) {
.admin-sidebar {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 0;
padding: 0.6rem 1rem;
}
.admin-sidebar .admin-brand {
margin: 0;
}
.admin-menu-toggle {
position: relative;
display: inline-grid;
place-items: center;
width: 40px;
height: 40px;
border: 1px solid var(--admin-border);
border-radius: var(--admin-radius);
background: transparent;
color: var(--admin-text);
cursor: pointer;
}
.admin-menu-toggle:focus-visible {
outline: var(--admin-focus-ring);
outline-offset: 2px;
}
.admin-menu-dot {
position: absolute;
top: 7px;
right: 7px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--admin-accent);
}
.admin-sidebar-menu {
display: none;
grid-column: 1 / -1;
padding-top: 0.75rem;
}
.admin-app.menu-open .admin-sidebar-menu {
display: block;
}
.admin-app.menu-open .admin-sidebar {
max-height: 100dvh;
overflow-y: auto;
}
/* One column: tap targets of a full row, not a wrapped cloud of links. */
.admin-nav {
flex-direction: column;
flex-wrap: nowrap;
gap: 0;
}
.admin-nav .admin-nav-item {
min-height: 44px;
}
}
/* ── Env-locked fields (#605) ──────────────────────────────── */
/* A field an environment variable overrides: the input is disabled and this
note names the variable. Neutral pill, not the warning or accent colour —
the lock is a fact about the deployment, not a problem. */
.admin-env-lock {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 8px;
margin-top: 0.4rem;
font-size: 0.75rem;
line-height: 1.4;
color: var(--admin-text-muted);
}
.admin-env-lock-pill {
display: inline-flex;
align-items: center;
gap: 5px;
height: 20px;
padding: 0 8px;
border: 1px solid var(--admin-border-hover);
border-radius: 999px;
font-family: var(--admin-font-label);
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
flex-shrink: 0;
}