diff --git a/web/book_sto.html b/web/book_sto.html
index 434152a..e8ff6a2 100644
--- a/web/book_sto.html
+++ b/web/book_sto.html
@@ -53,15 +53,18 @@
+
+ Добавить запись
+
+
+
+
+
+
+
+
diff --git a/web/static/app.js b/web/static/app.js
index 6bf459d..fddcb2c 100644
--- a/web/static/app.js
+++ b/web/static/app.js
@@ -4,6 +4,8 @@ tg?.expand();
const textNodes = new WeakMap();
const attrOriginals = new WeakMap();
+let translationObserver = null;
+let translationTimer = null;
const i18n = {
en: {
@@ -83,6 +85,14 @@ const i18n = {
"Марка": "Make",
"Модель": "Model",
"Добавить авто": "Add vehicle",
+ "Добавить запись": "Add entry",
+ "Расход": "Expense",
+ "дата, пробег, литры, цена": "date, odometer, liters, price",
+ "работа, стоимость, следующий срок": "work, cost, next due",
+ "страховка, штраф, парковка, прочее": "insurance, fine, parking, other",
+ "фото или файл": "photo or file",
+ "Дополнительно": "More options",
+ "Напоминание о следующем ТО": "Next maintenance reminder",
"За весь срок": "All time",
"За месяц": "This month",
"За день": "Per day",
@@ -215,6 +225,14 @@ const i18n = {
"Марка": "브랜드",
"Модель": "모델",
"Добавить авто": "차량 추가",
+ "Добавить запись": "기록 추가",
+ "Расход": "지출",
+ "дата, пробег, литры, цена": "날짜, 주행거리, 리터, 가격",
+ "работа, стоимость, следующий срок": "작업, 비용, 다음 예정",
+ "страховка, штраф, парковка, прочее": "보험, 벌금, 주차, 기타",
+ "фото или файл": "사진 또는 파일",
+ "Дополнительно": "추가 옵션",
+ "Напоминание о следующем ТО": "다음 정비 알림",
"За весь срок": "전체",
"За месяц": "월",
"За день": "일 평균",
@@ -304,6 +322,15 @@ function applyTranslations(root = document.body) {
});
}
+function observeTranslations(root = document.body) {
+ if (translationObserver || !root) return;
+ translationObserver = new MutationObserver(() => {
+ window.clearTimeout(translationTimer);
+ translationTimer = window.setTimeout(() => applyTranslations(root), 40);
+ });
+ translationObserver.observe(root, { childList: true, subtree: true });
+}
+
const state = {
user: null,
@@ -582,6 +609,7 @@ async function ensureUser() {
method: "POST",
body: JSON.stringify({ init_data: tg.initData }),
});
+ localStorage.setItem("carpassLocale", state.user.locale || "ru");
hideAuthOverlay();
updateRoleVisibility();
return;
@@ -590,6 +618,7 @@ async function ensureUser() {
const devId = localStorage.getItem("driversDevTelegramId") || "1";
localStorage.setItem("driversDevTelegramId", devId);
state.user = await api("/users/me");
+ localStorage.setItem("carpassLocale", state.user.locale || "ru");
hideAuthOverlay();
updateRoleVisibility();
return;
@@ -598,6 +627,36 @@ async function ensureUser() {
throw new Error("Требуется вход через Telegram");
}
+function installLocaleSwitch() {
+ const topActions = document.querySelector(".topbar .top-actions");
+ if (!topActions || document.querySelector("#globalLocaleSelect")) return;
+ const select = document.createElement("select");
+ select.id = "globalLocaleSelect";
+ select.className = "locale-switch";
+ select.setAttribute("aria-label", "Язык");
+ select.innerHTML = `
+
+
+
+ `;
+ select.value = state.user?.locale || "ru";
+ select.addEventListener("change", async () => {
+ await runAction(select, "Сохраняю...", async () => {
+ state.user = await api(`/users/${state.user.id}/preferences`, {
+ method: "PATCH",
+ body: JSON.stringify({ locale: select.value, currency: state.user.currency }),
+ });
+ localStorage.setItem("carpassLocale", state.user.locale || "ru");
+ document.querySelector("#localeSelect").value = state.user.locale || "ru";
+ applyTranslations();
+ renderCars();
+ renderStats(state.latestStats);
+ toast("Сохранено");
+ });
+ });
+ topActions.prepend(select);
+}
+
function hideAuthOverlay() {
document.querySelector("#authOverlay")?.classList.add("hidden");
document.body.classList.remove("auth-required");
@@ -2671,6 +2730,9 @@ document.querySelector("#settingsForm").addEventListener("submit", async (event)
method: "PATCH",
body: JSON.stringify({ locale: data.locale, currency: data.currency }),
});
+ localStorage.setItem("carpassLocale", state.user.locale || "ru");
+ const globalLocale = document.querySelector("#globalLocaleSelect");
+ if (globalLocale) globalLocale.value = state.user.locale || "ru";
applyTranslations();
initCarCatalog();
await loadSelectedCar();
@@ -2700,6 +2762,7 @@ document.querySelector("#fuelForm").addEventListener("submit", async (event) =>
});
form.reset();
form.entry_date.value = today();
+ form.is_full_tank.checked = true;
await loadSelectedCar();
toast("Сохранено");
haptic("success");
@@ -2802,6 +2865,20 @@ function mountEntryForms() {
}
}
+function fillEntryDefaults(sectionId) {
+ const car = selectedCar();
+ const odometer = car?.current_odometer || "";
+ const sections = sectionId ? [document.querySelector(`#${sectionId}`)] : [...document.querySelectorAll(".drawer-section")];
+ sections.filter(Boolean).forEach((section) => {
+ section.querySelectorAll('input[name="entry_date"]').forEach((input) => {
+ if (!input.value) input.value = today();
+ });
+ section.querySelectorAll('input[name="odometer"]').forEach((input) => {
+ if (!input.value && odometer) input.value = odometer;
+ });
+ });
+}
+
async function openDrawerSection(sectionId, options = {}) {
if (!canOpenDrawerSection(sectionId)) {
toast("Этот раздел недоступен для вашей роли", "error");
@@ -2817,6 +2894,7 @@ async function openDrawerSection(sectionId, options = {}) {
button.classList.toggle("active", button.dataset.menuSection === sectionId);
});
mountEntryForms();
+ fillEntryDefaults(sectionId);
if (sectionId === "carProfileSection") fillCarProfileForm();
if (sectionId === "settingsSection") {
document.querySelector("#localeSelect").value = state.user?.locale || "ru";
@@ -2891,7 +2969,18 @@ document.querySelector("#addCarQuickBtn").addEventListener("click", () => {
});
document.querySelector("#addRecordPrimaryBtn").addEventListener("click", () => {
- openDrawerSection("expensesSection");
+ openDrawerSection("quickAddSection");
+});
+
+document.querySelectorAll("[data-quick-entry]").forEach((button) => {
+ button.addEventListener("click", async () => {
+ haptic();
+ if (button.dataset.quickEntry === "scan") {
+ openScanModal();
+ return;
+ }
+ await openDrawerSection(button.dataset.quickEntry);
+ });
});
document.querySelectorAll("[data-menu-section]").forEach((button) => {
@@ -3043,6 +3132,8 @@ initPwa();
Promise.all([loadAuthConfig()])
.then(() => Promise.all([ensureUser(), loadCatalog()]))
.then(() => {
+ installLocaleSwitch();
+ observeTranslations();
document.querySelector("#localeSelect").value = state.user?.locale || "ru";
document.querySelector("#currencySelect").value = state.user?.currency || "RUB";
document.querySelector("#expenseForm").currency.value = state.user?.currency || "RUB";
diff --git a/web/static/book_sto.js b/web/static/book_sto.js
index d121e35..df8159a 100644
--- a/web/static/book_sto.js
+++ b/web/static/book_sto.js
@@ -113,6 +113,16 @@ document.querySelector("#filterForm").addEventListener("submit", async (event) =
});
});
+document.querySelectorAll("[data-booking-service]").forEach((button) => {
+ button.addEventListener("click", async () => {
+ document.querySelector("#serviceTypeSelect").value = button.dataset.bookingService;
+ document.querySelectorAll("[data-booking-service]").forEach((item) => {
+ item.classList.toggle("active", item === button);
+ });
+ await loadSlots().catch((error) => page.toast(error.message || "Не удалось обновить окна", "error"));
+ });
+});
+
document.querySelector("#bookingForm").addEventListener("submit", async (event) => {
event.preventDefault();
const center = selectedCenter();
diff --git a/web/static/page_common.js b/web/static/page_common.js
index 49000b3..16a2d6f 100644
--- a/web/static/page_common.js
+++ b/web/static/page_common.js
@@ -2,6 +2,238 @@ const tg = window.Telegram?.WebApp;
tg?.ready();
tg?.expand();
+const CarPassI18n = (() => {
+ const textNodes = new WeakMap();
+ const attrOriginals = new WeakMap();
+ let observer = null;
+ let timer = null;
+ const dictionaries = {
+ en: {
+ "Гараж": "Garage",
+ "Автомобили": "Vehicles",
+ "Автомобиль": "Vehicle",
+ "Авто": "Vehicles",
+ "Заправка": "Fuel",
+ "Сервис": "Service",
+ "Расход": "Expense",
+ "Расходы": "Expenses",
+ "ТО и ремонт": "Maintenance and repair",
+ "Дата": "Date",
+ "Одометр, км": "Odometer, km",
+ "Одометр": "Odometer",
+ "Литры": "Liters",
+ "Цена за литр": "Price per liter",
+ "АЗС": "Fuel station",
+ "Полный бак": "Full tank",
+ "Стоимость": "Cost",
+ "Валюта": "Currency",
+ "Категория": "Category",
+ "Название": "Title",
+ "Комментарий": "Comment",
+ "Сохранить": "Save",
+ "Сохранить запись": "Save entry",
+ "Сохранить расход": "Save expense",
+ "Сохранить заправку": "Save fuel entry",
+ "Сохранить настройки": "Save settings",
+ "Создать запись": "Create booking",
+ "Запись в СТО": "Book service",
+ "СТО": "Service centers",
+ "Сервисы": "Services",
+ "Каталог": "Catalog",
+ "Заявка": "Request",
+ "Выберите сервис": "Choose service",
+ "Город": "City",
+ "Специализация": "Specialization",
+ "Найти": "Search",
+ "Что нужно сделать": "What needs to be done",
+ "Услуга": "Service",
+ "Длительность": "Duration",
+ "Свободное окно": "Available slot",
+ "Окно записи": "Booking slot",
+ "Отправить заявку": "Send request",
+ "Проверить карточку авто": "Check vehicle card",
+ "Меню": "Menu",
+ "Проверить вход": "Check login",
+ "Открыть в Telegram": "Open in Telegram",
+ "Обновить": "Refresh",
+ "Настройки": "Settings",
+ "Язык": "Language",
+ "Панель СТО": "Service workplace",
+ "Записи клиентов": "Client bookings",
+ "Заказ-наряды": "Work orders",
+ "Сотрудники": "Staff",
+ "Пригласить": "Invite",
+ "Заказ-наряд": "Work order",
+ "Работы": "Labor",
+ "Запчасти и жидкости": "Parts and fluids",
+ "Проверьте смету": "Review estimate",
+ "Согласовать": "Approve",
+ "Отклонить": "Reject",
+ "Админ-панель": "Admin panel",
+ "Операционный обзор": "Operational overview",
+ "Последние события": "Latest events",
+ "Быстрые переходы": "Quick links",
+ "Заявки СТО": "Service applications",
+ "Записи": "Bookings",
+ "Фильтр": "Filter",
+ "Фильтры": "Filters",
+ "Запросить": "Query",
+ "Показать": "Show",
+ "Импорт и экспорт": "Import and export",
+ "Скачать JSON": "Download JSON",
+ "Проверить файл": "Preview file",
+ "Импортировать": "Import",
+ "Паспорт автомобиля": "Vehicle passport",
+ "Выберите автомобиль": "Choose vehicle",
+ "Параметры авто": "Vehicle settings",
+ "Сохранить паспорт": "Save passport",
+ "Удалить автомобиль": "Delete vehicle",
+ "Готов к работе": "Ready",
+ "Сохраняю...": "Saving...",
+ "Сохранено": "Saved",
+ "Ошибка": "Error",
+ "Нет данных": "No data",
+ "Нет доступа": "No access",
+ },
+ ko: {
+ "Гараж": "차고",
+ "Автомобили": "차량",
+ "Автомобиль": "차량",
+ "Авто": "차량",
+ "Заправка": "주유",
+ "Сервис": "정비",
+ "Расход": "지출",
+ "Расходы": "지출",
+ "ТО и ремонт": "정비 및 수리",
+ "Дата": "날짜",
+ "Одометр, км": "주행거리, km",
+ "Одометр": "주행거리",
+ "Литры": "리터",
+ "Цена за литр": "리터당 가격",
+ "АЗС": "주유소",
+ "Полный бак": "가득 주유",
+ "Стоимость": "비용",
+ "Валюта": "통화",
+ "Категория": "카테고리",
+ "Название": "제목",
+ "Комментарий": "메모",
+ "Сохранить": "저장",
+ "Сохранить запись": "기록 저장",
+ "Сохранить расход": "지출 저장",
+ "Сохранить заправку": "주유 저장",
+ "Сохранить настройки": "설정 저장",
+ "Создать запись": "예약 생성",
+ "Запись в СТО": "정비소 예약",
+ "СТО": "정비소",
+ "Сервисы": "서비스",
+ "Каталог": "목록",
+ "Заявка": "요청",
+ "Выберите сервис": "정비소 선택",
+ "Город": "도시",
+ "Специализация": "전문 분야",
+ "Найти": "검색",
+ "Что нужно сделать": "필요한 작업",
+ "Услуга": "서비스",
+ "Длительность": "소요 시간",
+ "Свободное окно": "예약 가능 시간",
+ "Окно записи": "예약 시간",
+ "Отправить заявку": "요청 보내기",
+ "Проверить карточку авто": "차량 카드 확인",
+ "Меню": "메뉴",
+ "Проверить вход": "로그인 확인",
+ "Открыть в Telegram": "텔레그램에서 열기",
+ "Обновить": "새로고침",
+ "Настройки": "설정",
+ "Язык": "언어",
+ "Панель СТО": "정비소 작업실",
+ "Записи клиентов": "고객 예약",
+ "Заказ-наряды": "작업지시서",
+ "Сотрудники": "직원",
+ "Пригласить": "초대",
+ "Заказ-наряд": "작업지시서",
+ "Работы": "공임",
+ "Запчасти и жидкости": "부품 및 오일",
+ "Проверьте смету": "견적 확인",
+ "Согласовать": "승인",
+ "Отклонить": "거절",
+ "Админ-панель": "관리자 패널",
+ "Операционный обзор": "운영 요약",
+ "Последние события": "최근 이벤트",
+ "Быстрые переходы": "빠른 이동",
+ "Заявки СТО": "정비소 신청",
+ "Записи": "예약",
+ "Фильтр": "필터",
+ "Фильтры": "필터",
+ "Запросить": "조회",
+ "Показать": "보기",
+ "Импорт и экспорт": "가져오기/내보내기",
+ "Скачать JSON": "JSON 다운로드",
+ "Проверить файл": "파일 확인",
+ "Импортировать": "가져오기",
+ "Паспорт автомобиля": "차량 패스포트",
+ "Выберите автомобиль": "차량 선택",
+ "Параметры авто": "차량 설정",
+ "Сохранить паспорт": "패스포트 저장",
+ "Удалить автомобиль": "차량 삭제",
+ "Готов к работе": "준비 완료",
+ "Сохраняю...": "저장 중...",
+ "Сохранено": "저장됨",
+ "Ошибка": "오류",
+ "Нет данных": "데이터 없음",
+ "Нет доступа": "접근 불가",
+ },
+ };
+
+ function t(text, locale = currentLocale()) {
+ return dictionaries[locale]?.[text] || text;
+ }
+
+ function currentLocale() {
+ return window.CarPassPage?.state?.user?.locale || localStorage.getItem("carpassLocale") || "ru";
+ }
+
+ function apply(root = document.body, locale = currentLocale()) {
+ document.documentElement.lang = locale;
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
+ acceptNode(node) {
+ const parent = node.parentElement;
+ if (!parent || ["SCRIPT", "STYLE", "TEXTAREA", "INPUT", "SELECT"].includes(parent.tagName)) return NodeFilter.FILTER_REJECT;
+ return node.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
+ },
+ });
+ while (walker.nextNode()) {
+ const node = walker.currentNode;
+ if (!textNodes.has(node)) textNodes.set(node, node.nodeValue.trim());
+ const original = textNodes.get(node);
+ node.nodeValue = node.nodeValue.replace(node.nodeValue.trim(), t(original, locale));
+ }
+ root.querySelectorAll?.("[placeholder], [aria-label], [title]").forEach((element) => {
+ ["placeholder", "aria-label", "title"].forEach((attr) => {
+ const value = element.getAttribute(attr);
+ if (!value) return;
+ let originals = attrOriginals.get(element);
+ if (!originals) {
+ originals = {};
+ attrOriginals.set(element, originals);
+ }
+ originals[attr] ||= value;
+ element.setAttribute(attr, t(originals[attr], locale));
+ });
+ });
+ }
+
+ function observe(root = document.body) {
+ if (observer || !root) return;
+ observer = new MutationObserver(() => {
+ window.clearTimeout(timer);
+ timer = window.setTimeout(() => apply(root), 40);
+ });
+ observer.observe(root, { childList: true, subtree: true });
+ }
+
+ return { apply, t, currentLocale, observe };
+})();
+
const CarPassPage = (() => {
const state = { user: null, authConfig: null };
@@ -51,13 +283,14 @@ const CarPassPage = (() => {
}
document.body.classList.remove("auth-required");
document.querySelector("#authOverlay")?.classList.add("hidden");
+ localStorage.setItem("carpassLocale", state.user.locale || "ru");
return state.user;
}
function toast(message, tone = "success") {
const node = document.querySelector("#toast");
if (!node) return;
- node.textContent = message;
+ node.textContent = t(message);
node.className = `toast ${tone}`;
window.clearTimeout(toast.timer);
toast.timer = window.setTimeout(() => node.classList.add("hidden"), 2600);
@@ -69,7 +302,7 @@ const CarPassPage = (() => {
button.dataset.label = button.textContent;
button.disabled = true;
button.classList.add("is-busy");
- button.innerHTML = `
${label}`;
+ button.innerHTML = `
${t(label)}`;
} else {
button.disabled = false;
button.classList.remove("is-busy");
@@ -115,18 +348,73 @@ const CarPassPage = (() => {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value).slice(0, 16).replace("T", " ");
- return date.toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
+ const locale = { ru: "ru-RU", en: "en-US", ko: "ko-KR" }[state.user?.locale] || "ru-RU";
+ return date.toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
}
function today() {
return new Date().toISOString().slice(0, 10);
}
+ function t(text) {
+ return CarPassI18n.t(text, state.user?.locale || "ru");
+ }
+
+ function applyTranslations(root = document.body) {
+ CarPassI18n.apply(root, state.user?.locale || "ru");
+ }
+
+ async function updateLocale(locale) {
+ if (!state.user || state.user.locale === locale) return;
+ state.user = await api(`/users/${state.user.id}/preferences`, {
+ method: "PATCH",
+ body: JSON.stringify({ locale }),
+ });
+ localStorage.setItem("carpassLocale", locale);
+ applyTranslations();
+ }
+
+ function installLocaleSwitch() {
+ const topbar = document.querySelector(".topbar");
+ if (!topbar || document.querySelector("#globalLocaleSelect")) return;
+ let host = topbar.querySelector(".top-actions");
+ if (!host) {
+ host = document.createElement("div");
+ host.className = "top-actions";
+ topbar.appendChild(host);
+ }
+ const select = document.createElement("select");
+ select.id = "globalLocaleSelect";
+ select.className = "locale-switch";
+ select.setAttribute("aria-label", "Язык");
+ select.innerHTML = `
+
+
+
+ `;
+ select.value = state.user?.locale || "ru";
+ select.addEventListener("change", async () => {
+ try {
+ await updateLocale(select.value);
+ toast("Сохранено");
+ } catch (error) {
+ toast(error.message || "Ошибка", "error");
+ }
+ });
+ const primarySelect = host.querySelector("select:not(.locale-switch)");
+ if (primarySelect) primarySelect.insertAdjacentElement("afterend", select);
+ else host.prepend(select);
+ }
+
async function boot(init) {
try {
await loadAuthConfig();
await ensureUser();
+ installLocaleSwitch();
+ applyTranslations();
+ CarPassI18n.observe();
await init();
+ applyTranslations();
} catch (error) {
if (error.message === "Требуется вход через Telegram") return;
console.error(error);
@@ -148,5 +436,9 @@ const CarPassPage = (() => {
csvList,
formatDateTime,
today,
+ t,
+ applyTranslations,
+ installLocaleSwitch,
+ updateLocale,
};
})();
diff --git a/web/static/styles.css b/web/static/styles.css
index f55a8dd..fe55396 100644
--- a/web/static/styles.css
+++ b/web/static/styles.css
@@ -2579,7 +2579,7 @@ select {
.sto-page .top-actions {
display: grid;
- grid-template-columns: minmax(0, 1fr) 44px;
+ grid-template-columns: minmax(0, 1fr) 58px 38px;
}
.staff-form button {
@@ -2597,3 +2597,265 @@ select {
padding: 20px;
}
}
+
+/* Compact UX pass */
+.locale-switch {
+ width: 66px;
+ min-height: 34px;
+ padding: 0 8px;
+ border-radius: 8px;
+ background: #fff;
+ color: var(--text);
+ font-size: 12px;
+ font-weight: 850;
+}
+
+.shell {
+ padding-top: 10px;
+}
+
+.topbar {
+ padding-block: 9px;
+}
+
+.topbar h1 {
+ font-size: clamp(23px, 4vw, 34px);
+}
+
+.eyebrow {
+ font-size: 11px;
+ letter-spacing: 0.03em;
+}
+
+button,
+.ghost-btn {
+ min-height: 38px;
+ padding-inline: 12px;
+}
+
+.icon-btn {
+ width: 38px;
+ min-height: 38px;
+}
+
+input,
+select,
+textarea {
+ min-height: 38px;
+ padding-inline: 10px;
+}
+
+label {
+ gap: 4px;
+ font-size: 12px;
+}
+
+.summary-card {
+ min-height: 92px;
+ padding: 13px;
+}
+
+.summary-card strong {
+ font-size: clamp(19px, 3.2vw, 27px);
+}
+
+.primary-add-btn {
+ min-height: 46px;
+ margin-bottom: 12px;
+}
+
+.panel,
+.workspace,
+.chart-card {
+ padding: 13px;
+}
+
+.passport-panel {
+ gap: 8px;
+ padding: 11px;
+}
+
+.passport-head h2,
+h2 {
+ font-size: 17px;
+}
+
+.stats,
+.hero-grid,
+.layout,
+.charts,
+.flow-layout,
+.sto-grid {
+ gap: 10px;
+}
+
+.stat,
+.stat-card {
+ min-height: 76px;
+ padding: 10px;
+}
+
+.stat strong,
+.stat-card strong {
+ font-size: clamp(18px, 2.1vw, 23px);
+}
+
+.grid-form,
+.entry-form,
+.flow-form {
+ gap: 9px;
+}
+
+.entry-form {
+ padding: 12px;
+}
+
+.drawer-panel {
+ width: min(560px, 100%);
+ gap: 9px;
+ padding: 14px;
+}
+
+.drawer-menu {
+ grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
+ gap: 8px;
+ max-height: min(28vh, 230px);
+}
+
+.menu-group {
+ padding: 8px;
+}
+
+.menu-row {
+ min-height: 40px;
+ padding-inline: 11px;
+}
+
+.quick-entry-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 9px;
+}
+
+.quick-entry-grid button {
+ display: grid;
+ gap: 4px;
+ min-height: 82px;
+ align-content: center;
+ background: #fff;
+ color: var(--text);
+ border: 1px solid var(--line);
+ box-shadow: 0 8px 20px rgba(27, 38, 34, 0.06);
+ text-align: left;
+}
+
+.quick-entry-grid button span {
+ font-size: 15px;
+}
+
+.quick-entry-grid button small {
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.25;
+}
+
+.advanced-fields {
+ display: grid;
+ grid-column: 1 / -1;
+ gap: 8px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: #fbfdfc;
+}
+
+.advanced-fields summary {
+ min-height: 38px;
+ padding: 10px 12px;
+ color: var(--text);
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 850;
+ list-style: none;
+}
+
+.advanced-fields summary::-webkit-details-marker {
+ display: none;
+}
+
+.advanced-fields summary::after {
+ content: "+";
+ float: right;
+ color: var(--muted);
+}
+
+.advanced-fields[open] summary::after {
+ content: "-";
+}
+
+.compact-inner-form {
+ margin: 0;
+ padding: 0 10px 10px;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.compact-filter {
+ width: 100%;
+ padding: 0;
+}
+
+.compact-filter label,
+.compact-filter button {
+ margin: 0 10px 10px;
+}
+
+.quick-service-pills {
+ margin-top: -4px;
+}
+
+.quick-service-pills button.active {
+ border-color: rgba(18, 115, 95, 0.45);
+ background: #e7f4ef;
+ color: #0e604f;
+}
+
+.flow-hero {
+ padding: 14px;
+}
+
+.flow-hero h2 {
+ font-size: clamp(20px, 2.4vw, 28px);
+}
+
+.form-block {
+ padding: 10px;
+}
+
+.admin-table th,
+.admin-table td {
+ padding: 7px 9px;
+}
+
+@media (max-width: 640px) {
+ .top-actions {
+ gap: 6px;
+ }
+
+ .locale-switch {
+ width: 58px;
+ }
+
+ .quick-entry-grid,
+ .compact-inner-form {
+ grid-template-columns: 1fr;
+ }
+
+ .drawer-menu {
+ grid-template-columns: 1fr;
+ max-height: min(32vh, 260px);
+ }
+
+ .summary-card,
+ .stat {
+ min-height: 74px;
+ }
+}