From 31f0276cb3954a64f005f6d215973bb12486d6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=91=D1=80=D0=B0=D1=82=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D1=85?= Date: Wed, 24 Jun 2026 15:45:51 +0300 Subject: [PATCH] first commit --- assets/ceph.svg | 1 + assets/config.svg | 1 + assets/database.svg | 1 + assets/gitea.svg | 1 + assets/grafana.svg | 1 + assets/kubernetes.svg | 1 + assets/ldap.svg | 1 + assets/messages.svg | 1 + assets/opensearch.svg | 1 + assets/prometheus.svg | 1 + assets/rabbitmq.svg | 1 + assets/redmine.svg | 1 + assets/rules.svg | 1 + assets/search.svg | 1 + assets/server.svg | 1 + assets/xmpp.svg | 1 + assets/xpath.svg | 1 + index.html | 127 ++++++++ script.js | 405 +++++++++++++++++++++++++ styles.css | 687 ++++++++++++++++++++++++++++++++++++++++++ 20 files changed, 1236 insertions(+) create mode 100644 assets/ceph.svg create mode 100644 assets/config.svg create mode 100644 assets/database.svg create mode 100644 assets/gitea.svg create mode 100644 assets/grafana.svg create mode 100644 assets/kubernetes.svg create mode 100644 assets/ldap.svg create mode 100644 assets/messages.svg create mode 100644 assets/opensearch.svg create mode 100644 assets/prometheus.svg create mode 100644 assets/rabbitmq.svg create mode 100644 assets/redmine.svg create mode 100644 assets/rules.svg create mode 100644 assets/search.svg create mode 100644 assets/server.svg create mode 100644 assets/xmpp.svg create mode 100644 assets/xpath.svg create mode 100644 index.html create mode 100644 script.js create mode 100644 styles.css diff --git a/assets/ceph.svg b/assets/ceph.svg new file mode 100644 index 0000000..4a8974f --- /dev/null +++ b/assets/ceph.svg @@ -0,0 +1 @@ + diff --git a/assets/config.svg b/assets/config.svg new file mode 100644 index 0000000..fa5878b --- /dev/null +++ b/assets/config.svg @@ -0,0 +1 @@ + diff --git a/assets/database.svg b/assets/database.svg new file mode 100644 index 0000000..578e7c7 --- /dev/null +++ b/assets/database.svg @@ -0,0 +1 @@ + diff --git a/assets/gitea.svg b/assets/gitea.svg new file mode 100644 index 0000000..83ba6e7 --- /dev/null +++ b/assets/gitea.svg @@ -0,0 +1 @@ + diff --git a/assets/grafana.svg b/assets/grafana.svg new file mode 100644 index 0000000..8546558 --- /dev/null +++ b/assets/grafana.svg @@ -0,0 +1 @@ + diff --git a/assets/kubernetes.svg b/assets/kubernetes.svg new file mode 100644 index 0000000..db237aa --- /dev/null +++ b/assets/kubernetes.svg @@ -0,0 +1 @@ + diff --git a/assets/ldap.svg b/assets/ldap.svg new file mode 100644 index 0000000..61ada3e --- /dev/null +++ b/assets/ldap.svg @@ -0,0 +1 @@ + diff --git a/assets/messages.svg b/assets/messages.svg new file mode 100644 index 0000000..8dddb1a --- /dev/null +++ b/assets/messages.svg @@ -0,0 +1 @@ + diff --git a/assets/opensearch.svg b/assets/opensearch.svg new file mode 100644 index 0000000..8cefb23 --- /dev/null +++ b/assets/opensearch.svg @@ -0,0 +1 @@ + diff --git a/assets/prometheus.svg b/assets/prometheus.svg new file mode 100644 index 0000000..1ff304a --- /dev/null +++ b/assets/prometheus.svg @@ -0,0 +1 @@ + diff --git a/assets/rabbitmq.svg b/assets/rabbitmq.svg new file mode 100644 index 0000000..d4996ea --- /dev/null +++ b/assets/rabbitmq.svg @@ -0,0 +1 @@ + diff --git a/assets/redmine.svg b/assets/redmine.svg new file mode 100644 index 0000000..5f3f737 --- /dev/null +++ b/assets/redmine.svg @@ -0,0 +1 @@ + diff --git a/assets/rules.svg b/assets/rules.svg new file mode 100644 index 0000000..3af5122 --- /dev/null +++ b/assets/rules.svg @@ -0,0 +1 @@ + diff --git a/assets/search.svg b/assets/search.svg new file mode 100644 index 0000000..bc0d852 --- /dev/null +++ b/assets/search.svg @@ -0,0 +1 @@ + diff --git a/assets/server.svg b/assets/server.svg new file mode 100644 index 0000000..24f4173 --- /dev/null +++ b/assets/server.svg @@ -0,0 +1 @@ + diff --git a/assets/xmpp.svg b/assets/xmpp.svg new file mode 100644 index 0000000..aa1376b --- /dev/null +++ b/assets/xmpp.svg @@ -0,0 +1 @@ + diff --git a/assets/xpath.svg b/assets/xpath.svg new file mode 100644 index 0000000..b87630a --- /dev/null +++ b/assets/xpath.svg @@ -0,0 +1 @@ + diff --git a/index.html b/index.html new file mode 100644 index 0000000..e71b2b2 --- /dev/null +++ b/index.html @@ -0,0 +1,127 @@ + + + + + + + Панель экосистемы + + + + + + +
+
+ +
+
+
+

