Files
webmin/gray-theme/unauthenticated/gray-theme.css
Ilia Ross cbe1f7f17e Add single page layout to Framed Theme
This PR adds a "Page layout" setting to the Framed Theme, with a new single page layout that shows the menu on every page instead of in a frame.

It's meant for people who can't or don't want to use frames, so the Legacy Theme is no longer the only option without frames.

The menu code moved out of `left.cgi` into a shared menu-lib.pl, so both layouts build the same menu, frames look the same as before, and `left.cgi` shrinks from about 400 lines to about 60.

The menu's module search box now follows the global "webminsearch" permission, which the old frame code never actually checked.

One exception to be aware of. A server opened through Webmin Servers Index always uses frames, because that index only rewrites the links and redirects of framed pages.
2026-10-01 05:44:49 +02:00

2679 lines
90 KiB
CSS

/*
* gray-theme.css
* Stylesheet of the framed theme. The frameset, the left menu, module
* pages and the system information page share this one file.
*
* Colors are custom properties. The light palette is set on :root and
* the dark one when a scheme is dark: forced by the theme settings on
* body[data-scheme="dark"], kept by the theme script on the html element,
* or preferred by the browser when neither marks the scheme light. The
* property names are the ones the ui-lib widgets read, so cards, badges
* and progress bars follow the same palette without a switch of their own.
*/
/* ---- Palette ---- */
:root {
color-scheme: light;
--font-family: system-ui, -apple-system, "Segoe UI", "Helvetica Neue",
Helvetica, Arial, sans-serif;
--font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo,
Consolas, "Liberation Mono", monospace;
--font-size: 13px;
--radius: 8px;
--radius-sm: 6px;
--control-height: 32px;
/* Room between cards, buttons and blocks, and the same room around a
* page at the top and the sides; the bottom keeps scroll room under
* the last element */
--card-gap: 10px;
--page-pad-x: var(--card-gap);
--page-pad-top: var(--card-gap);
--page-pad-bottom: 28px;
/* Titles on the ground sit a little in from the card edge, so they
* neither touch the edge nor go as deep as the text inside the
* cards */
--title-inset: 4px;
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
--shadow-color: rgba(16, 24, 40, 0.10);
/* The edge of the floating menu, a soft ring on the ground */
--border-color-panel: rgba(16, 24, 40, 0.17);
/* Which of two images, one per palette, is shown */
--show-light: block;
--show-dark: none;
/* Neutrals: a light gray ground, white surfaces and gray hairlines,
* with near-black text */
--bg-color: #dfdfdf;
--bg-color-frame: #ffffff;
--bg-color-hover: #f0f0f0;
--bg-color-switch: #e2e2e2;
--bg-color-switch-on: #ffffff;
--main-content: #ffffff;
--bg-table-header: #f5f5f5;
--bg-color-input: #ffffff;
--bg-color-disabled: #dfdfdf;
/* Card edges and lines inside cards; the darker one is for lines and
* controls drawn on the ground itself, which the gray ground would
* otherwise swallow */
--border-color: #cfcfcf;
--border-color-light: #e2e2e2;
--border-color-lighter: #e8e8e8;
--border-color-frame: #e2e2e2;
--border-color-darker: #b8b8b8;
--border-color-input: #bcbcbc;
--border-color-focused: #dbe5f5;
/* Text is black; secondary text keeps at least 12:1 contrast on the
* ground and the surfaces, and muted text at least 9:1 */
--text-color: #0b0c0e;
--text-color-soft: #1f2226;
--text-color-desc: #2b2f35;
--text-color-muted: #3d434b;
--text-color-disabled: #767d86;
--text-color-inverse: #ffffff;
/* Links are a deep blue, 9.5:1 on the ground, still apart from the
* black text */
--link-color: #163b82;
--link-color-hover: #0f2b63;
--accent-color: #2f66c4;
--accent-color-hover: #234d93;
--accent-color-soft: #a9c0e8;
--element-highlight-bg-color: #e3ebf8;
/* Text on a button filled with a notice color: white on the deep
* light-mode colors, the ground color on the bright dark-mode ones.
* The dark palette keeps its own amber for the warning button. */
--alert-button-text: var(--text-color-inverse);
/* The warning button is a warm orange with dark text: the deep amber
* of the warning text looked muddy on a button, and white text on
* orange would be too faint */
--alert-button-warning: #e8933c;
--alert-button-warning-text: var(--text-color);
/* Notice borders are darker than the ground, so a notice keeps a
* visible edge on it */
--text-color-success: #1f6f4a;
--text-color-success-stressed: #1f5c36;
--bg-color-success: #e8f5ec;
--border-color-success: #8fcaa3;
--text-color-info: #2f66c4;
--text-color-info-stressed: #1e3a5c;
--bg-color-info: #e9f1fa;
--border-color-info: #9dbde6;
--text-color-warning: #a86b12;
--text-color-warning-stressed: #4f3f0c;
--bg-color-warning: #fff6dd;
--border-color-warning: #e6c86f;
--text-color-danger: #b42323;
--text-color-danger-stressed: #7a1f1f;
--bg-color-danger: #fdecec;
--border-color-danger: #d98f8f;
--text-color-neutral-stressed: #4c535d;
--bg-color-neutral: #ececec;
--border-color-neutral: #d4d4d4;
--toggle-bg-color: #d0d5dc;
--toggle-thumb-color: #ffffff;
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3.5l3 3 3-3' fill='none' stroke='%236b7684' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
}
/* The dark palette. The two blocks below must be kept identical: the
* first applies when the browser prefers dark and nothing marks the
* scheme light, the second when the theme settings or the theme script
* mark it dark. */
@media (prefers-color-scheme: dark) {
html:not([data-scheme="light"]) body:not([data-scheme="light"]) {
color-scheme: dark;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-color: rgba(0, 0, 0, 0.4);
--border-color-panel: #30353d;
--show-light: none;
--show-dark: block;
--bg-color: #15171b;
--bg-color-frame: #1a1d22;
--bg-color-hover: #262a30;
--bg-color-switch: #262a30;
--bg-color-switch-on: #363b43;
--main-content: #1e2126;
--bg-table-header: #23262c;
--bg-color-input: #1b1e23;
--bg-color-disabled: #23262c;
--border-color: #2e333a;
--border-color-light: #2a2f36;
--border-color-lighter: #262a30;
--border-color-frame: #2a2f36;
--border-color-darker: #4d545e;
--border-color-input: #4d545e;
--border-color-focused: rgba(106, 168, 240, 0.3);
--text-color: #e6e8ea;
--text-color-soft: #c8ced5;
--text-color-desc: #a7b0ba;
--text-color-muted: #99a3ae;
--text-color-disabled: #6b7581;
--text-color-inverse: #ffffff;
--link-color: #6aa8f0;
--link-color-hover: #8fc0f3;
--accent-color: #3d6fc2;
--accent-color-hover: #5a8ad8;
--accent-color-soft: #3d5f8a;
--element-highlight-bg-color: #26395a;
--alert-button-text: var(--bg-color);
--alert-button-warning: #e0a63c;
--alert-button-warning-text: var(--bg-color);
--text-color-success: #7ec563;
--text-color-success-stressed: #8fd376;
--bg-color-success: rgba(126, 197, 99, 0.13);
--border-color-success: rgba(126, 197, 99, 0.5);
--text-color-info: #6aa8f0;
--text-color-info-stressed: #8fc0f3;
--bg-color-info: rgba(106, 168, 240, 0.13);
--border-color-info: rgba(106, 168, 240, 0.5);
--text-color-warning: #e0a63c;
--text-color-warning-stressed: #eab54f;
--bg-color-warning: rgba(224, 166, 60, 0.13);
--border-color-warning: rgba(224, 166, 60, 0.5);
--text-color-danger: #ef7b6f;
--text-color-danger-stressed: #f28c82;
--bg-color-danger: rgba(239, 123, 111, 0.13);
--border-color-danger: rgba(239, 123, 111, 0.5);
--text-color-neutral-stressed: #b6bec7;
--bg-color-neutral: #272b31;
--border-color-neutral: #3c424b;
--toggle-bg-color: #4d545e;
--toggle-thumb-color: #e6e8ea;
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3.5l3 3 3-3' fill='none' stroke='%2399a3ae' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
}
html:not([data-scheme="light"]) body:not([data-scheme="light"])
:is(.ui_page, .ui_multi, .ui_toggle) {
color-scheme: dark;
}
/* The root follows its body, so the scrollbars and the canvas behind
* the page are dark as well */
html:not([data-scheme="light"]):has(> body:not([data-scheme="light"])) {
color-scheme: dark;
}
/* The frameset page, in the dark ground color */
html:not([data-scheme="light"]):has(> frameset) {
color-scheme: dark;
background: #15171b;
}
}
html[data-scheme="dark"] body, body[data-scheme="dark"] {
color-scheme: dark;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-color: rgba(0, 0, 0, 0.4);
--border-color-panel: #30353d;
--show-light: none;
--show-dark: block;
--bg-color: #15171b;
--bg-color-frame: #1a1d22;
--bg-color-hover: #262a30;
--bg-color-switch: #262a30;
--bg-color-switch-on: #363b43;
--main-content: #1e2126;
--bg-table-header: #23262c;
--bg-color-input: #1b1e23;
--bg-color-disabled: #23262c;
--border-color: #2e333a;
--border-color-light: #2a2f36;
--border-color-lighter: #262a30;
--border-color-frame: #2a2f36;
--border-color-darker: #4d545e;
--border-color-input: #4d545e;
--border-color-focused: rgba(106, 168, 240, 0.3);
--text-color: #e6e8ea;
--text-color-soft: #c8ced5;
--text-color-desc: #a7b0ba;
--text-color-muted: #99a3ae;
--text-color-disabled: #6b7581;
--text-color-inverse: #ffffff;
--link-color: #6aa8f0;
--link-color-hover: #8fc0f3;
--accent-color: #3d6fc2;
--accent-color-hover: #5a8ad8;
--accent-color-soft: #3d5f8a;
--element-highlight-bg-color: #26395a;
--alert-button-text: var(--bg-color);
--alert-button-warning: #e0a63c;
--alert-button-warning-text: var(--bg-color);
--text-color-success: #7ec563;
--text-color-success-stressed: #8fd376;
--bg-color-success: rgba(126, 197, 99, 0.13);
--border-color-success: rgba(126, 197, 99, 0.5);
--text-color-info: #6aa8f0;
--text-color-info-stressed: #8fc0f3;
--bg-color-info: rgba(106, 168, 240, 0.13);
--border-color-info: rgba(106, 168, 240, 0.5);
--text-color-warning: #e0a63c;
--text-color-warning-stressed: #eab54f;
--bg-color-warning: rgba(224, 166, 60, 0.13);
--border-color-warning: rgba(224, 166, 60, 0.5);
--text-color-danger: #ef7b6f;
--text-color-danger-stressed: #f28c82;
--bg-color-danger: rgba(239, 123, 111, 0.13);
--border-color-danger: rgba(239, 123, 111, 0.5);
--text-color-neutral-stressed: #b6bec7;
--bg-color-neutral: #272b31;
--border-color-neutral: #3c424b;
--toggle-bg-color: #4d545e;
--toggle-thumb-color: #e6e8ea;
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3.5l3 3 3-3' fill='none' stroke='%2399a3ae' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
}
html[data-scheme="dark"] body :is(.ui_page, .ui_multi, .ui_toggle),
body[data-scheme="dark"] :is(.ui_page, .ui_multi, .ui_toggle) {
color-scheme: dark;
}
html[data-scheme="dark"], html:has(> body[data-scheme="dark"]) {
color-scheme: dark;
}
html[data-scheme="dark"]:has(> frameset) {
color-scheme: dark;
background: #15171b;
}
/* A forced light scheme holds the frames to light as well, so a frame
* that is still loading never paints dark under a dark system setting */
html[data-scheme="light"]:has(> frameset) {
color-scheme: light;
}
/* The frameset page has no body to carry the palette, so it paints the
* ground itself, showing behind a frame that is still loading. It
* supports both schemes, so its frames are not held to light. */
:root:has(> frameset) {
color-scheme: light dark;
background: var(--bg-color);
}
/* ---- Base ---- */
/* The root stays unpainted: the body's ground reaches the canvas, and
* the dark tokens live on the body */
html {
background: transparent;
}
body {
margin: 0;
padding: var(--page-pad-top) var(--page-pad-x) var(--page-pad-bottom);
/* The core header sets height:100% inline on html and body, which
* would end the body at the viewport and put the bottom padding
* before the end of a long page */
height: auto !important;
font-family: var(--font-family);
font-size: var(--font-size);
line-height: 1.45;
color: var(--text-color);
background: var(--bg-color);
text-align: start;
}
/* Kept at type-selector specificity, so that a class on a link can
* recolor it and the body's link attributes are ignored */
a {
color: var(--link-color);
text-decoration: none;
}
a:hover {
color: var(--link-color-hover);
text-decoration: underline;
}
/* Element defaults */
img, a img { border: 0; }
p { margin: 6px 0; }
h1, h2, h3, h4, h5 {
margin: 10px 0 6px;
color: var(--text-color);
font-weight: 600;
line-height: 1.3;
}
h1 { font-size: 18px; }
h2 { font-size: 15px; }
h3, h4, h5 { font-size: 13px; }
b, strong { font-weight: 600; }
hr {
height: 0;
margin: 12px 0;
border: 0;
border-top: 1px solid var(--border-color-darker);
}
code, kbd, samp, tt, pre {
font-family: var(--font-family-mono);
font-size: 12px;
}
pre { margin: 6px 0; }
table {
border-collapse: separate;
border-spacing: 0;
font-size: inherit;
}
th, td {
border: 0;
text-align: start;
}
font { font-size: inherit; }
label { cursor: pointer; }
/* Utility classes printed by ui-lib and by modules */
.muted { color: var(--text-color-muted); }
.text-danger { color: var(--text-color-danger); }
.text-success { color: var(--text-color-success); }
.text-warning { color: var(--text-color-warning); }
.text-info { color: var(--text-color-info); }
.w-100 { width: 100% !important; }
.field-sizing-content {
field-sizing: content !important;
min-width: 15px !important;
}
.inherit-color,
.inherit-color:hover, .inherit-color:active, .inherit-color:focus,
.inherit-color::before, .inherit-color::after,
.inherit-color:hover::before, .inherit-color:hover::after,
.inherit-color:active::before, .inherit-color:active::after,
.inherit-color:focus::before, .inherit-color:focus::after {
color: inherit !important;
}
/* The warning in the login heading over plain HTTP */
.not-secure {
float: right;
color: var(--text-color-danger);
}
/* Sections and tab bodies that the theme scripts show and hide */
.itemhidden, .opener_hidden { display: none; }
.itemshown, .opener_shown { display: block; }
/* Small chevron drawn with two borders, used for collapsible headings */
details > summary {
cursor: pointer;
}
details > summary::-webkit-details-marker { display: none; }
details.ui_hidden_table_start > summary::before,
details.section > summary::after,
details.menu-cat > summary::after {
content: "";
flex: 0 0 auto;
width: 7px;
height: 7px;
border-right: 1.5px solid var(--text-color-muted);
border-bottom: 1.5px solid var(--text-color-muted);
transform: rotate(-45deg);
transition: transform 0.12s;
}
details.ui_hidden_table_start > summary::before {
margin-right: 4px;
}
details.section > summary::after {
margin: 0 4px 2px 0;
}
/* The menu chevron is smaller than the one on cards */
details.menu-cat > summary::after {
width: 5px;
height: 5px;
margin-block: 0 1px;
margin-inline: 0 5px;
}
details[open].ui_hidden_table_start > summary::before,
details[open].section > summary::after,
details[open].menu-cat > summary::after {
transform: rotate(45deg);
}
/* ---- Form controls ---- */
/* Text fields, menus and text areas share one height, edge and radius */
input[type="text"], input[type="password"], input[type="email"],
input[type="number"], input[type="search"], input[type="url"],
input[type="tel"], input[type="date"], input[type="time"],
input[type="datetime-local"], input:not([type]), select, textarea {
box-sizing: border-box;
min-height: var(--control-height);
padding: 4px 10px;
border: 1px solid var(--border-color-input);
border-radius: var(--radius-sm);
background: var(--bg-color-input);
color: var(--text-color);
font: inherit;
line-height: 1.45;
}
select { padding: 4px 8px; }
/* Drop-down selects draw their own arrow, inset from the edge */
select:not([multiple]):not([size]), select[size="1"]:not([multiple]) {
appearance: none;
-webkit-appearance: none;
padding-right: 28px;
background-image: var(--select-arrow);
background-repeat: no-repeat;
background-position: right 9px center;
background-size: 10px 10px;
}
/* List boxes keep their own height */
select[multiple], select[size]:not([size="1"]) {
min-height: 0;
padding: 4px;
}
textarea {
min-height: 0;
padding: 8px 10px;
resize: vertical;
}
/* Text areas in forms are monospace, for config files and scripts */
table.ui_table td textarea {
line-height: normal;
font-family: var(--font-family-mono);
}
/* Focus ring in the accent color, and grayed disabled controls */
input:focus-visible, select:focus-visible, textarea:focus-visible {
outline: 0;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px var(--border-color-focused);
}
input:disabled, select:disabled, textarea:disabled {
background: var(--bg-color-disabled);
color: var(--text-color-disabled);
}
/* Checkboxes and radios in the accent color, with room after a label */
input[type="checkbox"], input[type="radio"] {
width: 15px;
height: 15px;
margin: 0 5px 0 0;
vertical-align: -3px;
accent-color: var(--accent-color);
}
input[type="checkbox"] + label, input[type="radio"] + label {
margin-right: 14px;
}
/* A checkbox or radio right after a button, a field or a menu keeps its
* distance */
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]) + input[type="checkbox"],
select + input[type="checkbox"],
textarea + input[type="checkbox"],
button + input[type="checkbox"],
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]) + input[type="radio"],
select + input[type="radio"],
textarea + input[type="radio"],
button + input[type="radio"] {
margin-left: 10px;
}
input[type="file"] { font: inherit; }
/* Labels of checkboxes inside bold cells stay plain */
.ui_form_value td > b > input + label + tt,
.ui_form_value td > b > input + label {
font-weight: normal !important;
}
/* ---- Buttons ---- */
/* Every button, from the core classes to plain button tags, is a white
* pill with a gray edge */
input[type="submit"], input[type="button"], input[type="reset"], button,
input.ui_submit, input.ui_button, input.ui_reset {
display: inline-block;
box-sizing: border-box;
min-height: var(--control-height);
margin: 1px 0;
padding: 5px 14px;
border: 1px solid var(--border-color-darker);
border-radius: var(--radius-sm);
background: var(--main-content);
color: var(--text-color);
font: inherit;
font-weight: 500;
line-height: 1.45;
cursor: pointer;
}
input[type="submit"]:hover, input[type="button"]:hover,
input[type="reset"]:hover, button:hover,
input.ui_submit:hover, input.ui_button:hover, input.ui_reset:hover {
background: var(--bg-color);
border-color: var(--text-color-disabled);
text-decoration: none;
}
input[type="submit"]:active, input[type="button"]:active,
input[type="reset"]:active, button:active {
background: var(--bg-color-switch);
}
input[type="submit"]:focus-visible, input[type="button"]:focus-visible,
input[type="reset"]:focus-visible, button:focus-visible {
outline: 0;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px var(--border-color-focused);
}
input[type="submit"]:disabled, input[type="button"]:disabled,
input[type="reset"]:disabled, button:disabled {
background: var(--bg-color-disabled);
border-color: var(--border-color);
color: var(--text-color-disabled);
cursor: default;
}
/* The first button of a form is the primary action, unless it deletes */
.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]),
.ui_form_end_side_left .ui_form_end_buttons td:first-child > input.ui_submit,
input.ui_submit.primary, button.primary {
background: var(--accent-color);
border-color: var(--accent-color-hover);
color: var(--text-color-inverse);
font-weight: 600;
}
.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]):hover,
.ui_form_end_side_left .ui_form_end_buttons td:first-child > input.ui_submit:hover,
input.ui_submit.primary:hover, button.primary:hover {
background: var(--accent-color-hover);
border-color: var(--accent-color-hover);
}
/* Destructive buttons are outlined in red */
input.ui_submit[name="delete"], input.ui_submit[name^="delete"],
input.ui_submit[name$="delete"], input.ui_submit[name="remove"],
input.ui_submit.danger, button.danger {
background: var(--main-content);
border-color: var(--border-color-danger);
color: var(--text-color-danger);
font-weight: 500;
}
input.ui_submit[name="delete"]:hover, input.ui_submit[name^="delete"]:hover,
input.ui_submit[name$="delete"]:hover, input.ui_submit[name="remove"]:hover,
input.ui_submit.danger:hover, button.danger:hover {
background: var(--bg-color-danger);
border-color: var(--text-color-danger);
}
/* Buttons under a form sit at the left, with the same 11px gap between
* them as the footer bar keeps above and below them, whatever alignment
* the core gave their cells; the spacer cells between them are dropped */
table.ui_form_end_buttons { margin: 4px 0 var(--card-gap); }
table.ui_form_end_buttons > tbody > tr {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 11px;
}
table.ui_form_end_buttons td {
display: block;
padding: 0;
text-align: start;
}
table.ui_form_end_buttons td:not(:has(*)) { display: none; }
div.ui_form_end_buttons { padding: 6px 0; }
/* Buttons split between the left and the right of a form */
.ui_form_end_side_by_side {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
/* Rows of action buttons with a description (ui_buttons_start) */
table.ui_buttons_table {
width: 100%;
margin: 0 0 var(--card-gap);
background: var(--main-content);
border: 1px solid var(--border-color);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
tr.ui_buttons_row > td {
padding: 10px 16px;
border-top: 1px solid var(--border-color-lighter);
vertical-align: middle;
}
tr.ui_buttons_row:first-child > td { border-top: 0; }
td.ui_buttons_label {
width: 1%;
padding-right: 16px;
white-space: nowrap;
}
td.ui_buttons_label > input.ui_submit { margin-right: 10px; }
td.ui_buttons_label input.ui_button { margin-left: 6px; }
td.ui_buttons_label label { margin-right: 6px; }
td.ui_buttons_value {
padding-left: 16px;
color: var(--text-color-desc);
}
tr.ui_buttons_row.disabled > td { opacity: 0.6; }
/* Rows of links above or below tables (ui_links_row): each link is a
* small button, styled with the header buttons below, and the bar
* separators between them are dropped */
.ui_links_row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
margin: 0 0 8px;
}
.ui_links_sep { display: none; }
td[align="right"] > .ui_links_row { justify-content: flex-end; }
/* Inside a table cell, such as the buttons of a login in the dashboard
* list, the row has no margin, so it centers with the cell's text */
table.ui_columns td > .ui_links_row { margin: 0; }
.ui_link_pagination.disabled {
filter: grayscale(1) opacity(0.6);
pointer-events: none;
}
/* ---- Module page header ---- */
/* The header table printed by header() becomes the page title with the
* module links beside it, on the page ground. It bleeds over the page
* padding and stays at the top while the page scrolls, or under the menu
* bar of the single page layout on a narrow screen. */
table.header {
position: sticky;
top: var(--sticky-top, 0);
z-index: 5;
display: block;
width: auto;
margin: calc(-1 * var(--page-pad-top)) calc(-1 * var(--page-pad-x)) 14px;
padding: 14px var(--page-pad-x) 12px;
background: var(--bg-color);
}
table.header > tbody { display: block; }
/* The cells sit 8px apart, like the buttons inside them; the title cell
* keeps a little more distance from the first button */
table.header > tbody > tr {
display: flex;
align-items: center;
gap: 8px;
}
table.header > tbody > tr > td {
width: auto;
padding: 0;
vertical-align: middle;
}
/* The status line above the title, when Webmin is set to show it */
#headln1 {
flex: 1 1 100%;
text-align: center;
font-size: 12px;
color: var(--text-color-muted);
}
#headln2c {
order: 1;
flex: 1 1 auto;
min-width: 0;
padding-left: var(--title-inset);
padding-right: 10px;
text-align: start;
font-size: 20px;
font-weight: 700;
letter-spacing: -0.3px;
line-height: 1.3;
}
#headln2c font { font-size: inherit; }
/* The lighter line under a bold title starts a couple of pixels further
* in, so it reads as even with the title's first letter */
.ui_header_below {
display: block;
margin-top: 2px;
padding-left: 2px;
font-size: 13px;
font-weight: 400;
letter-spacing: 0;
color: var(--text-color-muted);
}
#headln2c br:has(+ .ui_header_below) { display: none; }
#headln2l { order: 2; }
#headln2r { order: 3; }
#headln2l, #headln2r {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
font-size: 12.5px;
font-weight: 400;
white-space: nowrap;
}
#headln2l br, #headln2r br { display: none; }
#headln2l:empty, #headln2r:empty { display: none; }
/* The module links beside the title are small buttons; the flex box
* centers the icon and the text in the 26px height */
#headln2l a, #headln2r a, .page-action, .ui_links_row > a {
display: inline-flex;
align-items: center;
gap: 6px;
box-sizing: border-box;
min-height: 26px;
padding: 0 10px;
border: 1px solid var(--border-color-darker);
border-radius: var(--radius-sm);
background: var(--main-content);
color: var(--text-color);
font-size: 12.5px;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
text-decoration: none;
}
#headln2l a:hover, #headln2r a:hover, .page-action:hover,
.ui_links_row > a:hover {
background: var(--bg-color);
border-color: var(--text-color-disabled);
color: var(--text-color);
text-decoration: none;
}
#headln2r b { font-weight: 600; }
/* Text under the title, such as the domain a page is about, sits right
* below it */
table.header:has(+ .ui_post_header) {
margin-bottom: 0;
padding-bottom: 2px;
}
.ui_post_header {
margin: 0 0 12px;
padding-left: calc(var(--title-inset) + 2px);
color: var(--text-color-desc);
}
/* Subheadings between groups on a page are small capital labels, like
* the labels in the menu and on the tiles, inset like the page title */
.ui_subheading {
margin: 20px 0 8px;
padding-left: var(--title-inset);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-color-muted);
}
.ui_subheading:first-child { margin-top: 0; }
/* Return links printed under a page by theme_footer */
.ui_footer_links {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
margin: 12px 0 0;
color: var(--text-color-muted);
}
.ui_footer_links svg { vertical-align: middle; }
/* Paging arrows */
.ui_nav_link svg { vertical-align: middle; }
.ui_nav_link_disabled { color: var(--text-color-disabled); }
/* ---- Notices ---- */
.ui_alert_box, .alert {
box-sizing: border-box;
margin: 0 0 10px;
padding: 10px 14px;
line-height: 1.45;
border: 1px solid var(--border-color-info);
border-radius: var(--radius);
background: var(--bg-color-info);
color: var(--text-color-info-stressed);
}
.alert-success {
background: var(--bg-color-success);
border-color: var(--border-color-success);
color: var(--text-color-success-stressed);
}
.alert-warning {
background: var(--bg-color-warning);
border-color: var(--border-color-warning);
color: var(--text-color-warning-stressed);
}
.alert-danger, .alert-danger-fatal {
background: var(--bg-color-danger);
border-color: var(--border-color-danger);
color: var(--text-color-danger-stressed);
}
.ui_alert_box center, .alert center { text-align: inherit; }
.ui_alert_box a, .alert a { color: inherit; text-decoration: underline; }
/* Notices drawn by the theme: an icon, the text, and any trailing
* buttons on the same line as the text. The icon is as tall as a line
* of text, so it sits centered on the first line when there are more */
.ui_alert_box, .alert:has(> .ui_alert_icon) {
display: flex;
align-items: flex-start;
gap: 12px;
}
.ui_alert_icon {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
height: 1.45em;
color: var(--text-color-info);
}
.alert-success .ui_alert_icon { color: var(--text-color-success); }
.alert-warning .ui_alert_icon { color: var(--text-color-warning); }
.alert-danger .ui_alert_icon,
.alert-danger-fatal .ui_alert_icon { color: var(--text-color-danger); }
.ui_alert_body {
flex: 1 1 auto;
min-width: 0;
}
.ui_alert_body p { margin: 4px 0; }
.ui_alert_body p:first-child { margin-top: 0; }
.ui_alert_body p:last-child { margin-bottom: 0; }
.ui_alert_body form { margin: 0; }
.ui_alert_body:has(> .ui_alert_actions),
.ui_alert_body form:has(> .ui_alert_actions) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px 16px;
}
/* Buttons in a notice are slim, like the header buttons. With buttons,
* the icon and the text line are both as tall as one, so the three
* center on one another whether the buttons share the text line or sit
* under it */
.ui_alert_box { --alert-button-height: 26px; }
.ui_alert_box:has(.ui_alert_actions) > .ui_alert_icon {
height: var(--alert-button-height);
}
.ui_alert_box:has(.ui_alert_actions) .ui_alert_text {
padding: calc((var(--alert-button-height) - 1.45em) / 2) 0;
}
/* The text keeps its own width and grows into the rest of the line, so
* buttons that fit end at the right; when they do not fit they drop
* under the text, at its left edge */
.ui_alert_text {
flex: 1 0 auto;
max-width: 100%;
min-width: 0;
}
.ui_alert_actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 6px;
}
.ui_alert_actions form { display: contents; }
/* Buttons printed as a form end table become items of the actions row,
* like bare buttons */
.ui_alert_actions table.ui_form_end_buttons,
.ui_alert_actions table.ui_form_end_buttons > tbody,
.ui_alert_actions table.ui_form_end_buttons > tbody > tr,
.ui_alert_actions table.ui_form_end_buttons td { display: contents; }
.ui_alert_box table.ui_form_end_buttons { margin: 6px 0 0; }
.ui_alert_actions input.ui_submit, .ui_alert_actions input.ui_button,
.ui_alert_actions button,
.ui_alert_box table.ui_form_end_buttons input.ui_submit {
min-height: var(--alert-button-height);
margin: 0;
padding: 0 10px;
font-size: 12.5px;
line-height: 1.2;
}
/* The first of several buttons in a notice is its main action, filled
* in the notice's own color and a shade darker under the mouse. The
* buttons are either bare inputs or the cells of a form end table; in
* the table the first cell's button is the main one, unless it deletes,
* and the selectors outrank the primary button rule of ordinary forms. */
.ui_alert_box input.ui_submit:first-of-type:not(:last-of-type),
.ui_alert_box table.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]) {
background: var(--text-color-info);
border-color: transparent;
color: var(--alert-button-text);
font-weight: 600;
}
.ui_alert_box.alert-success input.ui_submit:first-of-type:not(:last-of-type),
.ui_alert_box.alert-success table.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]) {
background: var(--text-color-success);
}
.ui_alert_box.alert-warning input.ui_submit:first-of-type:not(:last-of-type),
.ui_alert_box.alert-warning table.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]) {
background: var(--alert-button-warning);
color: var(--alert-button-warning-text);
}
.ui_alert_box.alert-danger input.ui_submit:first-of-type:not(:last-of-type),
.ui_alert_box.alert-danger-fatal input.ui_submit:first-of-type:not(:last-of-type),
.ui_alert_box.alert-danger table.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]),
.ui_alert_box.alert-danger-fatal table.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]) {
background: var(--text-color-danger);
}
.ui_alert_box input.ui_submit:first-of-type:not(:last-of-type):hover,
.ui_alert_box table.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]):hover {
filter: brightness(0.92);
}
/* ---- Cards ---- */
/* Forms with a heading, tables of rows, collapsible sections, tab boxes
* and icon grids all sit in the same card */
table.shrinkwrapper, table.wrapper, div.wrapper,
details.ui_hidden_table_start, details.section, div.ui_tabs_box {
width: 100%;
margin: 0 0 var(--card-gap);
background: var(--main-content);
border: 1px solid var(--border-color);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
div.wrapper, details.ui_hidden_table_start, details.section,
div.ui_tabs_box { box-sizing: border-box; }
table.shrinkwrapper > tbody > tr > td,
table.wrapper > tbody > tr > td { padding: 0; }
/* A fixed layout keeps the label and value columns steady whatever the
* values hold */
table.shrinkwrapper > tbody > tr > td > .ui_table { table-layout: fixed; }
/* ---- Forms with a heading (ui_table_start) ---- */
/* Markup: table.shrinkwrapper > td > table.ui_table with the heading in
* thead and the label/value rows in a nested table. The heading is plain
* text over a hairline. */
table.ui_table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
table.ui_table td { vertical-align: top; }
table.ui_table > thead > tr > td,
table.ui_table > tbody > tr.maintitle > td {
padding: 9px 16px;
border-bottom: 1px solid var(--border-color-light);
font-size: 14px;
font-weight: 600;
vertical-align: middle;
}
table.ui_table > thead > tr > td * { vertical-align: middle; }
table.ui_table > thead > tr > td b { font-weight: 600; }
/* The right-hand part of a heading is plain text */
table.ui_table > thead > tr > td[align="right"] {
font-size: 13px;
font-weight: 400;
}
tr.ui_table_body > td { padding: 8px 16px 12px; }
/* Label and value cells align at the top in every form table, also in
* collapsible sections, whose table has no ui_table class */
tr.ui_form_pair > td {
padding: 4px 3px;
vertical-align: top;
}
td.ui_form_label {
text-align: end;
color: var(--text-color-desc);
}
td.ui_form_label b, td.ui_form_label label { font-weight: 400; }
/* Room between a label and its value */
tr.ui_form_pair .ui_form_value { padding-left: 10px; }
/* A row without a label, such as a grid of features, starts where a
* label would */
tr.ui_form_pair > .ui_form_value:first-child { padding-left: 3px; }
/* A row whose value holds a field, menu or button, on its own or in an
* inline wrapper, gets an invisible strut of the control height on the
* first line of both cells, so the label centers on the control and a
* value of several lines keeps its label level with the first one.
* Rows of radios or checkboxes, text areas and nested tables keep the
* plain top alignment. A select switch widget starts with its menu at
* full control height, so only its label gets the strut: one in the
* cell would push the whole widget down. */
tr.ui_form_pair:has(
> td.ui_form_value > input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="image"]),
> td.ui_form_value > select:not([multiple]):not([size]:not([size="1"])),
> td.ui_form_value > button,
> td.ui_form_value > :is(span, b, label, nobr, font) > input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="image"]),
> td.ui_form_value > :is(span, b, label, nobr, font) > select:not([multiple]):not([size]:not([size="1"]))
) > :is(td.ui_form_label, td.ui_form_value)::before,
tr.ui_form_pair:has(> td.ui_form_value > .ui_select_switch:first-child > select:first-child) > td.ui_form_label::before {
content: "";
display: inline-block;
width: 0;
height: var(--control-height);
vertical-align: middle;
}
/* Tables nested in a value cell are flush, radio tables apart */
.ui_form_pair td table tr td { padding: 0; }
.ui_form_pair td table.ui_radio_table tr td,
.ui_radio_table tr td { padding: 2px; }
/* Grids of checkboxes inside a form row, such as a server's features */
.ui_form_pair td table.ui_grid_table tr td { padding: 3px 4px; }
.ui_form_pair td table.ui_grid_table tr td:first-child { padding-left: 0; }
.ui_form_pair td > table.wrapper,
.ui_form_pair td > div > table.wrapper { margin: 0; }
/* Lists and the old bar charts some modules print inside forms */
table.ui_table dt, table.ui_table dd { padding: 0 2px; }
table.ui_table td div.barchart * { margin: 0; }
.barchart {
padding: 1px;
border: 1px solid var(--border-color);
white-space: nowrap;
}
/* A form inside another form's row is an inner box */
table.shrinkwrapper table.shrinkwrapper,
table.shrinkwrapper table.wrapper,
table.wrapper table.wrapper {
margin: 4px 0;
border-color: var(--border-color-light);
box-shadow: none;
}
/* A grid in a form row, such as the feature checkboxes, is plain: the
* card edge and shadow the theme gives every grid would show as a line
* under its last row */
.ui_form_pair td table.wrapper:has(> tbody > tr > td > table.ui_grid_table) {
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
/* Nested form tables have no edge, unless marked with the border class */
table.ui_table .ui_table { border: 0; }
table.ui_table.border {
border: 1px solid var(--border-color);
border-radius: var(--radius);
background: var(--main-content);
}
/* Cells of a grid of elements (ui_grid_table) */
table.ui_grid_table td { padding: 2px 4px; }
/* A grid of plain items in a card of its own, such as the list of Webmin
* users, keeps the card's 16px side padding in its first and last
* columns, with some air above its first row and under its last. Grids
* inside a form row, grids of tables and the two-column link bars keep
* their tight cells. */
table.wrapper:not(.ui_form_pair *) > tbody > tr > td > table.ui_grid_table:not(:has(.ui_links_row, table)) > tbody > tr.ui_grid_row > td.ui_grid_cell {
padding: 4px 8px;
}
table.wrapper:not(.ui_form_pair *) > tbody > tr > td > table.ui_grid_table:not(:has(.ui_links_row, table)) > tbody > tr.ui_grid_row > td.ui_grid_cell:first-child {
padding-left: 16px;
}
table.wrapper:not(.ui_form_pair *) > tbody > tr > td > table.ui_grid_table:not(:has(.ui_links_row, table)) > tbody > tr.ui_grid_row > td.ui_grid_cell:last-child {
padding-right: 16px;
}
table.wrapper:not(.ui_form_pair *) > tbody > tr > td > table.ui_grid_table:not(:has(.ui_links_row, table)) > tbody > tr.ui_grid_row:first-child > td.ui_grid_cell {
padding-top: 8px;
}
table.wrapper:not(.ui_form_pair *) > tbody > tr > td > table.ui_grid_table:not(:has(.ui_links_row, table)) > tbody > tr.ui_grid_row:last-child > td.ui_grid_cell {
padding-bottom: 8px;
}
/* Collapsible sections (ui_hidden_table_start) and the system
* information cards share one look */
details.ui_hidden_table_start > summary, details.section > summary {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 16px;
border-radius: var(--radius);
font-size: 14px;
font-weight: 600;
list-style: none;
}
details.section > summary > span { flex: 1 1 auto; }
details[open].ui_hidden_table_start > summary,
details[open].section > summary {
border-bottom: 1px solid var(--border-color-light);
border-radius: var(--radius) var(--radius) 0 0;
}
details.ui_hidden_table_start > summary span { white-space: nowrap; }
details.ui_hidden_table_start > summary span.rightheading {
margin-left: auto;
font-size: 13px;
font-weight: 400;
}
details.ui_hidden_table_start > table {
box-sizing: border-box;
width: 100%;
padding: 8px 14px 10px;
}
.section-body { padding: 10px 16px 14px; }
/* Text above a dashboard block's content */
.section-note {
margin: 0 0 8px;
color: var(--text-color-muted);
}
/* ---- Tables of rows (ui_columns_start) ---- */
/* The heading rows carry the maintitle class from the theme config as
* well as the ui_columns classes; body rows use mainbody, and the
* row0/row1, mainsel and mainhigh names are kept for modules that print
* them directly. */
table.ui_columns {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
table.ui_columns > thead > tr > td,
table.ui_columns > tbody > tr.maintitle > td,
tr.ui_columns_heads > td, tr.ui_columns_heading > td, tr.ui_columns_header > td {
padding: 8px 16px;
border-bottom: 1px solid var(--border-color);
font-size: 12px;
font-weight: 600;
color: var(--text-color-muted);
vertical-align: middle;
}
/* The title row above the column headings is larger */
table.ui_columns > thead > tr.ui_columns_heading > td {
font-size: 14px;
color: var(--text-color);
}
table.ui_columns > thead > tr > td b { font-weight: inherit; }
/* Rows are as tall as their content plus 4px above and below, in
* every table alike */
table.ui_columns > tbody > tr > td,
table.sortable > tbody > tr > td {
padding: 4px 16px;
border-bottom: 1px solid var(--border-color-lighter);
color: var(--text-color);
vertical-align: middle;
}
table.ui_columns > tbody > tr:last-child > td { border-bottom: 0; }
/* The first column, or the one after a checkbox, names the row and is
* a little bolder; the checkbox column is as narrow as it can be */
table.ui_columns > tbody > tr > td:first-child,
table.ui_columns > tbody > tr > td.ui_checked_checkbox + td,
table.ui_columns > tbody > tr > td.ui_radio_radio + td {
color: var(--text-color);
font-weight: 500;
}
table.ui_columns > tbody > tr > td.ui_checked_checkbox,
table.ui_columns > tbody > tr > td.ui_radio_radio {
width: 1%;
padding-right: 4px;
}
table.ui_columns > tbody > tr.ui_columns_header > td,
tr.ui_columns_header > td {
font-size: inherit;
color: var(--text-color);
background: var(--bg-table-header);
}
/* Rows carry bgcolor attributes from the theme config; the palette wins */
tr.row0, tr.row1, tr.mainbody { background: transparent; }
tr.maintitle { background: transparent; }
/* Rows highlight on hover, except in tables of input fields */
table.ui_columns > tbody > tr:not(:has(input:not([type="checkbox"]):not([type="radio"]), select, textarea)):hover > td,
tr.mainhigh > td {
background: var(--bg-table-header);
}
tr.mainsel > td, tr.mainhighsel > td,
tr.ui_checked_columns:has(> td > input:checked) > td,
tr.ui_radio_columns:has(> td > input:checked) > td {
background: var(--element-highlight-bg-color);
}
table.ui_columns.no-hover > tbody > tr:hover > td { background: transparent; }
.ui_checked_columns .ui_checked_checkbox { min-width: 20px; }
.ui_emptymsg { color: var(--text-color-muted); }
/* The icon grid table, which also carries the main id, and the compact
* tables some modules mark as simple */
table#main {
margin: 0;
padding: 0;
border: 0;
}
table#main td { padding: 4px; }
.table-simple {
width: auto !important;
max-width: 100%;
}
.table-simple tr td { padding: 0 4px !important; }
/* A colored word alone in a cell, such as Yes or No, becomes a pill */
table.ui_columns > tbody > tr > td > .ui_text_color:only-child {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 600;
line-height: 1.4;
background: var(--bg-color-neutral);
}
table.ui_columns > tbody > tr > td > .text_type_success:only-child {
background: var(--bg-color-success);
}
table.ui_columns > tbody > tr > td > .text_type_info:only-child {
background: var(--bg-color-info);
}
table.ui_columns > tbody > tr > td > .text_type_danger:only-child {
background: var(--bg-color-danger);
}
table.ui_columns > tbody > tr > td > .text_type_warn:only-child {
background: var(--bg-color-neutral);
color: var(--text-color-neutral-stressed) !important;
}
/* A links row right above a table is drawn as the card's toolbar, and
* a button row right after it as the card's footer. Both bars are
* tinted, so they stand apart from the white rows of the table. */
form.ui_form > .ui_links_row:has(+ table.wrapper),
form.ui_form > table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row):has(+ table.wrapper) {
margin: 0;
padding: 11px 16px;
background: var(--bg-table-header);
border: 1px solid var(--border-color);
border-bottom: 0;
border-radius: var(--radius) var(--radius) 0 0;
box-shadow: var(--shadow);
}
/* The two-column variant, with links at the left and the right, is a
* table: its cell carries the bar padding, not the table */
form.ui_form > table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row):has(+ table.wrapper) {
padding: 0;
}
form.ui_form > table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row):has(+ table.wrapper) > tbody > tr > td {
padding: 11px 16px;
}
form.ui_form > table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row) table.ui_grid_table td {
padding: 0;
}
form.ui_form > table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row) .ui_links_row {
margin: 0;
}
form.ui_form > .ui_links_row:has(+ table.wrapper) + table.wrapper,
form.ui_form > table.wrapper:has(.ui_links_row) + table.wrapper {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
form.ui_form > table.wrapper:has(+ table.ui_form_end_buttons) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
form.ui_form > table.wrapper + table.ui_form_end_buttons {
box-sizing: border-box;
width: 100%;
margin: calc(-1px - var(--card-gap)) 0 var(--card-gap);
padding: 11px 16px;
background: var(--bg-table-header);
border: 1px solid var(--border-color);
border-top: 1px solid var(--border-color-light);
border-radius: 0 0 var(--radius) var(--radius);
}
form.ui_form > table.wrapper + table.ui_form_end_buttons + .ui_links_row {
margin-top: -4px;
}
/* A links row right after a table is the card's footer, matching the
* toolbar above it; any button row after that footer joins it */
form.ui_form > table.wrapper:has(+ .ui_links_row),
form.ui_form > table.wrapper:has(+ table.wrapper > tbody > tr > td > table.ui_grid_table .ui_links_row) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
form.ui_form > table.wrapper + .ui_links_row,
form.ui_form > table.wrapper + table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row) {
margin: calc(-1px - var(--card-gap)) 0 var(--card-gap);
padding: 11px 16px;
background: var(--bg-table-header);
border: 1px solid var(--border-color);
border-top: 1px solid var(--border-color-light);
border-radius: 0 0 var(--radius) var(--radius);
box-shadow: var(--shadow);
}
form.ui_form > table.wrapper + table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row) {
padding: 0;
}
form.ui_form > table.wrapper + table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row) > tbody > tr > td {
padding: 11px 16px;
}
/* A links row and a button row under one table form a single footer
* bar: 11px above the links, 11px between the rows and 11px under the
* buttons, as in the toolbar above the table */
form.ui_form > table.wrapper + .ui_links_row:has(+ table.ui_form_end_buttons),
form.ui_form > table.wrapper + table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row):has(+ table.ui_form_end_buttons) {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: 0;
border-radius: 0;
box-shadow: none;
}
form.ui_form > table.wrapper + table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row):has(+ table.ui_form_end_buttons) > tbody > tr > td {
padding-bottom: 0;
}
form.ui_form > table.wrapper + .ui_links_row + table.ui_form_end_buttons,
form.ui_form > table.wrapper + table.wrapper:has(> tbody > tr > td > table.ui_grid_table .ui_links_row) + table.ui_form_end_buttons {
box-sizing: border-box;
width: 100%;
margin: 0 0 var(--card-gap);
padding: 11px 16px;
background: var(--bg-table-header);
border: 1px solid var(--border-color);
border-top: 0;
border-radius: 0 0 var(--radius) var(--radius);
box-shadow: var(--shadow);
}
/* A small form of controls before another form or a table, such as the
* search box over a list of web apps or the file menu over a config
* editor, is one row of controls with the card gap under it, so it
* never touches what follows. The core prints a script right after
* every form end, so the next block may come after that script. */
form:not(.menu-form):not(:has(table)):has(+ form, + table.wrapper, + table.shrinkwrapper, + script + form, + script + table.wrapper, + script + table.shrinkwrapper) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
margin: 0 0 var(--card-gap);
}
/* Icon grids (icons_table): the table is a grid of equal columns and
* its cells the items, so the icons reflow as the page narrows instead
* of squeezing their columns, and a partly filled last row keeps the
* columns of the rows above; the empty cells that fill the last row are
* dropped */
div.wrapper:has(> table.icons_table) { padding: 8px; }
table.icons_table {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
box-sizing: border-box;
width: 100%;
}
table.icons_table > tbody, table.icons_table > tbody > tr {
display: contents;
}
/* Each cell stacks the icon over its name, centered. The name is small
* text in the text color, balanced over two lines when it wraps, and
* takes the link color only on hover */
table.icons_table > tbody > tr > td {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
box-sizing: border-box;
/* The cell's own width attribute is a share of the old table, and
* would shrink the cell to a sliver of its grid column */
width: auto;
min-width: 0;
padding: 12px 8px 14px;
text-align: center;
}
table.icons_table > tbody > tr > td:empty { display: none; }
table.icons_table table {
display: table;
margin: 0;
}
table.icons_table a {
max-width: 100%;
color: var(--text-color);
font-size: 13px;
font-weight: 500;
line-height: 1.35;
text-decoration: none;
text-wrap: balance;
overflow-wrap: break-word;
}
table.icons_table a:hover {
color: var(--link-color);
text-decoration: none;
}
table.icons_table img { display: block; }
/* ---- Tabs (ui_tabs_start) ---- */
/* The tab row sits on the ground, so its line is the darker one */
div.ui_tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin: 0 0 12px;
border-bottom: 1px solid var(--border-color-darker);
}
div.ui_tabs a.ui_tab {
display: block;
margin-bottom: -1px;
padding: 8px 12px 10px;
border-bottom: 2px solid transparent;
color: var(--text-color-muted);
font-weight: 500;
text-decoration: none;
}
div.ui_tabs a.ui_tab:hover { color: var(--text-color); }
div.ui_tabs a.ui_tab.ui_tab_selected {
border-bottom-color: var(--accent-color);
color: var(--text-color);
font-weight: 600;
}
div.ui_tabs a.ui_tab sup {
vertical-align: text-top;
font-size: 70%;
}
div.ui_tabs_box { padding: 12px 14px 14px; }
/* The last block in a tab body ends at the box padding */
div.ui_tabs_box .ui_tabs_start > form:last-of-type > table.ui_form_end_buttons:last-child,
div.ui_tabs_box .ui_tabs_start > table.ui_form_end_buttons:last-of-type,
div.ui_tabs_box .ui_tabs_start > form:last-of-type > table.shrinkwrapper:last-child,
div.ui_tabs_box .ui_tabs_start > table.shrinkwrapper:last-of-type,
div.ui_tabs_box .ui_tabs_start > table.wrapper:last-of-type {
margin-bottom: 0;
}
/* ---- Help bubbles and tooltips ---- */
.ui_help {
display: inline-block;
margin: 0 0 0 5px;
font-size: 10px;
line-height: 14px;
min-width: 14px;
text-align: center;
color: var(--text-color-inverse);
background: var(--text-color-muted);
border-radius: 50%;
cursor: help;
vertical-align: super;
opacity: 0.8;
}
.ui_help samp { font: inherit; }
/* Help bubbles show while the mouse is over their cell; the one next to
* a form label always shows */
tr:has(.ui_help) td:not(:hover) .ui_help { visibility: hidden; }
tr:has(.ui_help) td.ui_form_label .ui_help { visibility: visible; }
td:not(:hover) .ui_link_help { visibility: hidden; }
.ui_link_help {
display: initial;
float: right;
margin-right: 4px;
}
.ui_text_wrap { float: right; }
/* Tooltips from the aria-label of elements marked data-tooltip: a dark
* box with an arrow above the element, shown on hover */
[data-tooltip][aria-label] {
position: relative;
vertical-align: top;
}
[data-tooltip][aria-label]::after {
content: "";
position: absolute;
bottom: 100%;
right: 50%;
width: 0;
height: 0;
margin-bottom: 5px;
border: 10px solid transparent;
border-top-color: var(--text-color);
border-bottom-width: 0;
transform: translateX(50%);
pointer-events: none;
opacity: 0;
visibility: hidden;
}
[data-tooltip][aria-label]::before {
content: attr(aria-label);
position: absolute;
bottom: calc(15px + 100%);
right: 50%;
width: max-content;
max-width: 50vw;
padding: 4px 8px;
border-radius: var(--radius-sm);
background: var(--text-color);
color: var(--bg-color);
font-size: 11px;
font-weight: normal;
text-align: center;
text-shadow: none;
word-break: break-all;
transform: translateX(50%);
pointer-events: none;
opacity: 0;
visibility: hidden;
}
[data-tooltip][aria-label]:hover::before,
[data-tooltip][aria-label]:hover::after {
visibility: visible;
opacity: 1;
transition-duration: 0.5s;
}
/* ---- Inline disclosures (ui_details) ---- */
/* The arrow of a closed disclosure marked on-hover shows only under the
* mouse */
details:not([open]).on-hover:not(:hover) summary::after {
visibility: hidden !important;
}
/* An inline disclosure is plain text with an arrow after its summary
* and its content indented under it */
details.inline {
margin: 0;
padding: 0;
background: transparent;
border: 0;
cursor: pointer;
}
details.inline > summary { list-style: none; }
details.inline > summary::after {
content: "\23F4";
position: relative;
margin: 0 7px;
color: var(--text-color-muted);
}
details[open].inline > summary::after { content: "\23F7"; }
details.inline > summary + span {
display: inline-block;
margin: 1px 0 0 18px;
font-size: 98%;
line-height: 1.33;
cursor: default;
}
details.inline.inlined > summary,
details.inline.inlined { display: inline !important; }
details.inline.inlined > span { margin-top: 10px; }
.ui_table.ui_columns td > details.inline.inlined > span { margin-top: 0; }
details.inline > summary + span > tt { font-size: 96%; }
details.inline.fit > summary + span span,
details.inline.fit > summary + span {
display: inline-block;
width: fit-content;
}
details > span .ui_alert_box { margin-top: 5px; }
details > span .ui_alert_box:not(:has(+ p)) { margin-bottom: 5px; }
details.error, details.inline.error > summary + span {
color: var(--text-color-danger-stressed);
}
/* Line breaks the core marks with data-x-br become small block gaps;
* one right after a disclosure is dropped */
[data-second-print]:has(details) + br:has(+ [data-x-br]) { display: none; }
br[data-x-br] {
content: "";
display: block;
margin: 2em;
font-size: 42%;
}
/* ---- Left frame (left.cgi) ---- */
/* The frame shows the page ground, and the menu floats on it as a card
* with a 7px gap at the top, the left and the bottom, and rounder
* corners than the other cards. At the right the card meets the frame
* edge, so the page padding of the right frame is the whole gap between
* the card and the content, and a page opened in a window of its own
* keeps that same padding on every side. The card is as tall as the
* frame, so its bottom edge stays in view, and the menu scrolls inside
* it. The body height has to win over the theme's own height reset. */
body.left-frame {
box-sizing: border-box;
height: 100vh !important;
padding: 7px 0 7px 7px;
overflow: hidden;
background: var(--bg-color);
}
.menu-panel {
box-sizing: border-box;
height: 100%;
overflow-y: auto;
background: var(--bg-color-frame);
border: 1px solid var(--border-color-panel);
border-radius: 14px;
box-shadow: 0 2px 6px var(--shadow-color);
}
/* The brand block and the product switch stick to the top of the panel
* while the menu scrolls under them */
.menu-top {
position: sticky;
top: 0;
z-index: 2;
padding-bottom: 12px;
background: var(--bg-color-frame);
border-bottom: 1px solid var(--border-color-frame);
}
/* A bar bouncing along the top edge of the panel while the right frame
* loads a page; the menu script sets the class on the root */
.menu-loader {
display: none;
position: absolute;
top: 0;
right: 0;
left: 0;
height: 3px;
overflow: hidden;
}
html.loading-right .menu-loader { display: block; }
.menu-loader::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 35%;
height: 100%;
border-radius: 2px;
background: var(--accent-color);
animation: menu-bounce 0.9s ease-in-out infinite alternate;
}
@keyframes menu-bounce { to { left: 65%; } }
/* The brand block: the logo has as much room above it as below it, and
* the logo files are trimmed to their drawing, so every product sits the
* same way. The logo links to the system information page. */
.menu-brand {
padding: 14px 16px 14px;
border-bottom: 1px solid var(--border-color-frame);
}
.menu-brand-name {
display: block;
margin: 0 0 12px;
line-height: 0;
}
/* The product logo, in the version matching the palette */
.menu-brand-logo {
display: var(--show-light);
height: 36px;
width: auto;
max-width: 100%;
}
.menu-brand-logo-dark { display: var(--show-dark); }
/* Host and login under the logo: small lines with a leading icon; a
* long hostname wraps rather than being cut */
.menu-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.menu-host, .menu-user {
display: flex;
align-items: flex-start;
gap: 6px;
min-width: 0;
font-size: 12px;
line-height: 1.4;
color: var(--text-color-desc);
}
.menu-host svg, .menu-user svg {
flex: 0 0 auto;
margin-top: 2px;
color: var(--text-color-muted);
}
.menu-host span, .menu-user span {
min-width: 0;
overflow-wrap: anywhere;
}
.menu-user b {
color: var(--text-color);
font-weight: 600;
}
/* Product switch between Virtualmin, Cloudmin, Webmin and Usermin */
.mode {
display: flex;
margin: 14px 16px 0;
padding: 3px;
background: var(--bg-color-switch);
border-radius: var(--radius);
}
.mode > b {
flex: 1 1 0;
padding: 3px 0;
border-radius: var(--radius-sm);
background: var(--bg-color-switch-on);
box-shadow: 0 1px 2px var(--shadow-color);
color: var(--text-color);
font-size: 12.5px;
font-weight: 600;
text-align: center;
white-space: nowrap;
cursor: default;
}
.mode > b:has(> a) {
background: transparent;
box-shadow: none;
color: var(--text-color-muted);
font-weight: 400;
cursor: pointer;
}
.mode > b > a {
display: flex;
justify-content: center;
align-items: center;
color: inherit;
text-decoration: none;
}
.mode > b > a:hover { color: var(--text-color); }
/* While the other product's menu loads, the switch ignores further
* clicks, the pill that was clicked reads as chosen with a small
* spinning ring before its name, and the old one fades */
.mode.loading { pointer-events: none; }
.mode.loading > b:not(.active) { opacity: 0.5; }
.mode.loading > b.active { color: var(--text-color); }
/* The ring sits 6px before the name. Its end margin and its negative
* start margin cancel its own width, so it takes no room in the centered
* row and the name does not move when it appears */
.mode.loading > b.active > a::before {
content: "";
flex: 0 0 auto;
box-sizing: border-box;
width: 10px;
height: 10px;
margin-block: 0;
margin-inline: -16px 6px;
border: 2px solid var(--border-color-darker);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: menu-spin 0.7s linear infinite;
}
@keyframes menu-spin { to { transform: rotate(360deg); } }
/* Room under the last item, so the menu scrolls past it */
.leftmenu { padding: 14px 8px 56px; }
/* The creation link of the menu is its one filled button */
.menu-create {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 0 8px 12px;
padding: 6px 12px;
border-radius: var(--radius-sm);
background: var(--accent-color);
color: var(--text-color-inverse);
font-weight: 600;
text-align: center;
text-decoration: none;
}
.menu-create:hover {
background: var(--accent-color-hover);
color: var(--text-color-inverse);
text-decoration: none;
}
/* Forms in the menu: a caps label over a field that fills the panel */
.menu-form { margin: 0 8px 12px; }
.menu-label {
display: block;
margin-bottom: 4px;
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--text-color-muted);
}
.menu-field {
display: flex;
gap: 6px;
}
.menu-field select, .menu-field input {
flex: 1 1 auto;
width: 100%;
min-width: 0;
font-size: 12.5px;
}
/* Menu rows: 26px links that highlight under the mouse */
.menu-link {
display: block;
padding-block: 4px;
padding-inline: 12px 8px;
border-radius: var(--radius-sm);
line-height: 18px;
color: var(--text-color);
text-decoration: none;
}
.menu-link:hover {
background: var(--bg-color-hover);
color: var(--text-color);
text-decoration: none;
}
/* Links inside a category are indented under its name */
.menu-link.menu-sub { padding-inline-start: 24px; }
/* The logout link stands a little apart, muted, with its icon before
* the text like any other icon in a link */
.menu-link.menu-logout {
margin-top: 8px;
color: var(--text-color-muted);
}
.menu-link svg {
margin-inline-end: 6px;
vertical-align: -3px;
opacity: 0.85;
}
/* Items the user cannot use are grayed; small marks after a name sit at
* the top of the line */
.menu-link.inactive, .menu-text.inactive {
opacity: 0.6;
filter: grayscale(1);
}
.menu-link sub {
vertical-align: top;
font-size: 80%;
}
/* Categories are rows with a chevron at the right, a little taller than
* the 26px link rows, and their names are cut with an ellipsis */
details.menu-cat > summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 28px;
padding-block: 0;
padding-inline: 12px 10px;
border-radius: var(--radius-sm);
color: var(--text-color);
list-style: none;
}
details.menu-cat > summary:hover {
background: var(--bg-color-hover);
color: var(--text-color);
}
details.menu-cat > summary span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
details.menu-cat[open] { margin-bottom: 4px; }
/* Separator lines, and plain text or HTML blocks between items */
.menu-divider {
margin: 8px 8px;
border: 0;
border-top: 1px solid var(--border-color-frame);
}
.menu-text, .menu-html {
padding: 4px 12px;
font-size: 12px;
color: var(--text-color-muted);
}
.menu-html img { max-width: 100%; }
/* The link to the current page, marked in the single page layout */
.menu-link.active,
.menu-link.active:hover {
background: var(--element-highlight-bg-color);
color: var(--text-color);
font-weight: 600;
}
/* ---- Single page layout ---- */
/* Without frames, every page carries the menu panel at the start of its
* body. The panel is fixed at the left with the same 7px gap as in
* the left frame, and the body leaves as much room for it as the left
* frame would take, so a page looks just as it does in the frameset. The
* page and the menu scroll separately. The logical properties put the
* menu at the right for right-to-left languages. */
body.single-page {
padding-inline-start: calc(var(--menu-width, 260px) + var(--page-pad-x));
}
/* The system information page pads its own content */
body.right-frame.single-page {
padding-inline-start: var(--menu-width, 260px);
}
.page-menu {
position: fixed;
z-index: 20;
top: 7px;
bottom: 7px;
inset-inline-start: 7px;
width: calc(var(--menu-width, 260px) - 7px);
height: auto;
}
/* The button that opens the menu on a narrow screen, square and with an
* icon only; it shows a cross while the menu is open */
.page-menu .menu-toggle {
display: none;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 34px;
min-height: 0;
height: 34px;
margin: 0;
padding: 0;
}
.page-menu .menu-toggle svg { display: block; }
.page-menu .menu-toggle .menu-toggle-close,
.page-menu.open .menu-toggle .menu-toggle-open { display: none; }
.page-menu.open .menu-toggle .menu-toggle-close { display: block; }
/* A page that still ends up in a frame, such as in the old frameset right
* after the layout was changed, hides its menu */
html.menu-framed .page-menu { display: none; }
html.menu-framed body.single-page {
--menu-bar-height: 0px;
padding-inline-start: var(--page-pad-x);
}
html.menu-framed body.right-frame.single-page { padding-inline-start: 0; }
/* On a narrow screen the menu is a bar across the top with the logo and
* the button, and the pages start under it; their sticky headers stop
* under it too. The button unfolds the bar into the whole menu, which
* covers the screen and scrolls, while the page behind it stays put. */
@media (max-width: 800px) {
body.single-page {
--menu-bar-height: 52px;
--sticky-top: var(--menu-bar-height);
padding-top: calc(var(--menu-bar-height) + var(--page-pad-top));
padding-inline-start: var(--page-pad-x);
}
body.right-frame.single-page {
padding-top: var(--menu-bar-height);
padding-inline-start: 0;
}
body.single-page:has(> .page-menu.open) { overflow: hidden; }
/* The bar has no rounded corners, and a hairline and a soft shadow
* along its bottom edge instead of a border, so that it is exactly
* as tall as its brand row */
.page-menu {
top: 0;
bottom: auto;
inset-inline: 0;
width: auto;
max-height: 100vh;
max-height: 100dvh;
overflow: hidden;
border: 0;
border-radius: 0;
box-shadow: 0 1px 0 var(--border-color-panel),
0 2px 6px var(--shadow-color);
}
.page-menu.open {
bottom: 0;
overflow-y: auto;
}
/* The brand row: the logo at the start, the button at the end, and
* the host and login lines under them while the menu is open */
.page-menu .menu-brand {
display: flex;
flex-wrap: wrap;
align-items: center;
column-gap: 12px;
box-sizing: border-box;
min-height: var(--menu-bar-height);
padding-block: 0;
padding-inline: 16px 10px;
border-bottom: 0;
}
.page-menu .menu-brand-name {
flex: 1 1 auto;
margin: 0;
}
.page-menu .menu-brand-logo { height: 28px; }
.page-menu .menu-toggle { display: inline-flex; }
.page-menu .menu-meta {
flex: 1 0 100%;
padding-bottom: 14px;
}
.page-menu.open .menu-brand {
border-bottom: 1px solid var(--border-color-frame);
}
/* The folded bar shows the brand row alone */
.page-menu:not(.open) :is(.menu-meta, .mode, .leftmenu) {
display: none;
}
.page-menu:not(.open) .menu-top {
padding-bottom: 0;
border-bottom: 0;
}
}
/* ---- System information page (right.cgi) ---- */
/* The page is a widget page: its header holds the title, the operating
* system and time, and the module links as buttons; then tiles, then
* the information blocks as collapsible cards. */
body.right-frame { padding: 0; }
/* Blocks are spaced by the widget page gap alone. The margins are set
* here again because the theme's own card and notice rules outrank the
* widget stylesheet's sibling gap. */
.right-frame .ui_page.page {
--ui-gap: var(--card-gap);
padding: 0 var(--page-pad-x) var(--page-pad-bottom);
}
.right-frame .ui_page.page > * { margin: 0; }
.right-frame .ui_page.page > * + * { margin-top: var(--ui-gap); }
/* The header bleeds over the page padding and stays at the top, or under
* the menu bar of the single page layout; its selector outranks the
* margin reset above */
.right-frame .ui_page.page > .ui_page_head {
position: sticky;
top: var(--sticky-top, 0);
z-index: 5;
align-items: center;
margin: 0 calc(-1 * var(--page-pad-x)) 12px;
padding: 14px var(--page-pad-x) 12px;
background: var(--bg-color);
}
.right-frame .ui_page_titles { padding-left: var(--title-inset); }
.right-frame .ui_page_title {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.3px;
}
.right-frame .ui_page_desc {
margin-top: 4px;
padding-left: 2px;
color: var(--text-color-desc);
}
.right-frame .ui_page_actions {
flex-wrap: wrap;
gap: 8px;
padding-top: 0;
}
.page-action svg { color: var(--text-color-muted); }
/* Tiles: one figure per card, with its label above and a bar under it.
* Four across, two on a narrow frame and one on a very narrow one. */
.tiles { --ui-gap: var(--card-gap); }
.tiles.ui_grid_fixed { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1100px) {
.tiles.ui_grid_fixed { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
.tiles.ui_grid_fixed { grid-template-columns: minmax(0, 1fr); }
}
.tile .ui_card_body { padding: 12px 16px 14px; }
.tile .ui_stat { gap: 4px; }
.tile .ui_stat_label {
order: -1;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--text-color-muted);
}
.tile .ui_stat_value {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.3px;
}
.tile .ui_stat_desc {
font-size: 12.5px;
color: var(--text-color-desc);
}
.tile .ui_progress { margin-top: 10px; }
/* Information cards */
.section .ui_dl {
--ui-dl-label: 150px;
gap: 8px 40px;
}
.section .ui_dl_cols > .ui_dl_row { min-width: calc((100% - 40px) / 2); }
.section .ui_dl_row { gap: 3px 14px; }
.section .ui_dl_row dt {
font-weight: 400;
color: var(--text-color-desc);
text-align: end;
}
.section .ui_dl_row dd { display: block; }
/* Values in the system status card are monospace */
.section[data-name="system-statussysinfo"] .ui_dl_row dd {
font-family: var(--font-family-mono);
font-size: 12.5px;
}
.section-body > .ui_dl + .ui_dl { margin-top: 8px; }
/* Bars in cards and tiles are thin and round, and their second segment
* is a soft accent instead of gray */
.section .ui_progress { margin-top: 6px; }
.section .ui_progress_track, .tile .ui_progress_track {
height: 6px;
border-radius: 999px;
}
.section .ui_progress_bar.ui_bg_neutral,
.tile .ui_progress_bar.ui_bg_neutral { background: var(--accent-color-soft); }
.section-body > table.wrapper:last-child { margin-bottom: 0; }
/* A paragraph break between a note and the table under it, as in the
* package updates block, leaves a clear gap */
.section-body p:has(+ table) { margin-bottom: 10px; }
/* Rows of counts: every count carries the separator at its right, so a
* second row of counts lines up with the first */
.section .ui_stats { gap: 8px 16px; }
.section .ui_stats .ui_stat {
padding: 2px 16px 2px 0;
border-right: 1px solid var(--border-color-light);
}
.section .ui_stats .ui_stat:last-child { border-right: 0; }
.section .ui_stats .ui_stat_value {
font-size: 24px;
font-weight: 700;
}
.section .ui_stats .ui_stat_label {
font-weight: 400;
color: var(--text-color-desc);
}
/* ---- Widgets ---- */
/* The ui-lib widgets keep square corners by default; this theme rounds
* them like its own cards. */
.ui_card, .ui_stat, .ui_empty, .ui_choice, .ui_radio_list,
.ui_select_switch_panel, .ui_code_block, .ui_multi_items {
border-radius: var(--radius);
}
.ui_card { box-shadow: var(--shadow); }
.ui_card_title { font-size: 15px; }
.ui_badge, .ui_chip, .ui_code, .ui_search_input {
border-radius: var(--radius-sm);
}
.ui_progress_track, .ui_progress_bar { border-radius: 999px; }
/* ---- Popup windows ---- */
/* Choosers and help open in small windows split into frames, some only
* 50px tall, so their pages use tighter spacing and controls */
body#popup:not(.left-frame):not(.right-frame) {
--control-height: 28px;
padding: 8px;
font-size: 12.5px;
}
body#popup:not(.left-frame):not(.right-frame) input:not([type="checkbox"]):not([type="radio"]),
body#popup:not(.left-frame):not(.right-frame) select,
body#popup:not(.left-frame):not(.right-frame) button {
min-height: 28px;
padding: 2px 8px;
}
body#popup:not(.left-frame):not(.right-frame) table.wrapper {
margin: 0 0 6px;
}
/* Forms in popups have no box of their own: the chooser's bottom frame
* is 50px tall and holds only a button and a field */
body#popup:not(.left-frame):not(.right-frame) table.shrinkwrapper {
margin: 0;
border: 0;
background: transparent;
box-shadow: none;
}
body#popup:not(.left-frame):not(.right-frame) table.ui_columns > thead > tr > td {
padding: 4px 8px;
}
body#popup:not(.left-frame):not(.right-frame) table.ui_columns > tbody > tr > td {
height: auto;
padding: 3px 8px;
white-space: nowrap;
}
body#popup:not(.left-frame):not(.right-frame) tr.ui_table_body > td {
padding: 0;
}
body#popup:not(.left-frame):not(.right-frame) tr.ui_form_pair > td {
padding: 2px 3px;
}
body#popup:not(.left-frame):not(.right-frame) table.ui_form_end_buttons {
margin: 4px 0;
}
body#popup:not(.left-frame):not(.right-frame) hr { margin: 6px 0; }
body#popup:not(.left-frame):not(.right-frame) p { margin: 4px 0; }
/* ---- Login page ---- */
/* The login form is a narrow card in the middle of the page. The theme
* merges its class into the card table, as the core prints it as a
* second class attribute. A header without a title or links, as on this
* page, takes no room. */
table.shrinkwrapper.loginform {
max-width: 440px;
margin: 24px auto var(--card-gap);
}
/* Notices above the login form, the certificate warning and the login
* messages the script tags as failed, logout or timed out, take the
* card's width and sit a card gap above it. The messages are drawn by
* the error page rules below. The empty paragraph after a message is
* dropped. */
body:has(> center > form.ui_form[action*="_login.cgi"]) > center > :is(.ui_alert_box, h3.ui_login_message) {
max-width: 640px;
margin: var(--card-gap) auto calc(var(--card-gap) * 2) auto;
text-align: start;
}
body:has(> center > form.ui_form[action*="_login.cgi"]) > center > h3.ui_login_message + p:empty {
display: none;
}
body:has(> center > form.ui_form[action*="_login.cgi"]) > center > .ui_alert_box + h3.ui_login_message {
margin-top: var(--card-gap);
}
body:has(> center > form.ui_form[action*="_login.cgi"]) > center > :is(.ui_alert_box + form.ui_form, h3.ui_login_message + p + form.ui_form) > table.shrinkwrapper.loginform {
margin-top: var(--card-gap);
}
table.header:has(#headln2c > font:empty):not(:has(a, img, #headln1)) {
display: none;
}
/* The login button is the main action of the page, and the forgotten
* password form under the card is a plain link with room above it,
* not a filled button; its selectors outrank the primary button rule */
form.ui_form[action*="session_login.cgi"] > input.ui_submit {
background: var(--accent-color);
border-color: var(--accent-color-hover);
color: var(--text-color-inverse);
font-weight: 600;
}
form.ui_form[action*="session_login.cgi"] > input.ui_submit:hover {
background: var(--accent-color-hover);
}
form.ui_form[action*="forgot_form.cgi"] { margin-top: 14px; }
form.ui_form[action*="forgot_form.cgi"] table.ui_form_end_buttons td:first-child > input.ui_submit {
min-height: 0;
padding: 4px 8px;
border-color: transparent;
background: transparent;
color: var(--link-color);
font-weight: 500;
}
form.ui_form[action*="forgot_form.cgi"] table.ui_form_end_buttons td:first-child > input.ui_submit:hover {
border-color: transparent;
background: transparent;
color: var(--link-color-hover);
text-decoration: underline;
}
/* The password reset flow: the form asking for the username, the page
* confirming the email, and the new password form from the emailed
* link. Each prints its title in the page header and its content inside
* a center block. The header becomes the head row of a card like the
* login form's, with the card's edge and top corners, and the center
* block continues the card under it. The confirmation page is told
* apart by its content, text with two tt elements and no form. The
* empty paragraph the core prints after the header is dropped, and a
* warning above the form takes the card's width. */
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > table.header {
position: static;
box-sizing: border-box;
width: 100%;
max-width: 440px;
margin: 24px auto 0;
padding: 9px 16px;
background: var(--main-content);
border: 1px solid var(--border-color);
border-bottom-color: var(--border-color-light);
border-radius: var(--radius) var(--radius) 0 0;
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > table.header #headln2c {
padding: 0;
font-size: 14px;
font-weight: 600;
letter-spacing: 0;
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > table.header + p:empty {
display: none;
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > .ui_alert_box {
max-width: 440px;
margin: 0 auto;
border-radius: 0;
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > center {
box-sizing: border-box;
max-width: 440px;
margin: 0 auto var(--card-gap);
padding: 12px 16px 6px;
background: var(--main-content);
border: 1px solid var(--border-color);
border-top: 0;
border-radius: 0 0 var(--radius) var(--radius);
box-shadow: var(--shadow);
text-align: start;
}
/* The username form: the field on a line of its own under its label.
* The new password form: its own card table melts into the card. Both:
* the button centered under the fields with room above it, like the
* buttons under the login card. The confirmation text keeps the card's
* bottom padding. */
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > b {
display: block;
margin: 12px 0 4px;
font-weight: 400;
color: var(--text-color-desc);
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > input.ui_textbox {
width: 100%;
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > table.shrinkwrapper {
margin: 0;
border: 0;
background: transparent;
box-shadow: none;
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > table.shrinkwrapper tr.ui_table_body > td {
padding: 0;
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > table.ui_form_end_buttons {
margin: 10px 0 8px;
}
:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]),
body:has(> center > form.ui_form[action*="forgot.cgi"]),
body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > table.ui_form_end_buttons > tbody > tr {
justify-content: center;
}
body:has(> table.header + p + center > tt:first-child) > center {
padding-bottom: 14px;
line-height: 1.5;
}
/* ---- Error page ---- */
/* The core prints an error as a heading between rules, with an optional
* call stack table, and its own inline style after the theme's. The
* heading becomes a danger notice with the notice icon, the rules are
* dropped, and the stack becomes a card table under a small label. The
* selectors repeat the classes of the inline style, so they outrank it. */
hr:has(+ h3[data-fatal-error-text]), h3[data-fatal-error-text] + hr,
hr:has(+ table.err-stack), table.err-stack + hr { display: none; }
/* The notice sits close under the title; the inline style block the
* core prints right after the header marks the error page */
table.header:has(+ style[data-err]) { margin-bottom: 2px; }
h3[data-fatal-error-text], h3.err-head[data-fatal-error-text],
h3.ui_login_message {
display: flex;
box-sizing: border-box;
align-items: flex-start;
gap: 12px;
margin: 0 0 var(--card-gap);
padding: 10px 14px;
border: 1px solid var(--border-color-danger);
border-radius: var(--radius);
background: var(--bg-color-danger);
color: var(--text-color-danger-stressed);
font: 500 13px/1.45 var(--font-family);
text-transform: none;
transform: none;
white-space: normal;
overflow-wrap: break-word;
}
/* The login messages: a logout is a success notice and a timed out
* session a warning; a failed login keeps the danger look */
h3.ui_login_logout {
background: var(--bg-color-success);
border-color: var(--border-color-success);
color: var(--text-color-success-stressed);
}
h3.ui_login_timed_out {
background: var(--bg-color-warning);
border-color: var(--border-color-warning);
color: var(--text-color-warning-stressed);
}
/* The icon is one of the theme's notice icons, drawn as a mask in the
* notice color: x-circle, check-circle or the warning triangle */
h3[data-fatal-error-text]::before, h3.ui_login_message::before {
content: "";
flex: 0 0 auto;
width: 18px;
height: 18px;
margin-top: 1px;
background: var(--text-color-danger);
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M8 14.25A6.25 6.25 0 1 0 8 1.75a6.25 6.25 0 0 0 0 12.5z'/><path d='M6 6l4 4'/><path d='M10 6l-4 4'/></svg>") center / contain no-repeat;
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M8 14.25A6.25 6.25 0 1 0 8 1.75a6.25 6.25 0 0 0 0 12.5z'/><path d='M6 6l4 4'/><path d='M10 6l-4 4'/></svg>") center / contain no-repeat;
}
h3.ui_login_logout::before {
background: var(--text-color-success);
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M8 14.25A6.25 6.25 0 1 0 8 1.75a6.25 6.25 0 0 0 0 12.5z'/><path d='M5.4 8.3l1.8 1.9 3.4-4'/></svg>");
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M8 14.25A6.25 6.25 0 1 0 8 1.75a6.25 6.25 0 0 0 0 12.5z'/><path d='M5.4 8.3l1.8 1.9 3.4-4'/></svg>");
}
h3.ui_login_timed_out::before {
background: var(--text-color-warning);
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M8 2.6l6.4 10.4H1.6L8 2.6z'/><path d='M8 6.8v2.7'/><path d='M8 11.3h.01'/></svg>");
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M8 2.6l6.4 10.4H1.6L8 2.6z'/><path d='M8 6.8v2.7'/><path d='M8 11.3h.01'/></svg>");
}
h3[data-fatal-error-text] pre {
margin: 0;
font-size: 12px;
white-space: pre-wrap;
}
table.err-stack, table.err-stack.captured {
width: 100%;
margin: 0 0 var(--card-gap);
border: 1px solid var(--border-color);
border-radius: var(--radius);
border-collapse: separate;
border-spacing: 0;
background: var(--main-content);
box-shadow: var(--shadow);
}
table.err-stack > caption.err-head, table.err-stack > caption.err-head.captured {
caption-side: top;
padding: 0 0 8px var(--title-inset);
color: var(--text-color-muted);
font: 600 12px/1.45 var(--font-family);
letter-spacing: 0.06em;
text-align: start;
text-transform: uppercase;
transform: none;
}
table.err-stack > tbody > tr > td, table.err-stack.captured > tbody > tr > td.captured {
padding: 4px 16px;
border-bottom: 1px solid var(--border-color-lighter);
color: var(--text-color);
font: 12.5px/1.45 var(--font-family-mono);
transform: none;
}
table.err-stack > tbody > tr:first-child > td,
table.err-stack.captured > tbody > tr:first-child > td.captured {
height: auto;
padding: 8px 16px;
border-bottom: 1px solid var(--border-color);
color: var(--text-color-muted);
font: 600 12px/1.45 var(--font-family);
text-transform: none;
vertical-align: middle;
}
table.err-stack > tbody > tr:first-child > td > b,
table.err-stack.captured > tbody > tr:first-child > td.captured > b {
border: 0;
color: inherit;
font-weight: inherit;
}
table.err-stack > tbody > tr:last-child > td { border-bottom: 0; }
/* ---- Module specific rules ---- */
/* The category selector above a module's configuration form */
form.ui_form[action="config.cgi"],
form.ui_form[action="uconfig.cgi"] {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
margin: 0 0 8px;
}
/* The terminal fills the frame under the header, and the page does not
* scroll. The module sizes the terminal from the whole viewport with no
* room for the header, so the page scrolled, and the scrollbar widened
* the terminal past the frame. The 66px are the sticky header and its
* margin, plus the menu bar of the single page layout on a narrow screen;
* the rules outrank the module's own by specificity. */
body.xterm { overflow: hidden; }
body.xterm #terminal {
box-sizing: border-box;
width: auto;
min-width: 0;
max-width: none;
height: calc(100vh - 66px - var(--card-gap) - var(--menu-bar-height, 0px));
min-height: 0;
max-height: none;
margin: 0;
padding: 6px;
border: 0;
border-radius: var(--radius);
overflow: hidden;
}
/* Label and value rows some modules print as divs */
.ui_div_row { margin: 4px 0; }
.ui_div_row > span:first-child {
display: inline-block;
width: 100px;
min-width: 10%;
max-width: 15%;
padding: 4px 8px 0 0;
vertical-align: top;
text-align: right;
font-weight: 600;
}
.ui_div_row > span:last-child {
display: inline-block;
width: 80%;
max-width: 80%;
padding-top: 4px;
}
/* The module configuration page centers its table headings */
form[action="config_save.cgi"] #sortableTable2 > thead > tr > td {
text-align: center;
}
/* Search box and page selector at the right above paged lists, with the
* clear button inside the search field */
.ui_form_elements_wrapper_search {
float: right;
margin-bottom: 3px;
}
.ui_form_elements_wrapper_search * { line-height: initial !important; }
.ui_form_elements_wrapper_search [data-class="ui_textbox_pagination_search"] {
padding-right: 17px;
}
.ui_form_elements_wrapper_search [data-class="ui_textbox_pagination_search"] + .ui_reset,
.ui_form_elements_wrapper_search [data-class="ui_textbox_pagination_search"] + .ui_reset:active,
.ui_form_elements_wrapper_search [data-class="ui_textbox_pagination_search"] + .ui_reset:focus {
position: absolute;
right: 17px;
min-height: 17px;
background: transparent;
border-color: transparent;
box-shadow: none;
cursor: pointer;
outline: none;
}
.ui_form_elements_wrapper_search [data-class="ui_textbox_pagination_search"]:not([value=""]) + .ui_reset {
color: var(--text-color-danger);
}
.ui_form_elements_wrapper_paginator {
float: right;
margin-top: 3px;
}
.ui_form_elements_wrapper_paginator .ui_textbox {
padding-right: 2px;
border: 1px dashed var(--link-color);
text-align: center;
}
/* Mail composer (Quill) */
.ql-compose-container {
border: 1px solid var(--border-color);
border-radius: var(--radius);
}
.ql-compose-container .ql-container.ql-snow { border: 0; }
.ql-compose-container .ql-toolbar.ql-snow {
border-top: 0;
border-right: 0;
border-left: 0;
border-color: var(--border-color-light);
}
.ql-compose-container .ql-editor {
min-height: 180px;
padding: 4px 6px 12px 6px;
}
.ql-snow .ql-editor pre { font-size: 92%; }
.ui_form_pair[data-contents="email"] > .ui_form_value,
.ql-compose-container .iframe_quote_details,
.ql-compose-container .ql-container.ql-snow .ql-editor {
background: var(--main-content);
color: var(--text-color);
}
.ui_form_pair > .ui_form_value > textarea[name='body'],
.ql-compose-container .ql-container.ql-snow .ql-editor {
font-size: 14px;
line-height: 1.231;
}
.ui_form_pair > .ui_form_value > textarea[name='body'] {
width: -webkit-fill-available !important;
min-height: 180px;
}
.ql-compose-container .ql-container.ql-snow .ql-editor * { line-height: 1.231; }
.ui_form_pair[data-contents="email"] > .ui_form_value { padding: 0; }
.ql-compose-container .ql-container.ql-snow .ql-editor blockquote {
margin: 0 0 0 6px;
padding-left: 12px;
border-left-width: 1px;
}
.ql-compose-container .ql-container.ql-snow .ql-editor pre.ql-syntax {
background: var(--bg-table-header);
color: initial;
}
.ql-compose-container .ql-snow .ql-picker.ql-expanded .ql-picker-label {
color: var(--text-color-muted);
}
.ql-compose-container .ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill {
fill: var(--text-color-muted);
}
.ql-compose-container .ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
stroke: var(--text-color-muted);
}
.ql-compose-container .ql-snow .ql-tooltip { z-index: 99; }
/* Command shell */
.shell pre {
overflow-x: auto;
overflow-y: hidden;
}
.shell input[name="cmd"] { max-width: 64.5vw; }
.shell select[name="pcmd"] { max-width: 65vw; }
/* Features not available in this edition */
[data-pro-disabled$="-link"] {
filter: grayscale(1) contrast(0.1);
opacity: 1;
}
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_label:not([data-pro-disabled="cell"]) > b > label > a,
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_value > .ui_multi_select .ui_multi_select_heads b,
[data-pro-disabled="cell"],
[data-pro-disabled$="-elem"] {
filter: grayscale(1);
opacity: 0.73;
}
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_value:not([data-pro-disabled="cell"]) > .ui_multi_select .ui_multi_select_heads b {
opacity: 0.61;
}
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_value > .ui_multi_select .ui_multi_select_row input[type="button"],
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_value > .ui_multi_select .ui_multi_select_row select {
color: rgba(118, 118, 118, 0.4);
pointer-events: none;
opacity: 0.8;
}
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_value > .ui_multi_select .ui_multi_select_row input[type="button"] {
opacity: 0.3;
}
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_value:only-child {
color: var(--text-color-muted);
filter: grayscale(1) contrast(0.9);
pointer-events: none;
}
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_value > .ui_multi_select .ui_multi_select_row select {
background: rgba(239, 239, 239, 0.3);
border-color: rgba(118, 118, 118, 0.3);
box-shadow: none;
}
[data-pro-disabled="cell"] + .ui_form_value,
.ui_form_pair:has([data-pro-disabled="cell"]) ~ .ui_form_pair:has(hr) ~ .ui_form_pair > .ui_form_value > .ui_multi_select .ui_multi_select_row > td,
.shrinkwrapper:has([data-pro-disabled="cell"]) ~ .ui_form_end_buttons {
cursor: not-allowed;
}
.shrinkwrapper:has([data-pro-disabled="cell"]) ~ .ui_form_end_buttons [name="create"] {
pointer-events: none;
opacity: 0.4;
}
[data-pro-disabled$="-elem"] input:not([type="radio"]):not([type="checkbox"]) {
filter: grayscale(1) contrast(0.9);
}
[data-pro-disabled$="-elem"] input { cursor: not-allowed !important; }
.menu-link.inactive, [data-pro-disabled$="-link"] a { cursor: alias; }
[data-pro-disabled$="-elem"] a { filter: grayscale(1) contrast(0.5); }
[data-pro-disabled$="-elem"] a:hover { filter: grayscale(1) contrast(1); }
/* ---- Print ---- */
@media print {
body { background: #fff; color: #000; }
.page-menu { display: none; }
body.single-page, body.right-frame.single-page {
padding-top: 0;
padding-inline-start: 0;
}
body > .header,
body > .header ~ center,
.ui_form ~ a,
.ui_table a[href*=".cgi"],
.ui_form .ui_link,
.ui_form > center,
.ui_form > [name],
.ui_form > .wrapper,
.ui_form > .wrapper ~ .shrinkwrapper,
.ui_table > thead > tr > td:not(:first-child):last-child,
.ui_footer_links,
.page-links {
display: none;
}
.ui_table > thead > tr > td > b { font-size: 1.33em; }
.ui_table > thead > tr > td {
position: relative;
padding: 10px;
border-bottom: 2px solid #d9d9d9;
text-align: center;
}
body > .header ~ .ui_form[action*=".cgi"]:first-of-type { margin-top: -50px; }
pre { white-space: pre-wrap; }
a { word-break: break-word; }
a[href]:after { content: none; }
}