/* * ui-lib.css * Stylesheet for the widgets added to ui-lib.pl: page header, grids, * cards, stat tiles, description lists, badges, chips, lists, activity * feeds, empty states, progress bars, toggles, search boxes and the * boxed choice, radio list and select switch widgets. * * Widgets use their own classes, with shared spacing and margin resets * inside .ui_page. Existing tabs, forms, tables and buttons retain their * theme styling. Widgets use the theme's --font-family and * --font-family-mono when defined, otherwise text inherits the page font * and code uses the monospace fallback below. * * .ui_page and standalone .ui_multi lists and .ui_toggle switches supply * the widget tokens. * The palette and shared spacing come from --ui-* custom properties, * which a theme may redefine on .ui_page to restyle every * widget at once. Setting data-ui-scheme="dark" (or "auto") on any * ancestor switches to the built-in dark palette. */ /* Nested widgets inherit their container's tokens. */ .ui_page, .ui_multi:not(:where(.ui_page *)), .ui_toggle:not(:where(.ui_page *, .ui_multi *)) { /* Palette tokens read the theme's own custom properties first - the * names Authentic defines for its palettes and night mode - and * fall back to the colors of the gray theme, which defines only the * four --text-color-* state colors itself. */ /* Core palette */ --ui-canvas: var(--bg-color, #f2f3f5); --ui-surface: var(--main-content, #ffffff); --ui-surface-2: var(--bg-table-header, #f5f5f5); --ui-border: var(--border-color, #d7dbe0); --ui-border-soft: var(--border-color-light, #e8eaee); --ui-border-strong: var(--border-color-darker, #b3bac2); --ui-fg: var(--text-color, #212121); --ui-fg-muted: var(--text-color-muted, #4b5563); /* Accent */ --ui-accent: var(--link-color, #0b46ab); --ui-accent-soft: var(--element-highlight-bg-color, #e8f1fb); --ui-ring: var(--border-color-focused, rgba(11, 70, 171, 0.25)); /* States. The base color is used for icons, dots and bars; the * -text variant is a darker shade for text sitting on the -soft * background, as in badges. */ --ui-success: var(--text-color-success, #3c763d); --ui-success-text: var(--text-color-success-stressed, #1e4f18); --ui-success-soft: var(--bg-color-success, #edf7e7); --ui-success-line: var(--border-color-success, #b9e0a4); --ui-warning: var(--text-color-warning, #b58900); --ui-warning-text: var(--text-color-warning-stressed, #795600); --ui-warning-soft: var(--bg-color-warning, #fdf4d8); --ui-warning-line: var(--border-color-warning, #eed483); --ui-danger: var(--text-color-danger, #bc0303); --ui-danger-text: var(--text-color-danger-stressed, #7d1007); --ui-danger-soft: var(--bg-color-danger, #fbeae8); --ui-danger-line: var(--border-color-danger, #f2bcb6); --ui-info: var(--text-color-info, #108eda); --ui-info-text: var(--text-color-info-stressed, #00477e); --ui-info-soft: var(--bg-color-info, #e8f1fb); --ui-info-line: var(--border-color-info, #b8d6f2); --ui-neutral: var(--text-color-muted, #4b5563); --ui-neutral-text: var(--text-color-neutral-stressed, #40474f); --ui-neutral-soft: var(--bg-color-neutral, #eff1f3); --ui-neutral-line: var(--border-color-neutral, #d7dbe0); /* Controls */ --ui-input-bg: var(--bg-color-input, var(--ui-surface)); --ui-input-border: var(--border-color-input, var(--ui-border-strong)); --ui-toggle-bg: var(--toggle-bg-color, var(--ui-border-strong)); /* Keep the default close to the unchecked track. */ --ui-toggle-bg-checked: color-mix(in srgb, var(--ui-toggle-bg) 75%, var(--ui-neutral)); --ui-toggle-thumb: var(--toggle-thumb-color, #ffffff); /* Shape and rhythm. Boxed widgets stay square; toggle corners are softened. */ --ui-gap: 7px; /* Shared inset for boxed choice widgets: padding, option spacing, * vertical field gaps and gaps between labels and inputs */ --ui-inset: 5px; /* Where the label text starts after a theme radio, so descriptions * and fields line up under it */ --ui-radio-indent: 1.6em; /* Fonts. A theme that defines --font-family and --font-family-mono * (Authentic does) sets the widgets' text and code fonts with them; * otherwise text inherits the page font and code uses the system * monospace stack. */ --ui-font: var(--font-family, inherit); --ui-font-mono: var(--font-family-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace); color-scheme: light; color: var(--ui-fg); font-family: var(--ui-font); text-align: left; } /* Built-in dark palette, enabled with data-ui-scheme="dark" on any * ancestor element (or on the .ui_page wrapper itself, via the scheme * option of ui_page_start). With data-ui-scheme="auto" the dark * palette follows the browser's prefers-color-scheme setting instead. * The two blocks below must be kept identical. */ [data-ui-scheme="dark"] .ui_page, .ui_page[data-ui-scheme="dark"], [data-ui-scheme="dark"] .ui_multi:not(:where(.ui_page *)), .ui_multi[data-ui-scheme="dark"]:not(:where(.ui_page *)), [data-ui-scheme="dark"] .ui_toggle:not(:where(.ui_page *, .ui_multi *)), .ui_toggle[data-ui-scheme="dark"]:not(:where(.ui_page *, .ui_multi *)) { --ui-canvas: #15171b; --ui-surface: #1e2126; --ui-surface-2: #23262c; --ui-border: #363b43; --ui-border-soft: #2a2f36; --ui-border-strong: #4d545e; --ui-fg: #e6e8ea; --ui-fg-muted: #99a3ae; --ui-accent: #5aa2ec; --ui-accent-soft: rgba(90, 162, 236, 0.14); --ui-ring: rgba(90, 162, 236, 0.35); --ui-success: #7ec563; --ui-success-text: #8fd376; --ui-success-soft: rgba(126, 197, 99, 0.13); --ui-success-line: rgba(126, 197, 99, 0.4); --ui-warning: #e0a63c; --ui-warning-text: #eab54f; --ui-warning-soft: rgba(224, 166, 60, 0.13); --ui-warning-line: rgba(224, 166, 60, 0.4); --ui-danger: #ef7b6f; --ui-danger-text: #f28c82; --ui-danger-soft: rgba(239, 123, 111, 0.13); --ui-danger-line: rgba(239, 123, 111, 0.4); --ui-info: #5aa2ec; --ui-info-text: #78b3ef; --ui-info-soft: rgba(90, 162, 236, 0.13); --ui-info-line: rgba(90, 162, 236, 0.4); --ui-neutral: #aeb6bf; --ui-neutral-text: #b6bec7; --ui-neutral-soft: #272b31; --ui-neutral-line: #3c424b; --ui-input-bg: var(--ui-surface); --ui-input-border: var(--ui-border-strong); color-scheme: dark; } @media (prefers-color-scheme: dark) { [data-ui-scheme="auto"] .ui_page, .ui_page[data-ui-scheme="auto"], [data-ui-scheme="auto"] .ui_multi:not(:where(.ui_page *)), .ui_multi[data-ui-scheme="auto"]:not(:where(.ui_page *)), [data-ui-scheme="auto"] .ui_toggle:not(:where(.ui_page *, .ui_multi *)), .ui_toggle[data-ui-scheme="auto"]:not(:where(.ui_page *, .ui_multi *)) { --ui-canvas: #15171b; --ui-surface: #1e2126; --ui-surface-2: #23262c; --ui-border: #363b43; --ui-border-soft: #2a2f36; --ui-border-strong: #4d545e; --ui-fg: #e6e8ea; --ui-fg-muted: #99a3ae; --ui-accent: #5aa2ec; --ui-accent-soft: rgba(90, 162, 236, 0.14); --ui-ring: rgba(90, 162, 236, 0.35); --ui-success: #7ec563; --ui-success-text: #8fd376; --ui-success-soft: rgba(126, 197, 99, 0.13); --ui-success-line: rgba(126, 197, 99, 0.4); --ui-warning: #e0a63c; --ui-warning-text: #eab54f; --ui-warning-soft: rgba(224, 166, 60, 0.13); --ui-warning-line: rgba(224, 166, 60, 0.4); --ui-danger: #ef7b6f; --ui-danger-text: #f28c82; --ui-danger-soft: rgba(239, 123, 111, 0.13); --ui-danger-line: rgba(239, 123, 111, 0.4); --ui-info: #5aa2ec; --ui-info-text: #78b3ef; --ui-info-soft: rgba(90, 162, 236, 0.13); --ui-info-line: rgba(90, 162, 236, 0.4); --ui-neutral: #aeb6bf; --ui-neutral-text: #b6bec7; --ui-neutral-soft: #272b31; --ui-neutral-line: #3c424b; --ui-input-bg: var(--ui-surface); --ui-input-border: var(--ui-border-strong); color-scheme: dark; } body[data-ui-scheme="auto"]:has(.ui_page), [data-ui-scheme="auto"] body:has(.ui_page), body:has(.ui_page[data-ui-scheme="auto"]) { background: var(--ui-canvas, #15171b); } } /* Paint the page canvas behind widget pages. Themes can override the * color by defining --ui-canvas on :root or body. */ body:has(.ui_page) { background: var(--ui-canvas, var(--bg-color, #f2f3f5)); } body[data-ui-scheme="dark"]:has(.ui_page), [data-ui-scheme="dark"] body:has(.ui_page), body:has(.ui_page[data-ui-scheme="dark"]) { background: var(--ui-canvas, #15171b); } /* ---- Spacing between blocks ---- * Top-level blocks are spaced by the page area. Inside any other wrapper * (a tab panel, a table cell) a widget that follows a sibling, and * whatever follows a widget, get the same gap, so rows of cards and the * forms between them line up whatever the theme wraps them in. * Children of grids, stacks, clusters and stat rows use their own gaps. */ .ui_page > * + * { margin-top: var(--ui-gap); } .ui_page :not(.ui_grid, .ui_stack, .ui_cluster, .ui_stats) > * + :is(.ui_grid, .ui_stack, .ui_card, .ui_empty), .ui_page :not(.ui_grid, .ui_stack, .ui_cluster, .ui_stats) > :is(.ui_grid, .ui_stack, .ui_card, .ui_empty) + * { margin-top: var(--ui-gap); } .ui_page [hidden] { display: none !important; } /* Widgets size their boxes including padding and borders */ .ui_page_head, .ui_grid, .ui_stack, .ui_cluster, .ui_card, .ui_card *, .ui_stats, .ui_stat, .ui_stat *, .ui_dl, .ui_dl *, .ui_badge, .ui_chip, .ui_list, .ui_list *, .ui_feed, .ui_feed *, .ui_empty, .ui_empty *, .ui_progress, .ui_progress *, .ui_toggle, .ui_toggle *, .ui_search, .ui_search * { box-sizing: border-box; } .ui_page h1, .ui_page h2, .ui_page p, .ui_page dl, .ui_page dd { margin: 0; } .ui_page svg.ui_svg_icon { flex-shrink: 0; vertical-align: -0.18em; } /* Plain links inside widget text are underlined so they still read as * links next to chips and badges. Theme buttons made of ui_link are not. */ .ui_dl_row dd a:not(.ui_link), .ui_feed_text a:not(.ui_link), .ui_list_desc a:not(.ui_link), .ui_empty_desc a:not(.ui_link), .ui_card_desc a:not(.ui_link) { text-decoration: underline; text-underline-offset: 0.15em; } /* ---- Utilities ---- */ .ui_push { margin-left: auto; } .ui_code { font-family: var(--ui-font-mono); font-size: 0.92em; background: var(--ui-neutral-soft); border: 1px solid var(--ui-border-soft); padding: 1px 5px; } .ui_code_block { font-family: var(--ui-font-mono); font-size: 0.92em; line-height: 1.5; background: var(--ui-surface-2); border: 1px solid var(--ui-border-soft); padding: 10px 12px; margin: 0; overflow: auto; white-space: pre; color: var(--ui-fg); } .ui_fg_success { color: var(--ui-success); } .ui_fg_warning { color: var(--ui-warning); } .ui_fg_danger { color: var(--ui-danger); } .ui_fg_info { color: var(--ui-info); } .ui_fg_neutral { color: var(--ui-neutral); } .ui_bg_success { background: var(--ui-success); } .ui_bg_warning { background: var(--ui-warning); } .ui_bg_danger { background: var(--ui-danger); } .ui_bg_info { background: var(--ui-info); } .ui_bg_neutral { background: var(--ui-neutral); } /* ---- Page header ---- */ .ui_page_head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px 24px; flex-wrap: wrap; margin: 4px 0 2px; } .ui_page_title { font-size: 1.85em; font-weight: 650; letter-spacing: -0.015em; line-height: 1.2; } .ui_page_titles { flex: 1 1 auto; min-width: 0; } .ui_page_desc { color: var(--ui-fg-muted); margin-top: 4px; } .ui_page_actions { display: flex; align-items: center; gap: 12px; padding-top: 7px; } .ui_page_help { display: inline-flex; align-items: center; gap: 6px; font-weight: 500; } /* ---- Layout primitives ---- */ .ui_grid { display: grid; gap: var(--ui-gap); grid-template-columns: repeat(auto-fit, minmax(min(var(--ui-grid-min, 340px), 100%), 1fr)); align-items: stretch; } .ui_grid_fixed { grid-template-columns: var(--ui-grid-template, repeat(var(--ui-grid-cols, 2), minmax(0, 1fr))); } .ui_stack { display: flex; flex-direction: column; gap: var(--ui-gap); } .ui_cluster { display: flex; flex-wrap: wrap; align-items: center; gap: calc(var(--ui-gap) / 2) var(--ui-gap); } .ui_cluster { --ui-gap: 10px; } /* Themes that regroup adjacent submit buttons into one box, as Authentic * does with its btn-group, wrap them inside that box with no vertical * space. Give those rows a gap while keeping the buttons joined. */ .ui_cluster > .btn-group { display: inline-flex; flex-wrap: wrap; row-gap: 7px; } .ui_cluster_center { justify-content: center; } .ui_cluster_end { justify-content: flex-end; } .ui_cluster_between { justify-content: space-between; } @media (max-width: 760px) { .ui_grid_fixed { grid-template-columns: 1fr; } } /* ---- Cards ---- */ .ui_card { background: var(--ui-surface); border: 1px solid var(--ui-border); display: flex; flex-direction: column; min-width: 0; } .ui_card_success { border-left: 3px solid var(--ui-success); } .ui_card_warning { border-left: 3px solid var(--ui-warning); } .ui_card_danger { border-left: 3px solid var(--ui-danger); } .ui_card_info { border-left: 3px solid var(--ui-info); } .ui_card_head { display: flex; justify-content: space-between; align-items: flex-start; gap: 4px 12px; padding: 5px 6px calc(var(--ui-gap) * 1.5) 7px; flex-wrap: wrap; } .ui_card_title { font-size: 1.07697em; font-weight: 650; line-height: 1.3; } .ui_card_desc { color: var(--ui-fg-muted); font-size: 0.93em; margin-top: 3px; } .ui_card_actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; /* Let taller controls use the space below the title. */ margin-bottom: calc(var(--ui-gap) * -1.5); } .ui_card_body { padding: 3px 7px 7px; flex: 1; min-width: 0; } .ui_card_body.ui_card_flush { padding: 1px 0 0; } .ui_card_head + .ui_card_body.ui_card_flush { padding-top: 3px; } .ui_card_foot { border-top: 1px solid var(--ui-border-soft); padding: 5px; color: var(--ui-fg-muted); font-size: 0.93em; } /* ---- Stat tiles ---- */ .ui_stats { display: grid; gap: 12px 24px; grid-template-columns: repeat(auto-fit, minmax(min(var(--ui-grid-min, 150px), 100%), 1fr)); } .ui_stat { display: flex; flex-direction: column; gap: 1px; min-width: 0; color: inherit; text-decoration: none; } .ui_stat_value { font-size: 1.85em; font-weight: 500; line-height: 1.2; letter-spacing: -0.01em; display: flex; align-items: center; gap: 8px; } .ui_stat_label { font-weight: 600; } .ui_stat_desc { color: var(--ui-fg-muted); font-size: 0.9em; } a.ui_stat, a.ui_stat:hover { color: inherit !important; text-decoration: none !important; } a.ui_stat:hover .ui_stat_value { color: var(--ui-accent); } /* ---- Description lists ---- */ /* Size rows to their content before deciding whether two can share a line. */ .ui_dl { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px 20px; } .ui_dl_row { display: flex; flex-wrap: wrap; flex: 1 1 100%; align-content: start; align-items: baseline; gap: 3px 8px; min-width: 0; max-width: 100%; } .ui_dl_cols > .ui_dl_row { flex-basis: auto; min-width: calc((100% - 20px) / 2); } .ui_dl_row dt { flex: 0 0 var(--ui-dl-label, auto); font-weight: 600; } /* A value moves below its label only after the row has used all available space. */ .ui_dl_row dd { flex: 1 1 auto; } .ui_dl_row dt, .ui_dl_row dd { min-width: 0; max-width: 100%; overflow-wrap: anywhere; } /* ---- Badges and chips ---- */ .ui_badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border: 1px solid transparent; font-size: 0.9em; font-weight: 600; line-height: 1.5; vertical-align: middle; white-space: nowrap; } .ui_badge_success { background: var(--ui-success-soft); border-color: var(--ui-success-line); color: var(--ui-success-text); } .ui_badge_warning { background: var(--ui-warning-soft); border-color: var(--ui-warning-line); color: var(--ui-warning-text); } .ui_badge_danger { background: var(--ui-danger-soft); border-color: var(--ui-danger-line); color: var(--ui-danger-text); } .ui_badge_info { background: var(--ui-info-soft); border-color: var(--ui-info-line); color: var(--ui-info-text); } .ui_badge_neutral { background: var(--ui-neutral-soft); border-color: var(--ui-neutral-line); color: var(--ui-neutral-text); } .ui_chip { display: inline-flex; align-items: center; padding: 1px 8px; background: var(--ui-neutral-soft); border: 1px solid var(--ui-border); color: var(--ui-neutral-text); font-size: 0.86em; line-height: 1.5; vertical-align: middle; white-space: nowrap; } /* ---- Lists ---- */ .ui_list { display: flex; flex-direction: column; } .ui_list_item { display: flex; align-items: flex-start; gap: 12px; padding: 5px 0; border-bottom: 1px solid var(--ui-border-soft); } .ui_list_item:last-child { border-bottom: 0; } .ui_list_flush .ui_list_item { padding: 5px 6px 5px 7px; } .ui_list_main { flex: 1; min-width: 0; } .ui_list_title { font-weight: 600; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; overflow-wrap: anywhere; } .ui_list_desc { color: var(--ui-fg-muted); font-size: 0.93em; margin-top: 2px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .ui_list_side { display: flex; align-items: center; gap: 10px; flex-shrink: 0; padding-top: 1px; } .ui_list_meta { color: var(--ui-fg-muted); font-size: 0.9em; } /* Badges in list rows are chip-sized, so they do not outweigh the text */ .ui_list_item .ui_badge { padding: 1px 8px; gap: 4px; font-size: 0.86em; } .ui_list_item .ui_badge .ui_svg_icon { width: 1em; height: 1em; } /* Description text is 0.93em, so badges and chips inside it are scaled * back by that much to match the ones next to the title */ .ui_list_desc :is(.ui_badge, .ui_chip) { font-size: calc(0.86em / 0.93); } .ui_list_icon { padding-top: 2px; color: var(--ui-fg-muted); } a.ui_list_link, a.ui_list_link:hover { color: inherit !important; text-decoration: none !important; } a.ui_list_link:hover { color: var(--ui-accent) !important; } /* ---- Activity feed ---- */ .ui_feed { display: flex; flex-direction: column; } .ui_feed_item { position: relative; padding: 0 0 16px 20px; border-left: 2px solid var(--ui-border-soft); margin-left: 5px; } .ui_feed_item:last-child { padding-bottom: 2px; } .ui_feed_item::before { content: ""; position: absolute; left: -6px; top: 4px; width: 10px; height: 10px; border-radius: 999px; background: var(--ui-border-strong); border: 2px solid var(--ui-surface); } .ui_feed_success::before { background: var(--ui-success); } .ui_feed_warning::before { background: var(--ui-warning); } .ui_feed_danger::before { background: var(--ui-danger); } .ui_feed_info::before { background: var(--ui-info); } .ui_feed_when { font-weight: 600; font-size: 1em; } .ui_feed_text { color: var(--ui-fg-muted); font-size: 0.93em; margin-top: 1px; } .ui_feed_flush { padding: 3px 19px 7px; } /* ---- Empty states ---- */ .ui_empty { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 1px 7px 7px; text-align: center; } .ui_empty_icon { color: var(--ui-border-strong); margin-bottom: 2px; } .ui_empty_title { font-weight: 600; font-size: 1.04em; } .ui_empty_desc { color: var(--ui-fg-muted); font-size: 0.93em; max-width: 46ch; } .ui_empty_actions { margin-top: 6px; } /* ---- Progress ---- */ .ui_progress_head { display: flex; justify-content: space-between; gap: 12px; font-size: 0.93em; font-weight: 600; margin-bottom: 6px; } .ui_progress_value { color: var(--ui-fg-muted); font-weight: 400; } .ui_progress_track { display: flex; height: 4px; background: var(--ui-neutral-soft); overflow: hidden; } .ui_progress_bar { height: 100%; flex-shrink: 0; transition: width 0.4s ease; } /* Inline layout : label, bar and value on one line */ .ui_progress_inline { display: flex; align-items: center; gap: 10px; font-size: 0.93em; } .ui_progress_inline .ui_progress_label { font-weight: 600; white-space: nowrap; } .ui_progress_inline .ui_progress_track { flex: 1 1 auto; } .ui_progress_inline .ui_progress_value { min-width: 3em; text-align: right; white-space: nowrap; } /* Value shown as a small marker riding on the end of the bar, which * keeps the same height as every other bar */ .ui_progress_inside .ui_progress_track { overflow: visible; } .ui_progress_inside .ui_progress_bar { position: relative; } .ui_progress_inside_value { position: absolute; top: 50%; left: 100%; transform: translate(-50%, -50%); padding: 1px 4px; /* The bar's own color, lightened by a white veil, with dark text */ background: inherit; background-image: linear-gradient(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.6)); color: #1c1c1c; font-size: 0.75em; font-weight: 600; line-height: 1.2; white-space: nowrap; } .ui_progress_inside_start .ui_progress_inside_value { transform: translate(0, -50%); } .ui_progress_inside_end .ui_progress_inside_value { transform: translate(-100%, -50%); } /* Legend under a segmented bar */ .ui_progress_legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6px; font-size: 0.9em; } .ui_progress_key { display: inline-flex; align-items: center; gap: 6px; } .ui_progress_dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; } /* Unknown progress : a bar sliding along the track */ .ui_progress_indeterminate .ui_progress_bar { width: 35%; animation: ui_progress_slide 1.4s ease-in-out infinite; } @keyframes ui_progress_slide { from { transform: translateX(-100%); } to { transform: translateX(300%); } } @media (prefers-reduced-motion: reduce) { .ui_progress_indeterminate .ui_progress_bar { width: 100%; animation: none; opacity: 0.6; background-image: repeating-linear-gradient(45deg, transparent 0 6px, rgba(255, 255, 255, 0.45) 6px 12px); } } /* Ring gauge : the bar is a circle of circumference 100 whose dash * length is the percentage */ .ui_progress_ring { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; } .ui_progress_ring_box { position: relative; display: inline-block; line-height: 0; } .ui_progress_ring_svg { transform: rotate(-90deg); } /* The stroke width is set on the circles by ui_progress, scaled to the * ring size so it is always 3 pixels */ .ui_progress_ring_track { fill: none; stroke: var(--ui-neutral-soft); } .ui_progress_ring_bar { fill: none; stroke: currentColor; stroke-linecap: round; transition: stroke-dasharray 0.4s ease; } .ui_progress_ring.ui_progress_indeterminate .ui_progress_ring_svg { animation: ui_progress_spin 1s linear infinite; } @keyframes ui_progress_spin { from { transform: rotate(-90deg); } to { transform: rotate(270deg); } } .ui_progress_ring_box .ui_progress_value { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 0.8em; font-weight: 600; line-height: 1; color: var(--ui-fg); } .ui_progress_ring .ui_progress_label { font-size: 0.85em; color: var(--ui-fg-muted); } /* ---- Choice lists ---- * One solid box holding a row per option : the theme's own radio button * with its label, the inline inputs beside it, and further fields under * them in as many columns as fit, indented under the label. Everything * stays visible. */ .ui_choice { display: grid; gap: var(--ui-inset); min-width: 0; /* Half the inset above and below, the full inset at the sides */ padding: calc(var(--ui-inset) / 2) var(--ui-inset); border: 1px solid var(--ui-border); background: var(--ui-surface); } .ui_choice_item { min-width: 0; } .ui_choice_head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--ui-inset); min-width: 0; } /* Only the option's own label is bold, not labels of inputs in its content */ .ui_choice_head label:not(.ui_choice_content label) { font-weight: 600; } .ui_choice_content { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px var(--ui-inset); min-width: 0; } .ui_choice_desc { margin-left: var(--ui-radio-indent); color: var(--ui-fg-muted); font-size: 0.93em; } .ui_choice_fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: var(--ui-inset) 20px; margin: var(--ui-inset) 0 0 var(--ui-radio-indent); } .ui_choice_field { display: flex; flex-wrap: wrap; align-items: center; gap: 4px var(--ui-inset); min-width: 0; } .ui_choice_field_label { color: var(--ui-fg-muted); } .ui_choice_field_input { min-width: 0; } .ui_choice_disabled { opacity: 0.6; } /* ---- Radio lists ---- * The compact form : the theme's radio buttons at the left edge of a * box, one option per line, an optional input after the label */ .ui_radio_list { display: grid; gap: var(--ui-inset); min-width: 0; padding: var(--ui-inset); border: 1px solid var(--ui-border); background: var(--ui-surface); } .ui_radio_list_item { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--ui-inset); min-width: 0; } .ui_radio_list_item label:not(.ui_radio_list_content label) { font-weight: 600; } .ui_radio_list_content { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px var(--ui-inset); min-width: 0; } .ui_radio_list_disabled { opacity: 0.6; } /* ---- Select switches ---- * A select, and under it the block of the chosen option only */ .ui_select_switch { display: grid; justify-items: start; gap: var(--ui-inset); min-width: 0; } .ui_select_switch_panel { display: grid; gap: var(--ui-inset); width: 100%; min-width: 0; padding: var(--ui-inset); border: 1px solid var(--ui-border); background: var(--ui-surface); } .ui_select_switch_desc { color: var(--ui-fg-muted); font-size: 0.93em; } .ui_select_switch_content { display: flex; flex-wrap: wrap; align-items: center; gap: 4px var(--ui-inset); min-width: 0; } .ui_select_switch_fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: var(--ui-inset) 20px; } .ui_select_switch_field { display: flex; flex-wrap: wrap; align-items: center; gap: 4px var(--ui-inset); min-width: 0; } .ui_select_switch_field_label { color: var(--ui-fg-muted); } .ui_select_switch_field_input { min-width: 0; } /* ---- Toggle switch ---- * Some themes wrap checkbox inputs in their own markup with JavaScript, * so the real input - or whatever wrapper it ends up inside - is parked * invisibly over the track, where clicks and keyboard focus still reach * it, and the checked state is read with :has() which does not depend on * the input being the track's sibling. */ .ui_toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; position: relative; font-weight: normal; } /* State colors share the theme-backed palette used by other widgets. */ .ui_toggle_primary { --ui-toggle-bg-checked: var(--toggle-bg-color-checked, var(--ui-accent)); } .ui_toggle_success { --ui-toggle-bg-checked: var(--ui-success); } .ui_toggle_warning { --ui-toggle-bg-checked: var(--ui-warning); } .ui_toggle_danger { --ui-toggle-bg-checked: var(--ui-danger); } .ui_toggle_info { --ui-toggle-bg-checked: var(--ui-info); } .ui_toggle > :not(.ui_toggle_track):not(.ui_toggle_label), .ui_toggle input { position: absolute !important; left: 0 !important; top: 0 !important; width: 28px !important; height: 14px !important; margin: 0 !important; padding: 0 !important; opacity: 0 !important; overflow: hidden; z-index: 1; cursor: pointer; } .ui_toggle_track { display: inline-flex; align-items: center; width: 26px; height: 12px; padding: 2px; border-radius: 3px; background: var(--ui-toggle-bg); transition: background 0.15s ease; flex-shrink: 0; } .ui_toggle_thumb { width: 8px; height: 8px; border-radius: 2px; background: var(--ui-toggle-thumb); box-shadow: 0 1px 1px rgba(16, 24, 40, 0.18); transition: transform 0.15s ease; } .ui_toggle_round .ui_toggle_track { width: 28px; height: 14px; border-radius: 999px; } .ui_toggle_round .ui_toggle_thumb { width: 10px; height: 10px; border-radius: 999px; box-shadow: 0 1px 2px rgba(16, 24, 40, 0.25); } .ui_toggle input:checked ~ .ui_toggle_track, .ui_toggle:has(input:checked) .ui_toggle_track { background: var(--ui-toggle-bg-checked); } .ui_toggle input:checked ~ .ui_toggle_track .ui_toggle_thumb, .ui_toggle:has(input:checked) .ui_toggle_thumb { transform: translateX(14px); } /* Trade one pixel of padding for the border to keep the thumb in place. */ .ui_toggle_outline .ui_toggle_track { padding: 1px; border: 1px solid currentColor; color: var(--ui-toggle-bg); background: transparent; } .ui_toggle_outline .ui_toggle_thumb { background: currentColor; box-shadow: none; } .ui_toggle_outline input:checked ~ .ui_toggle_track, .ui_toggle_outline:has(input:checked) .ui_toggle_track { color: var(--ui-toggle-bg-checked); background: transparent; } .ui_toggle input:focus-visible ~ .ui_toggle_track, .ui_toggle:has(input:focus-visible) .ui_toggle_track { box-shadow: 0 0 0 3px var(--ui-ring); } .ui_toggle input:disabled ~ .ui_toggle_track, .ui_toggle:has(input:disabled) .ui_toggle_track { opacity: 0.5; } /* ---- Search box ---- */ .ui_search { position: relative; display: inline-flex; max-width: 100%; } .ui_search .ui_search_icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--ui-fg-muted); pointer-events: none; } .ui_search .ui_search_input { padding-left: 30px; width: 100%; min-width: 0; font: inherit; color: inherit; background: var(--ui-surface); border: 1px solid var(--ui-border-strong); padding-top: 5px; padding-bottom: 5px; padding-right: 10px; } .ui_search .ui_search_input:focus { outline: none; border-color: var(--ui-accent); box-shadow: 0 0 0 3px var(--ui-ring); } /* ---- Multiple selection ---- * Scrollable checkbox lists, also usable outside .ui_page. */ .ui_multi, .ui_multi_body { display: grid; gap: var(--ui-inset); min-width: 0; } .ui_multi [hidden] { display: none !important; } .ui_multi_modes { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; } .ui_multi_count { color: var(--ui-fg-muted); font-size: 0.93em; white-space: nowrap; } .ui_multi_tools { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; } /* A hidden count alone does not need a toolbar row. */ .ui_multi_tools:not(:has(> :not([hidden]))) { display: none; } /* ui_links_row handles link spacing; add room for the count. */ .ui_multi_links { min-width: 0; } .ui_multi_tools .ui_multi_count:not(:first-child) { margin-left: 2px; margin-right: 10px; } /* Reserve filter space to prevent toolbar shifts on opening. */ .ui_multi_tools .ui_search { margin-left: auto; flex: 1 1 9em; min-width: 9em; max-width: 14em; } /* Match the height of compact link buttons. */ .ui_multi_tools .ui_search_input { height: 22px; padding: 0 26px; font-size: 0.93em; line-height: 20px; background: var(--ui-input-bg); border-color: var(--ui-input-border); } /* Keep dimensions while hiding closed controls from keyboard navigation. */ .ui_multi_filter:not(.ui_multi_filter_open) .ui_search_input, .ui_multi_filter:not(.ui_multi_filter_open) .ui_multi_filter_clear { visibility: hidden; } .ui_multi_filter .ui_search_input::-webkit-search-cancel-button { -webkit-appearance: none; } .ui_multi_tools .ui_multi_filter_button { appearance: none; position: absolute; top: 0; bottom: 0; display: flex; align-items: center; justify-content: center; width: 26px; margin: 0; padding: 0; border: 1px solid transparent; border-radius: 0; background: transparent; color: var(--ui-fg-muted); font: inherit; cursor: pointer; } .ui_multi_tools .ui_multi_filter_toggle { right: 0; /* Share the input's theme colors. */ border-color: var(--ui-input-border); background: var(--ui-input-bg); } .ui_multi_filter_toggle .ui_svg_icon { fill: currentColor; opacity: 0.75; } .ui_multi_tools .ui_multi_filter_clear { left: 0; } .ui_multi_filter_open .ui_multi_filter_toggle { border-color: transparent; background: transparent; color: var(--ui-accent); } .ui_multi_filter_active .ui_multi_filter_toggle { color: var(--ui-danger); } .ui_multi_filter_button:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: -2px; } .ui_multi_filter_button:disabled { cursor: default; } .ui_multi_list { border: 1px solid var(--ui-border); background: var(--ui-surface); min-width: 0; } .ui_multi_items { max-height: var(--ui-multi-height, 170px); overflow-y: scroll; } .ui_multi_item { display: flex; align-items: center; min-width: 0; padding: calc(var(--ui-inset) / 2) var(--ui-inset) calc(var(--ui-inset) / 2) var(--ui-inset); } /* Space unwrapped checkboxes; theme wrappers handle their own spacing. */ .ui_multi_item > input[type="checkbox"] { margin-right: 6px; } /* Keep themed checkbox glyphs upright when the row text is italic. */ .ui_multi_item input[type="checkbox"] + label::before, .ui_multi_item input[type="checkbox"] + label::after { font-style: normal; } .ui_multi_item:hover { background: var(--ui-surface-2); } .ui_multi_level1 { padding-left: 27px; } .ui_multi_level2 { padding-left: 47px; } .ui_multi_suffix { color: var(--ui-fg-muted); } .ui_multi_note { margin-left: 6px; color: var(--ui-fg-muted); font-size: 0.93em; white-space: nowrap; } .ui_multi_side { margin-left: auto; padding-left: 8px; flex-shrink: 0; } .ui_multi_disabled { opacity: 0.6; } .ui_multi_empty { padding: 1px 2px 2px 2px; color: var(--ui-fg-muted); text-align: center; } .ui_multi_empty .ui_svg_icon { vertical-align: -0.18em; transform: scale(0.87); margin-right: 0.025em; } /* ---- Reduced motion ---- */ @media (prefers-reduced-motion: reduce) { .ui_progress_ring.ui_progress_indeterminate .ui_progress_ring_svg { animation: none; } .ui_page *, .ui_page *::before, .ui_page *::after { transition: none !important; } }