feat(admin-ui): softer pre-material styling and dark theme

- Rebuild admin console styling on semantic CSS variables (light + dark palettes)
- Soften palette, shadows and corner radii for a calmer pre-material look
- Add theme provider with light/dark toggle in the topbar and login header
- Respect prefers-color-scheme and persist choice, with anti-FOUC inline script
- Polish Russian translations (yo letters, consistent 'Звёзды' currency, wording)
- Rebuild dist bundle to match the updated source
This commit is contained in:
epilepticseizureee 2026-07-22 23:45:09 +03:00
parent 045e0f6db4
commit ad9d535edc
15 changed files with 601 additions and 251 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,25 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title> <title>telesrv admin</title>
<script type="module" crossorigin src="/assets/index-Duge82ST.js"></script> <script>
<link rel="stylesheet" crossorigin href="/assets/index-DHdrFM5j.css"> (function () {
try {
var stored = localStorage.getItem("telesrv.admin.theme");
var theme =
stored === "light" || stored === "dark"
? stored
: window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;
} catch (e) {
document.documentElement.setAttribute("data-theme", "light");
}
})();
</script>
<script type="module" crossorigin src="/assets/index-CqgHld2y.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DuOdm70q.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -4,6 +4,23 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title> <title>telesrv admin</title>
<script>
(function () {
try {
var stored = localStorage.getItem("telesrv.admin.theme");
var theme =
stored === "light" || stored === "dark"
? stored
: window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;
} catch (e) {
document.documentElement.setAttribute("data-theme", "light");
}
})();
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -14,6 +14,7 @@ import { useEffect, useState, type ReactNode } from "react";
import { api } from "../api"; import { api } from "../api";
import { LanguageSwitch, useI18n } from "../i18n"; import { LanguageSwitch, useI18n } from "../i18n";
import { type Navigate, type RouteState, routeSubtitle, routeTitle } from "../routing"; import { type Navigate, type RouteState, routeSubtitle, routeTitle } from "../routing";
import { ThemeSwitch } from "../theme";
import { AppLink } from "./AppLink"; import { AppLink } from "./AppLink";
export function BootScreen() { export function BootScreen() {
@ -123,6 +124,7 @@ export function Shell({
<h1>{routeTitle(route.path, t)}</h1> <h1>{routeTitle(route.path, t)}</h1>
</div> </div>
<div className="topbar-actions"> <div className="topbar-actions">
<ThemeSwitch />
<LanguageSwitch /> <LanguageSwitch />
<span className="actor-pill">{t("layout.actor", { actor })}</span> <span className="actor-pill">{t("layout.actor", { actor })}</span>
<button className="btn ghost icon-text" type="button" onClick={logout} title={t("layout.logout")}> <button className="btn ghost icon-text" type="button" onClick={logout} title={t("layout.logout")}>

View file

@ -84,6 +84,8 @@ const translations: Record<Language, Record<string, string>> = {
"language.en": "EN", "language.en": "EN",
"language.zh": "中文", "language.zh": "中文",
"language.ru": "RU", "language.ru": "RU",
"theme.switchToDark": "Switch to dark theme",
"theme.switchToLight": "Switch to light theme",
"login.heading": "Operations Admin", "login.heading": "Operations Admin",
"login.body": "Enter credentials to open the console.", "login.body": "Enter credentials to open the console.",
"login.secret": "Admin password or token", "login.secret": "Admin password or token",
@ -456,6 +458,8 @@ const translations: Record<Language, Record<string, string>> = {
"language.en": "EN", "language.en": "EN",
"language.zh": "中文", "language.zh": "中文",
"language.ru": "RU", "language.ru": "RU",
"theme.switchToDark": "切换到深色主题",
"theme.switchToLight": "切换到浅色主题",
"login.heading": "运维后台", "login.heading": "运维后台",
"login.body": "输入凭据后进入控制台。", "login.body": "输入凭据后进入控制台。",
"login.secret": "管理员密码或 token", "login.secret": "管理员密码或 token",
@ -772,7 +776,7 @@ const translations: Record<Language, Record<string, string>> = {
"common.group": "Группа", "common.group": "Группа",
"common.id": "ID", "common.id": "ID",
"common.limit": "Лимит", "common.limit": "Лимит",
"common.loading": "Загрузка...", "common.loading": "Загрузка…",
"common.member": "Участник", "common.member": "Участник",
"common.members": "Участники", "common.members": "Участники",
"common.messageId": "ID сообщения", "common.messageId": "ID сообщения",
@ -794,7 +798,7 @@ const translations: Record<Language, Record<string, string>> = {
"common.updatedAt": "Обновлено", "common.updatedAt": "Обновлено",
"common.username": "Имя пользователя", "common.username": "Имя пользователя",
"common.valid": "Действителен", "common.valid": "Действителен",
"common.verified": "Подтвержден", "common.verified": "Подтверждён",
"common.views": "Просмотры", "common.views": "Просмотры",
"common.yes": "Да", "common.yes": "Да",
"route.accounts": "Аккаунты", "route.accounts": "Аккаунты",
@ -805,15 +809,15 @@ const translations: Record<Language, Record<string, string>> = {
"route.dashboardSubtitle": "Консоль / Обзор", "route.dashboardSubtitle": "Консоль / Обзор",
"route.messages": "Аудит сообщений", "route.messages": "Аудит сообщений",
"route.messagesSubtitle": "Консоль / Сообщения", "route.messagesSubtitle": "Консоль / Сообщения",
"route.gifts": "Звездные подарки", "route.gifts": "Звёздные подарки",
"route.giftsSubtitle": "Консоль / Звездные подарки", "route.giftsSubtitle": "Консоль / Звёздные подарки",
"layout.navigation": "Навигация", "layout.navigation": "Навигация",
"layout.primaryNav": "Основное меню", "layout.primaryNav": "Основное меню",
"layout.dashboard": "Обзор", "layout.dashboard": "Обзор",
"layout.accounts": "Аккаунты", "layout.accounts": "Аккаунты",
"layout.channels": "Супергруппы / Каналы", "layout.channels": "Супергруппы / Каналы",
"layout.messages": "Сообщения", "layout.messages": "Сообщения",
"layout.gifts": "Звездные подарки", "layout.gifts": "Звёздные подарки",
"layout.privateMessages": "Личные", "layout.privateMessages": "Личные",
"layout.groupMessages": "Группы", "layout.groupMessages": "Группы",
"layout.runtime": "Среда выполнения", "layout.runtime": "Среда выполнения",
@ -823,16 +827,18 @@ const translations: Record<Language, Record<string, string>> = {
"layout.readOnly": "Только чтение", "layout.readOnly": "Только чтение",
"layout.writeOps": "Операции записи", "layout.writeOps": "Операции записи",
"layout.dryRun": "Тестовый запуск", "layout.dryRun": "Тестовый запуск",
"layout.actor": "Вход выполнен как: {actor}", "layout.actor": "Вы вошли как: {actor}",
"layout.logout": "Выйти", "layout.logout": "Выйти",
"language.en": "EN", "language.en": "EN",
"language.zh": "中文", "language.zh": "中文",
"language.ru": "RU", "language.ru": "RU",
"theme.switchToDark": "Тёмная тема",
"theme.switchToLight": "Светлая тема",
"login.heading": "Панель администратора", "login.heading": "Панель администратора",
"login.body": "Введите учетные данные для входа в консоль.", "login.body": "Введите учётные данные для входа в консоль.",
"login.secret": "Пароль или токен администратора", "login.secret": "Пароль или токен администратора",
"login.submit": "Войти", "login.submit": "Войти",
"login.submitting": "Вход...", "login.submitting": "Вход…",
"dashboard.eyebrow": "Состояние системы", "dashboard.eyebrow": "Состояние системы",
"dashboard.title": "Обзор консоли", "dashboard.title": "Обзор консоли",
"dashboard.readPath": "Путь чтения", "dashboard.readPath": "Путь чтения",
@ -840,8 +846,8 @@ const translations: Record<Language, Record<string, string>> = {
"dashboard.writePath": "Путь записи", "dashboard.writePath": "Путь записи",
"dashboard.executionPolicy": "Политика выполнения", "dashboard.executionPolicy": "Политика выполнения",
"dashboard.dryRunFirst": "Сначала тестовый запуск", "dashboard.dryRunFirst": "Сначала тестовый запуск",
"dashboard.accountsText": "Статус аккаунтов, премиум, верификация, сессии.", "dashboard.accountsText": "Статус аккаунтов, Premium, подтверждение, сессии.",
"dashboard.channelsText": "Публичные каналы и группы, количество участников, статус верификации.", "dashboard.channelsText": "Публичные каналы и группы, число участников, статус подтверждения.",
"dashboard.messagesText": "Ящики сообщений, обновления, состояние исходящих.", "dashboard.messagesText": "Ящики сообщений, обновления, состояние исходящих.",
"dashboard.strip.dryRun": "Все опасные действия начинаются с тестового запуска", "dashboard.strip.dryRun": "Все опасные действия начинаются с тестового запуска",
"dashboard.strip.token": "Браузер никогда не сохраняет внутренние токены", "dashboard.strip.token": "Браузер никогда не сохраняет внутренние токены",
@ -858,10 +864,10 @@ const translations: Record<Language, Record<string, string>> = {
"account.userID": "ID пользователя", "account.userID": "ID пользователя",
"account.phone": "Телефон", "account.phone": "Телефон",
"account.lastActive": "Последняя активность", "account.lastActive": "Последняя активность",
"account.notVerified": "Не подтвержден", "account.notVerified": "Не подтверждён",
"account.notPremium": "Без Premium", "account.notPremium": "Без Premium",
"account.premiumUntil": "Premium истекает", "account.premiumUntil": "Premium истекает",
"account.starsBalance": "Баланс Звезд", "account.starsBalance": "Баланс Звёзд",
"account.startingGrantApplied": "стартовый бонус начислен", "account.startingGrantApplied": "стартовый бонус начислен",
"account.startingGrantPending": "ожидает стартового бонуса", "account.startingGrantPending": "ожидает стартового бонуса",
"account.activeSessions": "Авторизованные устройства", "account.activeSessions": "Авторизованные устройства",
@ -894,17 +900,17 @@ const translations: Record<Language, Record<string, string>> = {
"account.premiumMonthsAria": "Указать срок действия Premium в месяцах", "account.premiumMonthsAria": "Указать срок действия Premium в месяцах",
"account.setPremium": "Выдать Premium", "account.setPremium": "Выдать Premium",
"account.clearPremium": "Снять Premium", "account.clearPremium": "Снять Premium",
"account.starsAmount": "Количество звёзд", "account.starsAmount": "Количество Звёзд",
"account.starsAmountAria": "Указать количество начисляемых звёзд", "account.starsAmountAria": "Указать количество начисляемых Звёзд",
"account.grantStars": "Начислить звёзды", "account.grantStars": "Начислить Звёзды",
"account.setVerified": "Подтвердить аккаунт", "account.setVerified": "Подтвердить аккаунт",
"account.clearVerified": "Снять подтверждение", "account.clearVerified": "Снять подтверждение",
"channel.pageTitle": "Супергруппы и каналы", "channel.pageTitle": "Супергруппы и каналы",
"channel.recentUpdated": "Недавно обновленные", "channel.recentUpdated": "Недавно обновлённые",
"channel.currentPage": "Объекты на странице", "channel.currentPage": "Объекты на странице",
"channel.megagroups": "Супергруппы", "channel.megagroups": "Супергруппы",
"channel.broadcasts": "Каналы", "channel.broadcasts": "Каналы",
"channel.verifiedCount": "Подтверждено", "channel.verifiedCount": "Подтверждённые",
"channel.searchPlaceholder": "ID канала / имя пользователя / название", "channel.searchPlaceholder": "ID канала / имя пользователя / название",
"channel.channelID": "ID канала", "channel.channelID": "ID канала",
"channel.kind": "Тип", "channel.kind": "Тип",
@ -943,7 +949,7 @@ const translations: Record<Language, Record<string, string>> = {
"messages.outgoing": "Исходящее", "messages.outgoing": "Исходящее",
"messages.incoming": "Входящее", "messages.incoming": "Входящее",
"messages.ownerPeer": "Владелец / Собеседник", "messages.ownerPeer": "Владелец / Собеседник",
"messages.deleteSelected": "Указать и удалить выбранные сообщения", "messages.deleteSelected": "Удалить выбранные сообщения",
"messages.idsPlaceholder": "ID сообщений через запятую", "messages.idsPlaceholder": "ID сообщений через запятую",
"messages.revoke": "Удалить для обеих сторон", "messages.revoke": "Удалить для обеих сторон",
"messages.previewDelete": "Тестовое удаление", "messages.previewDelete": "Тестовое удаление",
@ -989,7 +995,7 @@ const translations: Record<Language, Record<string, string>> = {
"messages.channelGroup": "Канал / Группа", "messages.channelGroup": "Канал / Группа",
"messages.pinned": "Закреплено", "messages.pinned": "Закреплено",
"messages.channelPost": "Пост в канале", "messages.channelPost": "Пост в канале",
"gifts.pageTitle": "Каталог звездных подарков", "gifts.pageTitle": "Каталог звёздных подарков",
"gifts.eyebrow": "Каталог, неизменяемые версии и файлы анимаций", "gifts.eyebrow": "Каталог, неизменяемые версии и файлы анимаций",
"gifts.total": "Подарков в каталоге", "gifts.total": "Подарков в каталоге",
"gifts.enabled": "Включено", "gifts.enabled": "Включено",
@ -1000,7 +1006,7 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.listSummary": "Показано {shown} из {total}", "gifts.listSummary": "Показано {shown} из {total}",
"gifts.idRevision": "ID / Версия", "gifts.idRevision": "ID / Версия",
"gifts.price": "Цена / Конвертация", "gifts.price": "Цена / Конвертация",
"gifts.importTitle": "Импорт звездного подарка", "gifts.importTitle": "Импорт звёздного подарка",
"gifts.importEyebrow": "Управление каталогом подарков", "gifts.importEyebrow": "Управление каталогом подарков",
"gifts.newRevision": "Создать версию для подарка #{id}", "gifts.newRevision": "Создать версию для подарка #{id}",
"gifts.importHint": "Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.", "gifts.importHint": "Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.",
@ -1031,8 +1037,8 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.changeFile": "Изменить файл", "gifts.changeFile": "Изменить файл",
"gifts.title": "Отображаемое название", "gifts.title": "Отображаемое название",
"gifts.titlePlaceholder": "например, Праздничная звезда", "gifts.titlePlaceholder": "например, Праздничная звезда",
"gifts.stars": "Цена в Звездах", "gifts.stars": "Цена в Звёздах",
"gifts.convertStars": "Звезд при конвертации", "gifts.convertStars": "Звёзд при конвертации",
"gifts.sortOrder": "Порядок сортировки", "gifts.sortOrder": "Порядок сортировки",
"gifts.reason": "Причина для аудита", "gifts.reason": "Причина для аудита",
"gifts.reasonPlaceholder": "Кратко опишите причину импорта этого подарка", "gifts.reasonPlaceholder": "Кратко опишите причину импорта этого подарка",
@ -1047,7 +1053,7 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.replace": "Новая версия", "gifts.replace": "Новая версия",
"gifts.disable": "Отключить", "gifts.disable": "Отключить",
"gifts.enable": "Включить", "gifts.enable": "Включить",
"gifts.empty": "Звездные подарки еще не импортированы.", "gifts.empty": "Звёздные подарки ещё не импортированы.",
"gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.", "gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.",
"gifts.validationReady": "Проверка пройдена", "gifts.validationReady": "Проверка пройдена",
"gifts.validationHint": "Проверьте нормализованные метаданные и подтвердите импорт.", "gifts.validationHint": "Проверьте нормализованные метаданные и подтвердите импорт.",
@ -1061,7 +1067,7 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.noPoolHint": "Опубликуйте модели, узоры и фоны для активации улучшений.", "collectibles.noPoolHint": "Опубликуйте модели, узоры и фоны для активации улучшений.",
"collectibles.publishNew": "Опубликовать новую неизменяемую версию", "collectibles.publishNew": "Опубликовать новую неизменяемую версию",
"collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.", "collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.",
"collectibles.upgradeStars": "Цена улучшения в Звездах", "collectibles.upgradeStars": "Цена улучшения в Звёздах",
"collectibles.supply": "Уникальный тираж", "collectibles.supply": "Уникальный тираж",
"collectibles.slug": "Публичный префикс ссылки (slug)", "collectibles.slug": "Публичный префикс ссылки (slug)",
"collectibles.models": "Модели", "collectibles.models": "Модели",
@ -1071,7 +1077,7 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.pattern": "Узор", "collectibles.pattern": "Узор",
"collectibles.backdrop": "Фон", "collectibles.backdrop": "Фон",
"collectibles.rarity": "Редкость ‰", "collectibles.rarity": "Редкость ‰",
"collectibles.rarityHint": "В каждой категории должно быть не менее двух атрибутов. При добавлении и удалении веса permille перераспределяются до 1000.", "collectibles.rarityHint": "В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.",
"collectibles.minimumAttributes": "Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.", "collectibles.minimumAttributes": "Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.",
"collectibles.duplicateBackdropID": "ID фонов в одном наборе должны быть уникальными.", "collectibles.duplicateBackdropID": "ID фонов в одном наборе должны быть уникальными.",
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.", "collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.",
@ -1093,10 +1099,10 @@ const translations: Record<Language, Record<string, string>> = {
"auth.lastActive": "Последняя активность", "auth.lastActive": "Последняя активность",
"auth.revokeCurrent": "Отозвать текущую", "auth.revokeCurrent": "Отозвать текущую",
"auth.keepCurrent": "Оставить текущую", "auth.keepCurrent": "Оставить текущую",
"auth.revokeAll": "Разлогинить все устройства", "auth.revokeAll": "Отозвать все устройства",
"picker.userPlaceholder": "Поиск по user_id / телефону / имени пользователя", "picker.userPlaceholder": "Поиск по user_id / телефону / имени пользователя",
"picker.channelPlaceholder": "Поиск по channel_id / имени пользователя / названию", "picker.channelPlaceholder": "Поиск по channel_id / имени пользователя / названию",
"picker.verified": "Подтвержденные", "picker.verified": "Подтверждённые",
"picker.regular": "Обычные", "picker.regular": "Обычные",
"action.reasonRequired": "Пожалуйста, укажите причину операции", "action.reasonRequired": "Пожалуйста, укажите причину операции",
"action.flow": "Процесс выполнения", "action.flow": "Процесс выполнения",
@ -1111,7 +1117,7 @@ const translations: Record<Language, Record<string, string>> = {
"action.commandID": "ID команды", "action.commandID": "ID команды",
"action.status": "Статус", "action.status": "Статус",
"action.dryRun": "Тестовый запуск", "action.dryRun": "Тестовый запуск",
"action.runAgain": "Запустить тестовый запуск снова", "action.runAgain": "Повторить тестовый запуск",
"action.runDry": "Сначала выполните тестовый запуск", "action.runDry": "Сначала выполните тестовый запуск",
"action.confirm": "Подтвердить выполнение", "action.confirm": "Подтвердить выполнение",
"audit.id": "ID", "audit.id": "ID",

View file

@ -2,12 +2,15 @@ import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { App } from "./App"; import { App } from "./App";
import { I18nProvider } from "./i18n"; import { I18nProvider } from "./i18n";
import { ThemeProvider } from "./theme";
import "./styles.css"; import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<I18nProvider> <ThemeProvider>
<App /> <I18nProvider>
</I18nProvider> <App />
</I18nProvider>
</ThemeProvider>
</React.StrictMode> </React.StrictMode>
); );

View file

@ -3,6 +3,7 @@ import { useState } from "react";
import { api, errorMessage } from "../api"; import { api, errorMessage } from "../api";
import { Alert } from "../components/ui"; import { Alert } from "../components/ui";
import { LanguageSwitch, useI18n } from "../i18n"; import { LanguageSwitch, useI18n } from "../i18n";
import { ThemeSwitch } from "../theme";
export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) { export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
const { t } = useI18n(); const { t } = useI18n();
@ -36,6 +37,7 @@ export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
</span> </span>
</div> </div>
<div className="login-head-actions"> <div className="login-head-actions">
<ThemeSwitch />
<LanguageSwitch /> <LanguageSwitch />
<span className="login-chip">{t("app.localAccess")}</span> <span className="login-chip">{t("app.localAccess")}</span>
</div> </div>

View file

@ -1,24 +1,155 @@
:root { :root {
color-scheme: light; color-scheme: light;
--bg: #f3f5f7;
/* Surfaces */
--bg: #eef1f5;
--bg-accent: #e7ecf1;
--panel: #ffffff; --panel: #ffffff;
--panel-subtle: #f8fafb; --panel-subtle: #f5f8fb;
--panel-strong: #eef2f5; --panel-strong: #eef2f6;
--line: #d9e1e8; --surface-soft: #f2f7f6;
--line-strong: #c2ccd6; --overlay: rgba(24, 34, 47, 0.42);
--text: #101828; --topbar-bg: rgba(255, 255, 255, 0.86);
--muted: #667085;
--muted-2: #98a2b3; /* Lines */
--brand: #176d61; --line: #e5eaf0;
--brand-2: #245b9d; --line-strong: #d3dce4;
--good: #167447;
--warn: #a15c07; /* Text */
--danger: #b42318; --heading: #253040;
--sidebar: #11161d; --text: #333f4d;
--sidebar-soft: #1b222b; --text-soft: #45525f;
--sidebar-line: #2c3541; --muted: #6d7885;
--focus: rgba(23, 109, 97, 0.16); --muted-2: #9aa4b1;
--shadow: 0 18px 52px rgba(16, 24, 40, 0.14);
/* Brand */
--brand: #1f7d6f;
--brand-strong: #196155;
--brand-2: #3a6cae;
--brand-tint: #e8f4f0;
--brand-tint-border: #c8e2db;
--brand-tint-text: #235d53;
/* Semantic */
--good: #1f8a57;
--good-tint: #eaf6ef;
--good-border: #c1e1cf;
--warn: #a86a12;
--warn-tint: #fcf4e4;
--warn-border: #e7d09e;
--danger: #c0392b;
--danger-tint: #fcefec;
--danger-border: #eecac3;
--danger-text: #8f2f27;
/* Accent (collectibles / craft) */
--purple: #6a4fa3;
--purple-tint: #f4effb;
--purple-border: #dcd0f0;
--purple-text: #5a4590;
/* Inputs & controls */
--input-bg: #ffffff;
--btn-bg: #ffffff;
--btn-text: #29323d;
--btn-hover: #f4f7fa;
--switch-track: #c8d0d6;
/* Code / JSON blocks */
--code-bg: #1b2733;
--code-text: #d6e3ef;
--code-border: #2b3a49;
/* Sidebar */
--sidebar: #1c2530;
--sidebar-soft: #26313d;
--sidebar-line: #313c4a;
--sidebar-row: #232d38;
--sidebar-text: #dbe3ec;
--sidebar-muted: #8b98a8;
--sidebar-faint: #7c8a9a;
--sidebar-heading: #ffffff;
/* Effects */
--focus: rgba(31, 125, 111, 0.16);
--shadow: 0 12px 34px rgba(24, 39, 56, 0.1);
--shadow-sm: 0 2px 10px rgba(24, 39, 56, 0.05);
--shadow-brand: 0 8px 22px rgba(31, 125, 111, 0.22);
/* Radii */
--radius-xs: 8px;
--radius-sm: 9px;
--radius: 11px;
--radius-lg: 14px;
}
[data-theme="dark"] {
color-scheme: dark;
--bg: #0f141a;
--bg-accent: #131a22;
--panel: #171f28;
--panel-subtle: #1c2530;
--panel-strong: #212c38;
--surface-soft: #1a232d;
--overlay: rgba(5, 8, 12, 0.62);
--topbar-bg: rgba(21, 28, 36, 0.86);
--line: #29333f;
--line-strong: #38434f;
--heading: #eef3f8;
--text: #d5dde6;
--text-soft: #c2ccd6;
--muted: #98a4b1;
--muted-2: #6d7885;
--brand: #37a596;
--brand-strong: #45b6a6;
--brand-2: #6fa8e6;
--brand-tint: #14322d;
--brand-tint-border: #245349;
--brand-tint-text: #7fd3c4;
--good: #47c281;
--good-tint: #12301f;
--good-border: #245639;
--warn: #e0aa4d;
--warn-tint: #322810;
--warn-border: #574413;
--danger: #e6695c;
--danger-tint: #35201d;
--danger-border: #5c332d;
--danger-text: #f0a49b;
--purple: #ac90e2;
--purple-tint: #221b31;
--purple-border: #3d3357;
--purple-text: #c9b6ef;
--input-bg: #131a22;
--btn-bg: #1e2731;
--btn-text: #dbe2ea;
--btn-hover: #26313d;
--switch-track: #3a454f;
--code-bg: #0c1218;
--code-text: #cdd9e5;
--code-border: #232f3b;
--sidebar: #10151b;
--sidebar-soft: #1c242f;
--sidebar-line: #262f3a;
--sidebar-row: #161d25;
--sidebar-text: #cbd4de;
--sidebar-muted: #7c8794;
--sidebar-faint: #6f7b88;
--sidebar-heading: #f0f4f8;
--focus: rgba(55, 165, 150, 0.24);
--shadow: 0 16px 40px rgba(0, 0, 0, 0.46);
--shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.38);
--shadow-brand: 0 8px 22px rgba(55, 165, 150, 0.26);
} }
* { * {
@ -35,7 +166,10 @@ body {
margin: 0; margin: 0;
color: var(--text); color: var(--text);
background: var(--bg); background: var(--bg);
font: 13px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font: 13px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
transition: background-color 200ms ease, color 200ms ease;
} }
button, button,
@ -64,7 +198,7 @@ a {
gap: 16px; gap: 16px;
overflow-y: auto; overflow-y: auto;
padding: 18px 12px; padding: 18px 12px;
color: #eef2f6; color: var(--sidebar-text);
background: var(--sidebar); background: var(--sidebar);
border-right: 1px solid var(--sidebar-line); border-right: 1px solid var(--sidebar-line);
} }
@ -82,7 +216,7 @@ a {
} }
.brand-elevated .brand-mark { .brand-elevated .brand-mark {
box-shadow: 0 8px 24px rgba(23, 109, 97, 0.26); box-shadow: var(--shadow-brand);
} }
.brand-mark { .brand-mark {
@ -93,7 +227,7 @@ a {
color: #ffffff; color: #ffffff;
background: var(--brand); background: var(--brand);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; border-radius: var(--radius-sm);
font-weight: 800; font-weight: 800;
} }
@ -106,16 +240,17 @@ a {
.brand small { .brand small {
display: block; display: block;
margin-top: 3px; margin-top: 3px;
color: #aeb8c4; color: var(--sidebar-muted);
font-size: 11px; font-size: 11px;
} }
.sidebar-label { .sidebar-label {
padding: 0 8px; padding: 0 8px;
color: #8492a6; color: var(--sidebar-faint);
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.04em;
} }
.nav-list { .nav-list {
@ -136,26 +271,27 @@ a {
align-items: center; align-items: center;
gap: 9px; gap: 9px;
padding: 0 10px; padding: 0 10px;
color: #8fa0b4; color: var(--sidebar-muted);
background: transparent; background: transparent;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 7px; border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 800;
text-align: left; text-align: left;
transition: color 140ms ease, background-color 140ms ease, border-color 140ms ease;
} }
.nav-section-toggle:hover, .nav-section-toggle:hover,
.nav-section.active .nav-section-toggle { .nav-section.active .nav-section-toggle {
color: #ffffff; color: var(--sidebar-heading);
background: var(--sidebar-soft); background: var(--sidebar-soft);
border-color: #34404d; border-color: var(--sidebar-line);
} }
.nav-section-chevron { .nav-section-chevron {
justify-self: end; justify-self: end;
color: #8fa0b4; color: var(--sidebar-muted);
transition: transform 140ms ease; transition: transform 140ms ease;
} }
@ -176,24 +312,25 @@ a {
align-items: center; align-items: center;
gap: 9px; gap: 9px;
padding: 0 10px; padding: 0 10px;
color: #c6d0dc; color: var(--sidebar-text);
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 7px; border-radius: var(--radius-sm);
transition: color 140ms ease, background-color 140ms ease, border-color 140ms ease;
} }
.nav-dot { .nav-dot {
width: 6px; width: 6px;
height: 6px; height: 6px;
justify-self: center; justify-self: center;
background: #687789; background: var(--sidebar-faint);
border-radius: 999px; border-radius: 999px;
} }
.nav-item:hover, .nav-item:hover,
.nav-item.active { .nav-item.active {
color: #ffffff; color: var(--sidebar-heading);
background: var(--sidebar-soft); background: var(--sidebar-soft);
border-color: #34404d; border-color: var(--sidebar-line);
} }
.nav-item.active .nav-dot { .nav-item.active .nav-dot {
@ -213,14 +350,14 @@ a {
align-items: center; align-items: center;
gap: 7px; gap: 7px;
padding: 0 8px; padding: 0 8px;
color: #cbd5df; color: var(--sidebar-text);
background: #171d25; background: var(--sidebar-row);
border: 1px solid #27313c; border: 1px solid var(--sidebar-line);
border-radius: 7px; border-radius: var(--radius-sm);
} }
.runtime-row strong { .runtime-row strong {
color: #ffffff; color: var(--sidebar-heading);
font-size: 11px; font-size: 11px;
} }
@ -238,13 +375,14 @@ a {
justify-content: space-between; justify-content: space-between;
gap: 18px; gap: 18px;
padding: 12px 24px; padding: 12px 24px;
background: rgba(255, 255, 255, 0.94); background: var(--topbar-bg);
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
} }
.topbar h1 { .topbar h1 {
margin: 2px 0 0; margin: 2px 0 0;
color: var(--heading);
font-size: 20px; font-size: 20px;
line-height: 1.2; line-height: 1.2;
} }
@ -281,6 +419,7 @@ a {
border-radius: 999px; border-radius: 999px;
cursor: pointer; cursor: pointer;
font-weight: 800; font-weight: 800;
transition: color 140ms ease, background-color 140ms ease;
} }
.language-switch button.active { .language-switch button.active {
@ -293,12 +432,36 @@ a {
outline-offset: 2px; outline-offset: 2px;
} }
.theme-toggle {
display: inline-grid;
width: 34px;
height: 34px;
place-items: center;
color: var(--muted);
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease;
}
.theme-toggle:hover {
color: var(--brand);
border-color: var(--brand-tint-border);
background: var(--brand-tint);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.actor-pill { .actor-pill {
display: inline-flex; display: inline-flex;
min-height: 30px; min-height: 30px;
align-items: center; align-items: center;
padding: 0 10px; padding: 0 10px;
color: #344054; color: var(--text-soft);
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 999px; border-radius: 999px;
@ -315,4 +478,5 @@ a {
font-size: 11px; font-size: 11px;
font-weight: 800; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.04em;
} }

View file

@ -9,7 +9,8 @@
min-width: 0; min-width: 0;
background: var(--panel); background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
box-shadow: var(--shadow-sm);
} }
.overview-band { .overview-band {
@ -25,6 +26,7 @@
.section-head h2, .section-head h2,
.modal h2 { .modal h2 {
margin: 0; margin: 0;
color: var(--heading);
font-size: 18px; font-size: 18px;
line-height: 1.25; line-height: 1.25;
} }
@ -47,7 +49,7 @@
padding: 10px; padding: 10px;
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 7px; border-radius: var(--radius-sm);
} }
.status-item span, .status-item span,
@ -70,16 +72,16 @@
.status-item.good, .status-item.good,
.metric.good { .metric.good {
border-color: #afd8bf; border-color: var(--good-border);
} }
.status-item.warn, .status-item.warn,
.metric.warn { .metric.warn {
border-color: #e7c77e; border-color: var(--warn-border);
} }
.metric.danger { .metric.danger {
border-color: #efb4ad; border-color: var(--danger-border);
} }
.command-grid { .command-grid {
@ -97,11 +99,15 @@
padding: 14px; padding: 14px;
background: var(--panel); background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
box-shadow: var(--shadow-sm);
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
} }
.launcher:hover { .launcher:hover {
border-color: var(--brand); border-color: var(--brand-tint-border);
box-shadow: var(--shadow);
transform: translateY(-1px);
} }
.launcher-icon { .launcher-icon {
@ -110,9 +116,9 @@
height: 38px; height: 38px;
place-items: center; place-items: center;
color: var(--brand); color: var(--brand);
background: #edf7f4; background: var(--brand-tint);
border: 1px solid #c9e2dc; border: 1px solid var(--brand-tint-border);
border-radius: 8px; border-radius: var(--radius-sm);
} }
.launcher-copy { .launcher-copy {
@ -121,6 +127,7 @@
} }
.launcher-copy strong { .launcher-copy strong {
color: var(--heading);
font-size: 15px; font-size: 15px;
} }
@ -140,10 +147,10 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 0 10px; padding: 0 10px;
color: #344054; color: var(--text-soft);
background: var(--panel); background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius-sm);
} }
.page-frame { .page-frame {
@ -165,7 +172,7 @@
padding: 10px; padding: 10px;
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius-sm);
} }
.toolbar { .toolbar {
@ -195,9 +202,9 @@
min-width: 0; min-width: 0;
gap: 8px; gap: 8px;
padding: 10px; padding: 10px;
background: #ffffff; background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius-sm);
} }
.picker-head { .picker-head {
@ -206,7 +213,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
color: #344054; color: var(--text-soft);
font-weight: 800; font-weight: 800;
} }
@ -217,10 +224,10 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 7px 9px; padding: 7px 9px;
color: #0f3f38; color: var(--brand-tint-text);
background: #eef8f5; background: var(--brand-tint);
border: 1px solid #b9dcd3; border: 1px solid var(--brand-tint-border);
border-radius: 7px; border-radius: var(--radius-sm);
} }
.selected-entity strong, .selected-entity strong,
@ -237,8 +244,9 @@
} }
.selected-entity div span { .selected-entity div span {
color: #52606d; color: var(--brand-tint-text);
font-size: 11px; font-size: 11px;
opacity: 0.85;
} }
.picker-search { .picker-search {
@ -250,7 +258,7 @@
padding: 0 6px 0 9px; padding: 0 6px 0 9px;
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line-strong); border: 1px solid var(--line-strong);
border-radius: 7px; border-radius: var(--radius-sm);
} }
.picker-search input { .picker-search input {
@ -267,7 +275,7 @@
max-height: 236px; max-height: 236px;
overflow: auto; overflow: auto;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 7px; border-radius: var(--radius-sm);
} }
.picker-row { .picker-row {
@ -278,7 +286,7 @@
gap: 8px; gap: 8px;
padding: 6px 8px; padding: 6px 8px;
color: var(--text); color: var(--text);
background: #ffffff; background: var(--panel);
border: 0; border: 0;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
cursor: pointer; cursor: pointer;
@ -291,7 +299,7 @@
.picker-row:hover, .picker-row:hover,
.picker-row.selected { .picker-row.selected {
background: #f3f8f6; background: var(--surface-soft);
} }
.picker-row strong, .picker-row strong,
@ -311,18 +319,24 @@
.picker-error { .picker-error {
color: var(--danger); color: var(--danger);
background: #fff2f0; background: var(--danger-tint);
border: 1px solid #efb4ad; border: 1px solid var(--danger-border);
border-radius: 7px; border-radius: var(--radius-sm);
} }
input, input,
textarea { textarea {
color: var(--text); color: var(--text);
background: #ffffff; background: var(--input-bg);
border: 1px solid var(--line-strong); border: 1px solid var(--line-strong);
border-radius: 7px; border-radius: var(--radius-sm);
outline: none; outline: none;
transition: border-color 140ms ease, box-shadow 140ms ease;
}
input::placeholder,
textarea::placeholder {
color: var(--muted-2);
} }
input { input {
@ -366,9 +380,10 @@ textarea:focus {
width: min(380px, 100%); width: min(380px, 100%);
height: 34px; height: 34px;
padding: 0 10px; padding: 0 10px;
background: #ffffff; color: var(--text);
background: var(--input-bg);
border: 1px solid var(--line-strong); border: 1px solid var(--line-strong);
border-radius: 7px; border-radius: var(--radius-sm);
} }
.searchbox input { .searchbox input {
@ -386,16 +401,17 @@ textarea:focus {
justify-content: center; justify-content: center;
gap: 6px; gap: 6px;
padding: 0 12px; padding: 0 12px;
color: #1d2939; color: var(--btn-text);
background: #ffffff; background: var(--btn-bg);
border: 1px solid var(--line-strong); border: 1px solid var(--line-strong);
border-radius: 7px; border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease;
} }
.btn:hover:not(:disabled) { .btn:hover:not(:disabled) {
background: #f7f9fb; background: var(--btn-hover);
} }
.btn:disabled { .btn:disabled {
@ -410,7 +426,8 @@ textarea:focus {
} }
.btn.primary:hover:not(:disabled) { .btn.primary:hover:not(:disabled) {
background: #12594f; background: var(--brand-strong);
border-color: var(--brand-strong);
} }
.btn.ghost { .btn.ghost {
@ -419,22 +436,24 @@ textarea:focus {
.btn.danger { .btn.danger {
color: var(--danger); color: var(--danger);
background: #fff7f5; background: var(--danger-tint);
border-color: #efb4ad; border-color: var(--danger-border);
} }
.btn.danger:hover:not(:disabled) { .btn.danger:hover:not(:disabled) {
background: #ffeceb; background: var(--danger-tint);
border-color: var(--danger);
} }
.btn.warn { .btn.warn {
color: var(--warn); color: var(--warn);
background: #fff8ec; background: var(--warn-tint);
border-color: #e7c77e; border-color: var(--warn-border);
} }
.btn.warn:hover:not(:disabled) { .btn.warn:hover:not(:disabled) {
background: #fff1d6; background: var(--warn-tint);
border-color: var(--warn);
} }
.btn:disabled, .btn:disabled,
@ -442,7 +461,7 @@ textarea:focus {
.btn.warn:disabled, .btn.warn:disabled,
.btn.danger:disabled { .btn.danger:disabled {
color: var(--muted-2); color: var(--muted-2);
background: #f3f5f7; background: var(--panel-strong);
border-color: var(--line); border-color: var(--line);
cursor: not-allowed; cursor: not-allowed;
} }
@ -476,8 +495,9 @@ textarea:focus {
.table-wrap { .table-wrap {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
} }
.data-table { .data-table {
@ -500,13 +520,13 @@ textarea:focus {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 0; z-index: 0;
color: #475467; color: var(--muted);
background: var(--panel-strong); background: var(--panel-strong);
font-weight: 800; font-weight: 800;
} }
.data-table tbody tr:hover { .data-table tbody tr:hover {
background: #fbfcfd; background: var(--panel-subtle);
} }
.data-table tr:last-child td { .data-table tr:last-child td {
@ -528,29 +548,29 @@ textarea:focus {
min-height: 22px; min-height: 22px;
align-items: center; align-items: center;
padding: 1px 8px; padding: 1px 8px;
color: #4f5b68; color: var(--muted);
background: #f3f6f8; background: var(--panel-strong);
border: 1px solid #d7e0e8; border: 1px solid var(--line-strong);
border-radius: 999px; border-radius: 999px;
white-space: nowrap; white-space: nowrap;
} }
.badge.good { .badge.good {
color: var(--good); color: var(--good);
background: #eef8f2; background: var(--good-tint);
border-color: #b9dcc7; border-color: var(--good-border);
} }
.badge.danger { .badge.danger {
color: var(--danger); color: var(--danger);
background: #fff2f0; background: var(--danger-tint);
border-color: #efb4ad; border-color: var(--danger-border);
} }
.badge.warn { .badge.warn {
color: var(--warn); color: var(--warn);
background: #fff8e7; background: var(--warn-tint);
border-color: #e7c77e; border-color: var(--warn-border);
} }
.empty-cell { .empty-cell {

View file

@ -18,10 +18,11 @@
padding: 14px; padding: 14px;
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
} }
.entity-title { .entity-title {
color: var(--heading);
font-size: 20px; font-size: 20px;
font-weight: 800; font-weight: 800;
line-height: 1.25; line-height: 1.25;
@ -41,10 +42,10 @@
.about-text { .about-text {
margin: 0; margin: 0;
padding: 10px; padding: 10px;
color: #344054; color: var(--text-soft);
background: #fbfcfd; background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
} }
.section-block, .section-block,
@ -54,7 +55,8 @@
padding: 12px; padding: 12px;
background: var(--panel); background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
box-shadow: var(--shadow-sm);
} }
.section-head { .section-head {
@ -79,7 +81,7 @@
.dock-title { .dock-title {
padding-bottom: 4px; padding-bottom: 4px;
color: #344054; color: var(--text-soft);
font-weight: 800; font-weight: 800;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
} }
@ -184,7 +186,7 @@
padding: 10px; padding: 10px;
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
} }
.operation-title { .operation-title {
@ -192,6 +194,7 @@
width: 100%; width: 100%;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
color: var(--heading);
font-weight: 800; font-weight: 800;
} }
@ -212,10 +215,10 @@
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
padding: 9px 10px; padding: 9px 10px;
color: #8a251d; color: var(--danger-text);
background: #fff2f0; background: var(--danger-tint);
border: 1px solid #efb4ad; border: 1px solid var(--danger-border);
border-radius: 8px; border-radius: var(--radius);
} }
.json-block { .json-block {
@ -223,10 +226,10 @@
overflow: auto; overflow: auto;
margin: 0; margin: 0;
padding: 12px; padding: 12px;
color: #d8e6f0; color: var(--code-text);
background: #141a22; background: var(--code-bg);
border: 1px solid #2a3542; border: 1px solid var(--code-border);
border-radius: 8px; border-radius: var(--radius);
font-size: 12px; font-size: 12px;
} }
@ -250,12 +253,12 @@
color: var(--muted); color: var(--muted);
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
} }
.gift-metrics .metric { .gift-metrics .metric {
min-height: 68px; min-height: 68px;
padding: 12px; padding: 12px;
background: linear-gradient(145deg, #ffffff, #f6f9f9); background: var(--panel-subtle);
} }
.gift-metrics .metric strong { font-size: 17px; } .gift-metrics .metric strong { font-size: 17px; }
@ -265,12 +268,12 @@
flex: 0 0 auto; flex: 0 0 auto;
place-items: center; place-items: center;
color: var(--brand); color: var(--brand);
background: #eaf6f3; background: var(--brand-tint);
border: 1px solid #c7e3dc; border: 1px solid var(--brand-tint-border);
} }
.gift-format-chips { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; gap: 6px; } .gift-format-chips { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
.gift-format-chips span { padding: 4px 8px; color: #33645d; background: #eef8f5; border: 1px solid #cfe5df; border-radius: 999px; font-size: 10px; font-weight: 800; letter-spacing: .02em; } .gift-format-chips span { padding: 4px 8px; color: var(--brand-tint-text); background: var(--brand-tint); border: 1px solid var(--brand-tint-border); border-radius: 999px; font-size: 10px; font-weight: 800; letter-spacing: .02em; }
.gift-list-summary { margin-left: auto; color: var(--muted); font-size: 11px; font-weight: 700; } .gift-list-summary { margin-left: auto; color: var(--muted); font-size: 11px; font-weight: 700; }
@ -284,48 +287,48 @@
.official-gift-categories { display: flex; flex-wrap: wrap; gap: 7px; } .official-gift-categories { display: flex; flex-wrap: wrap; gap: 7px; }
.official-gift-categories button { .official-gift-categories button {
display: inline-flex; align-items: center; gap: 7px; min-height: 32px; padding: 5px 10px; display: inline-flex; align-items: center; gap: 7px; min-height: 32px; padding: 5px 10px;
color: #49605c; background: #f7faf9; border: 1px solid #d7e2df; border-radius: 999px; color: var(--text-soft); background: var(--panel-subtle); border: 1px solid var(--line-strong); border-radius: 999px;
font: inherit; font-size: 11px; font-weight: 800; cursor: pointer; font: inherit; font-size: 11px; font-weight: 800; cursor: pointer;
transition: color .15s ease, background .15s ease, border-color .15s ease, box-shadow .15s ease; transition: color .15s ease, background .15s ease, border-color .15s ease, box-shadow .15s ease;
} }
.official-gift-categories button:hover { color: var(--brand); border-color: #9fc9c0; } .official-gift-categories button:hover { color: var(--brand); border-color: var(--brand-tint-border); }
.official-gift-categories button.active { color: #ffffff; background: var(--brand); border-color: var(--brand); box-shadow: 0 4px 12px rgba(23, 109, 97, .17); } .official-gift-categories button.active { color: #ffffff; background: var(--brand); border-color: var(--brand); box-shadow: var(--shadow-brand); }
.official-gift-categories button span { .official-gift-categories button span {
display: grid; min-width: 20px; height: 20px; padding: 0 5px; place-items: center; display: grid; min-width: 20px; height: 20px; padding: 0 5px; place-items: center;
color: inherit; background: rgba(255,255,255,.65); border-radius: 999px; font-size: 10px; color: inherit; background: rgba(125, 140, 155, .22); border-radius: 999px; font-size: 10px;
} }
.official-gift-categories button.active span { color: var(--brand); } .official-gift-categories button.active span { color: var(--brand); background: rgba(255, 255, 255, .85); }
.official-gift-list { .official-gift-list {
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; max-height: 314px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; max-height: 314px;
min-height: 126px; padding: 8px; overflow: auto; border: 1px solid var(--line); border-radius: 14px; min-height: 126px; padding: 8px; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius-lg);
background: #f6f9f8; scrollbar-gutter: stable; background: var(--panel-subtle); scrollbar-gutter: stable;
} }
.official-gift-option { .official-gift-option {
display: grid; min-width: 0; gap: 8px; padding: 11px 12px; text-align: left; color: var(--text); display: grid; min-width: 0; gap: 8px; padding: 11px 12px; text-align: left; color: var(--text);
background: #ffffff; border: 1px solid #dce6e3; border-radius: 11px; cursor: pointer; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer;
box-shadow: 0 1px 2px rgba(32, 54, 50, .03); box-shadow: var(--shadow-sm);
transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
} }
.official-gift-option:hover { border-color: #9fc9c0; box-shadow: 0 5px 14px rgba(32, 76, 68, .08); transform: translateY(-1px); } .official-gift-option:hover { border-color: var(--brand-tint-border); box-shadow: var(--shadow); transform: translateY(-1px); }
.official-gift-option.selected { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(23, 109, 97, .12), 0 5px 14px rgba(32, 76, 68, .08); } .official-gift-option.selected { border-color: var(--brand); box-shadow: 0 0 0 2px var(--focus), var(--shadow); }
.official-gift-option-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: baseline; } .official-gift-option-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: baseline; }
.official-gift-option-head strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } .official-gift-option-head strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.official-gift-option-head .mono { color: var(--muted); font-size: 9px; } .official-gift-option-head .mono { color: var(--muted); font-size: 9px; }
.official-gift-option-meta { display: flex; flex-wrap: wrap; gap: 10px; color: #667773; font-size: 10px; font-weight: 700; } .official-gift-option-meta { display: flex; flex-wrap: wrap; gap: 10px; color: var(--muted); font-size: 10px; font-weight: 700; }
.official-gift-capabilities { display: flex; flex-wrap: wrap; gap: 5px; } .official-gift-capabilities { display: flex; flex-wrap: wrap; gap: 5px; }
.official-gift-capabilities > span { .official-gift-capabilities > span {
padding: 3px 7px; border: 1px solid transparent; border-radius: 999px; font-size: 9px; font-weight: 850; letter-spacing: .01em; padding: 3px 7px; border: 1px solid transparent; border-radius: 999px; font-size: 9px; font-weight: 850; letter-spacing: .01em;
} }
.official-gift-capabilities > span.yes { color: #136b4d; background: #e9f8f0; border-color: #bde6cf; } .official-gift-capabilities > span.yes { color: var(--good); background: var(--good-tint); border-color: var(--good-border); }
.official-gift-capabilities > span.craft { color: #6e3ca0; background: #f3ebfb; border-color: #d9c5ef; } .official-gift-capabilities > span.craft { color: var(--purple); background: var(--purple-tint); border-color: var(--purple-border); }
.official-gift-capabilities > span.no { color: #78837f; background: #f1f3f2; border-color: #dde2e0; } .official-gift-capabilities > span.no { color: var(--muted); background: var(--panel-strong); border-color: var(--line-strong); }
.official-gift-empty { .official-gift-empty {
display: grid; grid-column: 1 / -1; min-height: 108px; place-items: center; padding: 20px; display: grid; grid-column: 1 / -1; min-height: 108px; place-items: center; padding: 20px;
color: var(--muted); text-align: center; font-size: 12px; color: var(--muted); text-align: center; font-size: 12px;
} }
.official-gift-selected { .official-gift-selected {
display: grid; grid-template-columns: 108px minmax(0, 1fr); gap: 14px; align-items: center; display: grid; grid-template-columns: 108px minmax(0, 1fr); gap: 14px; align-items: center;
padding: 12px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-soft); padding: 12px; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface-soft);
} }
.official-gift-selected .gift-animation-shell { width: 96px; height: 96px; } .official-gift-selected .gift-animation-shell { width: 96px; height: 96px; }
.official-gift-selected > div:last-child { display: grid; gap: 5px; min-width: 0; } .official-gift-selected > div:last-child { display: grid; gap: 5px; min-width: 0; }
@ -341,22 +344,22 @@
gap: 12px; gap: 12px;
padding: 12px 14px; padding: 12px 14px;
color: var(--text); color: var(--text);
background: #ffffff; background: var(--panel);
border: 1px dashed #b7ccc8; border: 1px dashed var(--line-strong);
border-radius: 10px; border-radius: var(--radius);
cursor: pointer; cursor: pointer;
transition: border-color .16s ease, background .16s ease, box-shadow .16s ease; transition: border-color .16s ease, background .16s ease, box-shadow .16s ease;
} }
.gift-file-picker:hover, .gift-file-picker:hover,
.gift-file-picker.has-file { background: #f8fcfb; border-color: var(--brand); box-shadow: 0 0 0 2px rgba(23, 109, 97, .05); } .gift-file-picker.has-file { background: var(--brand-tint); border-color: var(--brand); box-shadow: 0 0 0 2px var(--focus); }
.gift-file-picker input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } .gift-file-picker input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.gift-file-icon { width: 40px; height: 40px; border-radius: 9px; } .gift-file-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); }
.gift-file-copy { display: grid; min-width: 0; gap: 2px; } .gift-file-copy { display: grid; min-width: 0; gap: 2px; }
.gift-field-label { color: var(--muted); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; } .gift-field-label { color: var(--muted); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
.gift-file-copy strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } .gift-file-copy strong { overflow: hidden; color: var(--heading); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.gift-file-copy small { color: var(--muted); font-size: 11px; font-weight: 500; } .gift-file-copy small { color: var(--muted); font-size: 11px; font-weight: 500; }
.gift-file-action { padding: 7px 10px; color: var(--brand); background: #f0f8f6; border: 1px solid #c7e3dc; border-radius: 7px; font-size: 11px; font-weight: 800; } .gift-file-action { padding: 7px 10px; color: var(--brand); background: var(--brand-tint); border: 1px solid var(--brand-tint-border); border-radius: var(--radius-sm); font-size: 11px; font-weight: 800; }
.gift-fields-grid { .gift-fields-grid {
display: grid; display: grid;
@ -380,37 +383,37 @@
height: 38px; height: 38px;
padding: 0 10px; padding: 0 10px;
color: var(--text); color: var(--text);
background: #fff; background: var(--input-bg);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 7px; border-radius: var(--radius-sm);
} }
.gift-fields-grid input:focus, .gift-fields-grid input:focus,
.gift-reason-field input:focus { border-color: #77b6aa; box-shadow: 0 0 0 3px rgba(23, 109, 97, .08); outline: none; } .gift-reason-field input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--focus); outline: none; }
.gift-switch { display: inline-flex; align-items: center; gap: 9px; color: #344054; font-size: 12px; font-weight: 700; cursor: pointer; } .gift-switch { display: inline-flex; align-items: center; gap: 9px; color: var(--text-soft); font-size: 12px; font-weight: 700; cursor: pointer; }
.gift-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; } .gift-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.gift-switch-track { display: flex; width: 34px; height: 19px; align-items: center; padding: 2px; background: #c8d0d5; border-radius: 999px; transition: background .16s ease; } .gift-switch-track { display: flex; width: 34px; height: 19px; align-items: center; padding: 2px; background: var(--switch-track); border-radius: 999px; transition: background .16s ease; }
.gift-switch-track span { width: 15px; height: 15px; background: #ffffff; border-radius: 50%; box-shadow: 0 1px 3px rgba(16, 24, 40, .22); transition: transform .16s ease; } .gift-switch-track span { width: 15px; height: 15px; background: #ffffff; border-radius: 50%; box-shadow: 0 1px 3px rgba(16, 24, 40, .22); transition: transform .16s ease; }
.gift-switch input:checked + .gift-switch-track { background: var(--brand); } .gift-switch input:checked + .gift-switch-track { background: var(--brand); }
.gift-switch input:checked + .gift-switch-track span { transform: translateX(15px); } .gift-switch input:checked + .gift-switch-track span { transform: translateX(15px); }
.gift-switch input:focus-visible + .gift-switch-track { outline: 3px solid rgba(23, 109, 97, .16); outline-offset: 2px; } .gift-switch input:focus-visible + .gift-switch-track { outline: 3px solid var(--focus); outline-offset: 2px; }
.gift-validation { overflow: hidden; color: #d5fff5; background: #173631; border: 1px solid #24564e; border-radius: 9px; } .gift-validation { overflow: hidden; color: var(--code-text); background: var(--code-bg); border: 1px solid var(--code-border); border-radius: var(--radius-sm); }
.gift-validation-head { display: flex; align-items: center; gap: 9px; padding: 10px 12px; color: #e3fff9; background: rgba(255, 255, 255, .035); border-bottom: 1px solid rgba(255, 255, 255, .09); } .gift-validation-head { display: flex; align-items: center; gap: 9px; padding: 10px 12px; color: var(--code-text); background: rgba(255, 255, 255, .035); border-bottom: 1px solid rgba(255, 255, 255, .09); }
.gift-validation-head div { display: grid; gap: 2px; } .gift-validation-head div { display: grid; gap: 2px; }
.gift-validation-head span { color: #99cfc4; font-size: 10px; } .gift-validation-head span { color: var(--brand); font-size: 10px; }
.gift-validation pre { max-height: 180px; overflow: auto; margin: 0; padding: 11px 12px; color: #d5fff5; font-size: 11px; } .gift-validation pre { max-height: 180px; overflow: auto; margin: 0; padding: 11px 12px; color: var(--code-text); font-size: 11px; }
.gift-animation-shell { position: relative; display: grid; min-height: 210px; place-items: center; background: radial-gradient(circle, #f9f3ff, #eef8f5); } .gift-animation-shell { position: relative; display: grid; min-height: 210px; place-items: center; background: var(--surface-soft); }
.gift-animation { width: 200px; height: 200px; } .gift-animation { width: 200px; height: 200px; }
.gift-animation canvas { width: 100% !important; height: 100% !important; } .gift-animation canvas { width: 100% !important; height: 100% !important; }
.gift-play { position: absolute; right: 8px; bottom: 8px; display: grid; width: 30px; height: 30px; place-items: center; color: var(--text); background: rgba(255,255,255,.9); border: 1px solid var(--line); border-radius: 50%; } .gift-play { position: absolute; right: 8px; bottom: 8px; display: grid; width: 30px; height: 30px; place-items: center; color: var(--text); background: var(--panel); border: 1px solid var(--line); border-radius: 50%; }
.gift-table-wrap { background: #ffffff; } .gift-table-wrap { background: var(--panel); }
.gift-table { min-width: 1080px; } .gift-table { min-width: 1080px; }
.gift-table th:first-child { width: 74px; } .gift-table th:first-child { width: 74px; }
.gift-table td { vertical-align: middle; } .gift-table td { vertical-align: middle; }
.gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; } .gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius-sm); }
.gift-animation-shell.compact .gift-animation { width: 54px; height: 54px; } .gift-animation-shell.compact .gift-animation { width: 54px; height: 54px; }
.gift-animation-shell.compact .gift-play { right: 3px; bottom: 3px; width: 20px; height: 20px; } .gift-animation-shell.compact .gift-play { right: 3px; bottom: 3px; width: 20px; height: 20px; }
.gift-row-disabled { opacity: .68; } .gift-row-disabled { opacity: .68; }
@ -422,60 +425,60 @@
.gift-sort-order, .gift-sort-order,
.gift-source-size, .gift-source-size,
.gift-convert-price { margin-top: 3px; color: var(--muted); font-size: 10px; } .gift-convert-price { margin-top: 3px; color: var(--muted); font-size: 10px; }
.gift-table-price { color: #755b00; } .gift-table-price { color: var(--warn); }
.gift-table-actions { display: flex; align-items: center; gap: 6px; } .gift-table-actions { display: flex; align-items: center; gap: 6px; }
.collectible-button { color: #6548a8; background: #f7f3ff; border-color: #ddd2f5; } .collectible-button { color: var(--purple); background: var(--purple-tint); border-color: var(--purple-border); }
.collectible-button:hover { background: #efe8ff; border-color: #cbbaf0; } .collectible-button:hover { background: var(--purple-tint); border-color: var(--purple); }
.collectible-modal { width: min(1180px, 100%); max-height: min(92vh, 980px); } .collectible-modal { width: min(1180px, 100%); max-height: min(92vh, 980px); }
.collectible-modal .modal-head p { margin: 4px 0 0; color: var(--muted); font-size: 11px; } .collectible-modal .modal-head p { margin: 4px 0 0; color: var(--muted); font-size: 11px; }
.collectible-modal-body { gap: 16px; overflow: auto; padding: 16px 18px 22px; background: #f5f7fa; } .collectible-modal-body { gap: 16px; overflow: auto; padding: 16px 18px 22px; background: var(--bg); }
.collectible-loading { display: flex; min-height: 90px; align-items: center; justify-content: center; gap: 8px; color: var(--muted); } .collectible-loading { display: flex; min-height: 90px; align-items: center; justify-content: center; gap: 8px; color: var(--muted); }
.collectible-empty { display: flex; align-items: center; gap: 12px; padding: 16px; color: #66568c; background: linear-gradient(135deg, #fbf9ff, #f2f7ff); border: 1px dashed #cfc3e9; border-radius: 12px; } .collectible-empty { display: flex; align-items: center; gap: 12px; padding: 16px; color: var(--purple-text); background: var(--purple-tint); border: 1px dashed var(--purple-border); border-radius: var(--radius); }
.collectible-empty div, .collectible-empty div,
.collectible-definition-head > div:first-child, .collectible-definition-head > div:first-child,
.collectible-section-head > div:first-child { display: grid; gap: 3px; } .collectible-section-head > div:first-child { display: grid; gap: 3px; }
.collectible-empty span, .collectible-empty span,
.collectible-definition-head span, .collectible-definition-head span,
.collectible-section-head span { color: var(--muted); font-size: 10px; font-weight: 500; } .collectible-section-head span { color: var(--muted); font-size: 10px; font-weight: 500; }
.collectible-active { overflow: hidden; background: #ffffff; border: 1px solid #ddd6ee; border-radius: 12px; box-shadow: 0 5px 16px rgba(66, 46, 110, .05); } .collectible-active { overflow: hidden; background: var(--panel); border: 1px solid var(--purple-border); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
.collectible-active-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: linear-gradient(100deg, #fbf9ff, #f4f9ff); border-bottom: 1px solid #e9e4f3; } .collectible-active-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: var(--purple-tint); border-bottom: 1px solid var(--purple-border); }
.collectible-active-head > div { display: flex; align-items: center; gap: 9px; color: #60458f; } .collectible-active-head > div { display: flex; align-items: center; gap: 9px; color: var(--purple-text); }
.collectible-active-head > div > div { display: grid; gap: 2px; } .collectible-active-head > div > div { display: grid; gap: 2px; }
.collectible-active-head span { color: var(--muted); font-size: 10px; } .collectible-active-head span { color: var(--muted); font-size: 10px; }
.collectible-active-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: 1px; background: var(--line); } .collectible-active-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: 1px; background: var(--line); }
.collectible-active-grid article { display: flex; min-width: 0; align-items: center; gap: 9px; padding: 9px 11px; background: #ffffff; } .collectible-active-grid article { display: flex; min-width: 0; align-items: center; gap: 9px; padding: 9px 11px; background: var(--panel); }
.collectible-active-grid article > div:last-child { display: grid; min-width: 0; gap: 2px; } .collectible-active-grid article > div:last-child { display: grid; min-width: 0; gap: 2px; }
.collectible-active-grid article strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } .collectible-active-grid article strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.collectible-active-grid article span { color: var(--muted); font-size: 9px; } .collectible-active-grid article span { color: var(--muted); font-size: 9px; }
.collectible-definition { overflow: hidden; background: #ffffff; border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 8px 24px rgba(16, 24, 40, .04); } .collectible-definition { overflow: hidden; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
.collectible-definition-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; background: linear-gradient(110deg, #f8fbfa, #fbf9ff); border-bottom: 1px solid var(--line); } .collectible-definition-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; background: var(--panel-subtle); border-bottom: 1px solid var(--line); }
.collectible-main-fields { padding: 14px 16px; background: #fbfcfd; border-bottom: 1px solid var(--line); } .collectible-main-fields { padding: 14px 16px; background: var(--panel-subtle); border-bottom: 1px solid var(--line); }
.collectible-section { padding: 14px 16px; border-bottom: 1px solid var(--line); } .collectible-section { padding: 14px 16px; border-bottom: 1px solid var(--line); }
.collectible-section:last-child { border-bottom: 0; } .collectible-section:last-child { border-bottom: 0; }
.collectible-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; } .collectible-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.collectible-section-tools { display: flex; align-items: center; gap: 7px; } .collectible-section-tools { display: flex; align-items: center; gap: 7px; }
.collectible-rows { display: grid; gap: 7px; } .collectible-rows { display: grid; gap: 7px; }
.collectible-row { position: relative; display: grid; align-items: end; gap: 7px; padding: 9px 9px 9px 36px; background: #fafbfc; border: 1px solid #e1e6eb; border-radius: 9px; } .collectible-row { position: relative; display: grid; align-items: end; gap: 7px; padding: 9px 9px 9px 36px; background: var(--panel-subtle); border: 1px solid var(--line); border-radius: var(--radius-sm); }
.collectible-row:hover { background: #ffffff; border-color: #cbd7dd; box-shadow: 0 3px 10px rgba(16, 24, 40, .035); } .collectible-row:hover { background: var(--panel); border-color: var(--line-strong); box-shadow: var(--shadow-sm); }
.collectible-row.animated { grid-template-columns: minmax(120px, 1.2fr) 90px 78px minmax(160px, 1.4fr) 48px 30px; } .collectible-row.animated { grid-template-columns: minmax(120px, 1.2fr) 90px 78px minmax(160px, 1.4fr) 48px 30px; }
.collectible-row.backdrop { grid-template-columns: minmax(110px, 1.2fr) 70px 80px 70px repeat(4, 52px) 48px 30px; } .collectible-row.backdrop { grid-template-columns: minmax(110px, 1.2fr) 70px 80px 70px repeat(4, 52px) 48px 30px; }
.collectible-row-index { position: absolute; top: 0; bottom: 0; left: 0; display: grid; width: 27px; place-items: center; color: #71668c; background: #f0edf7; border-right: 1px solid #e0d9ed; border-radius: 8px 0 0 8px; font-size: 10px; font-weight: 800; } .collectible-row-index { position: absolute; top: 0; bottom: 0; left: 0; display: grid; width: 27px; place-items: center; color: var(--purple-text); background: var(--purple-tint); border-right: 1px solid var(--purple-border); border-radius: var(--radius-xs) 0 0 var(--radius-xs); font-size: 10px; font-weight: 800; }
.collectible-row label { display: grid; min-width: 0; gap: 4px; } .collectible-row label { display: grid; min-width: 0; gap: 4px; }
.collectible-row label > span { color: var(--muted); font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .025em; } .collectible-row label > span { color: var(--muted); font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .025em; }
.collectible-row input:not([type="file"]) { width: 100%; min-width: 0; height: 32px; padding: 0 8px; color: var(--text); background: #ffffff; border: 1px solid #d5dde3; border-radius: 7px; font: inherit; font-size: 11px; } .collectible-row input:not([type="file"]) { width: 100%; min-width: 0; height: 32px; padding: 0 8px; color: var(--text); background: var(--input-bg); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); font: inherit; font-size: 11px; }
.collectible-row input:focus { border-color: #8d7aba; box-shadow: 0 0 0 3px rgba(111, 91, 174, .08); outline: none; } .collectible-row input:focus { border-color: var(--purple); box-shadow: 0 0 0 3px var(--purple-tint); outline: none; }
.collectible-file input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } .collectible-file input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.collectible-file em { display: flex; min-width: 0; height: 32px; align-items: center; gap: 5px; overflow: hidden; padding: 0 8px; color: #625080; background: #f7f4fd; border: 1px dashed #cfc4e1; border-radius: 7px; font-size: 10px; font-style: normal; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } .collectible-file em { display: flex; min-width: 0; height: 32px; align-items: center; gap: 5px; overflow: hidden; padding: 0 8px; color: var(--purple-text); background: var(--purple-tint); border: 1px dashed var(--purple-border); border-radius: var(--radius-sm); font-size: 10px; font-style: normal; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.collectible-inline-preview { display: grid; width: 42px; height: 42px; place-items: center; overflow: hidden; color: #8c7cae; background: radial-gradient(circle, #ffffff, #eee8f8); border: 1px solid #ded5ed; border-radius: 8px; } .collectible-inline-preview { display: grid; width: 42px; height: 42px; place-items: center; overflow: hidden; color: var(--purple); background: var(--purple-tint); border: 1px solid var(--purple-border); border-radius: var(--radius-sm); }
.collectible-animation { width: 100%; height: 100%; overflow: hidden; } .collectible-animation { width: 100%; height: 100%; overflow: hidden; }
.collectible-animation.compact { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; background: radial-gradient(circle, #ffffff, #f0ebfa); border: 1px solid #e0d9ec; border-radius: 8px; } .collectible-animation.compact { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; background: var(--purple-tint); border: 1px solid var(--purple-border); border-radius: var(--radius-sm); }
.collectible-animation canvas { width: 100% !important; height: 100% !important; } .collectible-animation canvas { width: 100% !important; height: 100% !important; }
.collectible-animation.failed { color: #b42318; background: #fff4f2; } .collectible-animation.failed { color: var(--danger); background: var(--danger-tint); }
.collectible-animation.loading { color: #807397; } .collectible-animation.loading { color: var(--purple-text); }
.collectible-file-error { grid-column: 1 / -1; color: #b42318; font-size: 10px; } .collectible-file-error { grid-column: 1 / -1; color: var(--danger); font-size: 10px; }
.collectible-color input { height: 32px !important; padding: 3px !important; cursor: pointer; } .collectible-color input { height: 32px !important; padding: 3px !important; cursor: pointer; }
.collectible-backdrop-preview { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; border: 1px solid rgba(42, 31, 71, .18); border-radius: 8px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.2); font-size: 11px; font-weight: 900; } .collectible-backdrop-preview { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; border: 1px solid rgba(42, 31, 71, .18); border-radius: var(--radius-sm); box-shadow: inset 0 0 0 1px rgba(255,255,255,.2); font-size: 11px; font-weight: 900; }
.collectible-row .icon-btn { align-self: center; } .collectible-row .icon-btn { align-self: center; }
.collectible-row .icon-btn:disabled { opacity: .28; } .collectible-row .icon-btn:disabled { opacity: .28; }

View file

@ -5,7 +5,8 @@
display: grid; display: grid;
place-items: center; place-items: center;
padding: 24px; padding: 24px;
background: rgba(17, 24, 39, 0.52); background: var(--overlay);
backdrop-filter: blur(2px);
} }
.modal { .modal {
@ -13,9 +14,9 @@
max-height: min(820px, calc(100vh - 48px)); max-height: min(820px, calc(100vh - 48px));
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
background: #ffffff; background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius-lg);
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
@ -43,10 +44,17 @@
width: 30px; width: 30px;
height: 30px; height: 30px;
place-items: center; place-items: center;
color: var(--text-soft);
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 7px; border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}
.icon-btn:hover {
background: var(--btn-hover);
border-color: var(--line-strong);
} }
.command-steps { .command-steps {
@ -73,7 +81,7 @@
color: var(--muted); color: var(--muted);
background: var(--panel-subtle); background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius-sm);
} }
.command-step span { .command-step span {
@ -81,7 +89,7 @@
width: 20px; width: 20px;
height: 20px; height: 20px;
place-items: center; place-items: center;
background: #ffffff; background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 999px; border-radius: 999px;
font-size: 11px; font-size: 11px;
@ -90,12 +98,12 @@
.command-step.active { .command-step.active {
color: var(--brand); color: var(--brand);
border-color: #a9d8ce; border-color: var(--brand-tint-border);
} }
.command-step.done { .command-step.done {
color: var(--good); color: var(--good);
border-color: #b9dcc7; border-color: var(--good-border);
} }
.form-field { .form-field {
@ -105,7 +113,7 @@
.form-field span, .form-field span,
.form-stack span { .form-stack span {
color: #4b5563; color: var(--text-soft);
font-weight: 800; font-weight: 800;
} }
@ -123,7 +131,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 7px; gap: 7px;
color: #344054; color: var(--text-soft);
font-weight: 800; font-weight: 800;
} }
@ -131,9 +139,9 @@
display: grid; display: grid;
gap: 8px; gap: 8px;
padding: 10px; padding: 10px;
background: #fbfcfd; background: var(--panel-subtle);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius);
} }
.result-line { .result-line {
@ -151,13 +159,13 @@
} }
.result-message { .result-message {
color: #344054; color: var(--text-soft);
} }
.modal-actions { .modal-actions {
justify-content: flex-end; justify-content: flex-end;
padding: 12px 18px; padding: 12px 18px;
background: #ffffff; background: var(--panel);
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
} }
@ -174,9 +182,9 @@
width: min(420px, 100%); width: min(420px, 100%);
gap: 18px; gap: 18px;
padding: 22px; padding: 22px;
background: #ffffff; background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: var(--radius-lg);
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
@ -201,14 +209,15 @@
align-items: center; align-items: center;
padding: 0 8px; padding: 0 8px;
color: var(--brand); color: var(--brand);
background: #edf7f4; background: var(--brand-tint);
border: 1px solid #c9e2dc; border: 1px solid var(--brand-tint-border);
border-radius: 999px; border-radius: 999px;
font-size: 12px; font-size: 12px;
} }
.login-copy h1 { .login-copy h1 {
margin: 0; margin: 0;
color: var(--heading);
font-size: 22px; font-size: 22px;
} }
@ -237,13 +246,14 @@
place-items: center; place-items: center;
align-content: center; align-content: center;
gap: 18px; gap: 18px;
background: var(--bg);
} }
.loader-bar { .loader-bar {
width: 180px; width: 180px;
height: 4px; height: 4px;
overflow: hidden; overflow: hidden;
background: #d7dde4; background: var(--line-strong);
border-radius: 999px; border-radius: 999px;
} }

View file

@ -0,0 +1,106 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { Moon, Sun } from "lucide-react";
import { useI18n } from "./i18n";
export type Theme = "light" | "dark";
const storageKey = "telesrv.admin.theme";
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function applyTheme(theme: Theme) {
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => initialTheme());
useEffect(() => {
applyTheme(theme);
try {
localStorage.setItem(storageKey, theme);
} catch {
// Theme persistence is best-effort.
}
}, [theme]);
// Follow the OS preference until the user makes an explicit choice.
useEffect(() => {
if (!window.matchMedia) {
return;
}
const media = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = (event: MediaQueryListEvent) => {
let stored: string | null = null;
try {
stored = localStorage.getItem(storageKey);
} catch {
stored = null;
}
if (stored !== "light" && stored !== "dark") {
setThemeState(event.matches ? "dark" : "light");
}
};
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
}, []);
const setTheme = useCallback((next: Theme) => setThemeState(next), []);
const toggleTheme = useCallback(() => setThemeState((current) => (current === "dark" ? "light" : "dark")), []);
const value = useMemo<ThemeContextValue>(() => ({ theme, setTheme, toggleTheme }), [theme, setTheme, toggleTheme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme(): ThemeContextValue {
const value = useContext(ThemeContext);
if (!value) {
throw new Error("useTheme must be used inside ThemeProvider");
}
return value;
}
export function ThemeSwitch() {
const { theme, toggleTheme } = useTheme();
const { t } = useI18n();
const nextIsDark = theme === "light";
const label = t(nextIsDark ? "theme.switchToDark" : "theme.switchToLight");
return (
<button
className="theme-toggle"
type="button"
onClick={toggleTheme}
aria-label={label}
title={label}
>
{theme === "dark" ? <Sun size={16} /> : <Moon size={16} />}
</button>
);
}
function initialTheme(): Theme {
try {
const stored = localStorage.getItem(storageKey);
if (stored === "light" || stored === "dark") {
return stored;
}
} catch {
// Storage is optional.
}
try {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
} catch {
// matchMedia can be unavailable in unusual embedded contexts.
}
return "light";
}