Infrastructure map

+

Панель экосистемы

+

+ Единая точка входа в пользовательские, административные, + мониторинговые и инфраструктурные интерфейсы. +

+
+ +
+ + +
+ Тема + + +
+ +
+ Вид + + +
+ +
+ Акцент + + + + +
+
+
+ +
+
+ 0 +

раздела

+
+
+ 0 +

подразделов

+
+
+ 0 +

элементов

+
+
+ 0 +

найдено

+
+
+ +
+ + +
+ + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..8e1480a --- /dev/null +++ b/script.js @@ -0,0 +1,405 @@ +const STORAGE_KEY = "ecosystem-panel-preferences"; + +// Меняй навигацию здесь: разделы -> подразделы -> элементы. +// url можно заменить на реальные адреса, tags помогают поиску. +const ecosystemSections = [ + { + id: "user", + number: "1", + title: "Интерфейс пользователя", + description: "Рабочие инструменты пользователя для фильтрации и обработки сообщений.", + groups: [ + { + id: "message-filtering", + number: "1.1", + title: "Управление фильтрацией сообщений", + description: "Правила, задания, события и статистика фильтрации.", + items: [ + createItem("1.1.1", "Редактирование правил", "rules", ["правила", "фильтрация", "редактор"]), + createItem("1.1.2", "Управление заданиями", "redmine", ["redmine", "задачи", "задания"]), + createItem("1.1.3", "Применение правил", "rules", ["правила", "применение", "фильтрация"]), + createItem("1.1.4", "Просмотр статистики", "grafana", ["grafana", "статистика", "дашборд"]), + createItem("1.1.5", "Управление событиями фильтрации", "grafana", ["grafana", "события", "алерты"]), + createItem("1.1.6", "Просмотр журнала событий фильтрации", "grafana", ["grafana", "журнал", "события"]), + ], + }, + { + id: "message-processing", + number: "1.2", + title: "Обработка сообщений", + description: "Просмотр, оформление и доступ к хранилищам сообщений.", + items: [ + createItem("1.2.1", "Просмотр и оформление", "messages", ["сообщения", "просмотр", "оформление"]), + createItem("1.2.2", "Прямой доступ", "ceph", ["ceph", "доступ", "объекты"]), + createItem("1.2.3", "S3 Хранилище", "ceph", ["ceph", "s3", "хранилище"]), + ], + }, + ], + }, + { + id: "admin", + number: "2", + title: "Интерфейс администратора", + description: "Администрирование сервисов, кластеров, мониторинга и аппаратной части.", + groups: [ + { + id: "service-monitoring", + number: "2.1", + title: "Управление и мониторинг сервисов", + description: "Кластер, модули, события мониторинга и журналы ошибок.", + items: [ + createItem("2.1.1", "Управление кластером", "kubernetes", ["kubernetes", "кластер", "k8s"]), + createItem("2.1.2", "Мониторинг состояния модулей", "grafana", ["grafana", "модули", "мониторинг"]), + createItem("2.1.3", "Управление событиями мониторинга", "grafana", ["grafana", "события", "алерты"]), + createItem("2.1.4", "Просмотр журнала событий мониторинга", "grafana", ["grafana", "журнал", "мониторинг"]), + createItem("2.1.5", "Просмотр журналов ошибок", "opensearch", ["opensearch", "ошибки", "логи"]), + ], + }, + { + id: "service-configuration", + number: "2.2", + title: "Настройка сервисов", + description: "Пользователи, очереди, xPath, модули и конфигурации.", + items: [ + createItem("2.2.1", "Управление пользователями и очередями", "rabbitmq", ["пользователи", "очереди", "rabbitmq"]), + createItem("2.2.2", "Отладка xPath-запросов", "xpath", ["xpath", "отладка", "запросы"]), + createItem("2.2.3", "Конфигурирование модулей", "config", ["конфигурация", "модули", "настройки"]), + createItem("2.2.4", "Просмотр журнала конфигурирования", "gitea", ["gitea", "журнал", "конфигурация"]), + createItem("2.2.5", "Прямой доступ к конфигурации", "gitea", ["gitea", "конфигурация", "git"]), + ], + }, + { + id: "hardware", + number: "2.3", + title: "Управление и мониторинг аппаратной части", + description: "Сенсоры, ресурсы и прямые панели серверов.", + items: [ + createItem("2.3.1", "Мониторинг показаний сенсоров", "grafana", ["grafana", "сенсоры", "железо"]), + createItem("2.3.2", "Мониторинг потребления ресурсов", "grafana", ["grafana", "ресурсы", "метрики"]), + createItem("2.3.3", "СЕРВЕР01", "ipmi", ["ipmi", "сервер01", "железо"]), + createItem("2.3.4", "СЕРВЕР02", "ipmi", ["ipmi", "сервер02", "железо"]), + createItem("2.3.5", "СЕРВЕР03", "ipmi", ["ipmi", "сервер03", "железо"]), + createItem("2.3.6", "СЕРВЕР04", "ipmi", ["ipmi", "сервер04", "железо"]), + createItem("2.3.7", "СЕРВЕР05", "ipmi", ["ipmi", "сервер05", "железо"]), + ], + }, + { + id: "service-panels", + number: "2.4", + title: "Настройка отдельных сервисов", + description: "Панели управления инфраструктурными сервисами.", + items: [ + createItem("2.4.1", "Панель управления LDAP Account Manager", "ldap", ["ldap", "accounts", "пользователи"]), + createItem("2.4.2", "Панель управления кластера RabbitMQ", "rabbitmq", ["rabbitmq", "кластер", "очереди"]), + createItem("2.4.3", "Панель управления кластера Ceph", "ceph", ["ceph", "кластер", "storage"]), + createItem("2.4.4", "Панель управления Prometheus", "prometheus", ["prometheus", "метрики", "мониторинг"]), + createItem("2.4.5", "Панель управления XMPP-сервера", "xmpp", ["xmpp", "чат", "сервер"]), + createItem("2.4.6", "Панель управления PostgreSQL", "postgresql", ["postgresql", "база", "db"]), + createItem("2.4.7", "Подключение к БД", "cloudbeaver", ["cloudbeaver", "database", "sql"]), + ], + }, + { + id: "packages", + number: "2.5", + title: "Образы и пакеты", + description: "Реестры образов и deb-пакетов.", + items: [ + createItem("2.5.1", "Реестр docker-образов", "gitea", ["gitea", "docker", "registry"]), + createItem("2.5.2", "Репозиторий deb-пакетов", "gitea", ["gitea", "deb", "packages"]), + ], + }, + ], + }, +]; + +const serviceMeta = { + ceph: { label: "Ceph", icon: "assets/ceph.svg" }, + cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" }, + config: { label: "Config", icon: "assets/config.svg" }, + gitea: { label: "Gitea", icon: "assets/gitea.svg" }, + grafana: { label: "Grafana", icon: "assets/grafana.svg" }, + ipmi: { label: "IPMI", icon: "assets/server.svg" }, + kubernetes: { label: "Kubernetes", icon: "assets/kubernetes.svg" }, + ldap: { label: "LDAP", icon: "assets/ldap.svg" }, + messages: { label: "Messages", icon: "assets/messages.svg" }, + opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" }, + postgresql: { label: "PostgreSQL", icon: "assets/database.svg" }, + prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" }, + rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" }, + redmine: { label: "Redmine", icon: "assets/redmine.svg" }, + rules: { label: "Rules", icon: "assets/rules.svg" }, + xpath: { label: "xPath", icon: "assets/xpath.svg" }, + xmpp: { label: "XMPP", icon: "assets/xmpp.svg" }, +}; + +const defaultPreferences = { + theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark", + view: "cards", + accent: "violet", +}; + +const preferences = { ...defaultPreferences, ...readPreferences() }; +const catalog = document.querySelector("[data-catalog-root]"); +const searchInput = document.querySelector("[data-search-input]"); +const themeButtons = document.querySelectorAll("[data-theme]"); +const viewButtons = document.querySelectorAll("[data-view]"); +const accentButtons = document.querySelectorAll("[data-accent]"); +const emptyTemplate = document.querySelector("#empty-template"); + +const totals = calculateTotals(ecosystemSections); +document.querySelector("[data-total-sections]").textContent = totals.sections; +document.querySelector("[data-total-groups]").textContent = totals.groups; +document.querySelector("[data-total-items]").textContent = totals.items; + +applyPreferences(preferences); +renderCatalog(); + +searchInput.addEventListener("input", renderCatalog); + +themeButtons.forEach((button) => { + button.addEventListener("click", () => { + preferences.theme = button.dataset.theme; + applyPreferences(preferences); + savePreferences(preferences); + }); +}); + +viewButtons.forEach((button) => { + button.addEventListener("click", () => { + preferences.view = button.dataset.view; + applyPreferences(preferences); + savePreferences(preferences); + }); +}); + +accentButtons.forEach((button) => { + button.addEventListener("click", () => { + preferences.accent = button.dataset.accent; + applyPreferences(preferences); + savePreferences(preferences); + }); +}); + +function createItem(number, title, type, tags = [], url = "#") { + return { number, title, type, tags, url }; +} + +function renderCatalog() { + const query = normalize(searchInput.value); + const fragment = document.createDocumentFragment(); + let visibleItems = 0; + + ecosystemSections.forEach((section) => { + const sectionElement = createSectionElement(section); + let visibleGroups = 0; + let sectionVisibleItems = 0; + + section.groups.forEach((group) => { + const filteredItems = group.items.filter((item) => matchesQuery(item, group, section, query)); + + if (!filteredItems.length) { + return; + } + + visibleGroups += 1; + sectionVisibleItems += filteredItems.length; + sectionElement.querySelector(".section-groups").append(createGroupElement(group, filteredItems)); + }); + + if (visibleGroups) { + sectionElement.querySelector("[data-section-count]").textContent = sectionVisibleItems; + fragment.append(sectionElement); + visibleItems += sectionVisibleItems; + } + }); + + catalog.replaceChildren(); + + if (visibleItems) { + catalog.append(fragment); + attachCardInteractions(); + } else { + catalog.append(emptyTemplate.content.cloneNode(true)); + } + + document.querySelector("[data-visible-items]").textContent = visibleItems; +} + +function createSectionElement(section) { + const sectionElement = document.createElement("article"); + sectionElement.className = "catalog-section"; + sectionElement.innerHTML = ` +
+
+

