universal-dashboard/styles.css

2191 lines
59 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ==========================================================================
01. Theme Tokens
--------------------------------------------------------------------------
Все ключевые цвета и размеры вынесены в CSS-переменные. JavaScript меняет
data-theme/data-accent и --sidebar-width, а остальная верстка реагирует
без прямого изменения inline-стилей у отдельных элементов.
========================================================================== */
:root {
color-scheme: dark;
--bg: #080a12;
--bg-soft: #111827;
--panel: rgba(15, 23, 42, 0.74);
--panel-strong: rgba(15, 23, 42, 0.92);
--surface: rgba(255, 255, 255, 0.055);
--text: #f8fafc;
--muted: #aeb8c8;
--border: rgba(255, 255, 255, 0.12);
--shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
--accent: #8b5cf6;
--accent-2: #22d3ee;
--accent-rgb: 139, 92, 246;
--accent-angle: 135deg;
--card-x: 50%;
--card-y: 50%;
--sidebar-width: 390px;
--top-bar-height: 140px;
--shell-max-width: 1920px;
--sidebar-max-width: 800px;
--catalog-cards-columns: 3;
--catalog-list-columns: 1;
--catalog-compact-cards-columns: 5;
--catalog-compact-list-columns: 3;
--pattern-color: rgba(255, 255, 255, 0.16);
--pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
--pattern-tile-hex-size: 96px 84px;
--pattern-tile-dots-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px);
--pattern-tile-dots-size: 18px 18px;
--pattern-tile-grid-image:
linear-gradient(var(--pattern-color) 1px, transparent 1px),
linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px);
--pattern-tile-grid-size: 26px 26px;
--pattern-tile-diagonal-image: repeating-linear-gradient(
-45deg,
transparent,
transparent 10px,
var(--pattern-color) 10px,
var(--pattern-color) 11px
);
--pattern-tile-diagonal-size: auto;
--pattern-tile-cross-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 14px,
var(--pattern-color) 14px,
var(--pattern-color) 15px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 14px,
var(--pattern-color) 14px,
var(--pattern-color) 15px
);
--pattern-tile-cross-size: auto;
--pattern-tile-mesh-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.22) 1px,
transparent 0
);
--pattern-tile-mesh-size: 20px 20px;
--settings-panel-width: 460px;
}
/* Светлая тема переопределяет только токены, структура стилей остается общей. */
html[data-theme="light"] {
color-scheme: light;
--bg: #f5f7fb;
--bg-soft: #ffffff;
--panel: rgba(255, 255, 255, 0.78);
--panel-strong: rgba(255, 255, 255, 0.96);
--surface: rgba(15, 23, 42, 0.045);
--text: #101827;
--muted: #5f6b7a;
--border: rgba(15, 23, 42, 0.12);
--shadow: 0 24px 70px rgba(56, 65, 91, 0.18);
--pattern-color: rgba(15, 23, 42, 0.13);
--pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
--pattern-tile-mesh-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.16) 1px,
transparent 0
);
}
html[data-panel-pattern-nav="hex"] { --panel-pattern-image-nav: var(--pattern-tile-hex-image); --panel-pattern-size-nav: var(--pattern-tile-hex-size); }
html[data-panel-pattern-nav="dots"] { --panel-pattern-image-nav: var(--pattern-tile-dots-image); --panel-pattern-size-nav: var(--pattern-tile-dots-size); }
html[data-panel-pattern-nav="grid"] { --panel-pattern-image-nav: var(--pattern-tile-grid-image); --panel-pattern-size-nav: var(--pattern-tile-grid-size); }
html[data-panel-pattern-nav="diagonal"] { --panel-pattern-image-nav: var(--pattern-tile-diagonal-image); --panel-pattern-size-nav: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-nav="cross"] { --panel-pattern-image-nav: var(--pattern-tile-cross-image); --panel-pattern-size-nav: var(--pattern-tile-cross-size); }
html[data-panel-pattern-nav="mesh"] { --panel-pattern-image-nav: var(--pattern-tile-mesh-image); --panel-pattern-size-nav: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-section="hex"] { --panel-pattern-image-section: var(--pattern-tile-hex-image); --panel-pattern-size-section: var(--pattern-tile-hex-size); }
html[data-panel-pattern-section="dots"] { --panel-pattern-image-section: var(--pattern-tile-dots-image); --panel-pattern-size-section: var(--pattern-tile-dots-size); }
html[data-panel-pattern-section="grid"] { --panel-pattern-image-section: var(--pattern-tile-grid-image); --panel-pattern-size-section: var(--pattern-tile-grid-size); }
html[data-panel-pattern-section="diagonal"] { --panel-pattern-image-section: var(--pattern-tile-diagonal-image); --panel-pattern-size-section: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-section="cross"] { --panel-pattern-image-section: var(--pattern-tile-cross-image); --panel-pattern-size-section: var(--pattern-tile-cross-size); }
html[data-panel-pattern-section="mesh"] { --panel-pattern-image-section: var(--pattern-tile-mesh-image); --panel-pattern-size-section: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-group="hex"] { --panel-pattern-image-group: var(--pattern-tile-hex-image); --panel-pattern-size-group: var(--pattern-tile-hex-size); }
html[data-panel-pattern-group="dots"] { --panel-pattern-image-group: var(--pattern-tile-dots-image); --panel-pattern-size-group: var(--pattern-tile-dots-size); }
html[data-panel-pattern-group="grid"] { --panel-pattern-image-group: var(--pattern-tile-grid-image); --panel-pattern-size-group: var(--pattern-tile-grid-size); }
html[data-panel-pattern-group="diagonal"] { --panel-pattern-image-group: var(--pattern-tile-diagonal-image); --panel-pattern-size-group: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-group="cross"] { --panel-pattern-image-group: var(--pattern-tile-cross-image); --panel-pattern-size-group: var(--pattern-tile-cross-size); }
html[data-panel-pattern-group="mesh"] { --panel-pattern-image-group: var(--pattern-tile-mesh-image); --panel-pattern-size-group: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-item="hex"] { --panel-pattern-image-item: var(--pattern-tile-hex-image); --panel-pattern-size-item: var(--pattern-tile-hex-size); }
html[data-panel-pattern-item="dots"] { --panel-pattern-image-item: var(--pattern-tile-dots-image); --panel-pattern-size-item: var(--pattern-tile-dots-size); }
html[data-panel-pattern-item="grid"] { --panel-pattern-image-item: var(--pattern-tile-grid-image); --panel-pattern-size-item: var(--pattern-tile-grid-size); }
html[data-panel-pattern-item="diagonal"] { --panel-pattern-image-item: var(--pattern-tile-diagonal-image); --panel-pattern-size-item: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-item="cross"] { --panel-pattern-image-item: var(--pattern-tile-cross-image); --panel-pattern-size-item: var(--pattern-tile-cross-size); }
html[data-panel-pattern-item="mesh"] { --panel-pattern-image-item: var(--pattern-tile-mesh-image); --panel-pattern-size-item: var(--pattern-tile-mesh-size); }
/* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */
html[data-accent="cyan"] {
--accent: #06b6d4;
--accent-2: #3b82f6;
--accent-rgb: 6, 182, 212;
}
html[data-accent="lime"] {
--accent: #84cc16;
--accent-2: #14b8a6;
--accent-rgb: 132, 204, 22;
}
html[data-accent="amber"] {
--accent: #f59e0b;
--accent-2: #f43f5e;
--accent-rgb: 245, 158, 11;
}
/* ==========================================================================
02. Base Reset And Page Background
--------------------------------------------------------------------------
Базовые правила страницы, плавная смена темы и фоновые декоративные слои.
========================================================================== */
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
min-height: 100vh;
margin: 0;
overflow-x: hidden;
background:
radial-gradient(circle at top left, rgba(var(--accent-rgb), 0.24), transparent 36rem),
radial-gradient(circle at bottom right, rgba(34, 211, 238, 0.16), transparent 34rem),
linear-gradient(135deg, var(--bg), var(--bg-soft));
color: var(--text);
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
transition: background 220ms ease, color 220ms ease;
}
button,
input,
a {
font: inherit;
}
a {
color: inherit;
}
/* Фоновые "ambient" пятна создают глубину, но не перехватывают клики. */
.ambient {
position: fixed;
z-index: -1;
width: 30rem;
height: 30rem;
border-radius: 999px;
filter: blur(18px);
opacity: 0.42;
pointer-events: none;
}
.ambient-one {
top: -10rem;
right: -8rem;
background: rgba(var(--accent-rgb), 0.46);
}
.ambient-two {
bottom: -13rem;
left: -10rem;
background: rgba(34, 211, 238, 0.3);
}
/* ==========================================================================
03. Application Layout
--------------------------------------------------------------------------
.shell ограничивает общую ширину, .app-layout делит экран на:
sidebar -> resizer -> catalog.
========================================================================== */
.shell {
width: min(var(--shell-max-width), calc(100% - 28px));
margin: 0 auto;
padding: 20px 0;
}
/* Левая колонка получает ширину из --sidebar-width, которую меняет JS. */
.app-layout {
display: grid;
grid-template-columns: var(--sidebar-width) 6px minmax(0, 1fr);
gap: 4px;
align-items: start;
}
html[data-nav-layout="sidebar"][data-show-resize-handles="false"] .app-layout {
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
}
/* Компактная шапка: название + поиск, без дерева и resizer. */
html[data-nav-layout="top"] .app-layout,
.app-layout[data-nav-layout="top"] {
grid-template-columns: 1fr;
}
html[data-nav-layout="top"] .sidebar-resizer {
display: none;
}
html[data-nav-layout="top"] .side-panel {
position: static;
display: flex;
flex-direction: column;
gap: 0;
}
html[data-nav-layout="top"] .side-panel .hero-content {
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
align-content: flex-start;
justify-content: flex-start;
gap: 12px 16px;
height: var(--top-bar-height);
min-height: var(--top-bar-height);
max-height: var(--top-bar-height);
overflow: auto;
padding: 14px 58px 14px 18px;
}
html[data-nav-layout="top"] .side-panel .hero-content > .eyebrow {
margin: 0;
flex: 0 1 auto;
max-width: min(100%, 360px);
align-self: center;
padding-right: 0;
}
.side-panel .hero-content > .eyebrow#page-title {
padding-right: 46px;
line-height: 1.35;
}
html[data-nav-layout="top"] .search-field {
margin-top: 0;
flex: 1 1 220px;
max-width: min(100%, 560px);
}
html[data-show-tree="false"] .nav-tree {
display: none;
}
html[data-nav-layout="top"][data-show-tree="true"] .nav-tree {
flex: 1 1 100%;
width: 100%;
max-height: none;
min-height: 0;
overflow: auto;
margin-top: 4px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
html[data-show-tree="false"] .side-panel .hero-content {
max-height: none;
}
.sidebar-resizer {
position: sticky;
top: 20px;
width: 6px;
height: calc(100vh - 40px);
margin: 0 -1px;
border-radius: 999px;
cursor: col-resize;
background: transparent;
transition: background 160ms ease;
}
.sidebar-resizer::before {
content: "";
display: block;
width: 2px;
height: 100%;
margin: 0 auto;
border-radius: inherit;
background: var(--border);
}
.sidebar-resizer:hover::before,
body.is-resizing-sidebar .sidebar-resizer::before {
background: var(--accent);
}
/* Во время drag меняем курсор по всей странице и запрещаем выделение текста. */
body.is-resizing-sidebar {
cursor: col-resize;
user-select: none;
}
/* ==========================================================================
04b. Top Bar Resizer
--------------------------------------------------------------------------
Горизонтальная ручка под шапкой в режиме navLayout: top.
========================================================================== */
.top-bar-resizer {
display: none;
width: 100%;
height: 8px;
margin-top: 8px;
margin-bottom: 6px;
border-radius: 999px;
cursor: row-resize;
background: transparent;
transition: background 160ms ease;
flex: 0 0 auto;
}
html[data-nav-layout="top"] .top-bar-resizer:not([hidden]) {
display: block;
}
.top-bar-resizer::before {
content: "";
display: block;
width: 100%;
height: 2px;
margin: 3px auto;
border-radius: inherit;
background: var(--border);
}
.top-bar-resizer:hover::before,
body.is-resizing-top-bar .top-bar-resizer::before {
background: var(--accent);
}
body.is-resizing-top-bar {
cursor: row-resize;
user-select: none;
}
/* ==========================================================================
05. Sidebar Panel
--------------------------------------------------------------------------
Sticky-панель слева: заголовок, шестеренка, поиск и дерево навигации.
========================================================================== */
.side-panel {
position: sticky;
top: 20px;
min-width: 0;
}
/* .hero остался как совместимый контейнерный класс после уплотнения шапки. */
.hero {
display: block;
margin-bottom: 12px;
}
/* Общая "стеклянная" поверхность для главных панелей. */
.hero-content,
.control-panel,
.catalog-section,
.empty-state {
border: 1px solid var(--border);
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(22px);
position: relative;
overflow: hidden;
}
.hero-content {
max-height: calc(100vh - 40px);
min-height: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: clamp(16px, 2.6vw, 24px);
border-radius: 22px;
overflow: visible;
}
.side-panel .hero-content {
justify-content: flex-start;
overflow: auto;
}
/* Акцентный градиент под узором — отдельно для каждого типа блоков. */
html:not([data-panel-pattern-nav="none"]) .hero-content::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-nav-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-section="none"]) .catalog-section::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-section-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-group="none"]) .catalog-group::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-group-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-nav="none"]) .hero-content::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-nav-opacity);
background-image: var(--panel-pattern-image-nav);
background-size: var(--panel-pattern-size-nav);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height));
}
html:not([data-panel-pattern-section="none"]) .catalog-section::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-section-opacity);
background-image: var(--panel-pattern-image-section);
background-size: var(--panel-pattern-size-section);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height));
}
html:not([data-panel-pattern-group="none"]) .catalog-group::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-group-opacity);
background-image: var(--panel-pattern-image-group);
background-size: var(--panel-pattern-size-group);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height));
}
html:not([data-panel-pattern-item="none"]) .nav-card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-item-opacity);
background-image: var(--panel-pattern-image-item);
background-size: var(--panel-pattern-size-item);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height));
}
.catalog-section > summary,
.catalog-section > .section-groups,
.catalog-group > summary,
.catalog-group > .items-grid {
position: relative;
z-index: 1;
}
.hero-content > * {
position: relative;
z-index: 1;
}
/* Общий стиль маленьких uppercase-меток и номеров. */
.eyebrow,
.section-number,
.group-number,
.item-number {
margin: 0;
color: var(--accent);
font-size: 0.82rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
/* Нумерация управляется toggle-настройкой "Показывать нумерацию". */
html:not([data-show-numbers="true"]) .section-number,
html:not([data-show-numbers="true"]) .group-number,
html:not([data-show-numbers="true"]) .item-number {
display: none;
}
/* Теги и тип в дереве управляются настройками showTreeTags / showTreeType. */
html[data-show-tree-tags="false"] .tree-tags {
display: none;
}
html[data-show-tree-type="false"] .tree-file-type {
display: none;
}
html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
display: none;
}
/* Подсветка совпадений поиска в дереве и в карточках. */
.search-mark {
border-radius: 4px;
padding: 0 2px;
color: white;
background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
}
/* ==========================================================================
06. Settings Popup
--------------------------------------------------------------------------
Настройки вынесены из sidebar в fixed popup, чтобы на мобильных устройствах
они не перекрывались потоком контента и всегда были поверх страницы.
========================================================================== */
.control-panel {
position: fixed;
z-index: 1001;
top: 24px;
right: 24px;
width: min(var(--settings-panel-width), calc(100% - 48px));
max-height: calc(100vh - 48px);
overflow: auto;
display: grid;
gap: 10px;
padding: 14px;
border-radius: 24px;
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(-8px) scale(0.98);
transform-origin: top right;
transition:
opacity 180ms ease,
visibility 180ms ease,
transform 180ms ease;
}
/* Затемнение и blur-подложка за открытыми настройками. */
.settings-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(2, 6, 23, 0.42);
opacity: 0;
visibility: hidden;
pointer-events: none;
backdrop-filter: blur(4px);
transition: opacity 180ms ease, visibility 180ms ease;
}
/* Состояния открытия popup и overlay. */
.settings-overlay.is-open {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.control-panel.is-open {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateY(0) scale(1);
}
/* Кнопки шестеренки и закрытия имеют один визуальный язык. */
.settings-button,
.settings-close {
border: 1px solid var(--border);
cursor: pointer;
color: var(--text);
background: var(--surface);
transition: transform 160ms ease, background 160ms ease, border-color 160ms ease;
}
.settings-button {
position: absolute;
z-index: 21;
top: 16px;
right: 16px;
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: 999px;
font-size: 1.18rem;
}
.settings-button:hover,
.settings-close:hover {
transform: translateY(-1px);
border-color: rgba(var(--accent-rgb), 0.5);
background: rgba(var(--accent-rgb), 0.12);
}
/* Заголовок popup-настроек. */
.settings-panel-head {
display: flex;
justify-content: space-between;
gap: 14px;
align-items: center;
}
.settings-panel-eyebrow {
margin: 0;
flex: 1 1 auto;
min-width: 0;
}
.settings-close {
width: 34px;
height: 34px;
flex: 0 0 auto;
border-radius: 999px;
font-size: 1.25rem;
line-height: 1;
}
.settings-width-row {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto auto;
gap: 6px 10px;
}
.settings-width-row span {
grid-column: 1;
grid-row: 1;
}
.settings-width-row output {
grid-column: 2;
grid-row: 1;
}
.settings-width-row input[type="range"] {
grid-column: 1 / -1;
grid-row: 2;
}
.settings-section {
display: grid;
gap: 10px;
}
.settings-section[hidden],
.settings-section-collapsible[hidden] {
display: none;
}
.settings-section-title {
margin: 0;
color: var(--text);
font-size: 0.82rem;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.settings-section-hint {
margin: 0;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.45;
}
.settings-section-hint code {
font-size: 0.78rem;
}
.catalog-columns-group {
display: grid;
gap: 8px;
}
.settings-section-collapsible {
border: 1px solid var(--border);
border-radius: 18px;
padding: 10px 12px;
background: var(--surface);
}
.settings-section-collapsible > summary {
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.settings-section-collapsible > summary::-webkit-details-marker {
display: none;
}
.settings-section-summary .settings-section-title {
margin: 0;
}
.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator {
transform: rotate(-90deg);
}
.settings-section-body {
display: grid;
gap: 10px;
padding-top: 10px;
}
.layout-limit-row {
position: relative;
grid-template-columns: minmax(128px, 1.5fr) minmax(0, 2fr) auto;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.02);
cursor: help;
}
.layout-limit-row[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
z-index: 30;
left: 8px;
right: 8px;
bottom: calc(100% + 6px);
top: auto;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--panel-strong);
color: var(--text);
font-size: 0.78rem;
font-weight: 500;
line-height: 1.45;
box-shadow: var(--shadow);
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(4px);
transition:
opacity 140ms ease,
visibility 140ms ease,
transform 140ms ease;
}
.layout-limit-row[data-tooltip]:hover::after,
.layout-limit-row[data-tooltip]:focus-within::after {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.layout-limit-row span {
line-height: 1.25;
}
/* ==========================================================================
07. Search Field And Control Inputs
--------------------------------------------------------------------------
Поиск живет в sidebar, а остальные контролы в popup. Здесь собраны общие
стили input, toggle-row, segmented buttons и accent swatches.
========================================================================== */
.search-field {
margin-top: 15px;
display: grid;
gap: 6px;
max-width: 720px;
position: relative;
}
.control-group > span {
color: var(--muted);
font-size: 0.82rem;
font-weight: 800;
}
/* Иконка поиска рисуется CSS-ом, чтобы не добавлять лишний DOM. */
.search-field::after {
content: "⌕";
position: absolute;
right: 11px;
top: 50%;
transform: translateY(-50%);
color: var(--accent);
font-size: 1.05rem;
font-weight: 900;
pointer-events: none;
}
.search-field input {
width: 100%;
min-height: 38px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
color: var(--text);
outline: none;
padding: 0 34px 0 12px;
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.search-field input:focus {
border-color: rgba(var(--accent-rgb), 0.62);
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16);
background: var(--panel-strong);
}
/* Toggle-row используется для checkbox-настроек с описанием. */
.toggle-row {
display: flex;
justify-content: space-between;
gap: 14px;
align-items: center;
min-height: 54px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 18px;
background: var(--surface);
}
.toggle-row span {
display: grid;
gap: 2px;
}
.toggle-row strong {
font-size: 0.9rem;
}
.toggle-row small {
color: var(--muted);
font-weight: 700;
}
/* Кастомный checkbox выглядит как компактный switch. */
.toggle-row input {
appearance: none;
width: 48px;
height: 28px;
flex: 0 0 auto;
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(148, 163, 184, 0.26);
cursor: pointer;
position: relative;
transition: background 160ms ease, border-color 160ms ease;
}
.toggle-row input::before {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
border-radius: 999px;
background: white;
transition: transform 160ms ease;
}
.toggle-row input:checked {
border-color: rgba(var(--accent-rgb), 0.6);
background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
}
.toggle-row input:checked::before {
transform: translateX(20px);
}
/* Контейнеры групп кнопок настроек. */
.control-group {
display: flex;
align-items: center;
gap: 6px;
min-height: 42px;
padding: 6px;
border: 1px solid var(--border);
border-radius: 18px;
background: var(--surface);
}
.control-group > span {
min-width: 68px;
padding-left: 6px;
}
/* Для четырех режимов отображения кнопки складываются в компактную сетку. */
.view-group {
flex-wrap: wrap;
}
.view-group > span {
width: 100%;
}
.view-group .segmented {
flex-basis: calc(50% - 3px);
}
/* Segmented buttons и color swatches имеют общий focus/hover baseline. */
.segmented,
.accent-swatch {
border: 0;
cursor: pointer;
transition: transform 160ms ease, background 160ms ease, color 160ms ease;
}
.segmented {
flex: 1;
min-height: 31px;
min-width: 0;
border-radius: 12px;
background: transparent;
color: var(--muted);
font-size: 0.84rem;
font-weight: 800;
}
.segmented:hover,
.accent-swatch:hover {
transform: translateY(-1px);
}
.segmented.is-active {
background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
color: white;
}
.accent-group {
flex-wrap: wrap;
justify-content: flex-start;
row-gap: 8px;
}
.accent-group > span {
width: 100%;
}
.accent-swatch {
width: 32px;
height: 32px;
border: 2px solid transparent;
border-radius: 999px;
box-shadow: inset 0 0 0 4px rgba(255, 255, 255, 0.18);
}
/* Акцентные кружки используют те же цвета, что и html[data-accent]. */
.accent-swatch[data-accent="violet"] {
background: linear-gradient(135deg, #8b5cf6, #22d3ee);
}
.accent-swatch[data-accent="cyan"] {
background: linear-gradient(135deg, #06b6d4, #3b82f6);
}
.accent-swatch[data-accent="lime"] {
background: linear-gradient(135deg, #84cc16, #14b8a6);
}
.accent-swatch[data-accent="amber"] {
background: linear-gradient(135deg, #f59e0b, #f43f5e);
}
.accent-swatch.is-active {
border-color: var(--text);
transform: scale(1.05);
}
/* Кнопка своего градиента: pill, пунктир, подпись и превию через CSS-переменные. */
.accent-swatch-custom {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
width: auto;
min-width: 58px;
height: 32px;
padding: 0 10px;
border: 2px dashed rgba(255, 255, 255, 0.42);
box-shadow: none;
background:
linear-gradient(45deg, rgba(255, 255, 255, 0.08) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, 0.08) 75%),
linear-gradient(45deg, rgba(255, 255, 255, 0.08) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, 0.08) 75%);
background-size: 8px 8px;
background-position: 0 0, 4px 4px;
}
html[data-theme="light"] .accent-swatch-custom {
border-color: rgba(15, 23, 42, 0.28);
background:
linear-gradient(45deg, rgba(15, 23, 42, 0.06) 25%, transparent 25%, transparent 75%, rgba(15, 23, 42, 0.06) 75%),
linear-gradient(45deg, rgba(15, 23, 42, 0.06) 25%, transparent 25%, transparent 75%, rgba(15, 23, 42, 0.06) 75%);
background-size: 8px 8px;
background-position: 0 0, 4px 4px;
}
.accent-swatch-custom::before {
content: "";
position: absolute;
inset: 2px;
border-radius: inherit;
background: linear-gradient(
var(--custom-accent-angle, 135deg),
var(--custom-accent-from, #8b5cf6),
var(--custom-accent-to, #22d3ee)
);
}
.accent-swatch-label {
position: relative;
z-index: 1;
color: white;
font-size: 0.72rem;
font-weight: 900;
letter-spacing: 0.04em;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
.accent-swatch-custom.is-active {
border-style: solid;
border-color: var(--text);
transform: scale(1.05);
}
.pattern-group {
align-items: stretch;
}
.pattern-targets-panel {
display: grid;
gap: 10px;
width: 100%;
}
.pattern-target-panel .settings-section-body {
display: grid;
gap: 10px;
padding-top: 10px;
}
.pattern-swatches {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
width: 100%;
}
.pattern-swatch {
min-height: 54px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
color: var(--muted);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
cursor: pointer;
position: relative;
overflow: hidden;
padding: 0;
}
.pattern-swatch::before {
content: "";
position: absolute;
inset: 0;
opacity: 0.95;
pointer-events: none;
}
.pattern-swatch[data-panel-pattern="none"]::before {
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), transparent 70%);
}
.pattern-swatch[data-panel-pattern="hex"]::before {
background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
background-size: 48px 42px;
}
html[data-theme="light"] .pattern-swatch[data-panel-pattern="hex"]::before {
background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
}
.pattern-swatch[data-panel-pattern="dots"]::before {
background-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px);
background-size: 12px 12px;
}
.pattern-swatch[data-panel-pattern="grid"]::before {
background-image:
linear-gradient(var(--pattern-color) 1px, transparent 1px),
linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px);
background-size: 16px 16px;
}
.pattern-swatch[data-panel-pattern="diagonal"]::before {
background-image: repeating-linear-gradient(
-45deg,
transparent,
transparent 7px,
var(--pattern-color) 7px,
var(--pattern-color) 8px
);
}
.pattern-swatch[data-panel-pattern="cross"]::before {
background-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 9px,
var(--pattern-color) 9px,
var(--pattern-color) 10px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 9px,
var(--pattern-color) 9px,
var(--pattern-color) 10px
);
}
.pattern-swatch[data-panel-pattern="mesh"]::before {
background-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.24) 1px,
transparent 0
);
background-size: 12px 12px;
}
.pattern-swatch-label {
position: relative;
z-index: 1;
display: grid;
place-items: end start;
width: 100%;
height: 100%;
padding: 6px 8px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 58%);
color: #f8fafc;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
html[data-theme="light"] .pattern-swatch-label {
background: linear-gradient(to top, rgba(255, 255, 255, 0.82), transparent 58%);
color: #101827;
text-shadow: none;
}
.pattern-swatch.is-active {
border-color: rgba(var(--accent-rgb), 0.72);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18);
}
.pattern-tuning {
display: grid;
gap: 12px;
width: 100%;
}
.pattern-tuning .accent-custom-row {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto auto;
gap: 6px 10px;
}
.pattern-tuning .accent-custom-row span {
grid-column: 1;
grid-row: 1;
}
.pattern-tuning .accent-custom-row output {
grid-column: 2;
grid-row: 1;
}
.pattern-tuning .accent-custom-row input[type="range"] {
grid-column: 1 / -1;
grid-row: 2;
}
/* Панель настройки своего градиента появляется при data-accent="custom". */
.accent-custom-panel {
display: grid;
gap: 8px;
padding: 10px;
border: 1px solid var(--border);
border-radius: 18px;
background: var(--surface);
}
.accent-custom-panel[hidden] {
display: none;
}
.accent-custom-title {
margin: 0;
color: var(--muted);
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.accent-custom-row {
display: grid;
grid-template-columns: 72px minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 0.84rem;
font-weight: 700;
}
.accent-custom-row input[type="range"] {
width: 100%;
accent-color: var(--accent);
}
.accent-custom-row input[type="color"] {
width: 100%;
height: 34px;
padding: 2px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--panel);
cursor: pointer;
}
.accent-custom-row:has(input[type="color"]) {
grid-template-columns: 72px 1fr;
}
.accent-custom-row output {
min-width: 3.2rem;
color: var(--accent);
font-size: 0.82rem;
font-weight: 800;
text-align: right;
}
.accent-custom-preview {
height: 36px;
border: 1px solid var(--border);
border-radius: 12px;
background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
}
/* ==========================================================================
08. File-Tree Navigation
--------------------------------------------------------------------------
Левая навигация визуально имитирует дерево файлов:
details/summary дают нативное раскрытие, а CSS добавляет линии, caret,
"folder" для разделов/групп и "file" для конечных элементов.
========================================================================== */
.nav-tree {
display: grid;
gap: 2px;
min-height: 0;
padding: 4px 2px 0;
font-size: 0.86rem;
}
/* Details в дереве intentionally выглядят не как карточки, а как вложенный список. */
.tree-section,
.tree-group {
border: 0;
border-radius: 0;
background: transparent;
overflow: visible;
}
.tree-group {
position: relative;
margin: 1px 0 1px 18px;
padding-left: 10px;
border-left: 1px solid var(--border);
}
/* Summary получает caret, иконку папки и кликабельный текст. */
.tree-section > summary,
.tree-group > summary {
display: grid;
grid-template-columns: 16px minmax(0, 1fr);
gap: 3px;
align-items: center;
list-style: none;
cursor: pointer;
min-height: 28px;
padding: 1px 2px;
border-radius: 9px;
}
/* Нативный маркер details скрываем, так как рисуем свой caret. */
.tree-section > summary::-webkit-details-marker,
.tree-group > summary::-webkit-details-marker {
display: none;
}
/* Caret поворачивается через состояние [open] у details. */
.tree-caret {
width: 16px;
height: 24px;
display: grid;
place-items: center;
color: var(--muted);
}
.tree-caret::before {
content: "▾";
font-size: 0.72rem;
transition: transform 140ms ease;
}
.tree-section:not([open]) > summary .tree-caret::before,
.tree-group:not([open]) > summary .tree-caret::before {
transform: rotate(-90deg);
}
/* Конечный элемент дерева: иконка файла, название и optional теги. */
.tree-node {
min-width: 0;
width: 100%;
min-height: 26px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
border: 0;
border-radius: 8px;
cursor: pointer;
color: var(--text);
background: transparent;
text-align: left;
font: inherit;
font-weight: 760;
}
.tree-node span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Вертикальная линия вложенности дерева. */
.tree-node::before {
color: var(--accent);
opacity: 0.92;
}
/* Папки и файлы нарисованы CSS-иконками, чтобы не тянуть отдельные SVG. */
.tree-folder {
grid-template-columns: 18px minmax(0, 1fr);
}
.tree-folder::before {
content: "📁";
font-size: 0.9rem;
}
.tree-file {
grid-template-columns: 20px minmax(0, 1fr);
align-items: start;
gap: 8px;
}
.tree-file::before {
content: none;
}
.tree-file-body {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.tree-file-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-file-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
min-width: 0;
}
html[data-tree-meta-layout="inline"] .tree-file-body {
flex-direction: row;
align-items: center;
gap: 8px;
}
html[data-tree-meta-layout="inline"] .tree-file-title {
flex: 1 1 auto;
}
html[data-tree-meta-layout="inline"] .tree-file-meta {
flex: 0 1 auto;
justify-content: flex-end;
}
html[data-tree-meta-layout="stacked"] .tree-file-body {
align-items: stretch;
}
.tree-file-type {
margin: 0;
color: var(--accent);
font-size: 0.72rem;
font-weight: 750;
white-space: nowrap;
}
.tree-tags.tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 0;
}
.tree-tags.tags span {
border: 1px solid var(--border);
border-radius: 999px;
padding: 2px 6px;
background: rgba(var(--accent-rgb), 0.1);
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
}
html:not([data-tree-columns="1"]) .tree-file {
font-size: 0.8rem;
}
html:not([data-tree-columns="1"]) .tree-node-icon {
width: 16px;
height: 16px;
border-radius: 4px;
}
.tree-node-icon {
width: 18px;
height: 18px;
border-radius: 5px;
object-fit: cover;
display: block;
}
/* Контейнер элементов внутри группы дерева. */
.tree-items {
position: relative;
display: grid;
grid-template-columns: repeat(var(--tree-columns, 1), minmax(0, 1fr));
gap: 2px 8px;
margin-left: 18px;
padding-left: 10px;
border-left: 1px solid var(--border);
}
.tree-node:hover,
.tree-section > summary:hover,
.tree-group > summary:hover {
background: rgba(var(--accent-rgb), 0.14);
}
/* ==========================================================================
09. Catalog Structure
--------------------------------------------------------------------------
Правая часть страницы. Разделы и подразделы тоже построены на details,
поэтому пользователь может сворачивать не только дерево, но и контент.
========================================================================== */
.catalog {
display: grid;
gap: 12px;
min-width: 0;
}
/* Главные glass-карточки разделов. */
.catalog-section {
border-radius: 24px;
overflow: hidden;
scroll-margin-top: 18px;
}
.catalog-section,
.catalog-group {
display: block;
}
/* Summary у разделов/групп заменяет заголовок и одновременно управляет collapse. */
.catalog-section > summary,
.catalog-group > summary {
list-style: none;
cursor: pointer;
}
.catalog-section > summary::-webkit-details-marker,
.catalog-group > summary::-webkit-details-marker {
display: none;
}
/* Шапка раздела: номер, название, описание, счетчик и индикатор collapse. */
.section-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
padding: 18px;
border-bottom: 1px solid var(--border);
background:
radial-gradient(circle at top left, rgba(var(--accent-rgb), 0.14), transparent 22rem),
rgba(255, 255, 255, 0.035);
}
.section-meta,
.group-meta {
display: inline-flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
}
.section-head h2,
.group-head h3,
.nav-card h4,
.empty-state h2 {
margin: 0;
color: var(--text);
letter-spacing: -0.04em;
}
.section-head h2 {
margin-top: 5px;
font-size: clamp(1.35rem, 3.2vw, 2.15rem);
}
.section-head p:not(.section-number),
.group-head p:not(.group-number) {
max-width: 760px;
margin: 5px 0 0;
color: var(--muted);
line-height: 1.5;
}
/* Счетчики помогают понять размер раздела без чтения всех карточек. */
.section-count {
white-space: nowrap;
border: 1px solid var(--border);
border-radius: 999px;
padding: 8px 12px;
color: var(--muted);
background: var(--surface);
font-size: 0.86rem;
font-weight: 800;
}
/* Стрелка сворачивания поворачивается через [open]. */
.collapse-indicator {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
color: var(--accent);
font-weight: 900;
transition: transform 160ms ease;
}
.catalog-section:not([open]) > .section-head .collapse-indicator,
.catalog-group:not([open]) > .group-head .collapse-indicator {
transform: rotate(-90deg);
}
/* Подразделы внутри секции. */
.section-groups {
display: grid;
gap: 12px;
padding: 12px;
}
/* Плоский список карточек без обёртки section/group. */
.catalog > .catalog-flat-grid {
padding: 12px;
}
/* Элементы раздела без промежуточного подраздела. */
.section-groups > .items-grid:first-child:last-child {
padding: 0;
}
/* Файлы сразу под разделом в дереве (без tree-group). */
.tree-items-direct {
margin-left: 18px;
padding-left: 10px;
border-left: 1px solid var(--border);
}
.catalog-group {
position: relative;
border: 1px solid var(--border);
border-radius: 18px;
background: rgba(255, 255, 255, 0.035);
overflow: hidden;
scroll-margin-top: 18px;
}
.group-head {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 14px;
border-bottom: 1px solid var(--border);
}
.group-head h3 {
margin-top: 4px;
font-size: 1.08rem;
}
.group-count {
width: 36px;
height: 36px;
display: grid;
place-items: center;
flex: 0 0 auto;
border-radius: 999px;
background: rgba(var(--accent-rgb), 0.14);
color: var(--accent);
font-weight: 900;
}
/* ==========================================================================
10. Navigation Cards
--------------------------------------------------------------------------
Конечные кликабельные элементы каталога. Карточки используют CSS-переменные
--card-x/--card-y, которые JS обновляет при движении мыши для shine-эффекта.
========================================================================== */
.items-grid {
display: grid;
gap: 10px;
padding: 10px;
}
.catalog.cards-view .items-grid {
grid-template-columns: repeat(var(--catalog-cards-columns, 3), minmax(0, 1fr));
}
/* Базовая карточка с иконкой, контентом, тегами, стрелкой и shine-слоем. */
.nav-card {
--tilt-x: 0deg;
--tilt-y: 0deg;
position: relative;
min-height: 196px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 18px;
background: var(--panel);
color: var(--text);
text-decoration: none;
overflow: hidden;
transform-origin: center;
transform: perspective(900px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
will-change: transform;
transition:
transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
border-color 220ms ease,
background 220ms ease,
box-shadow 220ms ease;
scroll-margin-top: 18px;
}
/* Временная подсветка цели после клика в дереве. */
.catalog-section.is-focused,
.catalog-group.is-focused,
.nav-card.is-focused {
border-color: rgba(var(--accent-rgb), 0.75);
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16), var(--shadow);
}
/* Градиентный бордер/подсветка под курсором. */
.nav-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(
circle at var(--card-x) var(--card-y),
rgba(var(--accent-rgb), 0.24),
transparent 34%
);
opacity: 0;
transition: opacity 180ms ease;
pointer-events: none;
z-index: 1;
}
/* Hover задается классом .is-hovered из JS, чтобы убрать скачки на границах. */
.nav-card.is-hovered,
.nav-card:focus-visible {
border-color: rgba(var(--accent-rgb), 0.52);
background: var(--panel-strong);
box-shadow: 0 22px 70px rgba(var(--accent-rgb), 0.16);
outline: none;
}
.nav-card.is-hovered::before,
.nav-card:focus-visible::before {
opacity: 1;
}
/* Иконка сервиса берется из serviceMeta. */
.nav-card img {
position: relative;
z-index: 1;
width: 50px;
height: 50px;
object-fit: contain;
filter: drop-shadow(0 14px 24px rgba(0, 0, 0, 0.22));
}
.card-content {
position: relative;
z-index: 1;
display: grid;
gap: 5px;
}
.nav-card h4 {
font-size: 1rem;
line-height: 1.25;
}
/* Тип сервиса выделен акцентом и помогает быстро считывать назначение. */
.item-type {
margin: 0;
color: var(--accent);
font-size: 0.9rem;
font-weight: 750;
}
/* Теги показывают, по каким словам карточку удобно искать. */
.tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 2px;
}
.tags span {
border: 1px solid var(--border);
border-radius: 999px;
padding: 3px 7px;
background: rgba(var(--accent-rgb), 0.1);
color: var(--muted);
font-size: 0.76rem;
font-weight: 800;
}
/* Стрелка справа подчеркивает, что карточка ведет по ссылке. */
.arrow {
position: absolute;
right: 16px;
bottom: 14px;
z-index: 1;
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 999px;
background: rgba(var(--accent-rgb), 0.14);
color: var(--accent);
font-size: 1.15rem;
transition: transform 180ms ease, background 180ms ease, color 180ms ease;
}
.nav-card.is-hovered .arrow,
.nav-card:focus-visible .arrow {
transform: translateX(4px);
background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
color: white;
}
/* Декоративный shine слой, положение которого обновляется pointermove. */
.shine {
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(
110deg,
transparent 0%,
rgba(255, 255, 255, 0.16) 42%,
transparent 64%
);
transform: translateX(-130%);
transition: transform 540ms ease;
pointer-events: none;
}
.nav-card.is-hovered .shine,
.nav-card:focus-visible .shine {
transform: translateX(130%);
}
/* ==========================================================================
11. View Modes
--------------------------------------------------------------------------
Режимы отображения переключаются классами на .catalog:
cards-view, list-view, compact-cards-view, compact-list-view.
========================================================================== */
.catalog.list-view .items-grid {
grid-template-columns: repeat(var(--catalog-list-columns, 1), minmax(0, 1fr));
}
/* В list-режимах карточка вытягивается в строку. */
.catalog.list-view .nav-card,
.catalog.compact-list-view .nav-card {
min-height: 96px;
display: grid;
grid-template-columns: 50px minmax(0, 1fr) 36px;
align-items: center;
gap: 12px;
}
.catalog.list-view .arrow,
.catalog.compact-list-view .arrow {
position: static;
justify-self: end;
}
/* Compact modes скрывают второстепенную информацию и уменьшают отступы. */
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(var(--catalog-compact-cards-columns, 5), minmax(0, 1fr));
}
/* Compact list на широких экранах использует настраиваемое число колонок. */
.catalog.compact-list-view .items-grid {
grid-template-columns: repeat(var(--catalog-compact-list-columns, 3), minmax(0, 1fr));
}
.catalog.compact-cards-view .section-head,
.catalog.compact-list-view .section-head {
padding: 14px 16px;
}
/* В compact-режимах описания и теги скрыты ради плотности. */
.catalog.compact-cards-view .section-head p:not(.section-number),
.catalog.compact-list-view .section-head p:not(.section-number),
.catalog.compact-cards-view .group-head p:not(.group-number),
.catalog.compact-list-view .group-head p:not(.group-number),
.catalog.compact-cards-view .tags,
.catalog.compact-list-view .tags,
.catalog.compact-list-view .item-type {
display: none;
}
.catalog.compact-cards-view .section-head h2,
.catalog.compact-list-view .section-head h2 {
font-size: clamp(1.35rem, 3vw, 2rem);
}
.catalog.compact-cards-view .group-head,
.catalog.compact-list-view .group-head {
padding: 10px 12px;
align-items: center;
}
.catalog.compact-cards-view .group-head h3,
.catalog.compact-list-view .group-head h3 {
margin-top: 4px;
font-size: 1.05rem;
}
.catalog.compact-cards-view .nav-card {
min-height: 116px;
gap: 8px;
padding: 12px;
}
.catalog.compact-cards-view .nav-card img {
width: 44px;
height: 44px;
}
.catalog.compact-cards-view .nav-card h4,
.catalog.compact-list-view .nav-card h4 {
font-size: 0.98rem;
}
.catalog.compact-cards-view .item-type,
.catalog.compact-list-view .item-type {
font-size: 0.82rem;
}
.catalog.compact-cards-view .arrow {
width: 30px;
height: 30px;
right: 12px;
bottom: 12px;
}
.catalog.compact-list-view .nav-card {
min-height: 56px;
padding: 8px 10px;
grid-template-columns: 32px minmax(0, 1fr) 28px;
}
.catalog.compact-list-view .nav-card img {
width: 32px;
height: 32px;
}
.catalog.compact-list-view .item-number {
font-size: 0.72rem;
}
/* ==========================================================================
12. Empty State
--------------------------------------------------------------------------
Показывается, когда фильтр поиска не оставил ни одной карточки.
========================================================================== */
.empty-state {
min-height: 280px;
display: grid;
place-items: center;
align-content: center;
gap: 12px;
padding: 36px;
border-radius: 30px;
text-align: center;
}
.empty-state img {
width: 74px;
height: 74px;
}
.empty-state p {
max-width: 480px;
margin: 0;
color: var(--muted);
}
/* ==========================================================================
13. Responsive Layout
--------------------------------------------------------------------------
Основной режим выбирается по типу ввода, а не по ширине экрана:
- touch / coarse pointer -> компактный "мобильный" layout;
- hover + fine pointer -> десктопный layout даже на широком touch-мониторе.
Узкое окно с мышью дополнительно поджимает сетку через max-width fallback.
========================================================================== */
/* Узкое окно на устройствах с мышью: sidebar и сетка карточек чуть компактнее. */
@media (hover: hover) and (pointer: fine) and (max-width: 1120px) {
.app-layout {
grid-template-columns: minmax(240px, min(var(--sidebar-width), 320px)) 6px minmax(0, 1fr);
}
.catalog.cards-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-cards-columns, 3), 2), minmax(0, 1fr));
}
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-compact-cards-columns, 5), 3), minmax(0, 1fr));
}
}
/* Touch-устройства: stacked layout, без resizer, крупнее touch-targets. */
@media (hover: none), (pointer: coarse) {
.shell {
width: min(var(--shell-max-width), calc(100% - 22px));
padding: 14px 0;
}
.app-layout {
grid-template-columns: 1fr;
}
.sidebar-resizer,
.top-bar-resizer {
display: none;
}
.side-panel {
position: static;
}
.hero-content {
min-height: auto;
max-height: none;
padding: 16px 14px;
border-radius: 20px;
}
.nav-tree {
max-height: 320px;
overflow: auto;
}
.control-panel,
.catalog-section,
.empty-state {
border-radius: 24px;
}
.control-panel {
top: 12px;
right: 12px;
width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
}
.control-group {
flex-wrap: wrap;
}
.control-group .segmented {
flex-basis: calc(50% - 4px);
}
.control-group > span {
width: 100%;
min-width: 0;
}
.accent-group > span {
width: auto;
}
.section-head,
.group-head {
display: grid;
}
.section-count {
width: fit-content;
}
.section-groups {
padding: 8px;
}
.catalog.cards-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-cards-columns, 3), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.list-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-list-columns, 1), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.compact-list-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-compact-list-columns, 3), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-compact-cards-columns, 5), 2), minmax(0, 1fr));
padding: 8px;
}
.nav-card {
min-height: 180px;
}
.catalog.list-view .nav-card {
grid-template-columns: 48px minmax(0, 1fr);
min-height: 136px;
}
.catalog.compact-list-view .nav-card {
grid-template-columns: 42px minmax(0, 1fr);
min-height: 84px;
}
.catalog.list-view .arrow,
.catalog.compact-list-view .arrow {
position: absolute;
}
.nav-card img {
width: 50px;
height: 50px;
}
}
/* ==========================================================================
14. Reduced Motion
--------------------------------------------------------------------------
Уважает системную настройку пользователя и отключает анимации/плавный скролл.
========================================================================== */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}