${section.number} раздел

+

${section.title}

+

${section.description}

+
+ 0 элементов +
+
+ `; + return sectionElement; +} + +function createGroupElement(group, items) { + const groupElement = document.createElement("section"); + groupElement.className = "catalog-group"; + + const cards = items.map(createCardElement); + groupElement.innerHTML = ` +
+
+

${group.number}

+

${group.title}

+

${group.description}

+
+ ${items.length} +
+ `; + + const grid = document.createElement("div"); + grid.className = "items-grid"; + grid.append(...cards); + groupElement.append(grid); + + return groupElement; +} + +function createCardElement(item) { + const meta = serviceMeta[item.type] || serviceMeta.config; + const link = document.createElement("a"); + link.className = "nav-card"; + link.href = item.url; + link.dataset.type = item.type; + + if (item.url === "#") { + link.setAttribute("aria-label", `${item.title}: URL еще не настроен`); + link.addEventListener("click", (event) => event.preventDefault()); + } else { + link.target = "_blank"; + link.rel = "noopener noreferrer"; + } + + link.innerHTML = ` + + +
+

${item.number}

+

${item.title}

+

${meta.label}

+
${item.tags.map((tag) => `${tag}`).join("")}
+
+ + `; + + return link; +} + +function matchesQuery(item, group, section, query) { + if (!query) { + return true; + } + + const meta = serviceMeta[item.type] || serviceMeta.config; + const haystack = normalize( + [ + item.number, + item.title, + item.type, + meta.label, + item.tags.join(" "), + group.title, + section.title, + ].join(" "), + ); + + return haystack.includes(query); +} + +function attachCardInteractions() { + document.querySelectorAll(".nav-card").forEach((card) => { + let leaveTimer; + + card.addEventListener("pointerenter", () => { + window.clearTimeout(leaveTimer); + card.classList.add("is-hovered"); + }); + + card.addEventListener("pointermove", (event) => { + window.clearTimeout(leaveTimer); + card.classList.add("is-hovered"); + + const rect = card.getBoundingClientRect(); + const xPercent = clamp(((event.clientX - rect.left) / rect.width) * 100, 8, 92); + const yPercent = clamp(((event.clientY - rect.top) / rect.height) * 100, 8, 92); + const rotateY = ((xPercent - 50) / 50) * 2.4; + const rotateX = ((50 - yPercent) / 50) * 2.4; + + card.style.setProperty("--card-x", `${xPercent}%`); + card.style.setProperty("--card-y", `${yPercent}%`); + card.style.setProperty("--tilt-x", `${rotateX}deg`); + card.style.setProperty("--tilt-y", `${rotateY}deg`); + }); + + card.addEventListener("pointerleave", () => { + leaveTimer = window.setTimeout(() => { + card.classList.remove("is-hovered"); + card.style.setProperty("--card-x", "50%"); + card.style.setProperty("--card-y", "50%"); + card.style.setProperty("--tilt-x", "0deg"); + card.style.setProperty("--tilt-y", "0deg"); + }, 80); + }); + }); +} + +function applyPreferences(nextPreferences) { + document.documentElement.dataset.theme = nextPreferences.theme; + document.documentElement.dataset.accent = nextPreferences.accent; + catalog.classList.toggle("cards-view", nextPreferences.view === "cards"); + catalog.classList.toggle("list-view", nextPreferences.view === "list"); + setActiveButton(themeButtons, "theme", nextPreferences.theme); + setActiveButton(viewButtons, "view", nextPreferences.view); + setActiveButton(accentButtons, "accent", nextPreferences.accent); +} + +function setActiveButton(buttons, key, value) { + buttons.forEach((button) => { + const isActive = button.dataset[key] === value; + button.classList.toggle("is-active", isActive); + button.setAttribute("aria-pressed", String(isActive)); + }); +} + +function calculateTotals(sections) { + return sections.reduce( + (total, section) => { + total.sections += 1; + total.groups += section.groups.length; + total.items += section.groups.reduce((sum, group) => sum + group.items.length, 0); + return total; + }, + { sections: 0, groups: 0, items: 0 }, + ); +} + +function normalize(value) { + return value.toLowerCase().replaceAll("ё", "е").trim(); +} + +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} + +function readPreferences() { + try { + return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; + } catch { + return {}; + } +} + +function savePreferences(nextPreferences) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(nextPreferences)); +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..2f3f3b9 --- /dev/null +++ b/styles.css @@ -0,0 +1,687 @@ +: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; + --card-x: 50%; + --card-y: 50%; +} + +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); +} + +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; +} + +* { + 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 { + 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); +} + +.shell { + width: min(1320px, calc(100% - 32px)); + margin: 0 auto; + padding: 42px 0; +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(330px, 460px); + gap: 24px; + align-items: stretch; + margin-bottom: 18px; +} + +.hero-content, +.control-panel, +.overview, +.catalog-section, +.empty-state { + border: 1px solid var(--border); + background: var(--panel); + box-shadow: var(--shadow); + backdrop-filter: blur(22px); +} + +.hero-content { + min-height: 330px; + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: clamp(28px, 5vw, 56px); + border-radius: 38px; + position: relative; + overflow: hidden; +} + +.hero-content::before { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(120deg, rgba(var(--accent-rgb), 0.28), transparent 34%), + url("data:image/svg+xml,%3Csvg width='140' height='140' viewBox='0 0 140 140' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.09'%3E%3Cpath d='M0 70h140M70 0v140'/%3E%3Ccircle cx='70' cy='70' r='38'/%3E%3C/g%3E%3C/svg%3E"); + mask-image: linear-gradient(to bottom, black, transparent 88%); + pointer-events: none; +} + +.hero-content > * { + position: relative; +} + +.eyebrow, +.group-number, +.item-number { + margin: 0; + color: var(--accent); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +h1 { + max-width: 840px; + margin: 14px 0 18px; + font-size: clamp(2.6rem, 8vw, 6.4rem); + line-height: 0.9; + letter-spacing: -0.08em; +} + +.lead { + max-width: 740px; + margin: 0; + color: var(--muted); + font-size: clamp(1rem, 2vw, 1.2rem); + line-height: 1.7; +} + +.control-panel { + display: grid; + gap: 14px; + padding: 18px; + border-radius: 28px; +} + +.search-field { + display: grid; + gap: 8px; +} + +.search-field span, +.control-group > span { + color: var(--muted); + font-size: 0.82rem; + font-weight: 800; +} + +.search-field input { + width: 100%; + min-height: 48px; + border: 1px solid var(--border); + border-radius: 16px; + background: var(--surface); + color: var(--text); + outline: none; + padding: 0 16px; + 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); +} + +.control-group { + display: flex; + align-items: center; + gap: 8px; + min-height: 48px; + padding: 8px; + border: 1px solid var(--border); + border-radius: 18px; + background: var(--surface); +} + +.control-group > span { + min-width: 68px; + padding-left: 6px; +} + +.segmented, +.accent-swatch { + border: 0; + cursor: pointer; + transition: transform 160ms ease, background 160ms ease, color 160ms ease; +} + +.segmented { + flex: 1; + min-height: 34px; + border-radius: 12px; + background: transparent; + color: var(--muted); + font-size: 0.88rem; + font-weight: 800; +} + +.segmented:hover, +.accent-swatch:hover { + transform: translateY(-1px); +} + +.segmented.is-active { + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + color: white; +} + +.accent-group { + justify-content: space-between; +} + +.accent-swatch { + width: 36px; + height: 36px; + border: 2px solid transparent; + border-radius: 999px; + box-shadow: inset 0 0 0 4px rgba(255, 255, 255, 0.18); +} + +.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); +} + +.overview { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1px; + margin-bottom: 18px; + border-radius: 26px; + overflow: hidden; +} + +.overview div { + padding: 18px 20px; + background: rgba(255, 255, 255, 0.035); +} + +.overview span { + display: block; + font-size: clamp(1.8rem, 4vw, 2.8rem); + font-weight: 850; + line-height: 1; + letter-spacing: -0.06em; +} + +.overview p { + margin: 8px 0 0; + color: var(--muted); + font-weight: 700; +} + +.catalog { + display: grid; + gap: 18px; +} + +.catalog-section { + border-radius: 32px; + overflow: hidden; +} + +.section-head { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: flex-start; + padding: 24px; + 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-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: 8px; + font-size: clamp(1.6rem, 4vw, 2.6rem); +} + +.section-head p:not(.eyebrow), +.group-head p:not(.group-number) { + max-width: 760px; + margin: 8px 0 0; + color: var(--muted); + line-height: 1.65; +} + +.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; +} + +.section-groups { + display: grid; + gap: 18px; + padding: 18px; +} + +.catalog-group { + border: 1px solid var(--border); + border-radius: 24px; + background: rgba(255, 255, 255, 0.035); + overflow: hidden; +} + +.group-head { + display: flex; + justify-content: space-between; + gap: 16px; + padding: 18px; + border-bottom: 1px solid var(--border); +} + +.group-head h3 { + margin-top: 6px; + font-size: 1.22rem; +} + +.group-head > span { + 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; +} + +.items-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + padding: 14px; +} + +.nav-card { + --tilt-x: 0deg; + --tilt-y: 0deg; + position: relative; + min-height: 224px; + display: flex; + flex-direction: column; + gap: 16px; + padding: 18px; + border: 1px solid var(--border); + border-radius: 22px; + 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; +} + +.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; +} + +.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; +} + +.nav-card img { + width: 58px; + height: 58px; + object-fit: contain; + filter: drop-shadow(0 14px 24px rgba(0, 0, 0, 0.22)); +} + +.card-content { + position: relative; + display: grid; + gap: 7px; +} + +.nav-card h4 { + font-size: 1.08rem; + line-height: 1.25; +} + +.item-type { + margin: 0; + color: var(--muted); + font-size: 0.9rem; + font-weight: 750; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 4px; +} + +.tags span { + border: 1px solid var(--border); + border-radius: 999px; + padding: 4px 8px; + background: rgba(var(--accent-rgb), 0.1); + color: var(--muted); + font-size: 0.76rem; + font-weight: 800; +} + +.arrow { + position: absolute; + right: 16px; + bottom: 14px; + 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(135deg, var(--accent), var(--accent-2)); + color: white; +} + +.shine { + position: absolute; + inset: 0; + 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%); +} + +.catalog.list-view .items-grid { + grid-template-columns: 1fr; +} + +.catalog.list-view .nav-card { + min-height: 112px; + display: grid; + grid-template-columns: 58px minmax(0, 1fr) 42px; + align-items: center; + gap: 16px; +} + +.catalog.list-view .arrow { + position: static; + justify-self: end; +} + +.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); +} + +@media (max-width: 1120px) { + .hero { + grid-template-columns: 1fr; + } + + .items-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + .shell { + width: min(100% - 22px, 1320px); + padding: 22px 0; + } + + .hero-content { + min-height: 300px; + border-radius: 28px; + } + + .control-panel, + .catalog-section, + .empty-state { + border-radius: 24px; + } + + .control-group { + flex-wrap: wrap; + } + + .control-group > span { + width: 100%; + min-width: 0; + } + + .accent-group > span { + width: auto; + } + + .overview { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .section-head, + .group-head { + display: grid; + } + + .section-count { + width: fit-content; + } + + .section-groups { + padding: 12px; + } + + .items-grid { + grid-template-columns: 1fr; + padding: 12px; + } + + .nav-card { + min-height: 210px; + } + + .catalog.list-view .nav-card { + grid-template-columns: 48px minmax(0, 1fr); + min-height: 136px; + } + + .catalog.list-view .arrow { + position: absolute; + } + + .nav-card img { + width: 50px; + height: 50px; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + } +}