Анализ сайта prestizh-les-crm.ru
Основное Готовность: 100%
Домен
prestizh-les-crm.ru
Состояние доменного имени
?
Проверяем корректность доменного имени и наличие технических проблем на уровне домена.
Длина домена велика. Но если вы продвигаете запрос, входящий в название домена, то это хорошо.
Домен второго уровня идеален для продвижения.
Регистратор домена - TIMEWEB-RU
Дата регистрации домена 31.07.2026 (более 2 недель назад)
Ответ сервера
200 Успешный ответ
HTTP-код ответа и цепочка редиректов
?
Код 200 — страница доступна. Коды 3xx — редиректы (цепочки замедляют загрузку и размывают ссылочный вес). Коды 4xx/5xx — ошибки, поисковик не сможет проиндексировать страницу.
Сервер настроен корректно.
Цепочка редиректов:
http://prestizh-les-crm.ru
301 MovedPermanently
https://prestizh-les-crm.ru/
200 OK
Безопасность
Сайт безопасен
Использование HTTPS и SSL-сертификат
?
HTTPS — обязательный стандарт. Google и Яндекс отдают предпочтение защищённым сайтам. Отсутствие SSL или просроченный сертификат ведут к предупреждениям в браузере и снижению позиций.
Не настроен HSTS (Strict-Transport-Security) — рекомендуется включить.
На сайте работает защищенный протокол ssl и сайт открывается по https.
Ssl-сертификат действителен до 10.11.2026 8:58:14.
HTTP автоматически перенаправляется на HTTPS.
Поздравляем! Сайт не содержится в реестре РКН.
Кодировка
utf-8
Кодировка символов страницы
?
Стандарт — UTF-8. Неправильная кодировка вызывает нечитаемые символы и мешает поисковику корректно распознать текст страницы.
Указана кодировка на странице utf-8.
Язык
ru
Атрибут lang в HTML-теге
?
Атрибут lang (<html lang="ru">) сообщает поисковикам и браузерам, на каком языке написана страница. Помогает при ранжировании в региональном поиске.
Указан язык документа ru.
Скорость загрузки
~0,29сек
Время отклика сервера (TTFB)
?
Time To First Byte — время до получения первого байта от сервера. Норма до 200 мс. Медленный отклик ухудшает пользовательский опыт и ранжирование: Яндекс и Google учитывают скорость страниц.
Скорость загрузки сайта 0,29сек оптимальна.
Объем документа
290Кб
Размер HTML-кода страницы
?
Слишком большой HTML замедляет парсинг браузером и сканирование поисковым роботом. Рекомендуется не более 200 Кб.
Объем html-документа 290Кб оптимален.
Структура html-документа корректна.
Ресурсы
Ресурсы: 0
Внешние ресурсы страницы (CSS, JS, изображения)
?
Количество и тип подключённых ресурсов влияют на скорость загрузки. Большое число запросов увеличивает время рендеринга страницы.
Ресурсы не найдены. Неужели ваш сайт на чистом html?
Серверные заголовки
Кол-во: 5
HTTP-заголовки ответа сервера
?
Заголовки сервера передают браузеру и поисковику служебную информацию: кеширование, безопасность (CSP, HSTS), сжатие (gzip). Правильная настройка ускоряет загрузку и повышает защищённость.
Найдены серверные заголовки 5шт. Подробнее про серверные заголовки.
Показать полный список серверных заголовков
| Ключ | Значение |
|---|---|
| Server | nginx/1.28.3 |
| Date | Wed, 19 Aug 2026 13:32:59 GMT |
| Connection | keep-alive |
| ETag | "6a7db683-4e7d1" |
| Accept-Ranges | bytes |
CMS
Не определена
Система управления сайтом (движок)
?
CMS — это движок, на котором работает сайт (WordPress, 1C-Bitrix, Tilda и др.). Знание CMS помогает понять возможности SEO-оптимизации и подобрать подходящие инструменты. «Не определена» — вероятно, самописный сайт или нестандартная сборка.
CMS не определена. Вероятно, сайт самописный либо движок надёжно скрыт. Это не ошибка.
Веб-сервер
Программное обеспечение сервера
?
Веб-сервер — это ПО, которое отдаёт страницы посетителям (nginx, Apache, IIS, LiteSpeed и др.). Определяется по серверным заголовкам ответа (Server, X-Powered-By и т.п.). «Не определён» — сервер намеренно скрывает эти заголовки, это нормальная практика безопасности.
Сайт работает на веб-сервере nginx.
Мета-теги Готовность: 32%
Title
Лесопилка · Расчёт заказа
Заголовок страницы в браузере и поисковой выдаче
?
Title — главный SEO-заголовок страницы. Влияет на CTR в поиске и ранжирование. Оптимальная длина: 50–70 символов. Ключевые слова — ближе к началу.
Необходимо увеличить число символов в title (текущее значение: 25, оптимально: от 40 до 45)
Дублей словоформ в title не найдено.
Description
Описание страницы в поисковой выдаче (сниппет)
?
Meta Description — текст под заголовком в выдаче. Напрямую на позиции не влияет, но влияет на CTR. Оптимальная длина: 120–160 символов.
Установите мета-тег description!
Keywords
Список ключевых слов страницы (устаревший тег)
?
Meta Keywords не учитывается Яндексом и Google для ранжирования с 2009–2012 годов. Заполнение не обязательно, но не вредит. Конкурент может использовать содержимое для анализа.
Установите мета-тег keywords!
Канонический Url
Указывает поисковику основную версию страницы
?
Canonical (rel=canonical) предотвращает проблему дублей страниц. Должен точно совпадать с URL проверяемой страницы. Неправильный canonical может передать ссылочный вес на другую страницу.
Рекомендуем прописать канонический Url.
Robots
Ошибок нет
Директивы для поисковых роботов на уровне страницы
?
Meta Robots управляет индексацией конкретной страницы: index/noindex — индексировать ли, follow/nofollow — следовать ли по ссылкам. Noindex полностью исключает страницу из поиска.
Meta-тег robots не указан. Страница свободна для индексации.
Адаптивность
width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover
Настройка масштабирования на мобильных устройствах
?
Тег viewport (<meta name="viewport">) сообщает браузеру, как масштабировать страницу на мобильных. Стандарт: width=device-width, initial-scale=1. Отсутствие — признак отсутствия мобильной версии.
Meta-тег viewport со значением-константой width=device-width задаёт ширину страницы в соответствии с размером экрана.
Meta-тег viewport со значением initial-scale=1.0 определяет масштаб 1:1, т.е. «не масштабировать».
Meta-тег viewport со значением user-scalable определяет доступность масштабирования страницы пользователем.
Meta-тег viewport со значением maximum-scale определяет максимальный масштаб области просмотра.
Разметка OpenGraph
Не найдено
Мета-теги для красивых превью в соцсетях
?
OpenGraph (og:title, og:description, og:image) управляет тем, как страница выглядит при репосте в социальных сетях и мессенджерах. Отсутствие OG-тегов — невзрачный превью при шеринге.
Разметка OpenGraph не задана. Страница не оптимизирована под социальные сети. Мета-теги с разметкой Og помогают социальным роботам лучше структурировать Ваш сайт.
Все мета-теги
Кол-во: 6
Полный список мета-тегов страницы
?
Таблица всех meta-тегов, включая нестандартные. Позволяет найти опечатки, дубли и лишние теги.
Найдены мета-теги 6шт. Мета-теги не видимы для человека и предназначены для обмена информацией между веб-страницей и поисковыми системами, браузерами и другими веб-службами. С ними роботы 🤖 и устройства ведут себя более ожидаемо.
Показать полный список мета-тегов
| Тип | Название | Значение |
|---|---|---|
| name | viewport | width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover |
| name | apple-mobile-web-app-capable | yes |
| name | apple-mobile-web-app-status-bar-style | black-translucent |
| name | apple-mobile-web-app-title | Лесопилка |
| name | mobile-web-app-capable | yes |
| name | theme-color | #F5F5F6 |
Оптимизация Готовность: 93%
Структура
Ошибок нет
Семантические HTML-элементы страницы
?
Проверяет наличие основных структурных элементов: nav, header, footer, main. Корректная семантическая структура помогает поисковику понять архитектуру страницы.
Структура документа корректна (теги <html> и <body> присутствуют в единственном экземпляре).
Контент
Есть ошибки
Объём и качество текстового содержимого
?
Анализирует объём полезного текста на странице. Слишком мало — страница может считаться малополезной. Слишком много — ухудшается читаемость и восприятие.
Абзацев с текстом 1 слишком мало. Добавьте больше абзацев с текстом (тег <p>)!
Кол-во слов 428 не очень много. Добавьте побольше текста (хотя бы 800 слов)!
Слова из title 4 встречаются в тексте достаточно.
Среднее число слов в абзаце 45 достаточно.
Кол-во знаков контента 2767 на странице оптимально.
Заголовки
Ошибок нет
Иерархия заголовков H1–H6
?
H1 должен быть один и содержать ключевой запрос. H2–H6 описывают подразделы. Пропуск уровней (H1 → H3) и несколько H1 — типичные ошибки, снижающие понятность страницы для поисковика.
На странице присутствуют заголовки <h1> 1. Это прекрасно.
На странице присутствуют заголовки <h2> 1. Это хорошо.
Тошнота
2,24
Насколько одно слово доминирует в тексте
?
Классическая тошнота = √(частота самого повторяющегося слова). Норма до 7–8: текст воспринимается естественно. Выше — поисковик может счесть страницу переспамленной.
Тошнота страницы в пределах нормы 3.
Академич. тошнота
5,37%
Насколько текст перенасыщен ключевыми словами
?
Академическая тошнота = (частота слова / общее количество слов) × 100%. Показывает долю конкретного слова в тексте. Норма 5–15%.
Академическая тошнота страницы в пределах нормы.
Семантическое ядро
20
Наиболее часто встречающиеся слова на странице
?
Топ слов по частоте использования. Показывает, какие слова доминируют в тексте с точки зрения поисковика.
Контент страницы содержит осмысленный текст и слова.
Показать список слов
| Слово | Кол-во | Частота |
|---|---|---|
| расчёт | 5 | 1,17% |
| доставки | 5 | 1,17% |
| печать | 5 | 1,17% |
| заказа | 4 | 0,93% |
| маршрут | 4 | 0,93% |
| очистить | 4 | 0,93% |
| лесопилка | 3 | 0,70% |
| сохранить | 3 | 0,70% |
| доставка | 3 | 0,70% |
| телефон | 3 | 0,70% |
| яндекс | 3 | 0,70% |
| движения | 3 | 0,70% |
| считается | 3 | 0,70% |
| заказы | 2 | 0,47% |
| настройки | 2 | 0,47% |
| естеств | 2 | 0,47% |
| влажность | 2 | 0,47% |
| строганный | 2 | 0,47% |
| сбросить | 2 | 0,47% |
| правки | 2 | 0,47% |
Индексация Готовность: 20%
Индексирование
Есть ошибки
Разрешено ли индексирование страницы
?
Проверяет, не закрыта ли страница от индексации через robots.txt, meta robots или X-Robots-Tag. Страница, закрытая от индексации, не появится в поисковой выдаче.
Анкоров на странице не обнаружено. Велика вероятность, что сайт - одностраничник, такой сайт продвинуть крайне затруднительно!
Robots.txt
Найден корректный robots.txt
Файл управления сканированием сайта роботами
?
Robots.txt указывает поисковым роботам, какие страницы сканировать, а какие — нет. Ошибки в файле могут случайно закрыть важные разделы от индексации.
Robots.txt настроен корректно. Размер файла: 297189 байт. Загружен за: 0сек.
Проверяемая страница не запрещена в robots.txt.
Robots.txt доступен по постоянному адресу
Цепочка редиректов для файла robots.txt:
http://prestizh-les-crm.ru/robots.txt
301 MovedPermanently
https://prestizh-les-crm.ru/robots.txt
200 OK
Показать содержимое robots.txt
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>Лесопилка · Расчёт заказа</title>
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="data:image/png;base64,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">
<link rel="icon" type="image/png" sizes="192x192" href="data:image/png;base64,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">
<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACHklEQVR42t2WMUhWURTHf+fcq5/ypVJmJGUZ4mRWQ4pBkE1tjUk0JERIDYXQUlBD0CKRuzVIFGVtQkoNgSS0NNQQBLWUEAUlSETpe++eBj9MRd5nve81dLZ7Lu//P//z7j33L6yM+FAL2cJPzSxfSgWh16TRPNCXA2oe6Ms5lJxDcip/KXJXsD4CEUD29ujQbenuzYNAAek5LN29cvAIgLrKtsiAMPmA9+9scmwpU9F/IIIIm5rwVatuaGYCEUQx08Fr1DXIuauolvgqRpDE0j8o+w4QRdLeoWcvk8TrVOCutDakFqCI6snz2j9IEuOrSGLZvV9qi/byOYBZBgUihMCGerbtZG4WdYSA83ybo6mZhkZCKNuoVAIznGdu1u6PUF3ArERZXbCHN/nyCe+zKVAliWlu0eF71BaXbhyFGr1+lx1txDGqmRRIe4cbmaB+IyGUsFQJgWKdG3kke7qzKQBa2mz6MQvzKypVJYlt+gnbd2U7pt7b03H7+IHqAiH5nQ8JzvP1s02M4VwGgiiSoyf01AWiCDNCAAgBM6IF6RuQvgHi+O/fAx24KMfPlBlS43fCjUtpXUj7ev6njQ5TqKFYhyit7dLZZW9e8fY1IeHHdxbmy47VNIIwOrxC7LHT0tnFi2fh1tAf2IvUOeIRMHCOJJHNWwEat+D8Yqa0mzqX/oM3eZXTq2z4qZl/4ipyErEIq2ta4gqaX8nbvv8Cl5m8MSF1XWAAAAAASUVORK5CYII=">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Лесопилка">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#F5F5F6">
<script>
(function(){
var pref;
try{ pref = localStorage.getItem("theme") || "auto"; }catch(e){ pref = "auto"; }
var mq = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
function apply(){
var dark = (pref === "dark") || (pref === "auto" && mq && mq.matches);
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
var m = document.querySelector('meta[name="theme-color"]');
if(m) m.setAttribute("content", dark ? "#000000" : "#F2F2F7");
}
window.__setTheme = function(v){
pref = v;
try{ localStorage.setItem("theme", v); }catch(e){}
apply();
};
window.__getTheme = function(){ return pref; };
if(mq){
try{ mq.addEventListener("change", function(){ if(pref === "auto") apply(); }); }
catch(e){ try{ mq.addListener(function(){ if(pref === "auto") apply(); }); }catch(e2){} }
}
apply();
})();
</script>
<style>
:root{
--ink:#1C1A17;
--ink-70:#5A544C;
--paper:#FBF9F4;
--grain:#E6DFD1;
--grain-2:#D3C9B6;
--mark:#F2C012;
--steel:#3A5C68;
--alert:#9E3B21;
--ok:#3D6B3A;
--r:2px;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
background:var(--paper);
color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;
font-size:15px;line-height:1.45;
-webkit-text-size-adjust:100%;
padding-bottom:96px;
}
.num{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-variant-numeric:tabular-nums}
h1,h2,h3,.disp{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:500;text-transform:uppercase;letter-spacing:.06em;margin:0}
.wrap{max-width:none;width:100%;margin:0;padding:0 20px}
/* --- шапка: чёрная планка + лента рулетки --- */
header{background:var(--ink);color:var(--paper)}
.head-in{max-width:none;margin:0;padding:16px 20px 12px;display:flex;align-items:baseline;justify-content:space-between;gap:12px}
header h1{font-size:20px;font-weight:600;letter-spacing:.1em}
header .sub{font-size:11px;letter-spacing:.14em;color:#A9A296;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase}
.tape{height:14px;background:var(--mark);position:relative;overflow:hidden}
.tape::before{content:"";position:absolute;inset:0;
background:repeating-linear-gradient(to right,var(--ink) 0 1px,transparent 1px 10px);
opacity:.55}
.tape::after{content:"";position:absolute;left:0;right:0;top:0;height:7px;
background:repeating-linear-gradient(to right,var(--ink) 0 1px,transparent 1px 50px);
opacity:.85}
section{margin-top:22px}
.eyebrow{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-70);margin-bottom:8px;display:flex;align-items:center;gap:10px}
.eyebrow::after{content:"";flex:1;height:1px;background:var(--grain-2)}
/* --- выбор прайса --- */
.tabs{display:grid;grid-template-columns:repeat(3,1fr);border:1.5px solid var(--ink);border-radius:var(--r);overflow:hidden}
.tabs button{appearance:none;border:0;background:transparent;padding:10px 6px;cursor:pointer;line-height:1.25;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.04em;font-size:11.5px;color:var(--ink)}
.tabs button+button{border-left:1.5px solid var(--ink)}
.tabs button[aria-selected="true"]{background:var(--ink);color:var(--paper)}
.search{width:100%;margin-top:10px;padding:11px 12px;border:1.5px solid var(--grain-2);border-radius:var(--r);
background:#fff;font:inherit;color:var(--ink)}
.search:focus{outline:2px solid var(--mark);outline-offset:1px;border-color:var(--ink)}
/* --- список позиций --- */
.group-label{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:12px;letter-spacing:.14em;text-transform:uppercase;
color:var(--ink-70);margin:16px 0 6px}
.rows{border-top:1px solid var(--grain-2)}
.row{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
padding:11px 6px;background:transparent;border:0;border-bottom:1px solid var(--grain);cursor:pointer;font:inherit;color:inherit}
.row:hover{background:#fff}
.row[aria-current="true"]{background:var(--mark);border-bottom-color:var(--ink)}
.row .body{flex:1 1 auto;min-width:0}
.row .sz{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:17px;letter-spacing:.01em;overflow-wrap:anywhere}
.row .sz i{font-style:normal;color:var(--ink-70);font-size:13px;margin-left:2px}
.row[aria-current="true"] .sz i{color:#5b4d15}
.row .meta{font-size:12.5px;color:var(--ink-70);display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:2px}
.row[aria-current="true"] .meta{color:#5b4d15}
.row .pc{flex:0 0 auto;text-align:right;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:15px;white-space:nowrap}
.row .pc small{display:block;font-weight:400;font-size:11px;color:var(--ink-70)}
.row[aria-current="true"] .pc small{color:#5b4d15}
.badge{padding:1px 7px 2px;border:1px solid currentColor;border-radius:999px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;
font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;white-space:nowrap}
.b1{color:var(--ok)}.b2{color:var(--steel)}.b3{color:#8A6A33}
.row[aria-current="true"] .badge{color:#5b4d15}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.chip{padding:7px 12px;border:1.5px solid var(--grain-2);background:transparent;border-radius:999px;cursor:pointer;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:12px;color:var(--ink-70);letter-spacing:.03em}
.chip:hover{border-color:var(--ink);color:var(--ink)}
.chip[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--paper)}
.flagdot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--alert);vertical-align:middle;margin-left:6px}
.empty{padding:18px 6px;color:var(--ink-70);font-size:14px}
/* --- панель расчёта --- */
.calc{border:1.5px solid var(--ink);border-radius:var(--r);background:#fff;margin-top:14px}
.calc-head{background:var(--ink);color:var(--paper);padding:12px 14px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
.calc-head .t{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:20px;letter-spacing:.01em}
.calc-head .s{margin-top:2px}
.calc-head .s{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;color:#A9A296;letter-spacing:.08em}
/* сечение доски в масштабе — фирменный элемент */
.section-view{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;padding:14px;border-bottom:1px solid var(--grain)}
.dims{flex:1 1 auto}
.dims .d{display:flex;align-items:baseline;gap:8px;padding:3px 0}
.dims .d span{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-70);min-width:74px}
.dims .d b{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:19px;font-weight:600;color:var(--ink)}
.dims .d b em{font-style:normal;font-size:12px;font-weight:400;color:var(--ink-70);margin-left:3px}
.dims .cap{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;color:var(--ink-70);margin-top:6px}
.scale-box{width:132px;height:132px;display:flex;align-items:flex-end;justify-content:center;flex:0 0 auto;overflow:hidden;padding:5px;
background:repeating-linear-gradient(to right,var(--grain) 0 1px,transparent 1px 11px),
repeating-linear-gradient(to bottom,var(--grain) 0 1px,transparent 1px 11px);
border:1px solid var(--grain-2)}
.board{max-width:100%;max-height:100%;background:linear-gradient(100deg,#E8C98D,#D9AE63 40%,#E8C98D);
border:1.5px solid #8A6A33;position:relative}
.board::after{content:"";position:absolute;inset:2px;
background:repeating-linear-gradient(to bottom,rgba(138,106,51,.22) 0 1px,transparent 1px 4px)}
.dims{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:12px;color:var(--ink-70);line-height:1.7}
.dims b{color:var(--ink);font-weight:600}
.mode{display:flex;padding:12px 14px 0;gap:0}
.mode button{flex:1;padding:9px;border:1.5px solid var(--grain-2);background:transparent;cursor:pointer;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;font-size:12px;letter-spacing:.07em;color:var(--ink-70)}
.mode button+button{border-left:0}
.mode button[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--paper)}
.qty-line{display:flex;gap:10px;padding:12px 14px;align-items:center}
.qty-line label{font-size:12px;color:var(--ink-70);white-space:nowrap}
input[type=number],input[type=text].mini{width:100%;padding:10px 12px;border:1.5px solid var(--grain-2);border-radius:var(--r);
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:17px;font-weight:600;background:#fff;color:var(--ink)}
input:focus{outline:2px solid var(--mark);outline-offset:1px;border-color:var(--ink)}
.stepper{display:flex;gap:6px}
.stepper button{width:38px;height:42px;border:1.5px solid var(--grain-2);background:#fff;font-size:20px;cursor:pointer;line-height:1;color:var(--ink)}
.stepper button:hover{border-color:var(--ink)}
.result{padding:4px 14px 14px}
.res-row{display:flex;justify-content:space-between;gap:12px;padding:7px 0;border-bottom:1px dotted var(--grain-2);font-size:14px}
.res-row span:first-child{color:var(--ink-70)}
.res-row span:last-child{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:500}
.total{display:flex;justify-content:space-between;align-items:baseline;margin-top:12px;padding:12px 14px;background:var(--mark)}
.total .lbl{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.1em;font-size:13px}
.total .val{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:24px}
.hint{font-size:12px;color:var(--ink-70);padding:0 14px 12px}
.warn{margin:0 14px 12px;padding:9px 11px;border-left:3px solid var(--alert);background:#FBF2EE;font-size:12.5px;color:#6C2C18}
.actions{display:flex;gap:8px;padding:14px}
.btn{flex:1;padding:12px;border:1.5px solid var(--ink);background:var(--ink);color:var(--paper);cursor:pointer;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.08em;font-size:13px;border-radius:var(--r)}
.btn.ghost{background:transparent;color:var(--ink)}
.btn:active{transform:translateY(1px)}
.btn:disabled{opacity:.4;cursor:not-allowed}
/* --- заказ --- */
.ocard{border:1.5px solid var(--ink);border-radius:var(--r);background:#fff}
.ohead{background:var(--ink);color:var(--paper);padding:11px 14px;display:flex;justify-content:space-between;align-items:baseline;gap:10px}
.ohead .t{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.09em;font-size:15px}
.ohead .s{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;color:#A9A296;letter-spacing:.08em}
.oi{display:flex;align-items:center;gap:10px;padding:11px 14px;border-bottom:1px solid var(--grain)}
.oi .no{flex:0 0 22px;align-self:flex-start;margin-top:3px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:12px;
color:var(--ink-70);border:1px solid var(--grain-2);height:22px;display:flex;align-items:center;justify-content:center}
.oi .body{flex:1 1 auto;min-width:0}
.oi .sz{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:16px;overflow-wrap:anywhere}
.oi .sz i{font-style:normal;font-size:12px;color:var(--ink-70);margin-left:2px}
.oi .meta{font-size:12.5px;color:var(--ink-70);margin-top:1px;overflow-wrap:anywhere}
.oi .sum{flex:0 0 auto;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:16px;white-space:nowrap;text-align:right}
.oi .sum small{display:block;font-weight:400;font-size:11px;color:var(--ink-70)}
.del{flex:0 0 30px;width:30px;height:30px;border:1px solid var(--grain-2);background:transparent;cursor:pointer;
color:var(--ink-70);border-radius:var(--r);font-size:15px;line-height:1;padding:0}
.del:hover{border-color:var(--alert);color:var(--alert)}
.svc{display:flex;align-items:center;gap:9px;padding:10px 14px;font-size:14px;border-bottom:1px solid var(--grain)}
.svc input{width:18px;height:18px;accent-color:var(--ink)}
.svc .p{margin-left:auto;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;color:var(--ink-70);font-size:13px}
.osum{padding:10px 14px 12px;background:#FCFAF5}
.osum .res-row{border-bottom:0;padding:5px 0}
.osum .res-row.big span:last-child{font-size:15px;font-weight:600}
.ototal{display:flex;justify-content:space-between;align-items:baseline;padding:14px;background:var(--mark);
border-top:1.5px solid var(--ink)}
.ototal .lbl{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.12em;font-size:15px}
.ototal .val{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:27px;line-height:1}
.ocard .actions{padding:14px;flex-wrap:wrap}
.ocard .actions .btn{flex:1 1 150px;min-width:0}
.ocard .actions .btn.wide{flex:1 1 100%}
@page{size:A4 portrait;margin:12mm}
@media print{
.no-print,.grand,header,.tape,.eyebrow,footer{display:none!important}
.ocard{zoom:var(--pscale,1)}
.oi{padding:9px 12px}
.cl{padding:10px 12px}
.osum{padding:8px 12px 10px}
.ototal{padding:11px 12px}
body{padding:0;background:#fff}
.ocard{border-color:#000}
.ohead{background:#fff;color:#000;border-bottom:1.5px solid #000}
.ohead .s{color:#444}
.ototal{background:#fff;border-top:2px solid #000}
}
.grand{position:fixed;left:0;right:0;bottom:0;background:var(--ink);color:var(--paper);z-index:20;
box-shadow:0 -2px 0 var(--mark)}
.grand-in{max-width:none;margin:0;padding:11px 20px;display:flex;align-items:center;gap:12px}
.grand .lbl{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.1em;font-size:12px;color:#A9A296}
.grand .val{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:20px;margin-left:auto}
.grand button{padding:10px 14px;border:1.5px solid var(--mark);background:var(--mark);color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.06em;font-size:12px;cursor:pointer;border-radius:var(--r)}
/* --- свой размер --- */
.custom-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
.custom-grid label{font-size:11px;color:var(--ink-70);font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.05em}
.custom-grid input{font-size:15px;padding:9px 8px}
details{border-top:1px solid var(--grain-2);padding-top:12px}
summary{cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.08em;font-size:13px;padding:4px 0}
footer{margin:28px 0 8px;font-size:11.5px;color:var(--ink-70);line-height:1.6}
.toast{position:fixed;left:50%;transform:translateX(-50%);bottom:76px;background:var(--ink);color:var(--paper);
padding:10px 16px;font-size:13px;border-radius:var(--r);opacity:0;pointer-events:none;transition:opacity .2s;z-index:30}
.toast.on{opacity:1}
.otitle{font-size:13px;color:#C9C2B6;margin-top:3px;line-height:1.3;overflow-wrap:anywhere}
.otitle:empty{display:none}
.odate{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:12.5px;color:var(--mark);white-space:nowrap}
.odate:empty{display:none}
@media print{
.ohead{display:flex;align-items:flex-start;gap:16px}
.ohead .t{font-size:11px;letter-spacing:.14em;color:#555}
.otitle{font-size:19px;font-weight:600;color:#000;margin-top:4px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif}
.ohead .s{display:block;margin-top:6px;color:#555}
.odate{font-size:17px;font-weight:700;color:#000;border:1.5px solid #000;padding:5px 10px;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;letter-spacing:.02em}
}
/* клиент и доставка */
.cgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.cgrid .f{display:flex;flex-direction:column;gap:4px}
.cgrid .f.full{grid-column:1/-1}
.cgrid label{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-70)}
.cgrid input{width:100%;padding:10px 12px;border:1.5px solid var(--grain-2);border-radius:var(--r);
background:#fff;color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:15px}
.cgrid input.mono{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600}
.cl{padding:12px 14px;border-bottom:1px solid var(--grain);background:#FCFAF5}
.cl .r{display:flex;gap:10px;align-items:baseline;padding:3px 0;font-size:14px}
.cl .r span{flex:0 0 92px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10.5px;letter-spacing:.09em;
text-transform:uppercase;color:var(--ink-70)}
.cl .r b{font-weight:500;overflow-wrap:anywhere}
.cl .priv{margin-top:6px;font-size:11.5px;color:var(--ink-70)}
.cl .r.hidden-in-copy b::after{content:" ●";color:var(--alert);font-size:9px;vertical-align:middle}
@media print{.cl{background:#fff}.cl .r.hidden-in-copy b::after{content:""}}
/* правки цен, наличие, длины, история */
.pdate{display:flex;align-items:center;gap:10px;margin-top:10px;font-size:12px;color:var(--ink-70);flex-wrap:wrap}
.pdate b{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);font-weight:500}
.lnk{background:none;border:0;padding:0;font:inherit;color:var(--steel);text-decoration:underline;cursor:pointer}
.seg{display:flex;gap:0;flex:1 1 auto}
.seg button{flex:1;padding:9px 6px;border:1.5px solid var(--grain-2);background:#fff;cursor:pointer;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;font-size:11.5px;letter-spacing:.05em;color:var(--ink-70)}
.seg button+button{border-left:0}
.seg button[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--paper)}
.qedit{display:flex;align-items:center;gap:4px;margin-top:5px}
.qedit button{width:28px;height:28px;border:1px solid var(--grain-2);background:#fff;cursor:pointer;font-size:16px;
line-height:1;color:var(--ink);border-radius:var(--r);padding:0}
.qedit button:hover{border-color:var(--ink)}
.qedit input{width:58px;padding:4px 6px;border:1px solid var(--grain-2);border-radius:var(--r);
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:14px;text-align:center;background:#fff}
.adj{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 14px;border-bottom:1px solid var(--grain)}
.adj .f{display:flex;align-items:center;gap:8px}
.adj label{font-size:12.5px;color:var(--ink-70)}
.adj input[type=number]{width:88px;padding:7px 9px;font-size:15px}
.adj input[type=checkbox]{width:18px;height:18px;accent-color:var(--ink)}
.hist{border:1px solid var(--grain-2);border-radius:var(--r);background:#fff}
.hrow{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--grain);font-size:13.5px}
.hrow:last-child{border-bottom:0}
.hrow .d{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11.5px;color:var(--ink-70);flex:0 0 76px}
.hrow .a{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
.hrow .s{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;white-space:nowrap}
.hrow .x{flex:0 0 26px;width:26px;height:26px;border:1px solid var(--grain-2);background:transparent;cursor:pointer;
color:var(--ink-70);border-radius:var(--r);font-size:13px;line-height:1;padding:0}
.hrow .x:hover{border-color:var(--alert);color:var(--alert)}
.only-print-qty{display:none}
@media print{.only-print-qty{display:block!important}.adj{display:none!important}}
/* широкий экран: две колонки — прайс слева, работа справа */
@media (min-width:1216px){
.wrap{max-width:none;display:grid;align-items:start;
grid-template-columns:minmax(320px,0.8fr) minmax(0,1.2fr);column-gap:32px;padding:0 24px}
.head-in,.grand-in{max-width:none;padding-left:24px;padding-right:24px}
#priceSection{grid-column:1;grid-row:1 / span 10;position:sticky;top:14px;
max-height:calc(100vh - 90px);overflow-y:auto;overflow-x:hidden;padding-right:10px;margin-top:22px}
#priceSection>.eyebrow{position:sticky;top:0;background:var(--paper);z-index:2;padding:4px 0 8px;margin:0}
#calcWrap,#clientSection,#orderSection,#histSection,#customSection,footer{grid-column:2}
#stockSection,#cfgSection,#histSection{grid-column:1 / -1;margin-top:22px}
#cfgSection .cgrid{grid-template-columns:repeat(3,1fr)}
body.view-stock #stockSection{grid-column:1 / -1}
#calcWrap{margin-top:22px}
body{padding-bottom:110px}
.section-view{align-items:center}
.scale-box{width:168px;height:168px}
.cgrid{grid-template-columns:repeat(3,1fr)}
.cgrid .f.full{grid-column:auto}
.cgrid .f.full:first-child{grid-column:span 2}
.custom-grid{grid-template-columns:repeat(5,1fr)}
}
@media (min-width:1736px){
.wrap{grid-template-columns:minmax(420px,0.7fr) minmax(0,1.3fr);column-gap:44px;padding:0 32px}
.head-in,.grand-in{padding-left:32px;padding-right:32px}
.row .sz{font-size:18px}
}
@media print{.wrap{display:block!important;max-width:none}}
/* печатные документы: товарный чек и акт */
#docSheet{display:none}
.doc{font-family:"Times New Roman",Times,serif;font-size:12pt;color:#000;line-height:1.35;padding:0 0 6mm}
.doc+.doc{page-break-before:always}
.doc .rq{font-size:10pt;line-height:1.3;border-bottom:1.2pt solid #000;padding-bottom:4pt;margin-bottom:10pt}
.doc h2{font-size:14pt;text-align:center;margin:0 0 2pt;font-weight:700}
.doc .sub{text-align:center;font-size:11pt;margin-bottom:10pt}
.doc .party{margin:6pt 0;font-size:11pt}
.doc .party b{font-weight:700}
.doc table{width:100%;border-collapse:collapse;margin:8pt 0;font-size:11pt}
.doc th,.doc td{border:0.8pt solid #000;padding:3pt 5pt;vertical-align:top}
.doc th{font-weight:700;text-align:center;font-size:10pt}
.doc td.n{text-align:right;white-space:nowrap}
.doc td.c{text-align:center}
.doc .tot{margin-top:6pt;font-size:11pt}
.doc .tot .r{display:flex;justify-content:space-between;padding:1pt 0}
.doc .tot .r.big{font-weight:700;font-size:12pt;border-top:1pt solid #000;padding-top:3pt;margin-top:3pt}
.doc .words{margin:6pt 0;font-size:11pt;font-style:italic}
.doc .text{margin:8pt 0;font-size:11pt;text-align:justify}
.doc .signs{display:flex;gap:14mm;margin-top:14pt;font-size:11pt}
.doc .signs .col{flex:1}
.doc .signs .lbl{font-weight:700;margin-bottom:10pt}
.doc .signs .line{border-bottom:0.8pt solid #000;height:16pt;margin-bottom:2pt}
.doc .signs .cap{font-size:9pt;color:#000}
.doc .mp{margin-top:16pt;display:flex;align-items:flex-start;gap:6mm}
.doc .mp .stampbox{width:42mm;height:42mm;border:0.8pt dashed #555;border-radius:50%;
display:flex;align-items:center;justify-content:center;font-size:9pt;color:#555;text-align:center}
.doc .mp .note{font-size:9pt;color:#333;padding-top:4pt}
@media print{
body.print-doc #orderSection,body.print-doc #stockPrint{display:none!important}
body.print-doc #docSheet{display:block!important}
body.print-doc .ocard{zoom:1}
}
/* заказы со статусами */
.orow{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--grain);flex-wrap:wrap}
.orow:last-child{border-bottom:0}
.orow .d{flex:0 0 92px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11.5px;color:var(--ink-70);line-height:1.5}
.orow .d b{display:block;color:var(--ink);font-size:13px}
.orow .a{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;font-size:13.5px}
.orow .a small{display:block;color:var(--ink-70);font-size:12px;margin-top:2px}
.orow .s{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;white-space:nowrap;flex:0 0 auto}
.orow select{padding:7px 8px;border:1.5px solid var(--grain-2);border-radius:var(--r);background:#fff;
font:inherit;font-size:12.5px;color:var(--ink);flex:0 0 auto}
.orow.st-new{border-left:4px solid var(--steel)}
.orow.st-paid{border-left:4px solid var(--ok)}
.orow.st-ready{border-left:4px solid var(--mark)}
.orow.st-shipped{border-left:4px solid var(--grain-2);opacity:.75}
.orow.st-cancel{border-left:4px solid var(--alert);opacity:.55}
.orow .ib{width:30px;height:30px;border:1px solid var(--grain-2);background:transparent;cursor:pointer;
border-radius:var(--r);color:var(--ink-70);font-size:13px;line-height:1;padding:0;flex:0 0 auto}
.orow .ib:hover{border-color:var(--ink);color:var(--ink)}
.ordnum{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:12.5px;color:#C9C2B6}
@media print{.ordnum{color:#000;font-size:13px}}
/* настройки */
.cfgbox{border:1.5px solid var(--grain-2);border-radius:var(--r);background:#fff;padding:14px}
.cfgbox.danger{border-color:var(--alert)}
.cfgtext{margin:0 0 4px;font-size:14px;line-height:1.5}
.cfghint{font-size:12px;color:var(--ink-70);margin-top:10px;line-height:1.5}
.cfgrow{margin-top:8px}
#cfgSection .cgrid input{width:100%}
/* панель разделов */
.viewbar{background:var(--ink);border-top:1px solid #35302A}
.viewbar-in{display:flex;gap:0;padding:0 20px}
.viewbar button{flex:0 1 240px;padding:15px 20px;border:0;background:transparent;color:#B9B2A6;cursor:pointer;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.1em;font-size:14px;
border-bottom:4px solid transparent;display:flex;align-items:center;justify-content:center;gap:8px}
.viewbar button:hover{color:#fff}
.viewbar button[aria-pressed="true"]{color:#fff;border-bottom-color:var(--mark);background:#2A241D}
.viewbar .vb{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;background:var(--alert);color:#fff;
border-radius:999px;padding:1px 7px;line-height:1.5}
.viewbar .vb:empty{display:none}
@media (max-width:520px){.viewbar button{flex:1 1 50%;padding:13px 8px;font-size:12.5px;letter-spacing:.05em}
.viewbar-in{padding:0 8px}}
/* переключатель разделов (старое) */
.views{display:flex;gap:0;border:1.5px solid #4A443B;border-radius:var(--r);overflow:hidden}
.views button{appearance:none;border:0;background:transparent;color:#C9C2B6;padding:7px 16px;cursor:pointer;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.09em;font-size:12px}
.views button+button{border-left:1.5px solid #4A443B}
.views button[aria-pressed="true"]{background:var(--mark);color:var(--ink)}
/* склад */
.stsum{display:flex;flex-wrap:wrap;gap:0;border:1.5px solid var(--ink);border-radius:var(--r);overflow:hidden;background:#fff}
.stsum .c{flex:1 1 150px;padding:11px 14px;border-right:1px solid var(--grain)}
.stsum .c:last-child{border-right:0}
.stsum .k{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-70)}
.stsum .v{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:19px;margin-top:2px}
.stsum .c.alert .v{color:var(--alert)}
.strow{display:flex;align-items:center;gap:10px;padding:9px 6px;border-bottom:1px solid var(--grain);flex-wrap:wrap}
.strow .body{flex:1 1 180px;min-width:0}
.strow .sz{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:15px}
.strow .meta{font-size:12px;color:var(--ink-70)}
.strow .fld{display:flex;align-items:center;gap:5px;flex:0 0 auto}
.strow .fld span{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10px;letter-spacing:.08em;
text-transform:uppercase;color:var(--ink-70)}
.strow input{width:72px;padding:6px 8px;border:1.5px solid var(--grain-2);border-radius:var(--r);
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:14px;text-align:center;background:#fff}
.strow .in{width:30px;height:30px;border:1px solid var(--grain-2);background:#fff;cursor:pointer;border-radius:var(--r);
font-size:15px;line-height:1;color:var(--ink);padding:0}
.strow .in:hover{border-color:var(--ink)}
.strow .cub{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:12.5px;color:var(--ink-70);flex:0 0 92px;text-align:right}
.strow.low{background:#FBF2EE}
.strow.low .sz{color:var(--alert)}
.strow.zero{opacity:.55}
.mrow{display:flex;align-items:baseline;gap:10px;padding:7px 6px;border-bottom:1px solid var(--grain);font-size:13px}
.mrow .d{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;color:var(--ink-70);flex:0 0 96px}
.mrow .q{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;flex:0 0 72px;text-align:right}
.mrow .q.plus{color:var(--ok)}.mrow .q.minus{color:var(--alert)}
.mrow .a{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
.stockline{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px}
.stockline.ok{color:var(--ok)}.stockline.no{color:var(--alert)}
#stockPrint{display:none}
.sheet h2{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:18px;margin:0 0 4px}
.sheet .sub{font-size:12px;color:#555;margin-bottom:10px}
.sheet table{width:100%;border-collapse:collapse;font-size:12px}
.sheet th,.sheet td{border:1px solid #999;padding:5px 7px;text-align:left}
.sheet th{background:#EEE}
.sheet td.n{text-align:right;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif}
.sheet td.fact{width:80px}
@media print{
body.print-stock #orderSection{display:none!important}
body.print-stock #stockPrint{display:block!important}
}
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
@media (max-width:420px){
.scale-box{width:108px;height:108px}
.oi{gap:8px;padding:11px 10px}
.oi .sz{font-size:15px}
.oi .sum{font-size:15px}
.row{gap:8px;padding:11px 4px}
.custom-grid{grid-template-columns:repeat(2,1fr)}
}
/* ================= БОКОВАЯ ПАНЕЛЬ ================= */
:root{ --rail:236px; --ink-2:#262019; --ink-3:#2E2820; }
#rail{
position:fixed;left:0;top:0;bottom:0;width:var(--rail);
background:var(--ink);color:#B9B2A6;z-index:900;
display:flex;flex-direction:column;
}
/* марка */
.rail-brand{display:flex;align-items:center;gap:11px;padding:20px 20px 18px;border-bottom:1px solid var(--ink-3)}
.rb-mark{width:34px;height:34px;flex:0 0 auto;background:var(--mark);color:var(--ink);
display:grid;place-items:center;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:18px;letter-spacing:.02em}
.rb-txt{display:flex;flex-direction:column;line-height:1.25;min-width:0}
.rb-txt b{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:500;font-size:15px;letter-spacing:.12em;
text-transform:uppercase;color:#fff}
.rb-txt span{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10px;letter-spacing:.14em;
text-transform:uppercase;color:#8A8377}
/* разделы */
.rail-nav{display:flex;flex-direction:column;padding:12px 0;gap:1px}
.rail-nav button{
position:relative;display:flex;align-items:center;gap:9px;width:100%;
padding:12px 20px;border:0;background:transparent;color:#B9B2A6;cursor:pointer;text-align:left;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.07em;font-size:13px;
transition:background .15s,color .15s;
}
.rail-nav button::before{content:"";position:absolute;left:0;top:7px;bottom:7px;width:3px;
background:transparent;transition:background .18s}
.rail-nav button:hover{color:#fff;background:#221D17}
.rail-nav button:focus-visible{outline:2px solid var(--mark);outline-offset:-2px}
.rail-nav button[aria-pressed="true"]{color:#fff;background:var(--ink-2)}
.rail-nav button[aria-pressed="true"]::before{background:var(--mark)}
.rail-nav .vb{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10.5px;background:var(--alert);color:#fff;
border-radius:999px;padding:1px 6px 2px;line-height:1.3}
.rail-nav .vb:empty{display:none}
/* мерная лента по краю панели */
.rail-tape{position:absolute;right:0;top:0;bottom:0;width:13px;background:var(--mark);overflow:hidden}
.rail-tape::before{content:"";position:absolute;inset:0;
background:repeating-linear-gradient(to bottom,var(--ink) 0 1px,transparent 1px 10px);opacity:.5}
.rail-tape::after{content:"";position:absolute;top:0;bottom:0;left:0;width:7px;
background:repeating-linear-gradient(to bottom,var(--ink) 0 1px,transparent 1px 50px);opacity:.85}
#tapeMark{position:absolute;left:0;right:0;top:0;height:24px;background:var(--ink);
transition:top .3s cubic-bezier(.2,.75,.3,1)}
#tapeMark::after{content:"";position:absolute;left:2px;right:2px;top:50%;height:2px;
background:var(--mark);transform:translateY(-1px)}
@media (prefers-reduced-motion:reduce){#tapeMark{transition:none}}
/* карточка пользователя */
#userBox{margin-top:auto;display:none;align-items:center;gap:10px;
padding:13px 20px;border-top:1px solid var(--ink-3);
font:12px/1.35 -apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif}
#userBox .uAva{width:28px;height:28px;flex:0 0 auto;border-radius:50%;
background:var(--mark);color:var(--ink);display:grid;place-items:center;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:13px}
#userBox .uTxt{display:flex;flex-direction:column;line-height:1.3;min-width:0;flex:1 1 auto}
#userBox .uName{font-weight:600;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#userBox .uMail{color:#8A8377;font-size:10.5px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#logoutBtn{flex:0 0 auto;background:transparent;border:1px solid #3E382F;color:#B9B2A6;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.06em;font-size:10.5px;
padding:5px 8px;border-radius:2px;cursor:pointer;transition:background .15s,color .15s}
#logoutBtn:hover{background:#3E382F;color:#fff}
/* содержимое сдвигается вправо */
body{padding-left:var(--rail)}
@media (min-width:901px){
header,.tape{display:none}
.wrap{padding:22px 30px 0}
#rail{padding-left:env(safe-area-inset-left)}
body{padding-left:calc(var(--rail) + env(safe-area-inset-left))}
}
/* ---- телефон: панель уходит вниз ---- */
@media (max-width:900px){
body{padding-left:0;padding-bottom:calc(126px + env(safe-area-inset-bottom))}
/* шапка не лезет под вырез и «челку» */
header{padding-top:env(safe-area-inset-top)}
.head-in{padding-left:calc(20px + env(safe-area-inset-left));
padding-right:calc(20px + env(safe-area-inset-right));
align-items:center;gap:10px}
/* карточка пользователя переезжает в шапку — внизу и так тесно */
#userBox{margin:0 0 0 auto;padding:0;border-top:0;gap:8px;flex:0 0 auto}
#userBox .uTxt{display:none}
#userBox .uAva{width:26px;height:26px;font-size:12px}
#logoutBtn{border-color:#4A443A;color:#C9C2B6}
#rail{top:auto;right:0;width:auto;max-width:100%;flex-direction:row;align-items:stretch;
border-top:1px solid var(--ink-3);
height:calc(56px + env(safe-area-inset-bottom));
padding-bottom:env(safe-area-inset-bottom);
padding-left:env(safe-area-inset-left);
padding-right:env(safe-area-inset-right)}
.rail-brand,.rail-tape{display:none}
.rail-nav{flex-direction:row;flex:1 1 auto;padding:0;gap:0;min-width:0}
.rail-nav button{flex:1 1 0;min-width:0;justify-content:center;gap:4px;
padding:0 3px;font-size:10.5px;letter-spacing:.01em;text-align:center;
line-height:1.15;hyphens:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rail-nav button::before{left:0;right:0;top:0;bottom:auto;width:auto;height:3px}
.rail-nav .vb{font-size:9.5px;padding:0 4px 1px}
#syncBar{bottom:calc(66px + env(safe-area-inset-bottom))}
}
@media (max-width:400px){
.rail-nav button{font-size:9.5px;letter-spacing:0}
.rail-nav .vb{display:none}
}
/* ============ стыковка старой вёрстки с боковой панелью ============ */
/* скрытые разделы не должны проступать ни при каких стилях */
[hidden]{display:none!important}
/* нижняя плашка «К оплате» и всплывашки не залезают под панель */
@media (min-width:901px){
.grand{left:var(--rail)}
.grand-in{padding-left:30px;padding-right:30px}
.toast{left:calc(50% + var(--rail)/2)}
}
/* телефон: плашка стоит НАД меню, а не под ним */
@media (max-width:900px){
.grand{bottom:calc(58px + env(safe-area-inset-bottom))}
.toast{bottom:calc(140px + env(safe-area-inset-bottom))}
.grand-in{padding-left:calc(20px + env(safe-area-inset-left));
padding-right:calc(20px + env(safe-area-inset-right))}
}
/* раздел аналитики занимает всю ширину содержимого */
@media (min-width:1216px){ #anaSection{grid-column:1 / -1;margin-top:22px} }
/* ничего не уезжает вбок.
overflow-x:hidden на html+body создаёт вложенную прокрутку и ломает колесо мыши —
поэтому clip только на html: он обрезает, но не создаёт контейнер прокрутки */
html{overflow-x:clip}
#rail{max-width:100vw}
.wrap{max-width:100%}
/* прокрутка списка прайса не должна «залипать»: дойдя до края, продолжаем крутить страницу */
#priceSection{overscroll-behavior-y:auto}
/* шапки на широком экране нет — список прайса может занять освободившуюся высоту.
Только внутри той же границы, где у него включается собственная прокрутка. */
@media (min-width:1216px){
#priceSection{max-height:calc(100vh - 44px);top:20px}
}
#ordSheet{position:fixed;inset:0;z-index:9500;display:none;background:rgba(28,26,23,.55)}
#ordSheet.on{display:flex;align-items:flex-end;justify-content:center}
@media (min-width:760px){#ordSheet.on{align-items:center}}
#ordCard{background:var(--paper);width:100%;max-width:680px;max-height:92vh;max-height:92dvh;
display:flex;flex-direction:column;border-radius:var(--r);
box-shadow:0 -6px 30px rgba(28,26,23,.35);animation:oc .22s cubic-bezier(.2,.75,.3,1)}
@keyframes oc{from{transform:translateY(18px);opacity:.4}to{transform:none;opacity:1}}
@media (prefers-reduced-motion:reduce){#ordCard{animation:none}}
#ordCard .oc-top{background:var(--ink);color:var(--paper);padding:14px 18px;
display:flex;align-items:center;gap:12px;flex:0 0 auto}
#ordCard .oc-top .n{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:17px;letter-spacing:.08em;text-transform:uppercase}
#ordCard .oc-top .dt{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;color:#A9A296;margin-top:2px}
#ordCard .oc-top .st{margin-left:auto;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;letter-spacing:.08em;
text-transform:uppercase;border:1px solid currentColor;border-radius:999px;padding:3px 10px 4px;white-space:nowrap}
#ordCard .oc-x{flex:0 0 auto;width:30px;height:30px;border:1px solid #4A443A;background:transparent;
color:#C9C2B6;cursor:pointer;border-radius:var(--r);font-size:15px;line-height:1;padding:0}
#ordCard .oc-x:hover{background:#3E382F;color:#fff}
#ordCard .oc-del{flex:0 0 auto;width:30px;height:30px;border:1px solid #4A443A;background:transparent;
color:#C9C2B6;cursor:pointer;border-radius:var(--r);padding:0;display:flex;align-items:center;justify-content:center}
#ordCard .oc-del:hover{background:rgba(214,0,28,.25);color:#fff;border-color:rgba(214,0,28,.4)}
#ordCard .oc-tape{height:6px;background:var(--mark);flex:0 0 auto;position:relative;overflow:hidden}
#ordCard .oc-tape::before{content:"";position:absolute;inset:0;
background:repeating-linear-gradient(to right,var(--ink) 0 1px,transparent 1px 10px);opacity:.5}
#ordCard .oc-body{overflow-y:auto;-webkit-overflow-scrolling:touch;padding:16px 18px 4px}
#ordCard .oc-lab{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10.5px;letter-spacing:.15em;
text-transform:uppercase;color:var(--ink-70);margin:18px 0 8px;display:flex;align-items:center;gap:10px}
#ordCard .oc-lab:first-child{margin-top:0}
#ordCard .oc-lab::after{content:"";flex:1;height:1px;background:var(--grain-2)}
#ordCard .f{display:flex;gap:10px;padding:7px 0;border-bottom:1px solid var(--grain);font-size:13.5px}
#ordCard .f:last-child{border-bottom:0}
#ordCard .f .k{flex:0 0 132px;color:var(--ink-70);font-size:12.5px}
#ordCard .f .v{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
#ordCard .f .v a{color:var(--steel)}
#ordCard .it{border:1px solid var(--grain-2);border-radius:var(--r);background:#fff;overflow:hidden}
#ordCard .it .r{display:flex;gap:10px;align-items:baseline;padding:10px 12px;border-top:1px solid var(--grain)}
#ordCard .it .r:first-child{border-top:0}
#ordCard .it .r .a{flex:1 1 auto;min-width:0}
#ordCard .it .r .sz{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:14px;overflow-wrap:anywhere}
#ordCard .it .r .sub{font-size:12px;color:var(--ink-70);margin-top:2px}
#ordCard .it .r .q{flex:0 0 auto;text-align:right;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;
font-size:13.5px;font-weight:600;white-space:nowrap}
#ordCard .it .r .q small{display:block;font-weight:400;font-size:11px;color:var(--ink-70)}
#ordCard .tot{border-top:1.5px solid var(--ink);margin-top:14px;padding-top:10px}
#ordCard .tot .f .v{text-align:right;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif}
#ordCard .tot .grandline{display:flex;align-items:center;gap:12px;background:var(--mark);
margin:10px -18px 0;padding:11px 18px}
#ordCard .tot .grandline span{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.1em;font-size:12px}
#ordCard .tot .grandline b{margin-left:auto;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:19px}
#ordCard .oc-act{flex:0 0 auto;display:flex;gap:8px;padding:12px 18px;
padding-bottom:calc(12px + env(safe-area-inset-bottom));border-top:1px solid var(--grain-2);background:var(--paper)}
#ordCard .oc-act button{flex:1 1 0;padding:11px 8px;border:1.5px solid var(--ink);background:transparent;
color:var(--ink);cursor:pointer;border-radius:var(--r);
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.05em;font-size:12px}
#ordCard .oc-act button.pri{background:var(--ink);color:var(--paper)}
.orow .ib.info{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600}
.stpanel{display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin-top:8px;
padding:8px 15px 12px 34px;border-top:.5px dashed var(--grain)}
.stpanel select.dssel{border:0;border-radius:999px;padding:6px 26px 6px 12px;font-size:12px;
font-weight:700;cursor:pointer;background-color:var(--fill);color:var(--ink-70);
-webkit-appearance:none;appearance:none;
background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
linear-gradient(135deg,currentColor 50%,transparent 50%);
background-position:calc(100% - 13px) 55%,calc(100% - 8px) 55%;
background-size:4px 4px,4px 4px;background-repeat:no-repeat}
.stpanel select.ds-pending{background-color:var(--fill);color:var(--ink-70)}
.stpanel select.ds-confirmed{background-color:var(--mark-soft);color:var(--mark)}
.stpanel select.ds-shipped2{background-color:var(--forest-soft);color:var(--ok)}
.stpanel select.ds-delivered{background-color:var(--forest-soft);color:var(--ok)}
.stpanel select.ds-cancel2{background-color:var(--fill);color:var(--alert)}
.stpanel .paybtn,.stpanel .pmbtn{border:0;border-radius:999px;padding:6px 12px;font-size:12px;
font-weight:700;cursor:pointer;white-space:nowrap}
.stpanel .paybtn:active,.stpanel .pmbtn:active{transform:scale(.96)}
.stpanel .paybtn.paid{background:var(--forest-soft);color:var(--ok)}
.stpanel .paybtn.unpaid{background:rgba(214,0,28,.10);color:var(--alert)}
.stpanel .pmbtn{background:var(--fill);color:var(--ink-70)}
/* «плашка оплаты» отдельно от статуса — используется в карточке заказа (order_card.html) */
.paypanel{display:flex;flex-wrap:wrap;align-items:center;gap:7px}
.paypanel .paybtn,.paypanel .pmbtn{border:0;border-radius:999px;padding:6px 12px;font-size:12px;
font-weight:700;cursor:pointer;white-space:nowrap}
.paypanel .paybtn:active,.paypanel .pmbtn:active{transform:scale(.96)}
.paypanel .paybtn.paid{background:var(--forest-soft);color:var(--ok)}
.paypanel .paybtn.unpaid{background:rgba(214,0,28,.10);color:var(--alert)}
.paypanel .pmbtn{background:var(--fill);color:var(--ink-70)}
/* прячем старую плоскую вёрстку заказов — на её месте встаёт новая */
#ordSummary,#ordFilters,#ordQuery,#histBox,#histSection > .actions{display:none!important}
#ocalRoot .period{margin-bottom:16px}
#ocalRoot .empty{padding:26px 18px;text-align:center;color:var(--ink-70);font-size:13.5px}
/* строка заказа: контент (открывает карточку) + панель статуса/удаления снизу */
#ocalRoot .trip{display:block;padding:0;border:0;border-top:.5px solid var(--grain);background:transparent}
#ocalRoot .trip:first-of-type{border-top:0}
/* строка заказа: адрес (кликабельно) слева, статус+Собран посередине справа, объём — у края */
#ocalRoot .triprow{display:flex;align-items:center;gap:10px;padding:12px 15px}
#ocalRoot .tripopen{display:flex;align-items:center;gap:11px;flex:1 1 auto;min-width:0;
border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;padding:0}
#ocalRoot .tripopen:hover{opacity:.75}
#ocalRoot .tripctl{display:flex;align-items:center;gap:8px;flex:0 0 auto}
#ocalRoot .tripctl select.dssel{border:0;border-radius:999px;padding:7px 28px 7px 13px;font-size:13px;
font-weight:700;cursor:pointer;-webkit-appearance:none;appearance:none;background-color:var(--fill);
color:var(--ink-70);
background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
linear-gradient(135deg,currentColor 50%,transparent 50%);
background-position:calc(100% - 13px) 55%,calc(100% - 9px) 55%;
background-size:4px 4px,4px 4px;background-repeat:no-repeat}
#ocalRoot .tripctl select.ds-pending{background-color:var(--fill);color:var(--ink-70)}
#ocalRoot .tripctl select.ds-confirmed{background-color:var(--mark-soft);color:var(--mark)}
#ocalRoot .tripctl select.ds-shipped2{background-color:var(--forest-soft);color:var(--ok)}
#ocalRoot .tripctl select.ds-delivered{background-color:var(--forest-soft);color:var(--ok)}
#ocalRoot .tripctl select.ds-cancel2{background-color:var(--fill);color:var(--alert)}
#ocalRoot .tripctl .stbtn{border:0;border-radius:999px;padding:7px 15px;font-size:13px;
font-weight:700;cursor:pointer;white-space:nowrap}
#ocalRoot .tripctl .stbtn:active{transform:scale(.96)}
#ocalRoot .tripctl .stbtn.notready{background:var(--fill);color:var(--ink-70)}
#ocalRoot .tripctl .stbtn.ready{background:var(--forest-soft);color:var(--ok)}
#ocalRoot .tripctl .stlabel{font-size:13px;font-weight:600;color:var(--ink-70);padding:7px 4px}
@media (max-width:560px){
#ocalRoot .triprow{flex-wrap:wrap}
#ocalRoot .tripopen{flex:1 1 100%}
#ocalRoot .tripctl{flex:1 1 auto;margin-left:34px}
#ocalRoot .n{margin-left:auto}
}
#anaSection .ana-period{display:grid;grid-template-columns:repeat(5,1fr);border:1.5px solid var(--ink);
border-radius:var(--r);overflow:hidden;margin-bottom:18px}
#anaSection .ana-period button{appearance:none;border:0;background:transparent;padding:10px 6px;cursor:pointer;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.05em;font-size:11.5px;color:var(--ink)}
#anaSection .ana-period button+button{border-left:1.5px solid var(--ink)}
#anaSection .ana-period button[aria-pressed="true"]{background:var(--ink);color:var(--paper)}
.kpis{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:26px}
@media (min-width:760px){.kpis{grid-template-columns:repeat(4,1fr)}}
.kpi{border:1px solid var(--grain-2);border-radius:var(--r);padding:13px 14px 15px;background:#fff;position:relative}
.kpi::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--mark)}
.kpi .k{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10.5px;letter-spacing:.13em;
text-transform:uppercase;color:var(--ink-70)}
.kpi .v{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:21px;
margin-top:7px;letter-spacing:-.01em;overflow-wrap:anywhere}
.kpi .d{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;margin-top:4px;color:var(--ink-70)}
.kpi .d.up{color:var(--ok)} .kpi .d.dn{color:var(--alert)}
/* столбики выручки по месяцам */
.chart{border:1px solid var(--grain-2);border-radius:var(--r);background:#fff;padding:16px 14px 10px;margin-bottom:26px}
.bars{display:flex;align-items:flex-end;gap:5px;height:170px;padding-bottom:4px;
border-bottom:1.5px solid var(--ink)}
.bars .b{flex:1 1 0;min-width:0;display:flex;flex-direction:column;justify-content:flex-end;height:100%;position:relative}
.bars .b i{display:block;background:var(--mark);border:1px solid var(--ink);border-bottom:0;
min-height:2px;transition:height .35s cubic-bezier(.2,.75,.3,1)}
.bars .b.now i{background:var(--ink)}
.bars .b .t{position:absolute;left:0;right:0;bottom:calc(100% + 3px);text-align:center;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:9.5px;color:var(--ink-70);white-space:nowrap}
.xlab{display:flex;gap:5px;margin-top:6px}
.xlab span{flex:1 1 0;min-width:0;text-align:center;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;
font-size:9.5px;letter-spacing:.02em;text-transform:uppercase;color:var(--ink-70)}
.xlab span.now{color:var(--ink);font-weight:600}
/* рейтинги */
.rank{border:1px solid var(--grain-2);border-radius:var(--r);background:#fff;overflow:hidden;margin-bottom:26px}
.rank .rr{display:flex;align-items:center;gap:10px;padding:10px 13px;position:relative;
border-top:1px solid var(--grain)}
.rank .rr:first-child{border-top:0}
.rank .rr .fill{position:absolute;left:0;top:0;bottom:0;background:var(--mark);opacity:.16}
.rank .rr .a{flex:1 1 auto;min-width:0;position:relative;overflow-wrap:anywhere;font-size:13.5px}
.rank .rr .a small{display:block;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:10.5px;
color:var(--ink-70);letter-spacing:.03em;margin-top:2px}
.rank .rr .n{flex:0 0 auto;position:relative;text-align:right;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-weight:600;font-size:14px;white-space:nowrap}
.rank .rr .n small{display:block;font-weight:400;font-size:10.5px;color:var(--ink-70)}
.ana-empty{border:1px dashed var(--grain-2);border-radius:var(--r);padding:26px 18px;text-align:center;
color:var(--ink-70);font-size:13.5px}
.split{display:flex;height:22px;border:1.5px solid var(--ink);border-radius:var(--r);overflow:hidden;margin-bottom:9px}
.split i{display:block;height:100%}
.split-leg{display:flex;flex-wrap:wrap;gap:14px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:11px;color:var(--ink-70)}
.split-leg b{color:var(--ink);font-weight:600}
.split-leg em{display:inline-block;width:9px;height:9px;margin-right:5px;border:1px solid var(--ink);vertical-align:baseline}
#dlvSection .dlv-period{display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:16px}
#dlvSection .dlv-sum{display:flex;flex-wrap:wrap;gap:0;margin-bottom:20px}
#dlvSection .dlv-sum .c{flex:1 1 130px;padding:12px 15px}
#dlvSection .dlv-sum .k{font-size:12px;text-transform:uppercase;color:var(--ink-70)}
#dlvSection .dlv-sum .v{font-weight:600;font-size:20px;margin-top:3px;letter-spacing:-.02em}
.day{margin-bottom:14px}
.day-head{display:flex;align-items:center;gap:10px;padding:12px 15px;border-bottom:.5px solid var(--grain)}
.day-head .dt{flex:1 1 auto;min-width:0}
.day-head .dt b{display:block;font-size:16px;font-weight:600;letter-spacing:-.02em}
.day-head .dt span{display:block;font-size:12.5px;color:var(--ink-70);margin-top:2px}
.day-head .today{flex:0 0 auto;background:var(--mark);color:#fff;border-radius:999px;
padding:3px 9px 4px;font-size:11.5px;font-weight:600}
.day-head .cp{flex:0 0 auto;width:32px;height:32px;border:0;background:var(--fill);
color:var(--ink-70);border-radius:8px;cursor:pointer;display:inline-flex;
align-items:center;justify-content:center;padding:0}
.day-head .cp:hover{background:rgba(118,118,128,.22);color:var(--ink)}
.trip{display:block;padding:0;border:0;border-top:.5px solid var(--grain);background:transparent}
.trip:first-of-type{border-top:0}
.trip .tripmain{display:flex;align-items:center;gap:11px;width:100%;text-align:left;
padding:12px 15px;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer}
.trip .tripmain:hover{background:rgba(118,118,128,.07)}
.trip .dot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:var(--grain-2)}
.trip.st-new .dot{background:var(--steel)}
.trip.st-paid .dot{background:var(--ok)}
.trip.st-ready .dot{background:var(--mark)}
.trip .a{flex:1 1 auto;min-width:0}
.trip .a b{display:block;font-size:14.5px;font-weight:500;overflow-wrap:anywhere}
.trip .a span{display:block;font-size:12.5px;color:var(--ink-70);margin-top:2px;overflow-wrap:anywhere}
.trip .n{flex:0 0 auto;text-align:right;font-weight:600;font-size:14px;white-space:nowrap}
.trip .n small{display:block;font-weight:400;font-size:11.5px;color:var(--ink-70)}
/* строка доставки: верхний ряд — адрес слева, объём/сумма справа;
нижний ряд — машина/доставка слева, статус+Собран справа, ОБА на одной высоте */
#dlvSection .triprow{display:flex;align-items:flex-start;gap:10px;padding:12px 15px 4px}
#dlvSection .dlvopen{display:flex;align-items:flex-start;gap:11px;flex:1 1 auto;min-width:0;
border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;padding:0}
#dlvSection .dlvopen:hover{opacity:.75}
#dlvSection .dlvopen .dot{margin-top:5px}
#dlvSection .dlvopen .a{flex:1 1 auto;min-width:0}
#dlvSection .dlvopen .a b{display:block;font-size:14.5px;font-weight:500;overflow-wrap:anywhere}
#dlvSection .dlvopen .a .meta{display:block;font-size:12.5px;color:var(--ink-70);margin-top:2px;overflow-wrap:anywhere}
#dlvSection .triprow .n{flex:0 0 auto;text-align:right;font-weight:600;font-size:13px;
white-space:nowrap;color:var(--ink);padding-top:1px}
#dlvSection .tripbottom{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;
gap:8px;padding:6px 15px 12px 34px}
#dlvSection .tripbottom .tags{display:flex;flex-wrap:wrap;gap:7px}
#dlvSection .tripbottom .tags i{font-style:normal;font-size:13.5px;font-weight:500;padding:6px 12px 7px;
border-radius:999px;white-space:nowrap;background:var(--fill);color:var(--ink)}
#dlvSection .tripbottom .tags .ship{background:var(--mark-soft);color:var(--mark)}
#dlvSection .tripbottom .tags .noship{color:var(--ink-70)}
#dlvSection .tripbottom .tags .tw{background:var(--steel);color:#fff;font-weight:700}
#dlvSection .dlvctl{flex:0 0 auto;display:flex;align-items:center;gap:7px}
#dlvSection .dlvctl select.dssel{border:0;border-radius:999px;padding:7px 28px 7px 13px;font-size:13px;
font-weight:700;cursor:pointer;-webkit-appearance:none;appearance:none;background-color:var(--fill);
color:var(--ink-70);
background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
linear-gradient(135deg,currentColor 50%,transparent 50%);
background-position:calc(100% - 13px) 55%,calc(100% - 9px) 55%;
background-size:4px 4px,4px 4px;background-repeat:no-repeat}
#dlvSection .dlvctl select.ds-pending{background-color:var(--fill);color:var(--ink-70)}
#dlvSection .dlvctl select.ds-confirmed{background-color:var(--mark-soft);color:var(--mark)}
#dlvSection .dlvctl select.ds-shipped2{background-color:var(--forest-soft);color:var(--ok)}
#dlvSection .dlvctl select.ds-delivered{background-color:var(--forest-soft);color:var(--ok)}
#dlvSection .dlvctl select.ds-cancel2{background-color:var(--fill);color:var(--alert)}
#dlvSection .dlvctl .stbtn{border:0;border-radius:999px;padding:7px 15px;font-size:13px;font-weight:700;
cursor:pointer;white-space:nowrap}
#dlvSection .dlvctl .stbtn:active{transform:scale(.96)}
#dlvSection .dlvctl .stbtn.notready{background:var(--fill);color:var(--ink-70)}
#dlvSection .dlvctl .stbtn.ready{background:var(--forest-soft);color:var(--ok)}
#dlvSection .dlvctl .stlabel{font-size:13px;font-weight:600;color:var(--ink-70)}
.dlv-empty{padding:26px 18px;text-align:center;color:var(--ink-70);font-size:13.5px}
.nodate .day-head .dt b{color:var(--alert)}
@media (max-width:480px){
#dlvSection .tripbottom{padding-left:15px}
}
/* меню «Ещё» на телефоне */
#moreSheet{position:fixed;left:0;right:0;z-index:940;display:none;
bottom:calc(56px + env(safe-area-inset-bottom));
background:var(--card);border-top:.5px solid var(--grain-2);
padding:8px 10px calc(10px + env(safe-area-inset-left)*0)}
#moreSheet.on{display:block}
#moreSheet button{display:flex;align-items:center;width:100%;padding:14px 14px;border:0;
background:transparent;color:var(--ink);font:inherit;font-size:16px;cursor:pointer;
border-radius:var(--r-sm);text-align:left}
#moreSheet button:hover{background:var(--fill)}
#moreSheet button[aria-pressed="true"]{color:var(--mark);font-weight:600}
#moreBack{position:fixed;inset:0;z-index:930;display:none;background:rgba(0,0,0,.28)}
#moreBack.on{display:block}
@media (min-width:901px){
#viewMore{display:none!important}
#moreSheet,#moreBack{display:none!important}
}
@media (max-width:900px){
#viewAna,#viewCfg,#viewPay{display:none!important}
}
@media print{#moreSheet,#moreBack{display:none!important}}
#paySection{max-width:720px}
#paySection .pay-empty{padding:30px 18px;text-align:center;color:var(--ink-70);font-size:13.5px}
#paySection .pay-empty b{display:block;color:var(--ink);font-size:15px;margin-bottom:6px;font-weight:700}
.dbt-tools{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
.dbt-search{flex:1 1 220px;min-width:0}
.dbt-search input{width:100%;box-sizing:border-box}
/* ---- поля ввода: тот же вид, что у остальных полей приложения ----
Здесь свои id (dbtName, dbtSearch и т.д.), под общий селектор
вроде .cgrid input они не попадали и оставались в браузерном
виде по умолчанию — с рамкой и без скругления. */
.dbt-form input,.dbt-search input{
border:0;background:var(--fill);border-radius:var(--r-sm)!important;color:var(--ink);
font-size:16px;font-weight:400;letter-spacing:-.01em;padding:11px 12px;box-sizing:border-box}
.dbt-form input::placeholder,.dbt-search input::placeholder{color:var(--ink-70);opacity:1}
.dbt-form input:focus,.dbt-search input:focus{
outline:0;background:var(--card);box-shadow:0 0 0 3px var(--mark-soft),0 0 0 1.5px var(--mark)}
.dbt-filter{flex:0 0 auto}
.dbt-add{margin-bottom:16px}
.dbt-add-btn{border:0;background:var(--mark);color:#fff;font-weight:700;border-radius:999px;
padding:11px 18px;font-size:14px;cursor:pointer}
.dbt-add-btn:active{transform:scale(.97)}
.dbt-form{background:var(--card);border-radius:var(--r);box-shadow:var(--shadow);
padding:16px;margin-top:12px}
.dbt-form .ft{font-weight:700;font-size:14px;margin-bottom:12px}
.dbt-form .f2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
.dbt-form label{display:block;font-size:11.5px;color:var(--ink-70);margin-bottom:5px}
.dbt-form input{width:100%;box-sizing:border-box}
.dbt-form .fw{margin-bottom:10px}
.dbt-form .acts{display:flex;gap:8px;margin-top:4px}
.dbt-form .acts button{border-radius:999px;font-weight:700;padding:9px 16px;font-size:13px;
border:0;cursor:pointer}
.dbt-form .save{background:var(--mark);color:#fff}
.dbt-form .cancel{background:var(--fill);color:var(--ink)}
.dbt-card{background:var(--card);border-radius:var(--r);box-shadow:var(--shadow);
overflow:hidden;margin-bottom:12px}
.dbt-head{display:flex;align-items:center;gap:10px;padding:14px 15px;cursor:pointer}
.dbt-head .nm{flex:1 1 auto;min-width:0;font-weight:700;font-size:14.5px;overflow-wrap:anywhere}
.dbt-head .nm small{display:block;font-weight:400;font-size:12px;color:var(--ink-70);margin-top:2px}
.dbt-head .bal{flex:0 0 auto;text-align:right;font-weight:700;color:var(--alert);white-space:nowrap}
.dbt-head .bal.zero{color:var(--ink-70);font-weight:500}
.dbt-head .bal.neg{color:var(--ok)}
.dbt-head .ic{flex:0 0 auto;border:0;background:transparent;color:var(--ink-70);
width:30px;height:30px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}
.dbt-head .ic:hover{background:var(--fill);color:var(--ink)}
.dbt-head .ic.del:hover{background:rgba(214,0,28,.10);color:var(--alert)}
.dbt-head .car{flex:0 0 auto;color:var(--ink-70);transition:transform .15s ease}
.dbt-card.open .dbt-head .car{transform:rotate(180deg)}
.dbt-body{border-top:.5px solid var(--grain);display:none}
.dbt-card.open .dbt-body{display:block}
.dbt-info{padding:12px 15px;border-bottom:.5px solid var(--grain);font-size:12.5px;color:var(--ink-70)}
.dbt-info div+div{margin-top:4px}
.dbt-info b{color:var(--ink);font-weight:600}
.dbt-entries{max-height:260px;overflow-y:auto}
.dbt-e{display:flex;align-items:baseline;gap:10px;padding:9px 15px;border-top:.5px solid var(--grain);font-size:13px}
.dbt-e:first-child{border-top:0}
.dbt-e .dt{flex:0 0 auto;color:var(--ink-70);font-size:11.5px;white-space:nowrap}
.dbt-e .nt{flex:1 1 auto;min-width:0;max-width:60%;color:var(--ink-70);overflow-wrap:anywhere}
.dbt-e .am{margin-left:auto;flex:0 0 auto;font-weight:700;white-space:nowrap}
.dbt-e.debt .am{color:var(--alert)}
.dbt-e.pay .am{color:var(--ok)}
.dbt-e-empty{padding:14px 15px;color:var(--ink-70);font-size:12.5px}
.dbt-actions{display:flex;flex-wrap:wrap;gap:8px;padding:12px 15px}
.dbt-actions button{border-radius:999px;font-weight:700;padding:9px 15px;font-size:12.5px;
border:0;cursor:pointer;white-space:nowrap}
.dbt-actions .add-debt{background:var(--mark-soft);color:var(--alert)}
.dbt-actions .add-pay{background:var(--forest-soft);color:var(--ok)}
.dbt-actions button:active{transform:scale(.96)}
#authGate{position:fixed;top:0;left:0;right:0;bottom:0;height:100vh;height:100dvh;
z-index:99999;background:#FBF9F4;display:flex;align-items:center;justify-content:center;
padding:calc(24px + env(safe-area-inset-top)) calc(24px + env(safe-area-inset-right))
calc(24px + env(safe-area-inset-bottom)) calc(24px + env(safe-area-inset-left));
overflow-y:auto;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif}
#authGate .agBox{width:100%;max-width:340px}
#authGate h2{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Inter,system-ui,sans-serif;font-size:22px;margin:0 0 6px;color:#1C1A17;font-weight:600}
#authGate .agSub{color:#5A544C;font-size:14px;margin:0 0 22px}
#authGate label{display:block;font-size:13px;margin:14px 0 5px;color:#5A544C}
#authGate input{width:100%;box-sizing:border-box;padding:12px;font-size:16px;border:1px solid #E6DFD1;border-radius:8px;background:#fff;color:#1C1A17;font-family:inherit}
#authGate button{width:100%;margin-top:20px;padding:13px;font-size:16px;border:0;border-radius:8px;background:#1C1A17;color:#fff;cursor:pointer;font-family:inherit}
#authGate button:disabled{opacity:.5}
#agErr{color:#B3261E;font-size:13px;margin-top:12px;min-height:18px}
#syncBar{position:fixed;right:10px;bottom:10px;z-index:9998;background:rgba(28,26,23,.88);color:#fff;font:12px/1.3 system-ui,sans-serif;padding:7px 11px;border-radius:6px;opacity:0;transition:opacity .25s;pointer-events:none}
#syncBar.on{opacity:1}
@media print{#syncBar,#authGate,#rail{display:none!important}
body{padding-left:0!important;padding-bottom:0!important}}
/* ==========================================================
ОФОРМЛЕНИЕ В СТИЛЕ APPLE
Слой поверх базовых стилей: переопределяет палитру, формы и элементы
========================================================== */
:root{
--paper:#F2F2F7; /* фон приложения */
--card:#FFFFFF; /* карточки и списки */
--ink:#1C1C1E; /* основной текст */
--ink-70:#6E6E73; /* второстепенный текст */
--grain:rgba(60,60,67,.10); /* разделитель */
--grain-2:rgba(60,60,67,.18); /* граница */
--mark:#EE5A00; /* акцент */
--mark-soft:rgba(238,90,0,.18);
--steel:#007AFF;
--alert:#FF3B30;
--ok:#34C759;
--fill:rgba(118,118,128,.12); /* заливка полей и сегментов */
--r:12px;
--r-sm:10px;
--shadow:0 1px 2px rgba(0,0,0,.04), 0 6px 18px rgba(0,0,0,.05);
}
body{
background:var(--paper);
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
letter-spacing:-.01em;
}
*{font-variant-numeric:tabular-nums}
/* заголовки — без капса, с плотным трекингом как в SF Pro Display */
h1,h2,h3,.disp{text-transform:none;letter-spacing:-.022em;font-weight:600}
header h1{font-size:22px;letter-spacing:-.024em;font-weight:600}
header .sub{text-transform:none;letter-spacing:-.005em;font-size:13px}
/* мелкие подписи групп — как заголовки секций в Настройках iOS */
.eyebrow,.group-label,.cgrid label,.cl .r span,.stsum .k,.strow .fld span,
.dims .d span,#ordCard .oc-lab,.kpi .k{
letter-spacing:.02em;font-size:12px;font-weight:500;color:var(--ink-70);text-transform:uppercase}
.eyebrow::after,#ordCard .oc-lab::after{background:var(--grain)}
/* ---------- светлые панели вместо чёрных ---------- */
header{
background:rgba(249,249,251,.82);color:var(--ink);
backdrop-filter:saturate(180%) blur(20px);-webkit-backdrop-filter:saturate(180%) blur(20px);
border-bottom:.5px solid var(--grain-2)}
.tape{display:none}
#rail{
background:rgba(249,249,251,.80);color:var(--ink);
backdrop-filter:saturate(180%) blur(20px);-webkit-backdrop-filter:saturate(180%) blur(20px);
border-right:.5px solid var(--grain-2)}
.rail-tape{display:none}
.rail-brand{border-bottom:0;padding:22px 16px 10px}
.rb-mark{background:var(--mark);color:#fff;border-radius:9px;font-weight:600;font-size:17px}
.rb-txt b{color:var(--ink);text-transform:none;letter-spacing:-.02em;font-size:16px;font-weight:600}
.rb-txt span{color:var(--ink-70);text-transform:none;letter-spacing:0;font-size:12px}
.rail-nav{padding:6px 10px;gap:2px}
.rail-nav button{
color:var(--ink);border-radius:var(--r-sm);padding:9px 12px;
text-transform:none;letter-spacing:-.01em;font-size:15px;font-weight:500}
.rail-nav button::before{display:none}
.rail-nav button:hover{background:rgba(118,118,128,.08)}
.rail-nav button[aria-pressed="true"]{background:var(--mark-soft);color:#B44400;font-weight:600}
.rail-nav .vb{background:var(--alert);font-size:11px;font-weight:600;padding:1px 7px 2px}
#userBox{border-top:.5px solid var(--grain-2);padding:12px 16px}
#userBox .uAva{background:var(--mark);color:#fff;font-weight:600;font-size:13px}
#userBox .uName{color:var(--ink);font-size:13px}
#userBox .uMail{color:var(--ink-70);font-size:11.5px;letter-spacing:0}
#logoutBtn{
border:0;background:var(--fill);color:var(--ink);border-radius:8px;
text-transform:none;letter-spacing:-.01em;font-size:12.5px;font-weight:500;padding:5px 10px}
#logoutBtn:hover{background:rgba(118,118,128,.20);color:var(--ink)}
/* нижняя плашка с итогом */
.grand{
background:rgba(249,249,251,.85);color:var(--ink);box-shadow:none;
backdrop-filter:saturate(180%) blur(20px);-webkit-backdrop-filter:saturate(180%) blur(20px);
border-top:.5px solid var(--grain-2)}
.grand .lbl{color:var(--ink-70);text-transform:none;letter-spacing:-.01em;font-size:14px;font-weight:400}
.grand .val{font-size:21px;font-weight:600;letter-spacing:-.02em;color:var(--ink)}
.grand button{
background:var(--mark);border:0;color:#fff;border-radius:var(--r-sm);
text-transform:none;letter-spacing:-.01em;font-size:15px;font-weight:600;padding:10px 18px}
.grand button:active{opacity:.8}
/* ---------- сегментированные переключатели ---------- */
.tabs,.mode,.seg,#anaSection .ana-period{
border:0;background:var(--fill);border-radius:9px;padding:2px;gap:2px}
.tabs button,.mode button,.seg button,#anaSection .ana-period button{
border:0!important;background:transparent;border-radius:7px;color:var(--ink);
text-transform:none;letter-spacing:-.01em;font-size:13px;font-weight:500;padding:7px 6px}
.tabs button+button,.mode button+button,.seg button+button,
#anaSection .ana-period button+button{border-left:0}
.tabs button[aria-selected="true"],.mode button[aria-pressed="true"],
.seg button[aria-pressed="true"],#anaSection .ana-period button[aria-pressed="true"]{
background:var(--card);color:var(--ink);font-weight:600;
box-shadow:0 1px 3px rgba(0,0,0,.10),0 0 0 .5px rgba(0,0,0,.04)}
/* фильтры-пилюли */
.chip{
border:0;background:var(--fill);color:var(--ink);border-radius:999px;
letter-spacing:-.01em;font-size:13.5px;font-weight:500;padding:7px 14px}
.chip:hover{background:rgba(118,118,128,.20);color:var(--ink)}
.chip[aria-pressed="true"]{background:var(--mark);border-color:transparent;color:#fff}
/* ---------- поля ввода ---------- */
.search,.cgrid input,input[type=number],input[type=text].mini,
.qedit input,.strow input,#authGate input{
border:0;background:var(--fill);border-radius:var(--r-sm);color:var(--ink);
font-size:16px;font-weight:400;letter-spacing:-.01em;padding:11px 12px}
input[type=number],input[type=text].mini{font-weight:600}
.search:focus,.cgrid input:focus,input:focus,#authGate input:focus{
outline:0;background:var(--card);box-shadow:0 0 0 3px rgba(238,90,0,.32),0 0 0 1px var(--mark)}
.stepper button,.qedit button,.strow .in{
border:0;background:var(--fill);color:var(--ink);border-radius:var(--r-sm)}
.stepper button:hover,.qedit button:hover,.strow .in:hover{background:rgba(118,118,128,.20)}
/* ---------- кнопки ---------- */
.btn,#ordCard .oc-act button{
border:0;border-radius:var(--r-sm);background:var(--mark);color:#fff;
text-transform:none;letter-spacing:-.01em;font-size:15px;font-weight:600;padding:13px 14px}
.btn.ghost,#ordCard .oc-act button:not(.pri){
background:var(--fill);color:var(--ink);font-weight:500}
.btn:active,#ordCard .oc-act button:active{transform:none;opacity:.75}
#ordCard .oc-act button.pri{background:var(--mark);color:#fff}
.del,.hrow .x,.orow .ib{border:0;background:var(--fill);color:var(--ink-70);border-radius:8px}
.del:hover,.hrow .x:hover{background:rgba(255,59,48,.14);color:var(--alert)}
.orow .ib:hover{background:rgba(118,118,128,.20);color:var(--ink)}
.lnk{color:var(--steel);text-decoration:none;font-weight:500}
.lnk:hover{text-decoration:underline}
/* ---------- карточки и списки ---------- */
.calc,.ocard,.hist,.cfgbox,.stsum,.rank,.chart,.kpi,.it{
border:0;border-radius:var(--r);background:var(--card);box-shadow:var(--shadow);overflow:hidden}
.calc-head,.ohead{
background:var(--card);color:var(--ink);
border-bottom:.5px solid var(--grain);padding:14px 16px}
.calc-head .t,.ohead .t{
text-transform:none;letter-spacing:-.022em;font-size:19px;font-weight:600;color:var(--ink)}
.calc-head .s,.ohead .s{color:var(--ink-70);letter-spacing:-.005em;font-size:13px}
.rows{border-top:0;background:var(--card);border-radius:var(--r);
box-shadow:var(--shadow);overflow:hidden;margin-top:8px}
.row{padding:12px 14px;border-bottom:.5px solid var(--grain)}
.row:last-child{border-bottom:0}
.row:hover{background:rgba(118,118,128,.06)}
.row[aria-current="true"]{background:var(--mark-soft);border-bottom-color:var(--grain)}
.row[aria-current="true"] .sz i,.row[aria-current="true"] .meta,
.row[aria-current="true"] .pc small,.row[aria-current="true"] .badge{color:var(--ink-70)}
.row .sz{letter-spacing:-.02em;font-weight:600}
.badge{border-radius:6px;border:0;background:var(--fill);padding:2px 7px 3px;
letter-spacing:0;font-size:11px;font-weight:500;text-transform:none}
.b1{color:var(--ok)}.b2{color:var(--steel)}.b3{color:#B44400}
.oi,.hrow,.strow,.mrow,#ordCard .it .r{border-bottom:.5px solid var(--grain)}
.oi .no{border:0;background:var(--fill);border-radius:6px;color:var(--ink-70)}
.osum,.cl{background:transparent;border-bottom:.5px solid var(--grain)}
.res-row{border-bottom:0}
.strow.low{background:rgba(255,59,48,.07)}
.warn{border-left:0;border-radius:var(--r-sm);background:rgba(255,59,48,.09);
color:#B3261E;padding:11px 13px}
/* итоговые плашки */
.total,.ototal{background:var(--mark);color:#fff;border-top:0}
.total .lbl,.ototal .lbl{
text-transform:none;letter-spacing:-.01em;font-weight:500;color:rgba(255,255,255,.9);font-size:15px}
.total .val,.ototal .val{color:#fff;font-weight:600;letter-spacing:-.025em}
/* сечение доски — фирменный элемент, оставляем */
.scale-box{border:0;background:var(--fill);border-radius:var(--r-sm);
background-image:repeating-linear-gradient(to right,var(--grain) 0 1px,transparent 1px 11px),
repeating-linear-gradient(to bottom,var(--grain) 0 1px,transparent 1px 11px)}
.board{border-radius:3px}
/* заказы */
.orow{padding:12px 14px;border-bottom:.5px solid var(--grain);border-left:0!important}
.orow::before{content:"";width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:var(--grain-2)}
.orow.st-new::before{background:var(--steel)}
.orow.st-paid::before{background:var(--ok)}
.orow.st-ready::before{background:var(--mark)}
.orow.st-shipped::before{background:var(--grain-2)}
.orow.st-cancel::before{background:var(--alert)}
.orow.st-shipped,.orow.st-cancel{opacity:1}
.orow.st-shipped .a,.orow.st-cancel .a{color:var(--ink-70)}
.orow select,.strow select{
border:0;background:var(--fill);border-radius:8px;color:var(--ink);
font-size:13px;font-weight:500;padding:7px 9px}
/* аналитика */
.kpi{padding:14px 16px 16px}
.kpi::before{display:none}
.kpi .v{letter-spacing:-.025em;font-weight:600;font-size:23px}
.bars .b i{border:0;border-radius:5px 5px 0 0;background:var(--mark)}
.bars .b.now i{background:#B44400}
.bars{border-bottom:.5px solid var(--grain-2)}
.rank .rr{border-top:.5px solid var(--grain)}
.rank .rr .fill{background:var(--mark);opacity:.12}
.split{border:0;border-radius:999px;height:10px}
.ana-empty{border:0;background:var(--card);border-radius:var(--r);box-shadow:var(--shadow)}
/* карточка заказа */
#ordSheet{background:rgba(0,0,0,.32);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px)}
#ordCard{border-radius:16px 16px 0 0;box-shadow:0 -8px 40px rgba(0,0,0,.22)}
@media (min-width:760px){#ordCard{border-radius:16px}}
#ordCard .oc-top{background:var(--card);color:var(--ink);border-bottom:.5px solid var(--grain);padding:16px 18px}
#ordCard .oc-top .n{text-transform:none;letter-spacing:-.024em;font-size:20px;font-weight:600}
#ordCard .oc-top .dt{color:var(--ink-70);font-size:13px;letter-spacing:-.005em}
#ordCard .oc-top .st{
border:0;background:var(--fill);color:var(--ink);
text-transform:none;letter-spacing:-.01em;font-size:12.5px;font-weight:500;padding:5px 11px}
#ordCard .oc-x{border:0;background:var(--fill);color:var(--ink-70);border-radius:999px;width:30px;height:30px}
#ordCard .oc-x:hover{background:rgba(118,118,128,.2);color:var(--ink)}
#ordCard .oc-tape{display:none}
#ordCard .f{border-bottom:.5px solid var(--grain)}
#ordCard .tot{border-top:.5px solid var(--grain-2)}
#ordCard .tot .grandline{background:var(--mark);border-radius:var(--r-sm);margin:12px 0 0;padding:13px 16px}
#ordCard .tot .grandline span{
text-transform:none;letter-spacing:-.01em;font-size:15px;font-weight:500;color:rgba(255,255,255,.9)}
#ordCard .tot .grandline b{color:#fff;letter-spacing:-.025em}
#ordCard .oc-act{border-top:.5px solid var(--grain)}
/* экран входа */
#authGate{background:var(--paper)}
#authGate h2{text-transform:none;letter-spacing:-.026em;font-size:28px;font-weight:600}
#authGate .agSub{font-size:15px}
#authGate label{font-size:13px;font-weight:500}
#authGate button{
background:var(--mark);border-radius:var(--r-sm);font-size:17px;font-weight:600;
letter-spacing:-.01em;padding:14px}
#authGate button:active{opacity:.8}
#syncBar{background:rgba(28,28,30,.86);border-radius:10px;font-weight:500;letter-spacing:-.01em;
backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
.toast{border-radius:var(--r-sm);background:rgba(28,28,30,.90);letter-spacing:-.01em;
backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
/* мобильная нижняя панель — как таб-бар iOS */
@media (max-width:900px){
#rail{background:rgba(249,249,251,.86);border-top:.5px solid var(--grain-2)}
.rail-nav{padding:0}
.rail-nav button{border-radius:0;font-size:11px;font-weight:500;letter-spacing:-.01em}
.rail-nav button[aria-pressed="true"]{background:transparent;color:var(--mark);font-weight:600}
.rail-nav button::before{display:block;background:transparent}
.rail-nav button[aria-pressed="true"]::before{background:var(--mark)}
#userBox{border-left:.5px solid var(--grain-2)}
}
/* печать: возвращаем строгий чёрно-белый вид документов */
@media print{
.calc,.ocard,.hist,.cfgbox{box-shadow:none;border:1px solid #000}
.total,.ototal{background:#fff;color:#000;border-top:2px solid #000}
.total .lbl,.ototal .lbl,.total .val,.ototal .val{color:#000}
}
/* ==========================================================
ТЁМНАЯ ТЕМА
Значение data-theme на <html> ставит скрипт в шапке документа
========================================================== */
html[data-theme="dark"]{
color-scheme:dark;
--paper:#000000;
--card:#1C1C1E;
--card-2:#2C2C2E;
--ink:#FFFFFF;
--ink-70:#98989F;
--grain:rgba(84,84,88,.42);
--grain-2:rgba(84,84,88,.68);
--mark:#FF7A0A;
--mark-soft:rgba(255,122,10,.26);
--steel:#0A84FF;
--alert:#FF453A;
--ok:#30D158;
--fill:rgba(118,118,128,.24);
--shadow:0 1px 2px rgba(0,0,0,.5);
}
/* полупрозрачные панели становятся тёмными */
html[data-theme="dark"] header,
html[data-theme="dark"] #rail,
html[data-theme="dark"] .grand{
background:rgba(28,28,30,.78);
border-color:var(--grain-2)}
html[data-theme="dark"] #rail{border-right-color:var(--grain-2)}
/* акцентный текст в тёмной теме светлее */
html[data-theme="dark"] .rail-nav button[aria-pressed="true"]{color:var(--mark)}
html[data-theme="dark"] .b3{color:#FFB340}
/* карточки чуть светлее фона, как в iOS */
html[data-theme="dark"] .calc,
html[data-theme="dark"] .ocard,
html[data-theme="dark"] .hist,
html[data-theme="dark"] .cfgbox,
html[data-theme="dark"] .stsum,
html[data-theme="dark"] .rows,
html[data-theme="dark"] .rank,
html[data-theme="dark"] .chart,
html[data-theme="dark"] .kpi,
html[data-theme="dark"] .ana-empty,
html[data-theme="dark"] #ordCard .it{background:var(--card)}
html[data-theme="dark"] .calc-head,
html[data-theme="dark"] .ohead,
html[data-theme="dark"] #ordCard .oc-top{background:var(--card)}
html[data-theme="dark"] #ordCard{background:var(--paper)}
/* выбранный сегмент — приподнятая тёмная пилюля */
html[data-theme="dark"] .tabs button[aria-selected="true"],
html[data-theme="dark"] .mode button[aria-pressed="true"],
html[data-theme="dark"] .seg button[aria-pressed="true"],
html[data-theme="dark"] #anaSection .ana-period button[aria-pressed="true"]{
background:var(--card-2);box-shadow:0 1px 3px rgba(0,0,0,.55)}
/* поля ввода и точечные фоны */
html[data-theme="dark"] .search:focus,
html[data-theme="dark"] .cgrid input:focus,
html[data-theme="dark"] input:focus,
html[data-theme="dark"] #authGate input:focus{
background:var(--card-2);box-shadow:0 0 0 3px rgba(255,122,10,.34),0 0 0 1px var(--mark)}
html[data-theme="dark"] .row:hover{background:rgba(118,118,128,.14)}
html[data-theme="dark"] .warn{background:rgba(255,69,58,.16);color:#FF8A80}
html[data-theme="dark"] .strow.low{background:rgba(255,69,58,.12)}
html[data-theme="dark"] .del:hover,
html[data-theme="dark"] .hrow .x:hover{background:rgba(255,69,58,.20)}
/* всплывающие подсказки на тёмном фоне должны быть светлее фона */
html[data-theme="dark"] #syncBar,
html[data-theme="dark"] .toast{background:rgba(72,72,74,.92);color:#fff}
html[data-theme="dark"] #ordSheet{background:rgba(0,0,0,.55)}
/* сечение доски: рамку делаем темнее, сама доска остаётся деревянной */
html[data-theme="dark"] .board{border-color:#6B5227}
/* печать всегда на белом, независимо от темы */
@media print{
html[data-theme="dark"]{--paper:#fff;--card:#fff;--ink:#000;--ink-70:#333;
--grain:rgba(0,0,0,.2);--grain-2:rgba(0,0,0,.4)}
html[data-theme="dark"] body{background:#fff;color:#000}
}
/* ---- переключатель оформления в настройках ---- */
#themeBox .seg button{padding:9px 6px}
/* непрозрачная подложка выпадающих списков в тёмной теме */
html[data-theme="dark"]{--fill-solid:#2C2C2E}
html[data-theme="dark"] select,
html[data-theme="dark"] select option,
html[data-theme="dark"] select optgroup{
background-color:#2C2C2E;color:#fff}
/* ==========================================================
ИСПРАВЛЕНИЯ ПОСЛЕ ПРОВЕРКИ НА ТЕЛЕФОНЕ
========================================================== */
/* ---- панели плотные, без размытия: сквозь них не должен просвечивать контент ---- */
header,#rail,.grand{
backdrop-filter:none!important;-webkit-backdrop-filter:none!important}
header{background:var(--card);border-bottom:.5px solid var(--grain-2)}
#rail{background:var(--card)}
.grand{background:var(--card)}
html[data-theme="dark"] header,
html[data-theme="dark"] #rail,
html[data-theme="dark"] .grand{background:#1C1C1E}
/* ---- шапка: только название, в одну строку ---- */
header .sub{display:none}
.head-in{padding:11px 16px;align-items:center;gap:10px;justify-content:space-between}
header h1{
font-size:19px;font-weight:600;letter-spacing:-.02em;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1 1 auto}
/* ---- карточка пользователя в шапке: без рамок и подложек ---- */
#userBox{background:transparent;border:0!important;box-shadow:none}
/* ---- две нижние полосы стыкуются без щели ---- */
@media (max-width:900px){
#rail{height:calc(56px + env(safe-area-inset-bottom))}
.grand{bottom:calc(56px + env(safe-area-inset-bottom))}
body{padding-bottom:calc(124px + env(safe-area-inset-bottom))}
.toast{bottom:calc(132px + env(safe-area-inset-bottom))}
#syncBar{bottom:calc(122px + env(safe-area-inset-bottom))}
.grand-in{padding-top:9px;padding-bottom:9px}
}
/* ---- выравнивание по вертикали ---- */
.tabs button,.mode button,.seg button,#anaSection .ana-period button{
display:flex;align-items:center;justify-content:center;
min-height:42px;line-height:1.2;text-align:center}
.chip{display:inline-flex;align-items:center;min-height:36px;line-height:1}
.badge{display:inline-flex;align-items:center;padding:3px 8px;line-height:1.3}
.grand-in{align-items:center}
.grand .lbl,.grand .val{display:flex;align-items:center}
.rail-nav button{display:flex;align-items:center;justify-content:center}
.stepper button,.qedit button,.strow .in,.del,.hrow .x,.orow .ib{
display:inline-flex;align-items:center;justify-content:center}
.row .pc,.oi .sum,.strow .cub{display:flex;flex-direction:column;justify-content:center}
.kpi .v,.stsum .v{line-height:1.15}
/* ---- поля не масштабируются: 16px не даёт iOS увеличивать страницу при вводе ---- */
input,select,textarea{font-size:16px}
/* ---- ряды кнопок не должны вылезать за экран ---- */
.actions,.ocard .actions{flex-wrap:wrap}
.btn{min-width:0;flex:1 1 150px;font-size:14.5px;
white-space:normal;overflow-wrap:break-word;line-height:1.25}
.btn.wide{flex:1 1 100%}
#ordCard .oc-act button{min-width:0;font-size:15px}
/* ---- поля в сетке «клиент и доставка» не наезжают друг на друга ---- */
.cgrid,.custom-grid{min-width:0}
.cgrid .f,.custom-grid>*{min-width:0}
.cgrid input,.custom-grid input{min-width:0;width:100%;max-width:100%}
input[type=date],input[type=time]{
-webkit-appearance:none;appearance:none;min-width:0;width:100%;max-width:100%}
select{-webkit-appearance:none;appearance:none;
background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
linear-gradient(135deg,currentColor 50%,transparent 50%);
background-position:calc(100% - 15px) 52%,calc(100% - 10px) 52%;
background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:28px}
/* ---- на узких экранах подписи разделов не должны переноситься ---- */
@media (max-width:900px){
.rail-nav button{font-size:11px;white-space:nowrap}
}
@media (max-width:390px){
.rail-nav button{font-size:10px}
}
/* ---- экран входа берёт цвета из темы ---- */
#authGate{background:var(--paper)}
#authGate h2{color:var(--ink)}
#authGate .agSub,#authGate label{color:var(--ink-70)}
#authGate input{background:var(--fill);border:0;color:var(--ink)}
#authGate button{background:var(--mark);color:#fff}
#agErr{color:var(--alert)}
html[data-theme="dark"] #authGate input{background:var(--fill);color:#fff}
/* ---- карточка заказа берёт цвета из темы ---- */
#ordCard{background:var(--paper)}
#ordCard .oc-body{color:var(--ink)}
#ordCard .f .k{color:var(--ink-70)}
#ordCard .f .v a{color:var(--steel)}
#ordCard .it .r .sub,#ordCard .it .r .q small{color:var(--ink-70)}
/* ---- разделы в боковой панели выравниваются по левому краю ---- */
@media (min-width:901px){
.rail-nav button{justify-content:flex-start;text-align:left;padding-left:14px}
}
/* ---- шаг количества: аккуратные одинаковые элементы ---- */
.stepper button{width:42px;height:44px;font-size:20px;flex:0 0 auto}
.qedit button{width:32px;height:32px;font-size:17px;flex:0 0 auto}
.qedit input{width:64px;flex:0 0 auto;text-align:center;font-size:15px;padding:6px 4px}
.strow input{width:76px;flex:0 0 auto}
.qty-line input[type=number]{min-width:0}
/* ---- флажки и переключатели в акцентном цвете ---- */
input[type=checkbox],input[type=radio],.svc input,.adj input[type=checkbox]{
accent-color:var(--mark);width:20px;height:20px;flex:0 0 auto}
/* ---- печать чека переехала в раздел «Заказы» ---- */
#docPrint{display:none!important}
/* ---- кнопка печати в строке заказа ---- */
.orow .ib.prn svg{display:block}
.orow .ib{width:30px;height:30px;flex:0 0 auto}
#ordCard .oc-act button.prn{flex:0 0 auto;padding:13px 15px}
/* ---- переключатель «штуки / кубы / площадь» не наезжает на строку длины ---- */
.mode{margin:12px 14px 0}
.tabs{margin-top:2px}
/* ---- кнопки маршрута на телефоне занимают всю ширину ---- */
@media (max-width:640px){
.actions .btn{flex:1 1 100%!important}
}
/* ---- подписи разделов в настройках не липнут к предыдущей карточке ---- */
#cfgSection .eyebrow{margin-top:24px}
#cfgSection>.eyebrow:first-child,#themeBox>.eyebrow{margin-top:0}
#themeBox{margin-bottom:4px}
.cfgbox{overflow:visible}
/* ---- в боковой панели только название ---- */
.rb-txt span{display:none}
.rail-brand{padding:20px 16px 12px}
/* ---- счётчик в кружке строго по центру ---- */
.vb,.rail-nav .vb,.viewbar .vb{
display:inline-flex;align-items:center;justify-content:center;
min-width:20px;height:20px;padding:0 6px;border-radius:999px;
font-size:11.5px;font-weight:600;line-height:1;box-sizing:border-box}
.vb:empty{display:none}
/* ---- поля в сетке выравниваются по нижнему краю ----
подписи бывают в одну и в две строки, из-за этого поля прыгали по высоте */
.cgrid .f{justify-content:flex-end}
.cgrid label{line-height:1.3}
/* ---- список прайса докручивается до конца ----
снизу его перекрывала плашка с итогом, последние позиции были недоступны */
@media (min-width:1216px){
#priceSection{max-height:calc(100vh - 104px);padding-bottom:24px}
}
/* ---- календарь доставок занимает всю ширину содержимого ---- */
@media (min-width:1216px){ #dlvSection{grid-column:1 / -1;margin-top:22px} }
/* ---- выпадающие списки: непрозрачный фон, иначе системный список
отрисовывается белым, а текст остаётся светлым и не читается ---- */
:root{--fill-solid:#EAEAEF}
select,.orow select,.strow select{
background-color:var(--fill-solid);
color:var(--ink);
border:0}
select option,select optgroup{
background-color:var(--fill-solid);
color:var(--ink)}
/* ---- текст на оранжевом всегда белый и плотный: так он читается лучше ---- */
.btn,.grand button,#authGate button,#ordCard .oc-act button.pri,
.chip[aria-pressed="true"],.day-head .today{color:#fff;font-weight:600}
.total .lbl,.ototal .lbl,#ordCard .tot .grandline span{color:#fff}
.total .val,.ototal .val,#ordCard .tot .grandline b{color:#fff}
/* ---- подсветка пунктов меню: старое правило задавало белый текст,
в светлой теме он пропадал на светлом фоне ---- */
.rail-nav button:hover,.rail-nav button:focus-visible{color:var(--ink)}
.rail-nav button[aria-pressed="true"]:hover{color:inherit}
html[data-theme="dark"] .rail-nav button:hover{background:rgba(255,255,255,.07)}
/* ---- на телефоне каждый заказ отдельной карточкой: волосяной разделитель
на чёрном фоне почти не читался ---- */
@media (max-width:900px){
.hist{background:transparent;box-shadow:none;border-radius:0;overflow:visible}
.orow{
background:var(--card);border-radius:var(--r);box-shadow:var(--shadow);
border-bottom:0;margin-bottom:10px;padding:13px 14px}
.orow:last-child{margin-bottom:0}
.hrow{background:var(--card);border-radius:var(--r);box-shadow:var(--shadow);
border-bottom:0;margin-bottom:8px}
/* дни в календаре доставок остаются едиными карточками */
.hist.day{background:var(--card);box-shadow:var(--shadow);border-radius:var(--r);margin-bottom:14px}
.hist.day .trip{margin:0;border-radius:0;box-shadow:none;background:transparent}
}
/* ---- фирменный знак в боковой панели ---- */
.rb-mark{
/* картинка и цвет фона теперь задаются в более поздних слоях (forest.css/yandex.css);
здесь остаётся только скрытие текстовой буквы-заглушки внутри */
font-size:0;color:transparent}
/* ==========================================================
ФИРМЕННЫЙ СЛОЙ — «Лесопилка»
Расширяет системную Apple-палитру до трёх фирменных цветов:
огонь пилорамы (акцент), хвоя, свежий срез. Плюс текстура,
типографика с характером и лёгкое движение.
========================================================== */
:root{
--forest:#1F6B3A; /* хвойный зелёный — вторичный акцент */
--forest-soft:rgba(31,107,58,.14);
--sawcut:#C68A46; /* цвет свежего среза — тёплые нейтральные акценты */
--sawcut-soft:rgba(198,138,70,.16);
}
html[data-theme="dark"]{
--forest:#3DA35D;
--forest-soft:rgba(61,163,93,.20);
--sawcut:#D9A05E;
--sawcut-soft:rgba(217,160,94,.20);
}
/* ---------- типографика с характером ---------- */
header h1{font-weight:700;letter-spacing:-.03em}
.rb-txt b{font-weight:700;letter-spacing:-.025em}
.eyebrow,.group-label{font-weight:600;letter-spacing:.03em}
.calc-head .t,.ohead .t{font-weight:700;letter-spacing:-.026em}
.kpi .v,.stsum .v{font-weight:700;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
.total .val,.ototal .val,#ordCard .tot .grandline b{font-weight:700;letter-spacing:-.03em}
.grand .val{font-weight:700;letter-spacing:-.028em}
#authGate h2{font-weight:700;letter-spacing:-.03em}
.day-head .dt b{font-weight:700;letter-spacing:-.025em}
/* ---------- вторичный акцент: хвойный зелёный там, где сейчас нейтральный синий/бирюзовый ---------- */
.b2{color:var(--forest)}
.badge.b2{background:var(--forest-soft);color:var(--forest)}
.trip.st-paid .dot,.orow.st-paid::before{background:var(--forest)}
.day-head .today{background:var(--forest)}
.bars .b i{background:var(--forest-soft);border:1px solid var(--forest)}
.bars .b.now i{background:var(--forest)}
.split i:nth-child(2){background:var(--forest)!important}
.split-leg em:nth-child(1){background:var(--forest)}
/* ---------- цвет свежего среза: нейтральные разделы, где раньше был серый акцент ---------- */
.b3{color:var(--sawcut)}
.badge.b3{background:var(--sawcut-soft);color:var(--sawcut)}
.rank .rr .fill{background:var(--sawcut);opacity:.16}
.split i:nth-child(3){background:var(--sawcut)!important}
.split-leg em:nth-child(3){background:var(--sawcut)}
/* ---------- текстура свежего среза: едва заметные годовые кольца ---------- */
.rail-brand{
position:relative;overflow:hidden;
background:
repeating-radial-gradient(circle at 18% 35%,
rgba(198,138,70,.16) 0 2px, transparent 2px 7px),
linear-gradient(135deg,var(--sawcut-soft),transparent 60%);
}
html[data-theme="dark"] .rail-brand{
background:
repeating-radial-gradient(circle at 18% 35%,
rgba(217,160,94,.10) 0 2px, transparent 2px 7px),
linear-gradient(135deg,rgba(217,160,94,.10),transparent 60%);
}
/* тонкая текстура на итоговой плашке — акцент как торец бруса */
.grand{position:relative}
.grand::before{
content:"";position:absolute;inset:0;pointer-events:none;
background:repeating-linear-gradient(90deg,
rgba(0,0,0,.025) 0 1px, transparent 1px 14px);
mix-blend-mode:multiply}
html[data-theme="dark"] .grand::before{
background:repeating-linear-gradient(90deg,
rgba(255,255,255,.03) 0 1px, transparent 1px 14px);
mix-blend-mode:screen}
/* ---------- лёгкое движение ---------- */
@media (prefers-reduced-motion: no-preference){
.calc,.ocard,.hist,.cfgbox,.stsum,.rank,.chart,.kpi,.it,
#dlvSection .stsum,.day{
animation:riseIn .32s cubic-bezier(.22,.8,.32,1) both}
.kpi:nth-of-type(2){animation-delay:.03s}
.kpi:nth-of-type(3){animation-delay:.06s}
.kpi:nth-of-type(4){animation-delay:.09s}
@keyframes riseIn{
from{opacity:0;transform:translateY(6px)}
to{opacity:1;transform:none}
}
.btn,.grand button,#authGate button,.chip,.rail-nav button,
.tabs button,.mode button,.seg button,.orow .ib,.trip,.row{
transition:transform .12s ease,background-color .15s ease,box-shadow .15s ease}
.btn:active,.grand button:active,#authGate button:active,
.chip:active,.orow .ib:active{transform:scale(.96)}
.row:active,.trip:active{transform:scale(.995)}
.badge,.vb{transition:transform .18s ease}
#syncBar.on{animation:popIn .2s ease both}
@keyframes popIn{from{opacity:0;transform:translateY(4px) scale(.97)}to{opacity:1;transform:none}}
}
/* ---------- фокус в фирменном цвете вместо системного синего ---------- */
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible{
outline:2px solid var(--mark);outline-offset:2px}
@media print{
.rail-brand,.grand::before{background:none!important}
*{animation:none!important;transition:none!important}
}
/* ==========================================================
ТЕМА «СТВОЛ И ХВОЯ»
Полная замена палитры: тёплый ствол дерева, глубокая хвоя,
светлое полотно вместо системной Apple-палитры.
Грузится последним слоем и переопределяет токены — почти
весь интерфейс уже построен на var(), поэтому перекрашивается
сам; здесь же — структурная перестройка шапки/панели и текстуры.
========================================================== */
:root{
--paper:#F6F1E7; /* светлое полотно, тёплый белый */
--card:#FFFFFF;
--ink:#2A2018; /* тёмный, почти чёрный ствол */
--ink-70:#6E5D4C;
--grain:rgba(42,32,24,.10);
--grain-2:rgba(42,32,24,.20);
--mark:#1B4B34; /* глубокая хвоя — главный акцент */
--mark-soft:rgba(27,75,52,.14);
--bark:#8A5A34; /* тёплый акцент «дерево» (бейджи, подсветки) */
--bark-soft:rgba(138,90,52,.13);
--trunk:#3B2A1C; /* сама панель — тёмный ствол, отдельно от акцента */
--trunk-ink:#F5EADA;
--trunk-ink-70:rgba(245,234,218,.66);
--trunk-line-lt:rgba(0,0,0,.14); /* текстура на светлом стволе */
--trunk-line-dk:rgba(0,0,0,.08);
--sawcut:#B98544; /* светлый срез — третий тон */
--sawcut-soft:rgba(185,133,68,.20);
--forest:#256B45; /* чуть светлее хвои — для графиков/статусов */
--forest-soft:rgba(37,107,69,.14);
--steel:#8C6A4F; /* вместо системного синего — тёплый бурый */
--alert:#A6402E; /* кирпичный, а не аптечный красный */
--ok:#1B4B34;
--fill:rgba(111,69,38,.08);
--fill-solid:#EDE6D6;
--shadow:0 1px 2px rgba(42,32,24,.06), 0 8px 20px rgba(42,32,24,.07);
}
html[data-theme="dark"]{
--paper:#1B1512;
--card:#241C16;
--ink:#F3ECDF;
--ink-70:#B9A88F;
--grain:rgba(230,214,190,.14);
--grain-2:rgba(230,214,190,.24);
--mark:#3DA35D;
--mark-soft:rgba(61,163,93,.20);
--bark:#C99A6A;
--bark-soft:rgba(201,154,106,.18);
--trunk:#332417; /* ствол в темноте: тёмный, но заметно отдельный от фона */
--trunk-ink:#F5EADA;
--trunk-ink-70:rgba(245,234,218,.62);
--trunk-line-lt:rgba(245,234,218,.05); /* текстура едва светлее фона */
--trunk-line-dk:rgba(0,0,0,.20);
--sawcut:#E0BD84;
--sawcut-soft:rgba(224,189,132,.20);
--forest:#54B579;
--forest-soft:rgba(84,181,121,.20);
--steel:#C9A97D;
--alert:#E2604A;
--ok:#3DA35D;
--fill:rgba(230,214,190,.10);
--fill-solid:#3A2E22;
--shadow:0 1px 2px rgba(0,0,0,.5);
}
/* ---------- текстура полотна: едва заметный холст ---------- */
body{
background-color:var(--paper);
background-image:
repeating-linear-gradient(0deg, rgba(111,69,38,.025) 0 1px, transparent 1px 3px),
repeating-linear-gradient(90deg, rgba(111,69,38,.02) 0 1px, transparent 1px 3px);
}
html[data-theme="dark"] body{
background-image:
repeating-linear-gradient(0deg, rgba(230,214,190,.02) 0 1px, transparent 1px 3px),
repeating-linear-gradient(90deg, rgba(230,214,190,.015) 0 1px, transparent 1px 3px);
}
/* ---------- боковая панель и мобильная шапка: цвет ствола ----------
Текстура волокон идёт по всей высоте панели, а не только в верхнем блоке —
иначе низ выглядел плоской заливкой без «материала». Направление и толщина
линий чуть разные в двух слоях градиента, чтобы не читаться как узор. */
header,#rail{
background-color:var(--trunk)!important;
background-image:
repeating-linear-gradient(91deg, var(--trunk-line-lt) 0 1px, transparent 1px 6px),
repeating-linear-gradient(89deg, var(--trunk-line-dk) 0 2px, transparent 2px 11px)!important;
border-color:rgba(0,0,0,.22)!important;
backdrop-filter:none!important;-webkit-backdrop-filter:none!important}
header h1,.rb-txt b{color:var(--trunk-ink)}
#userBox .uName{color:var(--trunk-ink)}
#userBox .uMail{color:var(--trunk-ink-70)}
#logoutBtn{border-color:rgba(245,234,218,.26);color:var(--trunk-ink-70)}
#logoutBtn:hover{background:rgba(245,234,218,.14);color:var(--trunk-ink)}
.rb-mark{background:transparent url("data:image/png;base64,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") center/72% no-repeat}
/* нижний край плашки бренда — тонкая линия среза, отделяющая её от меню */
.rail-brand{border-bottom:1px solid rgba(0,0,0,.18)}
html[data-theme="dark"] .rail-brand{border-bottom-color:rgba(0,0,0,.35)}
/* пункты меню на стволе */
.rail-nav button{color:var(--trunk-ink-70)}
.rail-nav button:hover,.rail-nav button:focus-visible{
color:var(--trunk-ink);background:rgba(245,234,218,.09)}
.rail-nav button[aria-pressed="true"]{
background:rgba(245,234,218,.16);color:var(--trunk-ink);font-weight:600}
.rail-nav button[aria-pressed="true"]::before{background:var(--forest)}
html[data-theme="dark"] .rail-nav button:hover{background:rgba(245,234,218,.07)}
/* мерная лента вдоль панели — хвойная, с кремовыми насечками */
.rail-tape{display:block!important;background:var(--forest)}
.rail-tape::before{background:repeating-linear-gradient(to bottom,var(--trunk-ink) 0 1px,transparent 1px 10px);opacity:.55}
.rail-tape::after{background:repeating-linear-gradient(to bottom,var(--trunk-ink) 0 1px,transparent 1px 50px);opacity:.9}
#tapeMark{background:var(--trunk-ink)}
#tapeMark::after{background:var(--trunk)}
@media (max-width:900px){ .rail-tape{display:none!important} }
/* ---------- тексты и заголовки: чуть более «резное» начертание ---------- */
header h1,.calc-head .t,.ohead .t,#authGate h2,#ordCard .oc-top .n{
font-family:ui-serif,Georgia,"Times New Roman",serif;font-weight:700;letter-spacing:-.01em}
.eyebrow,.group-label{font-family:inherit;font-weight:600;letter-spacing:.04em}
/* ---------- кнопки и акценты: хвоя вместо оранжевого ---------- */
.btn,.grand button,#authGate button,#ordCard .oc-act button.pri{
background:var(--mark);color:#fff}
.btn.ghost,#ordCard .oc-act button:not(.pri){background:var(--fill);color:var(--ink)}
.chip[aria-pressed="true"]{background:var(--mark);color:#fff}
.rail-nav .vb{background:var(--alert)}
input:focus,select:focus,#authGate input:focus,
.search:focus,.cgrid input:focus{
background:var(--card);box-shadow:0 0 0 3px var(--mark-soft),0 0 0 1px var(--mark)}
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible{
outline:2px solid var(--mark)}
.tabs button[aria-selected="true"],.mode button[aria-pressed="true"],
.seg button[aria-pressed="true"],#anaSection .ana-period button[aria-pressed="true"]{
background:var(--card);color:var(--ink)}
.total,.ototal{background:var(--mark)}
#ordCard .tot .grandline{background:var(--mark)}
.day-head .today{background:var(--forest)}
.bars .b i{background:var(--mark-soft);border:1px solid var(--mark)}
.bars .b.now i{background:var(--mark)}
.split i:nth-child(1){background:var(--mark)!important}
.split i:nth-child(2){background:var(--forest)!important}
.split i:nth-child(3){background:var(--sawcut)!important}
.split-leg em:nth-child(1){background:var(--mark)}
.split-leg em:nth-child(2){background:var(--forest)}
.split-leg em:nth-child(3){background:var(--sawcut)}
.rank .rr .fill{background:var(--sawcut);opacity:.18}
.kpi::before{background:var(--mark)}
.b1{color:var(--mark)}
.b2{color:var(--forest)}
.badge.b2{background:var(--forest-soft);color:var(--forest)}
.b3{color:#8A6423}
.badge.b3{background:var(--sawcut-soft);color:#8A6423}
/* статусы заказов и рейсов доставки */
.trip.st-new .dot,.orow.st-new::before{background:var(--steel)}
.trip.st-paid .dot,.orow.st-paid::before{background:var(--forest)}
.trip.st-ready .dot,.orow.st-ready::before{background:var(--mark)}
.trip.st-cancel .dot,.orow.st-cancel::before{background:var(--alert)}
/* ---------- выпадающие списки: тёплый нейтральный фон ---------- */
select,.orow select,.strow select,
select option,select optgroup{background-color:var(--fill-solid);color:var(--ink)}
/* ---------- текстура среза на итоговой плашке остаётся, но в тон коры ---------- */
.grand::before{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05) 0 1px,transparent 1px 14px)}
html[data-theme="dark"] .grand::before{background:repeating-linear-gradient(90deg,rgba(255,255,255,.035) 0 1px,transparent 1px 14px)}
/* пилообразный край над нижней плашкой — как срез на пиле */
.grand{position:relative}
.grand::after{
content:"";position:absolute;left:0;right:0;top:-7px;height:7px;pointer-events:none;
background:
linear-gradient(135deg,var(--bark) 50%,transparent 50%) 0 0/14px 14px,
linear-gradient(-135deg,var(--bark) 50%,transparent 50%) 0 0/14px 14px;
background-repeat:repeat-x}
@media print{.grand::after{display:none}}
/* ---------- прочее ---------- */
#syncBar,.toast{background:rgba(42,32,24,.92);color:#fff}
html[data-theme="dark"] #syncBar,html[data-theme="dark"] .toast{background:rgba(180,131,90,.22);color:#fff}
.warn{background:rgba(166,64,46,.10);color:var(--alert)}
.strow.low{background:rgba(166,64,46,.08)}
@media print{
body{background-image:none}
header,#rail{background:#fff!important}
header h1{color:#000}
.total,.ototal{background:#fff;color:#000;border-top:2px solid #000}
}
/* ==========================================================
ТЕМА В СТИЛЕ ЯНДЕКСА
Плоские чистые поверхности, фирменный красный, крупные
скругления, иконки в навигации. Финальный слой — сбрасывает
декор предыдущей темы (дерево/текстуры) и красит по-новому.
========================================================== */
:root{
--paper:#F5F5F6;
--card:#FFFFFF;
--ink:#16161A;
--ink-70:#77777E;
--grain:rgba(20,20,25,.08);
--grain-2:rgba(20,20,25,.14);
--mark:#FC3F1D; /* фирменный красный — главный акцент */
--mark-soft:rgba(252,63,29,.10);
--steel:#0077FF; /* информационный синий */
--ok:#17B26A; /* успех / оплачен */
--alert:#D6001C; /* отмена / ошибка — темнее основного красного */
--forest:#17B26A;
--forest-soft:rgba(23,178,106,.12);
--bark:#B8860B;
--bark-soft:rgba(184,134,11,.12);
--sawcut:#B8860B;
--sawcut-soft:rgba(184,134,11,.14);
--fill:rgba(20,20,25,.05);
--fill-solid:#EFEFF1;
--shadow:0 1px 2px rgba(20,20,25,.04), 0 10px 26px rgba(20,20,25,.07);
--r:18px;
--r-sm:12px;
}
html[data-theme="dark"]{
--paper:#131316;
--card:#1C1C20;
--ink:#FFFFFF;
--ink-70:#9A9AA3;
--grain:rgba(255,255,255,.08);
--grain-2:rgba(255,255,255,.14);
--mark:#FF5B3B;
--mark-soft:rgba(255,91,59,.16);
--steel:#4DA3FF;
--ok:#28D07A;
--alert:#FF6B4A;
--forest:#28D07A;
--forest-soft:rgba(40,208,122,.16);
--bark:#E0B84A;
--bark-soft:rgba(224,184,74,.16);
--sawcut:#E0B84A;
--sawcut-soft:rgba(224,184,74,.16);
--fill:rgba(255,255,255,.08);
--fill-solid:#2A2A2F;
--shadow:0 1px 2px rgba(0,0,0,.5);
}
/* ---------- сброс декора предыдущей темы: плоско и чисто ---------- */
body{background-image:none!important}
.rail-brand{background:none!important}
.grand::before,.grand::after{display:none!important}
.rail-tape{display:none!important}
header h1,.calc-head .t,.ohead .t,#authGate h2,#ordCard .oc-top .n,.rb-txt b{
font-family:inherit!important;letter-spacing:-.02em}
/* ---------- панели: белая поверхность, не выделенный «материал» ---------- */
header,#rail{
background-color:var(--card)!important;
background-image:none!important;
border-color:var(--grain-2)!important;
backdrop-filter:saturate(180%) blur(20px)!important;
-webkit-backdrop-filter:saturate(180%) blur(20px)!important}
header h1,.rb-txt b{color:var(--ink);font-weight:800}
.rb-txt span{display:none}
#userBox .uName{color:var(--ink)}
#userBox .uMail{color:var(--ink-70)}
#logoutBtn{border-color:var(--grain-2);color:var(--ink-70);border-radius:999px}
#logoutBtn:hover{background:var(--fill);color:var(--ink)}
.rail-brand{border-bottom:1px solid var(--grain)}
.rb-mark{
background-color:var(--mark);
background-size:60%!important;
border-radius:12px;box-shadow:none}
/* ---------- навигация: иконка + подпись ---------- */
.rail-nav button{
display:flex;align-items:center;gap:12px;color:var(--ink-70);
border-radius:var(--r-sm);margin:0 8px;padding:11px 12px!important;
font-weight:600;
/* rail.css задаёт кнопке width:100% — с ненулевым margin это распирает
её за правый край панели (margin не вычитается из жёсткой width).
calc() честно учитывает оба отступа, не завися от auto-подсчёта flex. */
width:calc(100% - 16px)}
.rail-nav button .ico{flex:0 0 auto;display:flex;color:inherit}
.rail-nav button .lbl{display:flex;align-items:center;gap:6px;min-width:0}
.rail-nav button::before{display:none}
.rail-nav button:hover{background:var(--fill);color:var(--ink)}
.rail-nav button[aria-pressed="true"]{
background:var(--mark-soft);color:var(--mark);font-weight:700}
.rail-nav .vb{background:var(--mark);color:#fff}
@media (min-width:901px){
.rail-nav{gap:2px;padding:10px 0}
}
@media (max-width:900px){
.rail-nav button{flex-direction:column;gap:3px;margin:6px 4px;padding:6px 2px!important;border-radius:12px}
.rail-nav button .ico svg{width:21px;height:21px}
.rail-nav button .lbl{font-size:10.5px;font-weight:600;flex-direction:column;gap:0}
.rail-nav button[aria-pressed="true"]{background:transparent}
.rail-nav .vb{position:absolute;top:2px;right:14%;min-width:15px;height:15px;font-size:9.5px;padding:0 3px}
.rail-nav button{position:relative}
}
/* меню «Ещё»: иконка + подпись в ряд */
#moreSheet button{display:flex;align-items:center;gap:13px;border-radius:var(--r-sm);font-weight:600}
#moreSheet .ico{display:flex;color:var(--ink-70)}
#moreSheet button[aria-pressed="true"] .ico{color:var(--mark)}
#moreSheet button:hover{background:var(--fill)}
#moreSheet,#authGate{border-radius:20px 20px 0 0}
/* ---------- кнопки: крупные пилюли ---------- */
.btn,.grand button,#authGate button,#ordCard .oc-act button.pri,.chip{
border-radius:999px!important}
.btn,.grand button,#authGate button,#ordCard .oc-act button.pri{
background:var(--mark);color:#fff;font-weight:700}
.btn.ghost,#ordCard .oc-act button:not(.pri){background:var(--fill);color:var(--ink);font-weight:600}
.chip[aria-pressed="true"]{background:var(--mark);color:#fff}
.del,.hrow .x,.orow .ib{border-radius:999px}
/* сегменты и вкладки — крупнее радиус, чёрная (не красная) заливка выбранного —
так осмысленнее контрастирует с красными CTA-кнопками рядом */
.tabs,.mode,.seg,#anaSection .ana-period,#themeBox .seg{border-radius:14px}
.tabs button[aria-selected="true"],.mode button[aria-pressed="true"],
.seg button[aria-pressed="true"],#anaSection .ana-period button[aria-pressed="true"]{
background:var(--ink);color:var(--card);border-radius:10px}
html[data-theme="dark"] .tabs button[aria-selected="true"],
html[data-theme="dark"] .mode button[aria-pressed="true"],
html[data-theme="dark"] .seg button[aria-pressed="true"],
html[data-theme="dark"] #anaSection .ana-period button[aria-pressed="true"]{
background:var(--card);color:var(--ink)}
/* ---------- поля и карточки: крупнее скругление ---------- */
input,select,textarea{border-radius:var(--r-sm)!important}
.calc,.ocard,.hist,.cfgbox,.stsum,.rank,.chart,.kpi,.it,#ordCard,#authGate .agBox,
.day,.hist.day{border-radius:var(--r)!important}
.row:first-child,.oi:first-child{border-top-left-radius:var(--r);border-top-right-radius:var(--r)}
input:focus,select:focus,#authGate input:focus,.search:focus,.cgrid input:focus{
box-shadow:0 0 0 3px var(--mark-soft),0 0 0 1.5px var(--mark)}
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--mark)}
/* мобильные карточки заказов — тоже крупный радиус */
@media (max-width:900px){
.orow,.hrow{border-radius:var(--r)!important}
}
/* ---------- акценты по смыслу ---------- */
.total,.ototal{background:var(--mark)}
#ordCard .tot .grandline{background:var(--mark);border-radius:var(--r-sm)}
.day-head .today{background:var(--mark);border-radius:999px}
.kpi::before{background:var(--mark);border-radius:999px 0 0 999px}
.bars .b i{background:var(--mark-soft);border:0;border-radius:6px 6px 0 0}
.bars .b.now i{background:var(--mark)}
.split{border-radius:999px}
.split i:nth-child(1){background:var(--mark)!important}
.split i:nth-child(2){background:var(--ok)!important}
.split i:nth-child(3){background:var(--bark)!important}
.split-leg em:nth-child(1){background:var(--mark);border-radius:3px}
.split-leg em:nth-child(2){background:var(--ok);border-radius:3px}
.split-leg em:nth-child(3){background:var(--bark);border-radius:3px}
.rank .rr .fill{background:var(--mark);opacity:.10}
.b1{color:var(--mark)}
.b2{color:var(--ok)}
.badge.b2{background:var(--forest-soft);color:var(--ok)}
.b3{color:var(--bark)}
.badge.b3{background:var(--sawcut-soft);color:var(--bark)}
.badge{border-radius:999px}
.trip.st-new .dot,.orow.st-new::before{background:var(--steel)}
.trip.st-paid .dot,.orow.st-paid::before{background:var(--ok)}
.trip.st-ready .dot,.orow.st-ready::before{background:var(--mark)}
.trip.st-cancel .dot,.orow.st-cancel::before{background:var(--alert)}
select,.orow select,.strow select,select option,select optgroup{
background-color:var(--fill-solid);color:var(--ink);border-radius:var(--r-sm)}
#syncBar,.toast{background:var(--ink);color:var(--card);border-radius:999px}
html[data-theme="dark"] #syncBar,html[data-theme="dark"] .toast{background:var(--card);color:var(--ink);
box-shadow:0 4px 18px rgba(0,0,0,.5)}
.warn{background:rgba(214,0,28,.08);color:var(--alert);border-radius:var(--r-sm)}
.strow.low{background:rgba(214,0,28,.06)}
/* ---------- лёгкое движение остаётся, но без «резного» акцента ---------- */
@media (prefers-reduced-motion: no-preference){
.btn:active,.grand button:active,#authGate button:active,.chip:active{transform:scale(.95)}
}
@media print{
header,#rail{background:#fff!important;backdrop-filter:none!important}
header h1{color:#000}
.total,.ototal{background:#fff;color:#000;border-top:2px solid #000}
}
/* ---- «Свой размер»: был единственным разделом без карточки —
рядом с остальными белыми блоками читался как пустой ---- */
#customSection{
background:var(--card);border-radius:var(--r);box-shadow:var(--shadow);
padding:16px 16px 18px;margin-top:22px}
#customSection .custom-grid{margin-top:2px}
.res-row span:last-child{color:var(--ink);font-weight:700}
.res-row span:first-child{color:var(--ink-70)}
/* ---- активный пункт меню: в тёмной теме полупрозрачный красный поверх
тёмной панели давал грязный буро-коричневый оттенок — делаем
заливку плотнее, чтобы читался как чистый красный ---- */
html[data-theme="dark"] .rail-nav button[aria-pressed="true"]{
background:rgba(255,91,59,.22)}
.rail-nav button[aria-pressed="true"] .ico{color:var(--mark)}
/* ---- плашка «Итого заказ» должна быть прижата к низу экрана ----
Два более ранних слоя (brand.css, forest.css) ставили этому блоку
position:relative под декоративные псевдоэлементы, которые сама же
эта тема отключила — но position:relative остался и ломал верстку:
position:fixed у элемента и без relative прекрасно заякоряет
потомков с position:absolute, так что защитное правило было лишним
и только мешало. Возвращаем исходное поведение. */
.grand{position:fixed!important}
</style>
</head>
<body>
<script>document.body.classList.add("gated");</script>
<header>
<div class="head-in">
<h1>Лесопилка</h1>
<div class="sub">Расчёт заказа · версия 3.1</div>
</div>
</header>
<aside id="rail" class="no-print">
<div class="rail-brand">
<div class="rb-mark">Л</div>
<div class="rb-txt"><b>Лесопилка</b><span>Расчёт · 3.1</span></div>
</div>
<nav class="rail-nav" id="railNav">
<button id="viewCalc" aria-pressed="true">Расчёт</button>
<button id="viewOrders" aria-pressed="false">Заказы<span class="vb" id="ordBadge"></span></button>
<button id="viewStock" aria-pressed="false">Склад<span class="vb" id="stBadge"></span></button>
<button id="viewCfg" aria-pressed="false">Настройки</button>
</nav>
<div class="rail-tape" aria-hidden="true"><i id="tapeMark"></i></div>
<div id="userBox">
<div class="uAva" id="uAva">—</div>
<div class="uTxt">
<div class="uName" id="uName">—</div>
<div class="uMail" id="uMail">—</div>
</div>
<button id="logoutBtn" type="button">Выйти</button>
</div>
</aside>
<div class="tape" aria-hidden="true"></div>
<div class="wrap">
<section class="no-print" id="priceSection">
<div class="eyebrow">Прайс</div>
<div class="tabs" role="tablist">
<button role="tab" id="tabA" aria-selected="true">Естеств.<br>влажность</button>
<button role="tab" id="tabB" aria-selected="false">Сухой<br>строганный</button>
<button role="tab" id="tabC" aria-selected="false">Сухой матер.<br>1 сорт</button>
</div>
<input class="search" id="q" type="search" placeholder="Поиск: 50х150, брус, шпунт…" autocomplete="off">
<div class="chips" id="sorts"></div>
<div class="hint" id="listNote" style="padding:8px 2px 0"></div>
<div class="pdate no-print"><span>Прайс от <b id="priceDate">—</b></span>
<button class="lnk" id="priceReset" hidden>Сбросить правки цен</button></div>
<div id="list"></div>
</section>
<section id="calcWrap" class="no-print" hidden>
<div class="eyebrow">Расчёт</div>
<div class="calc">
<div class="calc-head">
<div>
<div class="t" id="cName">—</div>
<div class="s" id="cKind">—</div>
</div>
<div class="s" id="cPrice">—</div>
</div>
<div class="section-view">
<div class="dims" id="dims"></div>
<div class="scale-box"><div class="board" id="board"></div></div>
</div>
<div class="qty-line" style="padding:12px 14px 0;gap:8px;flex-wrap:wrap">
<label style="flex:0 0 auto">Длина</label>
<div class="chips" id="lens" style="margin:0"></div>
</div>
<div class="mode">
<button id="mPcs" aria-pressed="true">Штуки</button>
<button id="mM3" aria-pressed="false">Кубы</button>
<button id="mArea" aria-pressed="false">Площадь</button>
</div>
<div class="qty-line">
<label id="qLbl" for="qty">Штук</label>
<div class="stepper"><button id="minus" aria-label="Меньше">−</button></div>
<input type="number" id="qty" min="0" step="1" value="10" inputmode="decimal">
<div class="stepper"><button id="plus" aria-label="Больше">+</button></div>
</div>
<div class="result" id="res"></div>
<div id="warnBox"></div>
<div class="total"><span class="lbl">К оплате</span><span class="val" id="cTotal">0 ₽</span></div>
<div class="actions">
<button class="btn" id="add">Добавить в заказ</button>
<button class="btn ghost" id="copyOne">Скопировать</button>
</div>
<details style="margin:0 14px 14px">
<summary>Своя цена за м³</summary>
<div class="qty-line" style="padding:10px 0 2px">
<input type="number" id="ovr" placeholder="напр. 17000" inputmode="decimal">
<button class="btn ghost" id="ovrReset" style="flex:0 0 auto;padding:10px 14px">Сброс</button>
</div>
<div class="actions" style="padding:10px 0 0">
<button class="btn ghost" id="savePrice">Сохранить как цену прайса</button>
</div>
<div class="hint" style="padding:6px 0 0">Меняет цену только для текущего расчёта — удобно, когда торгуетесь.</div>
</details>
</div>
</section>
<section class="no-print" id="clientSection">
<div class="eyebrow">Клиент и доставка</div>
<div class="cgrid">
<div class="f full"><label for="fFio">ФИО клиента</label><input id="fFio" type="text" placeholder="Иванов Иван Иванович" autocomplete="name"></div>
<div class="f full"><label for="fPhone">Телефон</label><input id="fPhone" class="mono" type="tel" placeholder="+7 900 000-00-00" autocomplete="tel"></div>
<div class="f full"><label for="fAddr">Адрес доставки</label><input id="fAddr" type="text" placeholder="г. Городец, ул. Лесная, 14"></div>
<div class="f"><label for="fDate">Дата доставки</label><input id="fDate" type="date"></div>
<div class="f"><label for="fTimeFrom">Время доставки, с</label><input id="fTimeFrom" type="time"></div>
<div class="f"><label for="fTimeTo">Время доставки, до</label><input id="fTimeTo" type="time"></div>
<div class="f"><label for="fCar">Машина</label><input id="fCar" type="text" list="carList" placeholder="Камаз"></div>
<div class="f"><label for="fKm">Километраж, км</label><input id="fKm" class="mono" type="number" inputmode="decimal" placeholder="0"></div>
<div class="f"><label for="fRate">Тариф, ₽/км</label><input id="fRate" class="mono" type="number" inputmode="decimal" placeholder="0"></div>
<div class="f"><label for="fShip">Доставка, ₽</label><input id="fShip" class="mono" type="number" inputmode="decimal" placeholder="0"></div>
</div>
<datalist id="carList">
<option value="Газель"></option><option value="Газель 6 м"></option><option value="Камаз"></option>
<option value="Камаз с манипулятором"></option><option value="Фура 13,6 м"></option><option value="Самовывоз"></option>
</datalist>
<div class="actions" style="padding:12px 0 0">
<button class="btn" id="btnRoute" style="flex:0 0 auto;padding:10px 14px">Маршрут в Яндекс.Картах</button>
<button class="btn ghost" id="btnExtra" style="flex:0 0 auto;padding:10px 14px" aria-expanded="false">Дополнительно</button>
<button class="btn ghost" id="clearClient" style="flex:0 0 auto;padding:10px 14px">Очистить данные клиента</button>
</div>
<div id="extraBox" class="cgrid" style="margin-top:12px" hidden>
<div class="f full"><label for="fGift">В подарок клиенту (бесплатно, не входит в стоимость)</label>
<input id="fGift" type="text" placeholder="например: горбыль, опилки"></div>
</div>
<div class="hint" style="padding:8px 2px 0">Маршрут строится от базы: Санкт-Петербург, Грузовой проезд, 29.
Впишите километр из Яндекс.Карт — доставка посчитается как км × тариф. Чтобы маршрут шёл без платных дорог,
включите в Яндекс.Картах настройку «Избегать платных дорог» — она запомнится.</div>
<div class="hint" style="padding:6px 2px 0">ФИО и телефон уходят только в печать. В скопированном тексте их не будет — помечены точкой.</div>
</section>
<section class="no-print" id="stockSection" hidden>
<div class="eyebrow">Склад</div>
<div id="stSummary"></div>
<div class="actions" style="padding:12px 0 0;flex-wrap:wrap">
<button class="btn" id="stPrint">Печать ведомости</button>
<button class="btn ghost" id="stXls">Выгрузка в Excel</button>
</div>
<div class="tabs" style="margin-top:14px" role="tablist">
<button role="tab" id="stTabA" aria-selected="true">Естеств.<br>влажность</button>
<button role="tab" id="stTabB" aria-selected="false">Сухой<br>строганный</button>
<button role="tab" id="stTabC" aria-selected="false">Сухой матер.<br>1 сорт</button>
</div>
<input class="search" id="stQuery" type="search" placeholder="Поиск: 50х150, брус…" autocomplete="off">
<div class="chips" id="stFilters"></div>
<div id="stTable"></div>
<div class="eyebrow" style="margin-top:26px">Движения</div>
<div id="stMoves"></div>
</section>
<section class="no-print" id="cfgSection" hidden>
<div class="eyebrow">Резервная копия</div>
<div class="cfgbox">
<p class="cfgtext">Прайс-правки, склад, движения, история заказов и настройки лежат в этом браузере.
Чистка кэша, переустановка системы или поломка диска — и данных нет. Копия сохраняется одним файлом:
положите его на флешку или отправьте себе в Телеграм.</p>
<div class="cfgrow"><span id="cfgBackupInfo" class="cfghint">Копия ещё не делалась</span></div>
<div class="actions" style="padding:12px 0 0;flex-wrap:wrap">
<button class="btn" id="cfgSave">Сохранить базу в файл</button>
<button class="btn ghost" id="cfgLoad">Загрузить базу из файла</button>
<input type="file" id="cfgFile" accept=".json,application/json" hidden>
</div>
</div>
<div class="eyebrow" style="margin-top:26px">Основные параметры</div>
<div class="cfgbox">
<div class="cgrid">
<div class="f full"><label for="cfgBrand">Название в шапке и на печати</label><input id="cfgBrand" type="text" placeholder="Лесопилка"></div>
<div class="f full"><label for="cfgOrigin">Адрес базы, от которого считается маршрут</label><input id="cfgOrigin" type="text" placeholder="Санкт-Петербург, Грузовой проезд, 29"></div>
<div class="f"><label for="cfgRate">Тариф доставки, ₽/км</label><input id="cfgRate" class="mono" type="number" inputmode="decimal"></div>
<div class="f"><label for="cfgAnti">Антисептирование, ₽/м³</label><input id="cfgAnti" class="mono" type="number" inputmode="decimal"></div>
<div class="f"><label for="cfgDry">Камерная сушка, ₽/м³</label><input id="cfgDry" class="mono" type="number" inputmode="decimal"></div>
<div class="f"><label for="cfgDryAdd">Наценка сухого 1 сорта, ₽/м³</label><input id="cfgDryAdd" class="mono" type="number" inputmode="decimal"></div>
</div>
<label class="svc" style="padding:12px 0 0;border:0">
<input type="checkbox" id="cfgFit"><span>Подгонять печать под один лист А4</span></label>
<div class="cfghint">Наценка сухого пересчитывает третий прайс от первого. Цены услуг применяются к новым расчётам сразу.</div>
</div>
<div class="eyebrow" style="margin-top:26px">Реквизиты для документов</div>
<div class="cfgbox">
<div class="cgrid">
<div class="f full"><label for="rqName">Наименование продавца</label><input id="rqName" type="text" placeholder="ООО «Лесопилка» или ИП Лашков Л. Г."></div>
<div class="f"><label for="rqInn">ИНН</label><input id="rqInn" class="mono" type="text" inputmode="numeric"></div>
<div class="f"><label for="rqKpp">КПП</label><input id="rqKpp" class="mono" type="text" inputmode="numeric"></div>
<div class="f"><label for="rqOgrn">ОГРН / ОГРНИП</label><input id="rqOgrn" class="mono" type="text" inputmode="numeric"></div>
<div class="f full"><label for="rqAddr">Адрес</label><input id="rqAddr" type="text" placeholder="Санкт-Петербург, Грузовой проезд, 29"></div>
<div class="f"><label for="rqPhone">Телефон</label><input id="rqPhone" class="mono" type="text"></div>
<div class="f"><label for="rqPost">Должность подписанта</label><input id="rqPost" type="text" placeholder="Генеральный директор"></div>
<div class="f"><label for="rqBoss">ФИО подписанта</label><input id="rqBoss" type="text" placeholder="Лашков Леонид Германович"></div>
<div class="f"><label for="rqCity">Место составления</label><input id="rqCity" type="text" placeholder="Санкт-Петербург"></div>
</div>
<div class="cfghint">Эти данные печатаются в товарном чеке и акте приёма-передачи. Без наименования и ИНН документ не будет соответствовать требованиям к первичным документам.</div>
</div>
<div class="eyebrow" style="margin-top:26px">Очистка</div>
<div class="cfgbox danger">
<div class="actions" style="padding:0;flex-wrap:wrap">
<button class="btn ghost" id="cfgClrHist">Очистить историю заказов</button>
<button class="btn ghost" id="cfgClrStock">Очистить склад и движения</button>
<button class="btn ghost" id="cfgClrPrices">Сбросить правки цен</button>
</div>
<div class="cfghint">Действия необратимы. Перед очисткой сохраните базу в файл.</div>
</div>
</section>
<section id="orderSection">
<div class="eyebrow">Заказ</div>
<div class="ocard">
<div class="ohead">
<div style="min-width:0">
<div class="t">Расчёт заказа <span class="ordnum" id="oNum"></span></div>
<div class="otitle" id="oTitle"></div>
</div>
<div style="text-align:right;flex:0 0 auto">
<div class="odate" id="oDate"></div>
<div class="s" id="oCount">пусто</div>
</div>
</div>
<div id="client"></div>
<div id="order"></div>
<div id="services"></div>
<div class="adj no-print">
<div class="f"><label for="fAdv">Аванс, ₽</label><input type="number" id="fAdv" class="mono" inputmode="decimal" placeholder="0"></div>
<div class="f"><input type="checkbox" id="fRound"><label for="fRound">Округлить итог до сотен</label></div>
</div>
<div id="orderSum"></div>
<div class="ototal"><span class="lbl">Итого</span><span class="val" id="oTotal">0 ₽</span></div>
<div class="actions no-print">
<button class="btn" id="ordSave" disabled>Сохранить заказ</button>
<button class="btn ghost" id="copyOrder" disabled>Скопировать</button>
<button class="btn wide" id="docPrint" disabled title="Товарный чек и акт приёма-передачи — два листа с местом для подписей и печати">Печать чека и акта</button>
<button class="btn ghost" id="printOrder" disabled title="Масштаб подбирается так, чтобы заказ уместился на один лист А4">Печать</button>
<button class="btn ghost" id="clear" disabled>Очистить</button>
</div>
</div>
</section>
<section class="no-print" id="histSection">
<div class="eyebrow">Заказы</div>
<div id="ordSummary"></div>
<div class="chips" id="ordFilters" style="margin-top:12px"></div>
<input class="search" id="ordQuery" type="search" placeholder="Поиск: адрес, клиент, номер…" autocomplete="off">
<div id="histBox" style="margin-top:12px"></div>
<div class="actions" style="padding:12px 0 0;flex-wrap:wrap">
<button class="btn ghost" id="histXlsWeek" disabled>Excel за неделю</button>
<button class="btn ghost" id="histXlsMonth" disabled>Excel за месяц</button>
</div>
<div class="hint" style="padding:8px 2px 0">Склад списывается в момент перевода заказа в «Отгружен». До этого материал считается зарезервированным. Возврат статуса назад возвращает материал на склад.</div>
</section>
<section class="no-print" id="customSection">
<div class="eyebrow">Свой размер</div>
<div class="custom-grid">
<div><label for="ct">Толщ., мм</label><input type="number" id="ct" value="50" inputmode="numeric"></div>
<div><label for="cw">Шир., мм</label><input type="number" id="cw" value="150" inputmode="numeric"></div>
<div><label for="cl">Длина, мм</label><input type="number" id="cl" value="6000" inputmode="numeric"></div>
<div><label for="cp">₽ за м³</label><input type="number" id="cp" value="18500" inputmode="numeric"></div>
</div>
<div class="qty-line" style="padding:10px 0 0">
<label for="cq">Штук</label>
<input type="number" id="cq" value="10" inputmode="numeric">
</div>
<div class="result" id="cRes" style="padding:8px 0 0"></div>
</section>
<footer>
Объём считается по кубатуре из вашего прайса. Цена за штуку = цена за м³ × кубатуру доски.
Услуги начисляются на весь объём заказа.
</footer>
</div>
<div class="grand">
<div class="grand-in">
<span class="lbl">Итого заказ</span>
<span class="val" id="grandVal">0 ₽</span>
<button id="toOrder">Заказ</button>
</div>
</div>
<div id="stockPrint"></div>
<div id="docSheet"></div>
<div class="toast" id="toast"></div>
<script>
/* ========== ПРАЙСЫ ==========
A — естественная влажность. B — сухой строганный.
v — кубатура одной доски (м³) из прайса, p — цена за м³. */
const PRICES = {
A:{title:"Естественная влажность",items:[
{g:"Рейка", n:"Рейка 25х50х6000", t:25,w:50, l:6000,s:"сорт 1", p:18500,v:0.00750},
{g:"Рейка", n:"Рейка 25х75х6000", t:25,w:75, l:6000,s:"сорт 1", p:18500,v:0.01120},
{g:"Рейка", n:"Рейка 25х50х6000", t:25,w:50, l:6000,s:"сорт 2-4", p:10500,v:0.00750},
{g:"Рейка", n:"Рейка 25х75х6000", t:25,w:75, l:6000,s:"сорт 2-4", p:12000,v:0.01120},
{g:"Доска", n:"Доска 25х100х6000", t:25,w:100,l:6000,s:"сорт 1", p:18500,v:0.01500},
{g:"Доска", n:"Доска 25х125х6000", t:25,w:125,l:6000,s:"сорт 1", p:18500,v:0.01875},
{g:"Доска", n:"Доска 25х150х6000", t:25,w:150,l:6000,s:"сорт 1", p:18500,v:0.02250},
{g:"Доска", n:"Доска 25х100х6000", t:25,w:100,l:6000,s:"сорт 2-4", p:10500,v:0.01500},
{g:"Доска", n:"Доска 25х150х6000", t:25,w:150,l:6000,s:"сорт 2-4", p:10500,v:0.02250},
{g:"Доска", n:"Доска 40х100х6000", t:40,w:100,l:6000,s:"сорт 1", p:18500,v:0.02400},
{g:"Доска", n:"Доска 40х125х6000", t:40,w:125,l:6000,s:"сорт 1", p:18500,v:0.03000},
{g:"Доска", n:"Доска 40х150х6000", t:40,w:150,l:6000,s:"сорт 1", p:18500,v:0.03600},
{g:"Доска", n:"Доска 40х100х6000", t:40,w:100,l:6000,s:"сорт 2-4", p:14000,v:0.02400},
{g:"Доска", n:"Доска 40х150х6000", t:40,w:150,l:6000,s:"сорт 2-4", p:10500,v:0.03600},
{g:"Доска", n:"Доска 50х100х6000", t:50,w:100,l:6000,s:"сорт 1", p:18500,v:0.03000},
{g:"Доска", n:"Доска 50х150х6000", t:50,w:150,l:6000,s:"сорт 1", p:18500,v:0.04500},
{g:"Доска", n:"Доска 50х200х6000", t:50,w:200,l:6000,s:"сорт 1", p:18500,v:0.06000},
{g:"Брусок", n:"Брусок 40х75х6000", t:40,w:75, l:6000,s:"сорт 1", p:18500,v:0.01800},
{g:"Брусок", n:"Брусок 40х75х6000", t:40,w:75, l:6000,s:"сорт 2-4", p:14500,v:0.01800},
{g:"Брусок", n:"Брусок 50х50х6000", t:50,w:50, l:6000,s:"сорт 1", p:18500,v:0.01500},
{g:"Брус", n:"Брус 100х100х6000", t:100,w:100,l:6000,s:"сорт 1", p:18500,v:0.06000},
{g:"Брус", n:"Брус 100х150х6000", t:100,w:150,l:6000,s:"сорт 1", p:18500,v:0.09000},
{g:"Брус", n:"Брус 100х200х6000", t:100,w:200,l:6000,s:"сорт 1", p:18500,v:0.12000},
{g:"Брус", n:"Брус 150х150х6000", t:150,w:150,l:6000,s:"сорт 1", p:18500,v:0.13500},
{g:"Брус", n:"Брус 150х200х6000", t:150,w:200,l:6000,s:"сорт 1", p:18500,v:0.18000},
{g:"Брус", n:"Брус 200х200х6000", t:200,w:200,l:6000,s:"сорт 1", p:18500,v:0.24000},
{g:"Брус", n:"Брус 200х250х6000", t:200,w:250,l:6000,s:"сорт 1", p:21500,v:0.30000},
{g:"Брус", n:"Брус 250х250х6000", t:250,w:250,l:6000,s:"сорт 1", p:21500,v:0.37500},
{g:"Строганный", n:"Имитация бруса 18х147х6000", t:18,w:147,l:6000,s:"сухая, сорт АВ", p:41000,v:0.01590}
]},
B:{title:"Сухой строганный",items:[
{g:"Доска", n:"Доска 20х93х6000", t:20,w:93, l:6000,s:"сухая строганная",p:37500,v:0.01116},
{g:"Доска", n:"Доска 20х143х6000", t:20,w:143,l:6000,s:"сухая строганная",p:36500,v:0.01720},
{g:"Доска", n:"Доска 33х93х6000", t:33,w:93, l:6000,s:"сухая строганная",p:35500,v:0.01840},
{g:"Доска", n:"Доска 33х143х6000", t:33,w:143,l:6000,s:"сухая строганная",p:35500,v:0.02830},
{g:"Доска", n:"Доска 33х193х6000", t:33,w:193,l:6000,s:"сухая строганная",p:35500,v:0.03820},
{g:"Доска", n:"Доска 43х93х6000", t:43,w:93, l:6000,s:"сухая строганная",p:34500,v:0.02400},
{g:"Доска", n:"Доска 43х143х6000", t:43,w:143,l:6000,s:"сухая строганная",p:34500,v:0.03690},
{g:"Доска", n:"Доска 43х193х6000", t:43,w:193,l:6000,s:"сухая строганная",p:34500,v:0.04980},
{g:"Рейка, брусок", n:"Рейка 20х40х6000", t:20,w:40,l:6000,s:"сухая строганная",p:42500,v:0.00480},
{g:"Рейка, брусок", n:"Брусок 43х43х6000", t:43,w:43,l:6000,s:"сухой строганный",p:37000,v:0.01110},
{g:"Брус", n:"Брус 93х93х6000", t:93,w:93, l:6000,s:"сухой строганный",p:32500,v:0.05190},
{g:"Брус", n:"Брус 93х143х6000", t:93,w:143, l:6000,s:"сухой строганный",p:33500,v:0.07980},
{g:"Брус", n:"Брус 143х143х6000", t:143,w:143,l:6000,s:"сухой строганный",p:31500,v:0.12280},
{g:"Брус", n:"Профилированный брус 143х143х6000",t:143,w:143,l:6000,s:"профилированный",p:31500,v:0.12280},
{g:"Погонаж",n:"Имитация бруса 16х146х6000", t:16,w:146,l:6000,s:"сорт АБ",p:40300,v:0.01400},
{g:"Погонаж",n:"Имитация бруса 18х147х6000", t:18,w:147,l:6000,s:"сорт АБ",p:41000,v:0.01590},
{g:"Погонаж",n:"Блок-хауз 33х143х6000", t:33,w:143,l:6000,s:"—",p:37000,v:0.02830},
{g:"Погонаж",n:"Шпунт половой 33х93х6000", t:33,w:93, l:6000,s:"—",p:37000,v:0.01840},
{g:"Погонаж",n:"Шпунт половой 33х143х6000", t:33,w:143,l:6000,s:"—",p:37000,v:0.02830},
{g:"Погонаж",n:"Террасная доска 33х143х6000",t:33,w:143,l:6000,s:"—",p:36500,v:0.02830},
{g:"Погонаж",n:"Прямой планкен 20х93х6000", t:20,w:93, l:6000,s:"—",p:39000,v:0.01120},
{g:"Погонаж",n:"Прямой планкен 20х143х6000", t:20,w:143,l:6000,s:"—",p:37500,v:0.01720},
{g:"Погонаж",n:"Косой планкен 20х93х6000", t:20,w:93, l:6000,s:"—",p:39000,v:0.01120},
{g:"Погонаж",n:"Косой планкен 20х143х6000", t:20,w:143,l:6000,s:"—",p:37500,v:0.01720}
]}};
/* Сухая доска 1 сорта: берём доску сорта 1 из прайса естественной влажности
и добавляем наценку за сушку на каждый куб. Меняете цену в прайсе A — здесь пересчитается само. */
let DRY_ADD=4000;
PRICES.C={
title:"Сухой материал, 1 сорт",
note:"Цена = прайс естественной влажности (сорт 1) + "+DRY_ADD.toLocaleString("ru-RU")+" ₽ за м³. Сушка уже в цене.",
items:PRICES.A.items
.filter(it=>it.s==="сорт 1")
.map(it=>({...it,s:"сухая, сорт 1",p:it.p+DRY_ADD,warn:null}))
};
const SERVICES=[{id:"anti",n:"Антисептирование",p:2000},{id:"dry",n:"Камерная сушка",p:4500}];
/* ========== хранилище (переживает перезагрузку) ========== */
const SKEY="lesopilka.v3";
const Store={
d:{prices:{},priceDate:"",draft:null,history:[]},
load(){try{const r=window.localStorage.getItem(SKEY);if(r)this.d=Object.assign(this.d,JSON.parse(r));}catch(e){}},
save(){try{window.localStorage.setItem(SKEY,JSON.stringify(this.d));}catch(e){}
try{if(window.__cloudSave)window.__cloudSave(this.d);}catch(e){}}
};
Store.load();
const BASE_DATE="29.07.2026";
let ORIGIN="Санкт-Петербург, Грузовой проезд, 29";
const today=()=>{const t=new Date();const z=n=>String(n).padStart(2,"0");
return z(t.getDate())+"."+z(t.getMonth()+1)+"."+t.getFullYear();};
/* ========== состояние ========== */
const S={list:"A",q:"",sort:null,sel:null,selKey:"",len:6000,mode:"pcs",qty:10,ovr:null,
order:[],svc:{anti:false,dry:false},cl:{fio:"",phone:"",addr:"",car:"",ship:0,date:"",km:0,rate:0,twFrom:"",twTo:""},
round:false,adv:0,orderId:null,gift:""};
const $=id=>document.getElementById(id);
const money=n=>new Intl.NumberFormat("ru-RU",{minimumFractionDigits:2,maximumFractionDigits:2}).format(n)+" ₽";
const money0=n=>new Intl.NumberFormat("ru-RU",{maximumFractionDigits:0}).format(n)+" ₽";
const cube=n=>new Intl.NumberFormat("ru-RU",{minimumFractionDigits:3,maximumFractionDigits:4}).format(n);
const num0=n=>new Intl.NumberFormat("ru-RU",{maximumFractionDigits:1}).format(n);
const num=n=>new Intl.NumberFormat("ru-RU",{minimumFractionDigits:2,maximumFractionDigits:2}).format(n);
const rusDate=d=>{if(!d)return "";const p=String(d).split("-");return p.length===3?p[2]+"."+p[1]+"."+p[0]:d;};
const stockLine=k=>{
const q=(Store.d.stock&&Store.d.stock[k]&&typeof Store.d.stock[k].q==="number")?Store.d.stock[k].q:0;
const mn=(Store.d.stock&&Store.d.stock[k]&&Store.d.stock[k].min)||0;
if(!q&&!mn)return "";
return '<span class="stockline '+(q>0&&(!mn||q>=mn)?"ok":"no")+'">склад '+q+' шт</span>';
};
const keyOf=(l,it)=>l+"|"+it.n+"|"+it.s;
const key=it=>keyOf(S.list,it);
function toast(t){const el=$("toast");el.textContent=t;el.classList.add("on");
clearTimeout(el._t);el._t=setTimeout(()=>el.classList.remove("on"),1800);}
/* ========== цены: правки пользователя ========== */
function applyPrices(){
["A","B"].forEach(l=>PRICES[l].items.forEach(it=>{
const v=Store.d.prices[keyOf(l,it)];if(v!=null&&v>0)it.p=v;}));
PRICES.C.items.forEach(ci=>{
const own=Store.d.prices[keyOf("C",ci)];
if(own!=null&&own>0){ci.p=own;return;}
const a=PRICES.A.items.find(x=>x.n===ci.n&&x.s==="сорт 1");
if(a)ci.p=a.p+DRY_ADD;
});
}
function priceEdits(){return Object.keys(Store.d.prices).length;}
function renderPriceDate(){
$("priceDate").textContent=Store.d.priceDate||BASE_DATE;
const n=priceEdits();
$("priceReset").hidden=!n;
$("priceReset").textContent="Сбросить правки цен ("+n+")";
}
/* ========== наличие ========== */
/* ========== вспомогательное ========== */
const sizeOf=it=>it.t+"\u00D7"+it.w+"\u00D7"+(S.sel===it?S.len:it.l);
const kindOf=it=>it.n.replace(/\s*\d+\s*[хx]\s*\d+.*$/i,"").trim()||it.g;
const badgeOf=s=>/2-4/.test(s)?"b2":/(^|\s)сорт 1/.test(s)?"b1":"b3";
const shortSort=s=>s.replace("сухая, ","").replace("сухой ","").replace("сухая ","");
const listSize=it=>it.t+"\u00D7"+it.w+"\u00D7"+it.l;
const effV=it=>it.v*(S.len/it.l);
/* ========== список ========== */
function sortsIn(list){const out=[];
PRICES[list].items.forEach(it=>{if(/сорт/i.test(it.s)&&!out.includes(it.s))out.push(it.s)});
return out;}
function renderSorts(){
const list=sortsIn(S.list),box=$("sorts");
if(list.length<2){box.innerHTML="";return;}
box.innerHTML='<button class="chip" data-s="" aria-pressed="'+(!S.sort)+'">Все</button>'+
list.map(x=>`<button class="chip" data-s="${x}" aria-pressed="${S.sort===x}">${x}</button>`).join("");
box.querySelectorAll(".chip").forEach(b=>b.onclick=()=>{S.sort=b.dataset.s||null;renderSorts();renderList();});
}
function renderList(){
const q=S.q.trim().toLowerCase().replace(/[x*]/g,"х").replace(/\s+/g,"");
const src=PRICES[S.list].items;
const gOrder=[];src.forEach(it=>{if(!gOrder.includes(it.g))gOrder.push(it.g)});
const items=src.filter(it=>{
if(S.sort&&it.s!==S.sort)return false;
if(!q)return true;
const hay=(it.n+it.s+it.g+listSize(it)).toLowerCase().replace(/[x*\u00D7]/g,"х").replace(/\s+/g,"");
return hay.includes(q);
}).slice().sort((a,b)=>gOrder.indexOf(a.g)-gOrder.indexOf(b.g)||a.t-b.t||a.w-b.w||b.p-a.p);
const box=$("list");box.innerHTML="";
if(!items.length){box.innerHTML='<div class="empty">Ничего не нашлось. Снимите фильтр по сорту или посмотрите блок «Свой размер» ниже.</div>';return;}
let g=null,rows=null;
items.forEach(it=>{
if(it.g!==g){g=it.g;
const h=document.createElement("div");h.className="group-label";h.textContent=g;box.appendChild(h);
rows=document.createElement("div");rows.className="rows";box.appendChild(rows);}
const k=key(it);
const b=document.createElement("button");
b.className="row";b.setAttribute("aria-current",String(S.selKey===k));
b.innerHTML=`<div class="body">
<div class="sz">${listSize(it)}<i> мм</i></div>
<div class="meta"><span>${kindOf(it)}</span>
<span class="badge ${badgeOf(it.s)}">${shortSort(it.s)}</span>
<span>${cube(it.v)} м³/шт</span>${stockLine(k)}${it.warn?'<span class="flagdot" title="Есть примечание"></span>':""}</div>
</div>
<div class="pc">${money0(it.p*it.v)}<small>${money0(it.p)}/м³</small></div>`;
b.onclick=()=>{S.sel=it;S.selKey=k;S.len=it.l;S.ovr=null;$("ovr").value="";
renderList();renderCalc();$("calcWrap").scrollIntoView({behavior:"smooth",block:"start"});};
rows.appendChild(b);
});
}
/* ========== расчёт позиции ========== */
function priceOf(it){return S.ovr!=null&&S.ovr>0?S.ovr:it.p;}
function calc(it,mode,qty){
const p=priceOf(it),v=effV(it),per=p*v;
const cover=(it.w/1000)*(S.len/1000);
let pcs,vol,sum,askedV=null,askedA=null,needA=null;
if(mode==="m3"){
/* от кубов: цена за куб фиксированная, сумма = объём × цена за м³ */
askedV=Math.max(0,qty||0);vol=askedV;sum=vol*p;pcs=Math.ceil(vol/v-1e-9);
} else {
if(mode==="pcs")pcs=Math.max(0,Math.round(qty||0));
else{askedA=Math.max(0,qty||0);needA=askedA*1.1;pcs=Math.ceil(needA/cover-1e-9);}
vol=pcs*v;sum=pcs*per;
}
return {p,v,per,pcs,vol,askedV,askedA,needA,cover,sum};
}
function renderLens(){
const it=S.sel;if(!it)return;
const opts=[it.l,4000,3000].filter((x,i,a)=>a.indexOf(x)===i&&x<=it.l);
$("lens").innerHTML=opts.map(l=>`<button class="chip" data-l="${l}" aria-pressed="${S.len===l}">${(l/1000).toString().replace(".",",")} м</button>`).join("");
$("lens").querySelectorAll(".chip").forEach(b=>b.onclick=()=>{S.len=+b.dataset.l;renderLens();renderCalc();});
}
function renderCalc(){
const it=S.sel;
$("calcWrap").hidden=!it;if(!it)return;
renderLens();
const r=calc(it,S.mode,S.qty);
$("cName").textContent=sizeOf(it)+" мм";
$("cKind").textContent=kindOf(it)+" · "+it.s;
$("cPrice").textContent=money0(r.p)+"/м³";
const mx=Math.max(it.t,it.w),sc=110/Math.max(mx,60);
const bd=$("board");bd.style.width=Math.max(8,it.w*sc)+"px";bd.style.height=Math.max(6,it.t*sc)+"px";
$("dims").innerHTML=`
<div class="d"><span>Толщина</span><b>${it.t}<em>мм</em></b></div>
<div class="d"><span>Ширина</span><b>${it.w}<em>мм</em></b></div>
<div class="d"><span>Длина</span><b>${(S.len/1000).toString().replace(".",",")}<em>м</em></b></div>
<div class="cap">сечение в масштабе →</div>`;
const rows=[
["Объём одной штуки",cube(r.v)+" м³"],
["В одном кубе",(1/r.v).toFixed(1).replace(".",",")+" шт"],
["Цена за 1 шт.",money(r.per)]
];
if(S.mode==="m3"){rows.push(["Заказано",cube(r.askedV)+" м³"]);rows.push(["Цена за м³",money(r.p)]);}
if(S.mode==="area"){
rows.push(["Площадь",num(r.askedA)+" м²"]);
rows.push(["С запасом 10%",num(r.needA)+" м²"]);
rows.push(["Перекрытие 1 доски",num(r.cover)+" м²"]);
}
rows.push([S.mode==="m3"?"Это примерно":"Количество",r.pcs+" шт"]);
const skl=stQ(S.selKey);
if(skl||reserved(S.selKey))rows.push(["На складе свободно",stFree(S.selKey)+" шт из "+skl]);
rows.push(["Общий объём",cube(r.vol)+" м³"]);
$("res").innerHTML=rows.map(x=>`<div class="res-row"><span>${x[0]}</span><span>${x[1]}</span></div>`).join("");
let w=it.warn?`<div class="warn">${it.warn}</div>`:"";
if(S.len!==it.l)w+='<div class="warn">Длина '+(S.len/1000).toString().replace(".",",")+' м: объём и цена пересчитаны от кубатуры. Проверьте, что вы пилите такую длину.</div>';
if(S.mode==="area")w+='<div class="warn">Считаю по полной ширине доски. У шпунта, планкена и имитации рабочая ширина меньше на 5–10 мм — материала уйдёт больше.</div>';
$("warnBox").innerHTML=w;
$("cTotal").textContent=money(r.sum);
$("qLbl").textContent=S.mode==="pcs"?"Штук":S.mode==="m3"?"Кубов":"м²";
$("qty").step=S.mode==="pcs"?"1":S.mode==="m3"?"0.1":"1";
}
/* ========== заказ ========== */
function orderVolume(){return S.order.reduce((a,x)=>a+x.vol,0);}
function orderGoods(){return S.order.reduce((a,x)=>a+x.sum,0);}
function svcSum(){const v=orderVolume();return SERVICES.reduce((a,s)=>a+(S.svc[s.id]?s.p*v:0),0);}
function shipSum(){return S.order.length?(+S.cl.ship||0):0;}
function preRound(){return orderGoods()+svcSum()+shipSum();}
function grand(){const t=preRound();return S.round?Math.round(t/100)*100:t;}
function roundDiff(){return grand()-preRound();}
function advSum(){return Math.min(+S.adv||0,grand());}
function restSum(){return grand()-advSum();}
function plural(n,a,b,c){const m=n%100,k=n%10;return n+" "+(m>10&&m<20?c:k===1?a:k>1&&k<5?b:c);}
function renderClient(){
const c=S.cl,rows=[];
$("oTitle").textContent=c.addr||"";
$("oDate").textContent=c.date?("Доставка "+rusDate(c.date)):"";
document.title=c.addr?("Заказ — "+c.addr):"Лесопилка · Расчёт заказа";
if(c.fio) rows.push(["ФИО",c.fio,1]);
if(c.phone)rows.push(["Телефон",c.phone,1]);
if(c.addr) rows.push(["Адрес",c.addr,0]);
if(c.car) rows.push(["Машина",c.car,0]);
if(+c.km) rows.push(["Расстояние",num0(c.km)+" км"+(+c.rate?" × "+num0(c.rate)+" ₽/км":"")+" · без платных дорог",0]);
if(+c.ship)rows.push(["Доставка",money(+c.ship),0]);
$("client").innerHTML=rows.length?'<div class="cl">'+
rows.map(r=>`<div class="r${r[2]?" hidden-in-copy":""}"><span>${r[0]}</span><b>${r[1]}</b></div>`).join("")+
'<div class="priv no-print">Отмеченное точкой не попадёт в скопированный текст</div></div>':"";
}
function setQty(i,val){
const x=S.order[i];if(!x)return;
if(x.unit==="m3"){
const vol=Math.max(0.001,+val||0);
x.vol=vol;x.rate=x.rate||(x.v?x.per/x.v:0);x.sum=vol*x.rate;x.pcs=Math.ceil(vol/x.v-1e-9);
} else {
const pcs=Math.max(1,Math.round(val||1));
x.pcs=pcs;x.vol=pcs*x.v;x.sum=pcs*x.per;
}
renderOrder();
}
function renderOrder(){
renderClient();
const box=$("order"),n=S.order.length;
$("oCount").textContent=n?plural(n,"позиция","позиции","позиций"):"пусто";
if(!n)box.innerHTML='<div class="empty" style="padding:18px 14px">Пока пусто. Выберите позицию выше и нажмите «Добавить в заказ» — можно набрать несколько.</div>';
else{
box.innerHTML=S.order.map((x,i)=>`<div class="oi">
<div class="no">${i+1}</div>
<div class="body">
<div class="sz">${x.sz}<i> мм</i></div>
<div class="meta">${x.kind} · ${x.sort}${x.unit==="m3"?" · по кубам, "+money(x.rate)+"/м³":" · "+cube(x.vol)+" м³"}</div>
<div class="qedit no-print">
<button data-m="${i}" aria-label="Меньше">−</button>
<input type="number" min="0" step="${x.unit==="m3"?"0.1":"1"}" value="${x.unit==="m3"?(+x.vol).toFixed(3):x.pcs}" data-q="${i}" inputmode="decimal">
<button data-p="${i}" aria-label="Больше">+</button>
<span class="hint" style="padding:0">${x.unit==="m3"?"м³":"шт"}</span>
</div>
<div class="meta only-print-qty">${x.unit==="m3"?cube(x.vol)+" м³ (примерно "+x.pcs+" шт)":x.pcs+" шт"}</div>
</div>
<div class="sum">${money(x.sum)}<small>${x.unit==="m3"?money(x.rate)+"/м³":money(x.per)+"/шт"}</small></div>
<button class="del no-print" data-i="${i}" aria-label="Убрать">✕</button></div>`).join("");
box.querySelectorAll(".del").forEach(b=>b.onclick=()=>{S.order.splice(+b.dataset.i,1);renderOrder();});
box.querySelectorAll("[data-m]").forEach(b=>b.onclick=()=>{const x=S.order[+b.dataset.m];
setQty(+b.dataset.m,x.unit==="m3"?+(x.vol-0.1).toFixed(3):x.pcs-1);});
box.querySelectorAll("[data-p]").forEach(b=>b.onclick=()=>{const x=S.order[+b.dataset.p];
setQty(+b.dataset.p,x.unit==="m3"?+(x.vol+0.1).toFixed(3):x.pcs+1);});
box.querySelectorAll("[data-q]").forEach(inp=>inp.onchange=()=>setQty(+inp.dataset.q,+inp.value));
}
const v=orderVolume();
$("services").innerHTML=n?SERVICES.map(s=>`<label class="svc${S.svc[s.id]?"":" no-print"}">
<input type="checkbox" data-s="${s.id}" ${S.svc[s.id]?"checked":""}>
<span>${s.n}</span><span class="p">${money0(s.p)}/м³ → ${money(s.p*v)}</span></label>`).join("")+
'<div class="hint no-print" style="padding:8px 14px 0">Услуга начисляется на весь объём заказа. Сухой материал сушить не нужно — сушка уже в его цене.</div>':"";
$("services").querySelectorAll("input").forEach(i=>i.onchange=()=>{S.svc[i.dataset.s]=i.checked;renderOrder();});
$("orderSum").innerHTML=n?`<div class="osum">
<div class="res-row"><span>Общий объём</span><span>${cube(v)} м³</span></div>
<div class="res-row"><span>Всего досок</span><span>${S.order.reduce((a,x)=>a+x.pcs,0)} шт</span></div>
<div class="res-row big"><span>Материал</span><span>${money(orderGoods())}</span></div>
${svcSum()?`<div class="res-row big"><span>Услуги</span><span>${money(svcSum())}</span></div>`:""}
${shipSum()?`<div class="res-row big"><span>Доставка${S.cl.car?" · "+S.cl.car:""}</span><span>${money(shipSum())}</span></div>`:""}
${S.round&&roundDiff()?`<div class="res-row"><span>Округление</span><span>${money(roundDiff())}</span></div>`:""}
${advSum()?`<div class="res-row big"><span>Аванс</span><span>−${money(advSum())}</span></div>
<div class="res-row big"><span>Остаток к оплате</span><span>${money(restSum())}</span></div>`:""}
</div>`:"";
$("oTotal").textContent=money(grand());
$("grandVal").textContent=money0(grand());
$("copyOrder").disabled=$("clear").disabled=$("printOrder").disabled=$("ordSave").disabled=$("docPrint").disabled=!n;
saveDraft();
}
/* ========== черновик ========== */
let draftReady=false;
function saveDraft(){
if(!draftReady)return;
Store.d.draft={order:S.order,cl:S.cl,svc:S.svc,round:S.round,adv:S.adv,orderId:S.orderId,gift:S.gift};
Store.save();
}
function loadDraft(){
const d=Store.d.draft;
if(d){
if(Array.isArray(d.order))S.order=d.order;
if(d.cl)S.cl=Object.assign(S.cl,d.cl);
if(d.svc)S.svc=Object.assign(S.svc,d.svc);
S.round=!!d.round;S.adv=+d.adv||0;S.orderId=d.orderId||null;S.gift=d.gift||"";
$("fFio").value=S.cl.fio;$("fPhone").value=S.cl.phone;$("fAddr").value=S.cl.addr;
$("fCar").value=S.cl.car;$("fShip").value=S.cl.ship||"";$("fDate").value=S.cl.date||"";
$("fTimeFrom").value=S.cl.twFrom||"";$("fTimeTo").value=S.cl.twTo||"";
$("fKm").value=S.cl.km||"";$("fRate").value=S.cl.rate||"";
$("fRound").checked=S.round;$("fAdv").value=S.adv||"";
if($("fGift"))$("fGift").value=S.gift||"";
}
if(!S.cl.rate&&Store.d.rate){S.cl.rate=+Store.d.rate;$("fRate").value=S.cl.rate;}
draftReady=true;
}
/* ========== текст и печать ========== */
function copy(text){
const done=()=>toast("Скопировано");
if(navigator.clipboard&&window.isSecureContext)navigator.clipboard.writeText(text).then(done,()=>fallback(text,done));
else fallback(text,done);
}
function fallback(text,done){
const ta=document.createElement("textarea");ta.value=text;ta.style.position="fixed";ta.style.opacity="0";
document.body.appendChild(ta);ta.select();
try{document.execCommand("copy");done();}catch(e){toast("Не вышло скопировать");}
document.body.removeChild(ta);
}
function textOne(){
const it=S.sel,r=calc(it,S.mode,S.qty);
return `${sizeOf(it)} мм · ${kindOf(it)}, ${it.s}
Объём 1 шт: ${cube(r.v)} м³ · в кубе ${(1/r.v).toFixed(1).replace(".",",")} шт
Количество: ${r.pcs} шт · ${cube(r.vol)} м³
Цена: ${money(r.per)}/шт · ${money0(r.p)}/м³
Итого: ${money(r.sum)}`;
}
function textOrder(){
const v=orderVolume();
const cur=S.orderId?(Store.d.history||[]).find(x=>String(x.id)===String(S.orderId)):null;
let s="РАСЧЁТ ЗАКАЗА"+(cur?" № "+cur.num:"")+"\n\n";
if(S.cl.addr)s+=`Адрес доставки: ${S.cl.addr}\n`;
if(S.cl.date)s+=`Дата доставки: ${rusDate(S.cl.date)}${(S.cl.twFrom||S.cl.twTo)?" ("+(S.cl.twFrom||"?")+"–"+(S.cl.twTo||"?")+")":""}\n`;
if(S.cl.car)s+=`Машина: ${S.cl.car}\n`;
if(+S.cl.km)s+=`Расстояние: ${num0(S.cl.km)} км без платных дорог${+S.cl.rate?" × "+num0(S.cl.rate)+" ₽/км":""}\n`;
if(S.cl.addr||S.cl.car||S.cl.date)s+="\n";
S.order.forEach((x,i)=>{s+=`${i+1}. ${x.sz} мм · ${x.kind}, ${x.sort}\n ${x.unit==="m3"?cube(x.vol)+" м³ × "+money(x.rate)+"/м³":x.pcs+" шт × "+money(x.per)}= ${money(x.sum)}\n`;});
s+=`\nОбъём: ${cube(v)} м³\nВсего досок: ${S.order.reduce((a,x)=>a+x.pcs,0)} шт\nМатериал: ${money(orderGoods())}\n`;
SERVICES.forEach(sv=>{if(S.svc[sv.id])s+=`${sv.n}: ${money(sv.p*v)}\n`;});
if(shipSum())s+=`Доставка${S.cl.car?" ("+S.cl.car+")":""}: ${money(shipSum())}\n`;
if((S.gift||"").trim())s+=`\nВ подарок клиенту: ${S.gift.trim()}\n`;
s+=`\nИТОГО: ${money(grand())}`;
if(advSum())s+=`\nАванс: ${money(advSum())}\nОстаток к оплате: ${money(restSum())}`;
return s;
}
/* ========== заказы со статусами ========== */
const STATUSES=[["new","Согласован"],["paid","Оплачен"],["ready","Напилен"],["shipped","Отгружен"],["cancel","Отменён"]];
const ACTIVE=["new","paid","ready"];
const stName=id=>{const f=STATUSES.find(x=>x[0]===id);return f?f[1]:"Отгружен";};
const stOf=o=>o.status||"shipped";
const ORD={filter:"active",q:""};
function nextNum(){
const y=new Date().getFullYear();
Store.d.seq=Store.d.seq||{};
Store.d.seq[y]=(Store.d.seq[y]||0)+1;
return y+"-"+String(Store.d.seq[y]).padStart(3,"0");
}
function activeOrders(){return (Store.d.history||[]).filter(o=>ACTIVE.includes(stOf(o)));}
function reservedOrders(k){
let q=0;
activeOrders().forEach(o=>(o.items||[]).forEach(x=>{if(x.k===k)q+=x.pcs;}));
return q;
}
/* списание и возврат по статусу */
function writeOff(o){
if(o.moves&&o.moves.length)return 0;
o.moves=[];
(o.items||[]).forEach(x=>{if(!x.k)return;
const applied=addMove(x.k,-x.pcs,"out","заказ "+(o.num||"")+" · "+(o.addr||""));
if(applied)o.moves.push({k:x.k,q:-applied});});
return o.moves.length;
}
function writeBack(o){
if(!o.moves||!o.moves.length)return 0;
const n=o.moves.length;
o.moves.forEach(m=>addMove(m.k,m.q,"ret","возврат по заказу "+(o.num||"")));
o.moves=[];
return n;
}
function setStatus(id,st){
const o=(Store.d.history||[]).find(x=>String(x.id)===String(id));if(!o)return;
const was=stOf(o);
if(st===was)return;
if(st==="shipped"){
const n=writeOff(o);
o.status=st;Store.save();
toast(n?"Отгружено, склад списан":"Статус изменён");
} else if(was==="shipped"){
if(!confirm("Заказ снимается с отгрузки. Вернуть материал на склад?")){
o.status=st;Store.save();toast("Статус изменён, склад не тронут");
} else {
writeBack(o);o.status=st;Store.save();toast("Материал возвращён на склад");
}
} else {o.status=st;Store.save();toast("Статус: "+stName(st));}
renderHistory();renderList();if(!$("stockSection").hidden)renderStock();
}
/* сохранение и открытие заказа */
function saveOrder(){
if(!S.order.length)return;
const now=new Date(),z=n=>String(n).padStart(2,"0");
Store.d.history=Store.d.history||[];
const body={
date:z(now.getDate())+"."+z(now.getMonth()+1)+"."+now.getFullYear(),
time:z(now.getHours())+":"+z(now.getMinutes()),
ym:now.getFullYear()+"-"+z(now.getMonth()+1),
fio:S.cl.fio,phone:S.cl.phone,addr:S.cl.addr,car:S.cl.car,dlv:rusDate(S.cl.date),
twFrom:(S.cl.twFrom||"").trim(),twTo:(S.cl.twTo||"").trim(),
km:+S.cl.km||0,rate:+S.cl.rate||0,
pos:S.order.length,pcs:S.order.reduce((a,x)=>a+x.pcs,0),vol:orderVolume(),
goods:orderGoods(),svc:svcSum(),ship:shipSum(),total:grand(),
svcFlags:Object.assign({},S.svc),round:!!S.round,adv:advSum(),rest:restSum(),
gift:(S.gift||"").trim(),
items:S.order.map(x=>({k:x.k,sz:x.sz,kind:x.kind,sort:x.sort,pcs:x.pcs,v:x.v,vol:x.vol,per:x.per,sum:x.sum}))
};
const ex=S.orderId?Store.d.history.find(o=>String(o.id)===String(S.orderId)):null;
if(ex){
if(stOf(ex)==="shipped"&&ex.moves&&ex.moves.length){
if(!confirm("Заказ "+ex.num+" уже отгружен и списан со склада. Обновить его состав? Списание будет пересчитано."))return;
writeBack(ex);
}
Object.assign(ex,body);
if(stOf(ex)==="shipped")writeOff(ex);
Store.save();toast("Заказ "+ex.num+" обновлён");
} else {
const rec=Object.assign({id:Date.now(),num:nextNum(),status:"new",moves:[]},body);
Store.d.history.push(rec);S.orderId=rec.id;Store.save();
toast("Заказ "+rec.num+" сохранён");
}
renderOrderNum();renderHistory();renderList();
}
function openOrder(id){
const o=(Store.d.history||[]).find(x=>String(x.id)===String(id));if(!o)return;
S.order=(o.items||[]).map(x=>Object.assign({},x));
S.cl=Object.assign({fio:"",phone:"",addr:"",car:"",ship:0,date:"",km:0,rate:0,twFrom:"",twTo:""},
{fio:o.fio||"",phone:o.phone||"",addr:o.addr||"",car:o.car||"",ship:o.ship||0,
date:o.dlv?o.dlv.split(".").reverse().join("-"):"",km:o.km||0,rate:o.rate||0,
twFrom:o.twFrom||"",twTo:o.twTo||""});
if(o.svcFlags)S.svc=Object.assign({anti:false,dry:false},o.svcFlags);
S.round=!!o.round;S.adv=+o.adv||0;S.orderId=o.id;S.gift=o.gift||"";
$("fFio").value=S.cl.fio;$("fPhone").value=S.cl.phone;$("fAddr").value=S.cl.addr;
$("fCar").value=S.cl.car;$("fShip").value=S.cl.ship||"";$("fDate").value=S.cl.date||"";
$("fTimeFrom").value=S.cl.twFrom||"";$("fTimeTo").value=S.cl.twTo||"";
$("fKm").value=S.cl.km||"";$("fRate").value=S.cl.rate||"";$("fRound").checked=S.round;$("fAdv").value=S.adv||"";
if($("fGift"))$("fGift").value=S.gift||"";
renderOrderNum();renderOrder();setView("calc");
if(typeof showExtraIfNeeded==="function")showExtraIfNeeded();
$("orderSection").scrollIntoView({behavior:"smooth",block:"start"});
toast("Заказ "+(o.num||"")+" открыт в расчёте");
}
function newOrder(){
S.orderId=null;S.order=[];S.svc={anti:false,dry:false};S.adv=0;$("fAdv").value="";
S.gift="";if($("fGift"))$("fGift").value="";
if($("extraBox")){$("extraBox").hidden=true;}
if($("btnExtra"))$("btnExtra").setAttribute("aria-expanded","false");
renderOrderNum();renderOrder();toast("Начат новый заказ");
}
function renderOrderNum(){
const o=S.orderId?(Store.d.history||[]).find(x=>String(x.id)===String(S.orderId)):null;
const el=$("oNum");if(el)el.textContent=o?("№ "+o.num+" · "+stName(stOf(o))):"";
}
function delHistory(id){
const h=Store.d.history||[],o=h.find(x=>String(x.id)===String(id));
if(!o)return;
if(!confirm("Удалить заказ "+(o.num||"")+" от "+o.date+" ("+(o.addr||"без адреса")+") на "+money0(o.total)+"?"))return;
if(o.moves&&o.moves.length&&confirm("Вернуть материал этого заказа на склад?"))writeBack(o);
Store.d.history=h.filter(x=>String(x.id)!==String(id));
if(String(S.orderId)===String(id)){S.orderId=null;renderOrderNum();}
Store.save();renderHistory();renderList();
if(!$("stockSection").hidden)renderStock();
toast("Заказ удалён");
}
/* список заказов */
function ordFiltered(){
const q=ORD.q.trim().toLowerCase();
return (Store.d.history||[]).slice().reverse().filter(o=>{
const st=stOf(o);
if(ORD.filter==="active"&&!ACTIVE.includes(st))return false;
if(ORD.filter!=="all"&&ORD.filter!=="active"&&st!==ORD.filter)return false;
if(!q)return true;
return ((o.num||"")+" "+(o.addr||"")+" "+(o.fio||"")+" "+(o.phone||"")).toLowerCase().includes(q);
});
}
function renderOrdFilters(){
const f=[["active","В работе"],["all","Все"],["shipped","Отгружены"],["cancel","Отменённые"]];
$("ordFilters").innerHTML=f.map(([v,t])=>{
const n=v==="all"?(Store.d.history||[]).length:v==="active"?activeOrders().length:
(Store.d.history||[]).filter(o=>stOf(o)===v).length;
return `<button class="chip" data-f="${v}" aria-pressed="${ORD.filter===v}">${t} ${n}</button>`;
}).join("");
$("ordFilters").querySelectorAll(".chip").forEach(b=>b.onclick=()=>{ORD.filter=b.dataset.f;renderHistory();});
}
function ordBadge(){
const b=$("ordBadge");if(!b)return;
const n=(Store.d.history||[]).filter(o=>stOf(o)!=="cancel" && (o.dstatus||"pending")!=="delivered").length;
b.textContent=n?String(n):"";
}
function renderOrdSummary(){
const act=activeOrders();
const sum=act.reduce((a,o)=>a+(+o.total||0),0);
const vol=act.reduce((a,o)=>a+(+o.vol||0),0);
const ship=act.filter(o=>stOf(o)==="ready").length;
$("ordSummary").innerHTML='<div class="stsum">'+
`<div class="c"><div class="k">В работе</div><div class="v">${act.length}</div></div>`+
`<div class="c"><div class="k">На сумму</div><div class="v">${money0(sum)}</div></div>`+
`<div class="c"><div class="k">Объём к отгрузке</div><div class="v">${cube(vol)} м³</div></div>`+
`<div class="c${ship?" alert":""}"><div class="k">Готовы к отгрузке</div><div class="v">${ship}</div></div>`+
'</div>';
}
function renderHistory(){
const rows=ordFiltered();
renderOrdSummary();renderOrdFilters();ordBadge();
$("histBox").innerHTML=rows.length?rows.map(o=>{
const st=stOf(o);
return `<div class="orow st-${st}">
<div class="d"><b>${o.num||"—"}</b>${o.date}</div>
<div class="a">${o.addr||"без адреса"}${o.dlv?" · доставка "+o.dlv:""}${o.car?" · "+o.car:""}
<small>${o.pos||0} поз. · ${o.pcs||0} шт · ${cube(o.vol||0)} м³${o.fio?" · "+o.fio:""}</small></div>
<select data-st="${o.id}">${STATUSES.map(([v,t])=>`<option value="${v}"${st===v?" selected":""}>${t}</option>`).join("")}</select>
<span class="s">${money0(o.total)}</span>
<button class="ib" data-open="${o.id}" title="Открыть в расчёте">↥</button>
<button class="ib" data-del="${o.id}" title="Удалить">✕</button></div>`;
}).join(""):'<div class="empty" style="padding:14px 12px">Ничего не найдено. Посчитайте заказ и нажмите «Сохранить заказ».</div>';
$("histBox").querySelectorAll("[data-st]").forEach(sel=>sel.onchange=()=>setStatus(sel.dataset.st,sel.value));
$("histBox").querySelectorAll("[data-open]").forEach(b=>b.onclick=()=>openOrder(b.dataset.open));
$("histBox").querySelectorAll("[data-del]").forEach(b=>b.onclick=()=>delHistory(b.dataset.del));
const any=(Store.d.history||[]).length;
$("histXlsWeek").disabled=$("histXlsMonth").disabled=!any;
}
const MONTHS=["январь","февраль","март","апрель","май","июнь","июль","август","сентябрь","октябрь","ноябрь","декабрь"];
function weekStart(){const t=new Date();t.setHours(0,0,0,0);t.setDate(t.getDate()-6);return t.getTime();}
function period(range){
const now=new Date();
if(range==="month")return MONTHS[now.getMonth()]+" "+now.getFullYear();
const a=new Date(weekStart()),z=n=>String(n).padStart(2,"0");
return "неделя "+z(a.getDate())+"."+z(a.getMonth()+1)+" — "+z(now.getDate())+"."+z(now.getMonth()+1)+"."+now.getFullYear();
}
function pick(range){
const now=new Date(),z=n=>String(n).padStart(2,"0");
const ym=now.getFullYear()+"-"+z(now.getMonth()+1);
const h=Store.d.history||[];
if(range==="month")return h.filter(o=>o.ym===ym);
const from=weekStart();
return h.filter(o=>{
if(o.id&&+o.id>=from)return true;
const p=String(o.date||"").split(".");
return p.length===3?new Date(+p[2],+p[1]-1,+p[0]).getTime()>=from:false;
});
}
function download(text,name,mime){
try{
const blob=new Blob(["\uFEFF"+text],{type:mime+";charset=utf-8"});
const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=name;
document.body.appendChild(a);a.click();document.body.removeChild(a);
setTimeout(()=>URL.revokeObjectURL(a.href),3000);
toast("Файл "+name+" готов");
}catch(e){copy(text);toast("Скачивание недоступно — данные скопированы");}
}
/* ========== свой размер ========== */
function renderCustom(){
const t=+$("ct").value||0,w=+$("cw").value||0,l=+$("cl").value||0,p=+$("cp").value||0,
q=Math.max(0,Math.round(+$("cq").value||0));
const v=(t/1000)*(w/1000)*(l/1000),per=v*p,sum=per*q;
$("cRes").innerHTML=v>0?[
["Объём одной штуки",cube(v)+" м³"],
["В одном кубе",(1/v).toFixed(1).replace(".",",")+" шт"],
["Цена за 1 шт.",money(per)],
["Объём "+q+" шт",cube(v*q)+" м³"],
["Итого",money(sum)]
].map(x=>`<div class="res-row"><span>${x[0]}</span><span>${x[1]}</span></div>`).join("")
:'<div class="empty">Введите толщину, ширину и длину в миллиметрах.</div>';
}
/* ========== события ========== */
function setList(l){
S.list=l;S.sel=null;S.selKey="";S.sort=null;S.ovr=null;$("ovr").value="";
["A","B","C"].forEach(x=>$("tab"+x).setAttribute("aria-selected",String(x===l)));
$("listNote").textContent=PRICES[l].note||"";
renderSorts();renderList();renderCalc();
}
["A","B","C"].forEach(l=>$("tab"+l).onclick=()=>setList(l));
$("q").oninput=e=>{S.q=e.target.value;renderList();};
function setMode(m){
S.mode=m;
if(m==="pcs")S.qty=Math.max(1,Math.round(S.qty));
else if(m==="m3")S.qty=1;
else S.qty=10;
$("qty").value=S.qty;
[["mPcs","pcs"],["mM3","m3"],["mArea","area"]].forEach(([id,v])=>$(id).setAttribute("aria-pressed",String(m===v)));
renderCalc();
}
$("mPcs").onclick=()=>setMode("pcs");
$("mM3").onclick=()=>setMode("m3");
$("mArea").onclick=()=>setMode("area");
$("qty").oninput=e=>{S.qty=+e.target.value;renderCalc();};
$("plus").onclick=()=>{S.qty=+(S.qty+(S.mode==="m3"?0.1:1)).toFixed(3);$("qty").value=S.qty;renderCalc();};
$("minus").onclick=()=>{S.qty=Math.max(0,+(S.qty-(S.mode==="m3"?0.1:1)).toFixed(3));$("qty").value=S.qty;renderCalc();};
$("ovr").oninput=e=>{S.ovr=+e.target.value||null;renderCalc();};
$("ovrReset").onclick=()=>{S.ovr=null;$("ovr").value="";renderCalc();};
$("savePrice").onclick=()=>{
if(!S.sel||!S.ovr||S.ovr<=0){toast("Впишите цену за м³");return;}
Store.d.prices[S.selKey]=S.ovr;Store.d.priceDate=today();Store.save();
applyPrices();S.ovr=null;$("ovr").value="";
renderPriceDate();renderList();renderCalc();toast("Цена сохранена в прайс");
};
$("priceReset").onclick=()=>{
Store.d.prices={};Store.d.priceDate="";Store.save();
location.reload();
};
$("add").onclick=()=>{
if(!S.sel)return;const r=calc(S.sel,S.mode,S.qty);
if(r.pcs<=0){toast("Укажите количество");return;}
const свободно=stFree(S.selKey);
S.order.push({sz:sizeOf(S.sel),kind:kindOf(S.sel),sort:S.sel.s,k:S.selKey,
unit:S.mode==="m3"?"m3":"pcs",rate:r.p,
pcs:r.pcs,v:r.v,vol:r.vol,per:r.per,sum:r.sum});
renderOrder();
if(stQ(S.selKey)>0&&свободно<r.pcs)toast("На складе свободно "+свободно+" шт — не хватает "+(r.pcs-свободно));
else toast("Добавлено в заказ");
};
$("copyOne").onclick=()=>{if(S.sel)copy(textOne());};
$("copyOrder").onclick=()=>copy(textOrder());
/* масштаб печати: подгоняем карточку заказа под один лист А4 */
function printScale(){
const n=S.order.length;
const c=S.cl,cRows=[c.fio,c.phone,c.addr,c.car,+c.km,+c.ship].filter(Boolean).length;
const svc=SERVICES.filter(x=>S.svc[x.id]).length;
const h = 104 /* шапка с адресом и датой */
+ (cRows?cRows*24+22:0)
+ n*52 /* строка позиции: размер + подпись + количество */
+ svc*36
+ 132 /* свод */
+ 66; /* итоговая полоса */
if(Store.d.cfg&&Store.d.cfg.fit===false){document.documentElement.style.setProperty("--pscale","1");return 1;}
const avail=1010; /* А4 книжная за вычетом полей 12 мм */
const sc=Math.max(0.38,Math.min(1,avail/h));
document.documentElement.style.setProperty("--pscale",sc.toFixed(3));
return sc;
}
window.addEventListener("beforeprint",printScale);
$("printOrder").onclick=()=>{
const sc=printScale();
if(sc<1)toast("Масштаб печати "+Math.round(sc*100)+"% — заказ уложен на один лист");
setTimeout(()=>window.print(),sc<1?300:0);
};
$("toOrder").onclick=()=>$("order").scrollIntoView({behavior:"smooth",block:"center"});
const CF=[["fFio","fio"],["fPhone","phone"],["fAddr","addr"],["fDate","date"],["fTimeFrom","twFrom"],["fTimeTo","twTo"],["fCar","car"],["fShip","ship"],["fKm","km"],["fRate","rate"]];
CF.forEach(([id,k])=>$(id).oninput=e=>{S.cl[k]=k==="ship"?(+e.target.value||0):e.target.value.trim();renderOrder();});
$("clearClient").onclick=()=>{S.cl={fio:"",phone:"",addr:"",car:"",ship:0,date:"",km:0,rate:+Store.d.rate||0,twFrom:"",twTo:""};
CF.forEach(([id])=>$(id).value="");$("fRate").value=S.cl.rate||"";renderOrder();toast("Данные клиента очищены");};
/* «Дополнительно» — подарок клиенту (горбыль, опилки и т.п.), бесплатно, вне суммы заказа */
function showExtraIfNeeded(){ if((S.gift||"").trim()){$("extraBox").hidden=false;$("btnExtra").setAttribute("aria-expanded","true");} }
$("btnExtra").onclick=()=>{
const open=$("extraBox").hidden;
$("extraBox").hidden=!open;
$("btnExtra").setAttribute("aria-expanded",String(open));
if(open)$("fGift").focus();
};
$("fGift").oninput=e=>{S.gift=e.target.value;saveDraft();};
showExtraIfNeeded();
/* километраж × тариф = доставка */
function recalcShip(){
const km=+S.cl.km||0,rate=+S.cl.rate||0;
if(km>0&&rate>0){S.cl.ship=Math.round(km*rate);$("fShip").value=S.cl.ship;}
if(rate>0){Store.d.rate=rate;Store.save();}
renderOrder();
}
$("fKm").oninput=e=>{S.cl.km=+e.target.value||0;recalcShip();};
$("fRate").oninput=e=>{S.cl.rate=+e.target.value||0;recalcShip();};
$("btnRoute").onclick=()=>{
if(!S.cl.addr){toast("Сначала впишите адрес доставки");$("fAddr").focus();return;}
const url="https://yandex.ru/maps/?rtext="+encodeURIComponent(ORIGIN)+"~"+encodeURIComponent(S.cl.addr)+"&rtt=auto";
window.open(url,"_blank","noopener");
};
$("fRound").onchange=e=>{S.round=e.target.checked;renderOrder();};
$("fAdv").oninput=e=>{S.adv=+e.target.value||0;renderOrder();};
$("ordSave").onclick=saveOrder;
$("clear").onclick=newOrder;
$("histXlsWeek").onclick=()=>xls("week");
$("histXlsMonth").onclick=()=>xls("month");
["ct","cw","cl","cp","cq"].forEach(id=>$(id).oninput=renderCustom);
/* ========== старт ========== */
applyPrices();
renderPriceDate();
loadDraft();
if(typeof showExtraIfNeeded==="function")showExtraIfNeeded();
renderSorts();renderList();renderOrder();renderHistory();renderCustom();
/* ================= СКЛАД ================= */
const ST={list:"A",q:"",filter:"all"};
function stSanitize(){
const st=Store.d.stock||{};
Object.keys(st).forEach(k=>{
const r=st[k];
if(!r||typeof r!=="object"||typeof r.q!=="number")st[k]={q:0,min:0};
if(typeof r.min!=="number")st[k].min=0;
});
Store.d.stock=st;Store.d.moves=Store.d.moves||[];
}
stSanitize();
function allItems(){
const out=[];
["A","B","C"].forEach(l=>PRICES[l].items.forEach(it=>out.push({l,it,k:keyOf(l,it)})));
return out;
}
function findItem(k){return allItems().find(x=>x.k===k);}
function stRec(k){return Store.d.stock[k]||{q:0,min:0};}
function stQ(k){return stRec(k).q||0;}
function stMin(k){return stRec(k).min||0;}
function reserved(k){
const draft=S.order.filter(x=>x.k===k).reduce((a,x)=>a+x.pcs,0);
const inOrders=typeof reservedOrders==="function"?reservedOrders(k):0;
const mine=S.orderId?(Store.d.history||[]).filter(o=>String(o.id)===String(S.orderId))
.reduce((a,o)=>a+(o.items||[]).filter(x=>x.k===k).reduce((b,x)=>b+x.pcs,0),0):0;
return draft+inOrders-mine; /* открытый заказ не считаем дважды */
}
function stFree(k){return stQ(k)-reserved(k);}
function addMove(k,delta,type,note){
if(!delta)return 0;
const before=stRec(k).q||0;
const after=Math.max(0,before+delta);
const applied=after-before; /* сколько реально прошло: остаток не уходит в минус */
if(!applied)return 0;
const now=new Date(),z=n=>String(n).padStart(2,"0");
const f=findItem(k);
Store.d.moves.push({id:Date.now()+Math.random(),ts:Date.now(),
date:z(now.getDate())+"."+z(now.getMonth()+1)+"."+now.getFullYear(),
time:z(now.getHours())+":"+z(now.getMinutes()),
k,sz:f?listSize(f.it):"",kind:f?kindOf(f.it):"",sort:f?f.it.s:"",
q:applied,type,note:note||""});
Store.d.stock[k]={q:after,min:stRec(k).min||0};
Store.save();
return applied;
}
function setMin(k,v){const r=stRec(k);Store.d.stock[k]={q:r.q||0,min:Math.max(0,v||0)};Store.save();}
/* сводка */
function stTotals(){
let pcs=0,vol=0,money_=0,low=0,pos=0;
allItems().forEach(({it,k})=>{
const q=stQ(k);if(q>0){pos++;pcs+=q;vol+=q*it.v;money_+=q*it.v*it.p;}
if(stMin(k)>0&&q<stMin(k))low++;
});
return {pcs,vol,money:money_,low,pos};
}
function renderStSummary(){
const t=stTotals();
$("stSummary").innerHTML='<div class="stsum">'+
`<div class="c"><div class="k">Позиций на складе</div><div class="v">${t.pos}</div></div>`+
`<div class="c"><div class="k">Всего досок</div><div class="v">${t.pcs}</div></div>`+
`<div class="c"><div class="k">Объём</div><div class="v">${cube(t.vol)} м³</div></div>`+
`<div class="c"><div class="k">Сумма по прайсу</div><div class="v">${money0(t.money)}</div></div>`+
`<div class="c${t.low?" alert":""}"><div class="k">Ниже минимума</div><div class="v">${t.low}</div></div>`+
'</div>';
}
/* таблица остатков */
function stFiltered(){
const q=ST.q.trim().toLowerCase().replace(/[x*]/g,"х").replace(/\s+/g,"");
return PRICES[ST.list].items.map(it=>({it,k:keyOf(ST.list,it)})).filter(({it,k})=>{
if(ST.filter==="have"&&stQ(k)<=0)return false;
if(ST.filter==="low"&&!(stMin(k)>0&&stQ(k)<stMin(k)))return false;
if(!q)return true;
const hay=(it.n+it.s+it.g+listSize(it)).toLowerCase().replace(/[x*\u00D7]/g,"х").replace(/\s+/g,"");
return hay.includes(q);
});
}
function renderStFilters(){
const f=[["all","Все позиции"],["have","С остатком"],["low","Ниже минимума"]];
$("stFilters").innerHTML=f.map(([v,t])=>`<button class="chip" data-f="${v}" aria-pressed="${ST.filter===v}">${t}</button>`).join("");
$("stFilters").querySelectorAll(".chip").forEach(b=>b.onclick=()=>{ST.filter=b.dataset.f;renderStFilters();renderStTable();});
}
function renderStTable(){
const rows=stFiltered(),box=$("stTable");
if(!rows.length){box.innerHTML='<div class="empty">Ничего не найдено.</div>';return;}
box.innerHTML=rows.map(({it,k})=>{
const q=stQ(k),mn=stMin(k),res=reserved(k),low=mn>0&&q<mn;
return `<div class="strow${low?" low":""}${q<=0?" zero":""}">
<div class="body">
<div class="sz">${listSize(it)}<i> мм</i></div>
<div class="meta">${kindOf(it)} · ${it.s}${res?` · зарезервировано ${res} шт`:""}</div>
</div>
<button class="in" data-in="${k}" title="Приход">+</button>
<div class="fld"><span>Остаток</span><input type="number" min="0" value="${q}" data-q="${k}" inputmode="numeric"></div>
<div class="fld"><span>Минимум</span><input type="number" min="0" value="${mn||""}" data-min="${k}" inputmode="numeric"></div>
<div class="cub">${cube(q*it.v)} м³<br>${money0(q*it.v*it.p)}</div>
</div>`;
}).join("");
box.querySelectorAll("[data-in]").forEach(b=>b.onclick=()=>{
const v=prompt("Приход, шт:","0");if(v===null)return;
const n=Math.round(+String(v).replace(",",".")||0);
if(!n){toast("Количество не распознано");return;}
addMove(b.dataset.in,n,"in","приход");
renderStock();toast("Приход "+n+" шт");
});
box.querySelectorAll("[data-q]").forEach(inp=>inp.onchange=()=>{
const k=inp.dataset.q,was=stQ(k),now=Math.max(0,Math.round(+inp.value||0));
if(now===was){renderStock();return;}
addMove(k,now-was,"fix","инвентаризация");
renderStock();toast((now>was?"Добавлено ":"Списано ")+Math.abs(now-was)+" шт");
});
box.querySelectorAll("[data-min]").forEach(inp=>inp.onchange=()=>{
setMin(inp.dataset.min,Math.round(+inp.value||0));renderStock();
});
}
function moveLabel(t){return t==="in"?"приход":t==="out"?"заказ":t==="ret"?"возврат":"корректировка";}
function renderStMoves(){
const m=(Store.d.moves||[]).slice().reverse().slice(0,60);
$("stMoves").innerHTML=m.length?m.map(x=>
`<div class="mrow"><span class="d">${x.date} ${x.time}</span>
<span class="q ${x.q>0?"plus":"minus"}">${x.q>0?"+":""}${x.q} шт</span>
<span class="a">${x.sz} ${x.kind} · ${moveLabel(x.type)}${x.note?" · "+x.note:""}</span></div>`
).join(""):'<div class="empty">Движений пока нет.</div>';
}
function renderStockPrint(){
const rows=stFiltered();
const t=stTotals(),now=new Date(),z=n=>String(n).padStart(2,"0");
$("stockPrint").innerHTML='<div class="sheet"><h2>Ведомость остатков — '+PRICES[ST.list].title+'</h2>'+
'<div class="sub">Дата '+z(now.getDate())+"."+z(now.getMonth()+1)+"."+now.getFullYear()+
' · позиций в списке: '+rows.length+' · объём по учёту: '+cube(t.vol)+' м³</div>'+
'<table><tr><th>Размер, мм</th><th>Материал</th><th>Сорт</th><th>По учёту, шт</th><th>Факт, шт</th><th>Расхождение</th></tr>'+
rows.map(({it,k})=>`<tr><td>${listSize(it)}</td><td>${kindOf(it)}</td><td>${it.s}</td>
<td class="n">${stQ(k)}</td><td class="fact"></td><td class="fact"></td></tr>`).join("")+
'</table><p style="font-size:12px;margin-top:14px">Пересчитал: ______________________ Подпись: ____________</p></div>';
}
function renderStock(){renderStSummary();renderStFilters();renderStTable();renderStMoves();stBadge();}
/* выгрузка склада */
function xlsStock(){
const n=v=>String((+v||0).toFixed(2)).replace(".",",");
const n3=v=>String((+v||0).toFixed(3)).replace(".",",");
const M='style="mso-number-format:\'# ##0,00 \\₽\';text-align:right"';
const I='style="text-align:right"';
const T='style="mso-number-format:\'\\@\'"';
let h='<html><head><meta charset="utf-8"><style>td,th{border:.5pt solid #B6B6B6;padding:3px 6px;'+
'font-family:Calibri;font-size:11pt}th{background:#1C1A17;color:#fff;font-weight:700;text-align:center}'+
'.t1{font-size:16pt;font-weight:700;border:0}.t2{font-size:11pt;color:#555;border:0}.g{background:#F2F2F2;font-weight:700}'+
'</style></head><body>';
const t=stTotals();
h+='<table><tr><td class="t1" colspan="6">Лесопилка — остатки на складе</td></tr>'+
'<tr><td class="t2" colspan="6">Выгружено '+today()+' · объём '+cube(t.vol)+' м³ · на сумму '+money0(t.money)+'</td></tr></table><br>';
h+='<table><tr>'+["Прайс","Размер, мм","Материал","Сорт","Остаток, шт","Объём, м³","Цена ₽/м³","Сумма","Минимум","Нужно докупить, шт"]
.map(x=>"<th>"+x+"</th>").join("")+'</tr>';
allItems().forEach(({l,it,k})=>{
const q=stQ(k),mn=stMin(k);
if(!q&&!mn)return;
const need=mn>q?mn-q:0;
h+='<tr><td>'+PRICES[l].title+'</td><td '+T+'>'+listSize(it)+'</td><td>'+kindOf(it)+'</td><td>'+it.s+'</td>'+
'<td '+I+'>'+q+'</td><td '+I+'>'+n3(q*it.v)+'</td><td '+M+'>'+n(it.p)+'</td><td '+M+'>'+n(q*it.v*it.p)+'</td>'+
'<td '+I+'>'+(mn||"")+'</td><td '+I+'>'+(need||"")+'</td></tr>';
});
h+='<tr class="g"><td colspan="4">Итого</td><td '+I+'>'+t.pcs+'</td><td '+I+'>'+n3(t.vol)+'</td><td></td>'+
'<td '+M+'>'+n(t.money)+'</td><td></td><td></td></tr></table><br><br>';
h+='<table><tr><td class="t1" colspan="6">Движения</td></tr></table><table><tr>'+
["Дата","Время","Размер, мм","Материал","Операция","Количество, шт","Примечание"].map(x=>"<th>"+x+"</th>").join("")+'</tr>';
(Store.d.moves||[]).slice().reverse().forEach(x=>{
h+='<tr><td '+T+'>'+x.date+'</td><td '+T+'>'+x.time+'</td><td '+T+'>'+x.sz+'</td><td>'+x.kind+'</td>'+
'<td>'+moveLabel(x.type)+'</td><td '+I+'>'+x.q+'</td><td>'+(x.note||"")+'</td></tr>';
});
h+='</table></body></html>';
download(h,"lesopilka-sklad-"+today().replace(/\./g,"-")+".xls","application/vnd.ms-excel");
}
/* события склада */
["A","B","C"].forEach(l=>$("stTab"+l).onclick=()=>{
ST.list=l;["A","B","C"].forEach(x=>$("stTab"+x).setAttribute("aria-selected",String(x===l)));
renderStock();
});
$("stQuery").oninput=e=>{ST.q=e.target.value;renderStTable();};
$("stXls").onclick=xlsStock;
$("stPrint").onclick=()=>{
renderStockPrint();
document.body.classList.add("print-stock");
setTimeout(()=>{window.print();setTimeout(()=>document.body.classList.remove("print-stock"),400);},200);
};
/* переключение разделов */
function stBadge(){
const b=$("stBadge");if(!b)return;
const t=stTotals();b.textContent=t.low?String(t.low):"";
}
function setView(v){
const CALC=["priceSection","calcWrap","clientSection","orderSection","customSection"];
document.body.classList.toggle("view-stock",v==="stock");
[["viewCalc","calc"],["viewOrders","orders"],["viewStock","stock"],["viewCfg","cfg"]].forEach(([id,val])=>{
const b=$(id);if(b)b.setAttribute("aria-pressed",String(v===val));
});
CALC.forEach(id=>{const el=$(id);if(el)el.hidden=(v!=="calc");});
if($("histSection"))$("histSection").hidden=(v!=="orders");
if(v==="calc"&&$("calcWrap"))$("calcWrap").hidden=!S.sel;
if($("stockSection"))$("stockSection").hidden=(v!=="stock");
if($("cfgSection"))$("cfgSection").hidden=(v!=="cfg");
if(v==="stock")renderStock();else stBadge();
if(v==="orders")renderHistory();else ordBadge();
if(v==="cfg")fillCfgForm();
window.scrollTo(0,0);
}
if($("viewCalc"))$("viewCalc").onclick=()=>setView("calc");
if($("viewStock"))$("viewStock").onclick=()=>setView("stock");
if($("viewOrders"))$("viewOrders").onclick=()=>setView("orders");
if($("viewCfg"))$("viewCfg").onclick=()=>setView("cfg");
renderStock();
/* ================= НАСТРОЙКИ И РЕЗЕРВНАЯ КОПИЯ ================= */
const CFG_DEF={brand:"Лесопилка",origin:ORIGIN,rate:0,anti:2000,dry:4500,dryAdd:4000,fit:true,lastBackup:""};
function cfg(){Store.d.cfg=Object.assign({},CFG_DEF,Store.d.cfg||{});return Store.d.cfg;}
function applyCfg(){
const c=cfg();
ORIGIN=c.origin||CFG_DEF.origin;
const a=SERVICES.find(x=>x.id==="anti"),d=SERVICES.find(x=>x.id==="dry");
if(a)a.p=+c.anti||0; if(d)d.p=+c.dry||0;
if(+c.dryAdd>0&&+c.dryAdd!==DRY_ADD){
DRY_ADD=+c.dryAdd;
PRICES.C.note="Цена = прайс естественной влажности (сорт 1) + "+DRY_ADD.toLocaleString("ru-RU")+" ₽ за м³. Сушка уже в цене.";
}
applyPrices();
const h=document.querySelector("header h1");if(h&&c.brand)h.textContent=c.brand;
if(!S.cl.rate&&+c.rate>0){S.cl.rate=+c.rate;if($("fRate"))$("fRate").value=S.cl.rate;}
}
function fillCfgForm(){
const c=cfg();
$("cfgBrand").value=c.brand||"";
$("cfgOrigin").value=c.origin||"";
$("cfgRate").value=c.rate||"";
$("cfgAnti").value=c.anti||"";
$("cfgDry").value=c.dry||"";
$("cfgDryAdd").value=c.dryAdd||"";
$("cfgFit").checked=c.fit!==false;
const b=Store.d.cfg.lastBackup;
const st=Store.d.stock||{},hs=Store.d.history||[],mv=Store.d.moves||[],pr=Store.d.prices||{};
const vol=Object.keys(st).length;
$("cfgBackupInfo").innerHTML=(b?"Последняя копия: <b>"+b+"</b>":"Копия ещё не делалась")+
" · в базе: заказов "+hs.length+", позиций на складе "+vol+", движений "+mv.length+", правок цен "+Object.keys(pr).length;
}
function saveCfgField(k,v){const c=cfg();c[k]=v;Store.save();applyCfg();}
/* резервная копия */
function backupSave(){
const data={app:"lesopilka",version:3,saved:new Date().toISOString(),data:Store.d};
const c=cfg();c.lastBackup=today();Store.save();
const name="lesopilka-baza-"+today().replace(/\./g,"-")+".json";
try{
const blob=new Blob([JSON.stringify(data,null,1)],{type:"application/json"});
const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=name;
document.body.appendChild(a);a.click();document.body.removeChild(a);
setTimeout(()=>URL.revokeObjectURL(a.href),3000);
toast("Копия сохранена: "+name);
}catch(e){copy(JSON.stringify(data));toast("Скачивание недоступно — база скопирована в буфер");}
fillCfgForm();
}
function backupLoad(file){
const r=new FileReader();
r.onload=()=>{
let d;
try{d=JSON.parse(String(r.result));}catch(e){toast("Файл не читается как база");return;}
const body=(d&&d.data)?d.data:d;
if(!body||typeof body!=="object"||(!body.history&&!body.stock&&!body.prices)){
toast("Это не похоже на базу приложения");return;}
const hs=(body.history||[]).length,mv=(body.moves||[]).length;
if(!confirm("Заменить текущие данные на данные из файла?\n\nВ файле: заказов "+hs+", движений "+mv+
".\nТекущие данные будут потеряны, если вы их не сохранили."))return;
Store.d=Object.assign({prices:{},priceDate:"",stock:{},draft:null,history:[],moves:[],cfg:{}},body);
Store.save();
alert("База загружена. Страница перезагрузится.");
location.reload();
};
r.onerror=()=>toast("Не удалось прочитать файл");
r.readAsText(file);
}
/* события настроек */
$("cfgSave").onclick=backupSave;
$("cfgLoad").onclick=()=>$("cfgFile").click();
$("cfgFile").onchange=e=>{const f=e.target.files&&e.target.files[0];if(f)backupLoad(f);e.target.value="";};
$("cfgBrand").oninput=e=>saveCfgField("brand",e.target.value);
$("cfgOrigin").oninput=e=>saveCfgField("origin",e.target.value.trim());
$("cfgRate").oninput=e=>{saveCfgField("rate",+e.target.value||0);Store.d.rate=+e.target.value||0;Store.save();};
$("cfgAnti").oninput=e=>{saveCfgField("anti",+e.target.value||0);renderOrder();};
$("cfgDry").oninput=e=>{saveCfgField("dry",+e.target.value||0);renderOrder();};
$("cfgDryAdd").oninput=e=>{saveCfgField("dryAdd",+e.target.value||0);renderList();renderCalc();
$("listNote").textContent=PRICES[S.list].note||"";};
$("cfgFit").onchange=e=>saveCfgField("fit",e.target.checked);
$("cfgClrHist").onclick=()=>{
if(!confirm("Удалить всю историю заказов? Склад и движения останутся."))return;
Store.d.history=[];Store.save();renderHistory();fillCfgForm();toast("История очищена");};
$("cfgClrStock").onclick=()=>{
if(!confirm("Обнулить все остатки и удалить журнал движений?"))return;
Store.d.stock={};Store.d.moves=[];Store.save();renderStock();renderList();fillCfgForm();toast("Склад очищен");};
$("cfgClrPrices").onclick=()=>{
if(!confirm("Сбросить все правки цен к исходному прайсу?"))return;
Store.d.prices={};Store.d.priceDate="";Store.save();location.reload();};
/* ================= ДОКУМЕНТЫ: ТОВАРНЫЙ ЧЕК И АКТ ПРИЁМА-ПЕРЕДАЧИ ================= */
const RQ_DEF={name:"",inn:"",kpp:"",ogrn:"",addr:"Санкт-Петербург, Грузовой проезд, 29",
phone:"",post:"Генеральный директор",boss:"Лашков Леонид Германович",city:"Санкт-Петербург"};
function rq(){Store.d.rq=Object.assign({},RQ_DEF,Store.d.rq||{});return Store.d.rq;}
function fillRqForm(){
const r=rq();
[["rqName","name"],["rqInn","inn"],["rqKpp","kpp"],["rqOgrn","ogrn"],["rqAddr","addr"],
["rqPhone","phone"],["rqPost","post"],["rqBoss","boss"],["rqCity","city"]].forEach(([id,k])=>{
const el=$(id);if(el){el.value=r[k]||"";el.oninput=e=>{rq()[k]=e.target.value;Store.save();};}
});
}
/* сумма прописью */
const W1=["","один","два","три","четыре","пять","шесть","семь","восемь","девять","десять","одиннадцать",
"двенадцать","тринадцать","четырнадцать","пятнадцать","шестнадцать","семнадцать","восемнадцать","девятнадцать"];
const W10=["","","двадцать","тридцать","сорок","пятьдесят","шестьдесят","семьдесят","восемьдесят","девяносто"];
const W100=["","сто","двести","триста","четыреста","пятьсот","шестьсот","семьсот","восемьсот","девятьсот"];
function trio(n,fem){
const femW=w=>fem?(w==="один"?"одна":w==="два"?"две":w):w;
const h=W100[Math.floor(n/100)];
const t=n%100;
let mid="";
if(t<20)mid=femW(W1[t]);
else mid=(W10[Math.floor(t/10)]+" "+femW(W1[t%10])).trim();
return [h,mid].filter(Boolean).join(" ");
}
function plur(n,a,b,c){const m=n%100,k=n%10;return (m>10&&m<20)?c:k===1?a:(k>1&&k<5)?b:c;}
function rubWords(v){
const total=Math.round((+v||0)*100);
let rub=Math.floor(total/100),kop=total%100;
if(!rub&&!kop)return "Ноль рублей 00 копеек";
const parts=[],units=[["",""],["тысяча","тысячи"],["миллион","миллиона"],["миллиард","миллиарда"]];
const chunks=[];let x=rub;
while(x>0){chunks.push(x%1000);x=Math.floor(x/1000);}
for(let i=chunks.length-1;i>=0;i--){
const c=chunks[i];if(!c)continue;
const fem=(i===1);
parts.push(trio(c,fem));
if(i===1)parts.push(plur(c,"тысяча","тысячи","тысяч"));
if(i===2)parts.push(plur(c,"миллион","миллиона","миллионов"));
if(i===3)parts.push(plur(c,"миллиард","миллиарда","миллиардов"));
}
let str=parts.filter(Boolean).join(" ").trim();
str=str.charAt(0).toUpperCase()+str.slice(1);
return str+" "+plur(rub,"рубль","рубля","рублей")+" "+String(kop).padStart(2,"0")+" "+
plur(kop,"копейка","копейки","копеек");
}
/* строки документа */
function docLines(){
const out=[];
S.order.forEach(x=>{
out.push({name:x.kind+" "+x.sz+" мм, "+x.sort,
unit:x.unit==="m3"?"м³":"шт",
qty:x.unit==="m3"?(+x.vol).toFixed(3).replace(".",","):String(x.pcs),
price:x.unit==="m3"?x.rate:x.per,sum:x.sum});
});
const v=orderVolume();
SERVICES.forEach(sv=>{if(S.svc[sv.id])out.push({name:sv.n,unit:"м³",
qty:(+v).toFixed(3).replace(".",","),price:sv.p,sum:sv.p*v});});
if(shipSum())out.push({name:"Доставка"+(S.cl.car?" ("+S.cl.car+")":"")+(+S.cl.km?", "+num0(S.cl.km)+" км":""),
unit:"усл.",qty:"1",price:shipSum(),sum:shipSum()});
return out;
}
function docNumDate(){
const cur=S.orderId?(Store.d.history||[]).find(x=>String(x.id)===String(S.orderId)):null;
return {num:cur&&cur.num?cur.num:"б/н",date:today()};
}
function docHeadRq(){
const r=rq();
const bits=[r.name||"[укажите наименование продавца в настройках]"];
const ids=[];if(r.inn)ids.push("ИНН "+r.inn);if(r.kpp)ids.push("КПП "+r.kpp);if(r.ogrn)ids.push("ОГРН "+r.ogrn);
if(ids.length)bits.push(ids.join(", "));
if(r.addr)bits.push("Адрес: "+r.addr);
if(r.phone)bits.push("Телефон: "+r.phone);
return '<div class="rq">'+bits.map(x=>"<div>"+x+"</div>").join("")+'</div>';
}
function docBuyer(){
const c=S.cl,b=[];
if(c.fio)b.push(c.fio);else b.push("физическое лицо");
if(c.phone)b.push("тел. "+c.phone);
if(c.addr)b.push("адрес доставки: "+c.addr);
return b.join(", ");
}
function docTable(lines){
return '<table><tr><th style="width:22pt">№</th><th>Наименование товара, работ, услуг</th>'+
'<th style="width:34pt">Ед.</th><th style="width:52pt">Кол-во</th>'+
'<th style="width:66pt">Цена, ₽</th><th style="width:76pt">Сумма, ₽</th></tr>'+
lines.map((l,i)=>'<tr><td class="c">'+(i+1)+'</td><td>'+l.name+'</td><td class="c">'+l.unit+'</td>'+
'<td class="n">'+l.qty+'</td><td class="n">'+num(l.price)+'</td><td class="n">'+num(l.sum)+'</td></tr>').join("")+
'</table>';
}
function docTotals(){
let h='<div class="tot">';
h+='<div class="r"><span>Материал</span><span>'+money(orderGoods())+'</span></div>';
if(svcSum())h+='<div class="r"><span>Услуги</span><span>'+money(svcSum())+'</span></div>';
if(shipSum())h+='<div class="r"><span>Доставка</span><span>'+money(shipSum())+'</span></div>';
h+='<div class="r big"><span>Итого к оплате</span><span>'+money(grand())+'</span></div>';
if(advSum()){
h+='<div class="r"><span>Получен аванс</span><span>'+money(advSum())+'</span></div>';
h+='<div class="r big"><span>Остаток к оплате</span><span>'+money(restSum())+'</span></div>';
}
return h+'</div>';
}
function docStamp(note){
return '<div class="mp"><div class="stampbox">М.П.<br>место печати</div>'+
'<div class="note">'+(note||"")+'</div></div>';
}
function buildDocs(){
const r=rq(),nd=docNumDate(),lines=docLines();
const sumWords=rubWords(grand());
const boss=r.post+" "+(r.boss||"");
const initials=(function(f){const p=(f||"").trim().split(/\s+/);
return p.length>=3?p[0]+" "+p[1][0]+". "+p[2][0]+".":f;})(r.boss);
/* 1. Товарный чек */
let cheque='<div class="doc">'+docHeadRq()+
'<h2>ТОВАРНЫЙ ЧЕК № '+nd.num+'</h2>'+
'<div class="sub">от '+nd.date+' г.</div>'+
'<div class="party"><b>Продавец:</b> '+(r.name||"—")+'</div>'+
'<div class="party"><b>Покупатель:</b> '+docBuyer()+'</div>'+
docTable(lines)+
'<div class="tot"><div class="r"><span>Всего наименований '+lines.length+'</span><span></span></div></div>'+
docTotals()+
'<div class="words">Сумма прописью: '+sumWords+'</div>'+
'<div class="signs"><div class="col"><div class="lbl">'+r.post+'</div>'+
'<div class="line"></div><div class="cap">подпись / '+initials+'</div></div>'+
'<div class="col"><div class="lbl">Товар получил</div><div class="line"></div>'+
'<div class="cap">подпись покупателя / расшифровка</div></div></div>'+
docStamp('Товарный чек подтверждает наименование, количество и цену переданного товара.<br>'+
'При расчётах с применением ККТ выдаётся вместе с кассовым чеком.')+
'</div>';
/* 2. Акт приёма-передачи */
let act='<div class="doc">'+docHeadRq()+
'<h2>АКТ приёма-передачи товара № '+nd.num+'</h2>'+
'<div class="sub">от '+nd.date+' г., '+(r.city||"")+'</div>'+
'<div class="text"><b>'+(r.name||"—")+'</b>, в лице '+(r.post||"").toLowerCase()+' '+(r.boss||"")+
', именуемое далее Продавец, с одной стороны, и '+docBuyer()+
', именуемый далее Покупатель, с другой стороны, составили настоящий акт о том, что Продавец передал, '+
'а Покупатель принял следующий товар:</div>'+
docTable(lines)+
docTotals()+
'<div class="words">Сумма прописью: '+sumWords+'</div>'+
'<div class="text">Товар передан в количестве, ассортименте и по ценам, указанным в настоящем акте. '+
'Претензий по количеству, качеству, комплектности и внешнему виду товара Покупатель не имеет. '+
'Право собственности и риск случайной гибели товара переходят к Покупателю с момента подписания настоящего акта. '+
'Настоящий акт составлен в двух экземплярах, имеющих одинаковую юридическую силу, по одному для каждой из сторон.</div>'+
'<div class="signs"><div class="col"><div class="lbl">Товар передал</div>'+
'<div class="cap" style="margin-bottom:6pt">'+boss+'</div>'+
'<div class="line"></div><div class="cap">подпись / '+initials+'</div></div>'+
'<div class="col"><div class="lbl">Товар принял</div>'+
'<div class="cap" style="margin-bottom:6pt">'+((S.cl.fio)||"Покупатель")+'</div>'+
'<div class="line"></div><div class="cap">подпись / расшифровка</div></div></div>'+
docStamp('Составлен по требованиям к первичному учётному документу: наименование, дата, наименование сторон, '+
'содержание операции, натуральное и денежное измерение, должности и подписи сторон.')+
'</div>';
$("docSheet").innerHTML=cheque+act;
}
function printDocs(){
if(!S.order.length)return;
const r=rq();
if(!r.name||!r.inn){
if(!confirm("В настройках не заполнены наименование продавца и ИНН — без них документ не будет соответствовать требованиям к первичным документам.\n\nПечатать всё равно?"))
{setView("cfg");return;}
}
buildDocs();
document.body.classList.add("print-doc");
setTimeout(()=>{window.print();setTimeout(()=>document.body.classList.remove("print-doc"),400);},250);
}
$("docPrint").onclick=printDocs;
fillRqForm();
applyCfg();
fillCfgForm();
renderList();
</script>
<!-- ===== ПОДРОБНАЯ КАРТОЧКА ЗАКАЗА ===== -->
<script>
(function(){
"use strict";
if(typeof Store === "undefined") return;
/* ---------- окно ---------- */
var TRASH =
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" '+
'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
'<path d="M4 7h16"/><path d="M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>'+
'<path d="M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"/>'+
'<path d="M10 11v6M14 11v6"/></svg>';
var sheet = document.createElement("div");
sheet.id = "ordSheet";
sheet.className = "no-print";
sheet.innerHTML = '<div id="ordCard" role="dialog" aria-modal="true" aria-label="Заказ">'+
'<div class="oc-top"><div><div class="n" id="ocNum">—</div><div class="dt" id="ocDate">—</div></div>'+
'<div class="st" id="ocSt">—</div>'+
'<button class="oc-del" type="button" id="ocDel" title="Удалить заказ" aria-label="Удалить заказ">'+TRASH+'</button>'+
'<button class="oc-x" type="button" id="ocX" aria-label="Закрыть">✕</button></div>'+
'<div class="oc-tape" aria-hidden="true"></div>'+
'<div class="oc-body" id="ocBody"></div>'+
'<div class="oc-act">'+
'<button type="button" id="ocCopy">Скопировать</button>'+
'<button type="button" id="ocPrint">Чек и акт</button>'+
'<button type="button" class="pri" id="ocOpen">Открыть в расчёте</button>'+
'</div></div>';
document.body.appendChild(sheet);
var curId = null;
function esc(t){ return String(t==null?"":t).replace(/[&<>"]/g,function(c){
return {"&":"&","<":"<",">":">",'"':"""}[c]; }); }
function m0(n){ try{ return money0(n); }catch(e){ return Math.round(+n||0).toLocaleString("ru-RU")+" \u20BD"; } }
function m2(n){ try{ return money(n); }catch(e){ return (+n||0).toFixed(2)+" \u20BD"; } }
function cb(n){ try{ return cube(n); }catch(e){ return (+n||0).toFixed(3); } }
function find(id){ return (Store.d.history||[]).find(function(o){ return String(o.id)===String(id); }); }
function row(k,v){ return v ? '<div class="f"><div class="k">'+k+'</div><div class="v">'+v+'</div></div>' : ''; }
/* ---------- наполнение ---------- */
function fill(o){
curId = o.id;
var st = (typeof stOf==="function") ? stOf(o) : (o.status||"shipped");
document.getElementById("ocNum").textContent = "Заказ № " + (o.num || "—");
document.getElementById("ocDate").textContent = (o.date||"") + (o.time? " · "+o.time : "");
document.getElementById("ocSt").textContent = (typeof stName==="function") ? stName(st) : st;
var h = '';
/* клиент и доставка */
var phone = (o.phone||"").trim();
var cli =
row("Клиент", esc(o.fio)) +
row("Телефон", phone ? '<a href="tel:'+esc(phone.replace(/[^\d+]/g,""))+'">'+esc(phone)+'</a>' : '') +
row("Адрес", esc(o.addr)) +
row("Дата доставки", esc(o.dlv) + (((o.twFrom||"").trim()||(o.twTo||"").trim()) ?
' · <span style="color:var(--steel);font-weight:700">🕐 '+esc((o.twFrom||"?").trim())+'–'+esc((o.twTo||"?").trim())+'</span>' : "")) +
row("Машина", esc(o.car)) +
(o.km ? row("Расстояние", cb(o.km).replace(",000","") + " км" + (o.rate? " · "+m0(o.rate)+"/км" : "")) : "") +
((o.gift||"").trim() ? row("Подарок клиенту", '<span style="color:var(--mark);font-weight:600">🎁 '+esc(o.gift.trim())+'</span>') : "");
if(cli) h += '<div class="oc-lab">Клиент и доставка</div>' + cli;
else h += '<div class="oc-lab">Клиент и доставка</div><div class="f"><div class="v" style="color:var(--ink-70)">Данные не заполнялись</div></div>';
/* позиции */
var items = o.items || [];
h += '<div class="oc-lab">Позиции · '+items.length+'</div>';
if(items.length){
h += '<div class="it">' + items.map(function(it){
var sub = [it.kind, it.sort].filter(Boolean).join(" · ");
var per = it.per ? m2(it.per)+"/шт" : "";
return '<div class="r"><div class="a">'+
'<div class="sz">'+esc(it.sz||"—")+'</div>'+
(sub? '<div class="sub">'+esc(sub)+(per? " · "+per : "")+'</div>' : '')+
'</div>'+
'<div class="q">'+m0(it.sum)+
'<small>'+(it.pcs||0)+' шт · '+cb(it.vol||0)+' м³</small>'+
'</div></div>';
}).join("") + '</div>';
} else {
h += '<div class="f"><div class="v" style="color:var(--ink-70)">Позиций нет</div></div>';
}
/* услуги */
var sv = o.svcFlags || {};
var svNames = [];
try{
SERVICES.forEach(function(s){ if(sv[s.id]) svNames.push(s.n); });
}catch(e){
if(sv.anti) svNames.push("Антисептирование");
if(sv.dry) svNames.push("Камерная сушка");
}
if(svNames.length){
h += '<div class="oc-lab">Услуги</div>' + row(svNames.join(", "), m0(o.svc));
}
/* итоги */
h += '<div class="oc-lab">Итоги</div><div class="tot">'+
row("Материал", m0(o.goods)) +
(o.svc ? row("Услуги", m0(o.svc)) : "") +
(o.ship ? row("Доставка", m0(o.ship)) : "") +
row("Объём", cb(o.vol||0)+" м³ · "+(o.pcs||0)+" шт") +
(o.adv ? row("Аванс", m0(o.adv)) : "") +
(o.rest ? row("Остаток к оплате", m0(o.rest)) : "") +
'<div class="grandline"><span>К оплате</span><b>'+m0(o.total)+'</b></div>'+
'</div>';
/* статус доставки и оплата */
var dsName = (typeof window.__dstatusName === "function") ? window.__dstatusName(o.dstatus || "pending") : "На согласование";
h += '<div class="oc-lab">Статус и оплата</div>' +
row("Статус доставки", esc(dsName)) +
'<div class="f"><div class="k">Оплата</div><div class="v">' +
((typeof window.__payPanelHTML === "function") ? window.__payPanelHTML(o) : '') +
'</div></div>';
document.getElementById("ocBody").innerHTML = h;
document.getElementById("ocBody").scrollTop = 0;
}
/* ---------- оплата внутри карточки: биндим общие обработчики + подхватываем изменения ---------- */
function bindPayPanel(){
var body = document.getElementById("ocBody");
if(typeof window.__bindStatusPanel === "function") window.__bindStatusPanel(body);
Array.prototype.forEach.call(body.querySelectorAll("[data-paytoggle],[data-pmtoggle]"), function(b){
var orig = b.onclick;
b.onclick = function(e){
if(orig) orig(e);
refreshOpen();
};
});
}
function refreshOpen(){
if(!curId) return;
var o = find(curId); if(!o) return;
var body = document.getElementById("ocBody");
var y = body.scrollTop;
fill(o);
body.scrollTop = y;
bindPayPanel();
}
/* ---------- текст для копирования ---------- */
function asText(o){
var L = [];
L.push("Заказ № " + (o.num||"—") + " от " + (o.date||""));
L.push("Статус: " + ((typeof stName==="function") ? stName(o.status||"shipped") : (o.status||"")));
if(o.fio) L.push("Клиент: " + o.fio);
if(o.phone) L.push("Телефон: " + o.phone);
if(o.addr) L.push("Адрес: " + o.addr);
if(o.dlv) L.push("Доставка: " + o.dlv + (o.car? ", "+o.car : ""));
L.push("");
(o.items||[]).forEach(function(it){
L.push("· " + (it.sz||"") + (it.kind? " "+it.kind : "") + (it.sort? ", "+it.sort : "") +
" — " + (it.pcs||0) + " шт, " + cb(it.vol||0) + " м³, " + m0(it.sum));
});
L.push("");
L.push("Материал: " + m0(o.goods));
if(o.svc) L.push("Услуги: " + m0(o.svc));
if(o.ship) L.push("Доставка: " + m0(o.ship));
L.push("Объём: " + cb(o.vol||0) + " м³, " + (o.pcs||0) + " шт");
L.push("К ОПЛАТЕ: " + m0(o.total));
if(o.adv) L.push("Аванс: " + m0(o.adv) + ", остаток: " + m0(o.rest));
return L.join("\n");
}
/* ---------- открытие и закрытие ---------- */
function open(id){
var o = find(id); if(!o) return;
fill(o);
bindPayPanel();
sheet.classList.add("on");
document.body.style.overflow = "hidden";
document.getElementById("ocX").focus();
}
function close(){
sheet.classList.remove("on");
document.body.style.overflow = "";
curId = null;
}
/* страховка: окно закрыто — прокрутка страницы обязана вернуться */
setInterval(function(){
if(!sheet.classList.contains("on") && document.body.style.overflow === "hidden"
&& !document.body.classList.contains("gated")){
document.body.style.overflow = "";
}
}, 1500);
document.getElementById("ocX").onclick = close;
document.getElementById("ocDel").onclick = function(){
var id = curId; if(!id) return;
try{ delHistory(id); }catch(err){}
if(!find(id)) close(); // закрываем, только если заказ реально удалён (confirm подтверждён)
};
sheet.addEventListener("click", function(e){ if(e.target === sheet) close(); });
document.addEventListener("keydown", function(e){
if(e.key === "Escape" && sheet.classList.contains("on")) close();
});
document.getElementById("ocOpen").onclick = function(){
var id = curId; close();
try{ openOrder(id); setView("calc"); }catch(e){}
};
document.getElementById("ocPrint").onclick = function(){
var id = curId; close();
printOrder(id);
};
document.getElementById("ocCopy").onclick = function(){
var o = find(curId); if(!o) return;
try{ copy(asText(o)); }
catch(e){
try{ navigator.clipboard.writeText(asText(o)); toast("Скопировано"); }catch(e2){}
}
};
/* ---------- печать чека и акта по конкретному заказу ---------- */
var PRINTER =
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" '+
'stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
'<path d="M7 9V3h10v6"/><rect x="3" y="9" width="18" height="7" rx="2"/>'+
'<rect x="7" y="15" width="10" height="6" rx="1"/></svg>';
function printOrder(id){
var o = find(id); if(!o) return;
if(!(o.items || []).length){
try{ toast("В заказе нет позиций"); }catch(e){}
return;
}
try{
openOrder(id); // подставляем заказ в расчёт
setTimeout(function(){
try{ printDocs(); }catch(e){}
}, 150);
}catch(e){}
}
/* ---------- кнопка в каждой строке списка ---------- */
function addButtons(){
var box = document.getElementById("histBox"); if(!box) return;
Array.prototype.forEach.call(box.querySelectorAll(".orow"), function(r){
if(r.querySelector("[data-info]")) return;
var op = r.querySelector("[data-open]"); if(!op) return;
var id = op.getAttribute("data-open");
var b = document.createElement("button");
b.className = "ib info";
b.type = "button";
b.setAttribute("data-info", id);
b.title = "Подробности заказа";
b.textContent = "i";
r.insertBefore(b, op);
b.onclick = function(){ open(id); };
var pr = document.createElement("button");
pr.className = "ib prn";
pr.type = "button";
pr.title = "Печать чека и акта";
pr.setAttribute("aria-label", "Печать чека и акта");
pr.innerHTML = PRINTER;
r.insertBefore(pr, op);
pr.onclick = function(){ printOrder(id); };
});
}
window.__orderCard = open; /* открытие карточки из других разделов */
var baseRender = window.renderHistory;
window.renderHistory = function(){
try{ baseRender.apply(null, arguments); }catch(e){}
addButtons();
};
addButtons();
})();
</script>
<!-- ===== СТАТУС ДОСТАВКИ / ОПЛАТА — ОБЩИЙ МОДУЛЬ ДЛЯ ЗАКАЗОВ И ДОСТАВОК ===== -->
<script>
(function(){
"use strict";
if(typeof Store === "undefined") return;
window.DSTATUSES = [
["pending", "На согласование"],
["confirmed", "Подтвержден"],
["shipped2", "Отгружен"],
["delivered", "Доставлен"],
["cancel2", "Отменен"]
];
function esc(t){ return String(t==null?"":t).replace(/[&<>"]/g,function(c){
return {"&":"&","<":"<",">":">",'"':"""}[c]; }); }
function findOrder(id){ return (Store.d.history||[]).find(function(x){ return String(x.id)===String(id); }); }
function dstatusOf(o){ return o.dstatus || "pending"; }
function dstatusName(k){ var f = DSTATUSES.filter(function(x){ return x[0]===k; })[0]; return f ? f[1] : k; }
function paidOf(o){ return !!o.paidFlag; }
function payMethodOf(o){ return o.payMethod || "cash"; }
function payMethodName(k){ return k === "transfer" ? "Переводом" : "Наличными"; }
/* ---------- разметка панели — вызывается из orders_calendar.html и delivery.html ---------- */
function dstatusSelectHTML(o){
var ds = dstatusOf(o);
var opts = DSTATUSES.map(function(pair){
return '<option value="'+pair[0]+'"'+(pair[0]===ds?' selected':'')+'>'+esc(pair[1])+'</option>';
}).join("");
return '<select class="dssel ds-'+ds+'" data-dssel="'+o.id+'" aria-label="Статус доставки">'+opts+'</select>';
}
function payButtonsInner(o){
var paid = paidOf(o), pm = payMethodOf(o);
return '<button type="button" class="paybtn '+(paid?'paid':'unpaid')+'" data-paytoggle="'+o.id+'">'+
(paid ? "Оплачен" : "Не оплачен")+
'</button>'+
'<button type="button" class="pmbtn" data-pmtoggle="'+o.id+'" title="Способ оплаты, нажмите чтобы сменить">'+
esc(payMethodName(pm))+
'</button>';
}
/* компактная плашка «оплачен / способ оплаты» — используется в карточке заказа */
function payPanelHTML(o){
return '<div class="paypanel" data-oid="'+o.id+'">' + payButtonsInner(o) + '</div>';
}
/* полная панель: статус доставки + оплата — используется в строке Заказов/Доставок */
function statusPanelHTML(o){
return '<div class="stpanel" data-oid="'+o.id+'">' + dstatusSelectHTML(o) + payButtonsInner(o) + '</div>';
}
/* ---------- обработчики — вызываются после вставки разметки в DOM ---------- */
function bindStatusPanel(root){
if(!root) return;
Array.prototype.forEach.call(root.querySelectorAll("[data-dssel]"), function(sel){
sel.onchange = function(e){
if(e && e.stopPropagation) e.stopPropagation();
var o = findOrder(sel.getAttribute("data-dssel")); if(!o) return;
o.dstatus = sel.value;
Store.save();
try{ toast("Статус доставки: "+dstatusName(o.dstatus)); }catch(err){}
try{ renderHistory(); }catch(err){}
};
sel.onclick = function(e){ e.stopPropagation(); };
});
Array.prototype.forEach.call(root.querySelectorAll("[data-paytoggle]"), function(b){
b.onclick = function(e){
e.stopPropagation();
var o = findOrder(b.getAttribute("data-paytoggle")); if(!o) return;
o.paidFlag = !o.paidFlag;
Store.save();
try{ toast(o.paidFlag ? "Отмечено как оплачено" : "Отмечено как не оплачено"); }catch(err){}
try{ renderHistory(); }catch(err){}
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-pmtoggle]"), function(b){
b.onclick = function(e){
e.stopPropagation();
var o = findOrder(b.getAttribute("data-pmtoggle")); if(!o) return;
o.payMethod = (payMethodOf(o) === "cash") ? "transfer" : "cash";
Store.save();
try{ toast("Способ оплаты: "+payMethodName(o.payMethod)); }catch(err){}
try{ renderHistory(); }catch(err){}
};
});
}
window.__statusPanelHTML = statusPanelHTML;
window.__bindStatusPanel = bindStatusPanel;
window.__dstatusName = dstatusName;
window.__dstatusSelectHTML = dstatusSelectHTML;
window.__payPanelHTML = payPanelHTML;
})();
</script>
<!-- ===== ЗАКАЗЫ КАК КАЛЕНДАРЬ ПРОИЗВОДСТВА (дизайн «Доставок») ===== -->
<script>
(function(){
"use strict";
var histSection = document.getElementById("histSection");
if(!histSection || typeof Store === "undefined") return;
var root = document.createElement("div");
root.id = "ocalRoot";
root.innerHTML =
'<div class="seg period" id="ocalPeriod">'+
'<button type="button" data-p="all" aria-pressed="true">Все</button>'+
'<button type="button" data-p="w" aria-pressed="false">Неделя</button>'+
'<button type="button" data-p="m" aria-pressed="false">Месяц</button>'+
'<button type="button" data-p="arch" aria-pressed="false">Архив</button>'+
'</div>'+
'<div id="ocalBody"></div>';
histSection.appendChild(root);
var PERIOD = "all";
var MON = ["января","февраля","марта","апреля","мая","июня","июля","августа",
"сентября","октября","ноября","декабря"];
var WD = ["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"];
var ACTIVE_SET = {};
(window.ACTIVE || ["new","paid","ready"]).forEach(function(s){ ACTIVE_SET[s] = true; });
var COPY_ICON =
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" '+
'stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
'<rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>';
function esc(t){ return String(t==null?"":t).replace(/[&<>"]/g,function(c){
return {"&":"&","<":"<",">":">",'"':"""}[c]; }); }
function m0(n){ try{ return money0(n); }catch(e){ return Math.round(+n||0).toLocaleString("ru-RU")+" \u20BD"; } }
function cb(n){ try{ return cube(n); }catch(e){ return (+n||0).toFixed(3); } }
function plu(n,a,b,c){ var d=n%100,e=n%10; if(d>4&&d<21)return c; if(e===1)return a; if(e>1&&e<5)return b; return c; }
function zpad(n){ return String(n).padStart(2,"0"); }
function stOfSafe(o){ try{ return stOf(o); }catch(e){ return o.status||"shipped"; } }
function parseRu(s){
var p = String(s||"").split(".");
if(p.length !== 3) return null;
var d = new Date(+p[2], +p[1]-1, +p[0]);
return isNaN(d) ? null : d;
}
function midnight(d){ var x=new Date(d); x.setHours(0,0,0,0); return x; }
var TRASH =
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" '+
'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
'<path d="M4 7h16"/><path d="M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>'+
'<path d="M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"/>'+
'<path d="M10 11v6M14 11v6"/></svg>';
function statName(st){
try{ return stName(st); }
catch(e){
return {new:"Согласован",paid:"Оплачен",ready:"Напилен",shipped:"Отгружен",cancel:"Отменён"}[st] || st;
}
}
/* «Собран» = заказ напилен и готов («ready»), «Не собран» — ещё в работе.
Отгруженные и отменённые заказы этой кнопкой не трогаем — там просто подпись. */
function toggleHTML(o, st){
if(st === "shipped" || st === "cancel"){
return '<span class="stlabel">'+esc(statName(st))+'</span>';
}
if(st === "ready"){
return '<button type="button" class="stbtn ready" data-toggle="'+o.id+'" data-to="new">Собран</button>';
}
return '<button type="button" class="stbtn notready" data-toggle="'+o.id+'" data-to="ready">Не собран</button>';
}
function orderRow(o){
var st = stOfSafe(o);
var dstatusSel = (typeof window.__dstatusSelectHTML === "function") ? window.__dstatusSelectHTML(o) : '';
return '<div class="trip st-'+st+'" data-oid="'+o.id+'">'+
'<div class="triprow">'+
'<button type="button" class="tripopen" data-open="'+o.id+'">'+
'<i class="dot"></i>'+
'<div class="a"><b>'+esc(o.addr || "адрес не указан")+'</b>'+
'<span>№ '+esc(o.num||"")+(o.fio ? ' · '+esc(o.fio) : '')+' · создан '+esc(o.date||"—")+'</span>'+
'</div>'+
'</button>'+
'<div class="tripctl">'+
dstatusSel +
toggleHTML(o, st)+
'</div>'+
'<div class="n">'+cb(o.vol||0)+' м³<small>'+m0(o.total)+'</small></div>'+
'</div>'+
'</div>';
}
function dayCard(dateStr, list, today){
var d = parseRu(dateStr);
var isToday = d && midnight(d).getTime() === today.getTime();
var vol = 0; list.forEach(function(o){ vol += (+o.vol||0); });
var sub = list.length+' '+plu(list.length,"заказ","заказа","заказов")+' \u00B7 '+cb(vol)+' м³';
var label = d ? (d.getDate()+" "+MON[d.getMonth()]+", "+WD[d.getDay()]) : "Дата доставки не указана";
return '<div class="hist day">'+
'<div class="day-head">'+
'<div class="dt"><b>'+esc(label)+'</b><span>'+sub+'</span></div>'+
(isToday ? '<span class="today">сегодня</span>' : '')+
(d ? '<button type="button" class="cp" data-copyday="'+dateStr+'" title="Скопировать план дня" aria-label="Скопировать план дня">'+COPY_ICON+'</button>' : '')+
'</div>'+
list.map(orderRow).join("") +
'</div>';
}
function dayPlanText(dateStr, list){
var L = [];
L.push("План на " + dateStr);
L.push("");
var vol = 0;
list.forEach(function(o, i){
vol += (+o.vol||0);
L.push((i+1)+") "+(o.addr||"адрес не указан")+" — заказ №"+(o.num||""));
(o.items||[]).forEach(function(it){
var bits = [];
if(it.pcs) bits.push(it.pcs+" шт");
if(it.vol) bits.push(cb(it.vol)+" м³");
L.push(" · "+(it.sz||"")+(bits.length?" — "+bits.join(", "):""));
});
L.push("");
});
L.push("Итого: "+list.length+" "+plu(list.length,"заказ","заказа","заказов")+", "+cb(vol)+" м³");
return L.join("\n");
}
function dstatusOfSafe(o){ return (o && o.dstatus) || "pending"; }
function build(){
var box = document.getElementById("ocalBody");
var isArch = (PERIOD === "arch");
box.parentNode.classList.toggle("arch-mode", isArch);
var all = (Store.d.history||[]).filter(function(o){
var st = stOfSafe(o);
if(st === "cancel") return false;
var delivered = (dstatusOfSafe(o) === "delivered");
return isArch ? delivered : !delivered;
});
if(!all.length){
box.innerHTML = '<div class="empty">'+(isArch ? "В архиве пока пусто — сюда попадают заказы со статусом «Доставлен»." : "Заказов пока нет.")+'</div>';
return;
}
var today = midnight(new Date());
var to = null;
if(!isArch && PERIOD === "w"){ to = new Date(today); to.setDate(to.getDate()+7); }
if(!isArch && PERIOD === "m"){ to = new Date(today); to.setDate(to.getDate()+31); }
var groups = {}, order = [], noDate = [];
all.forEach(function(o){
var d = parseRu(o.dlv);
if(!d){ noDate.push(o); return; }
var md = midnight(d);
if(!isArch && PERIOD !== "all" && (md < today || (to && md >= to))) return;
var key = o.dlv;
if(!groups[key]){ groups[key] = []; order.push(key); }
groups[key].push(o);
});
order.sort(function(a,b){
var t = parseRu(a).getTime() - parseRu(b).getTime();
return isArch ? -t : t; // в архиве — сначала недавние
});
var totalCnt = 0, totalVol = 0, totalSum = 0;
order.forEach(function(k){ groups[k].forEach(function(o){ totalCnt++; totalVol+=(+o.vol||0); totalSum+=(+o.total||0); }); });
noDate.forEach(function(o){ totalCnt++; totalVol+=(+o.vol||0); totalSum+=(+o.total||0); });
var h = '<div class="kpis">'+
'<div class="kpi"><div class="k">'+(isArch?"Завершено":"Заказов")+'</div><div class="v">'+totalCnt+'</div></div>'+
'<div class="kpi"><div class="k">Объём</div><div class="v">'+cb(totalVol)+' м³</div></div>'+
'<div class="kpi"><div class="k">На сумму</div><div class="v">'+m0(totalSum)+'</div></div>'+
'</div>';
if(!order.length && !noDate.length){
h += '<div class="empty">'+(isArch ? "В архиве пока пусто." : "На этот период заказов нет.")+'</div>';
}
order.forEach(function(key){ h += dayCard(key, groups[key], today); });
if(noDate.length){
h += dayCard(null, noDate, today);
}
box.innerHTML = h;
bind();
}
function bind(){
var box = document.getElementById("ocalBody");
if(typeof window.__bindStatusPanel === "function") window.__bindStatusPanel(box);
Array.prototype.forEach.call(box.querySelectorAll("[data-open]"), function(b){
b.onclick = function(){
var id = b.getAttribute("data-open");
if(typeof window.__orderCard === "function") window.__orderCard(id);
};
});
Array.prototype.forEach.call(box.querySelectorAll("[data-copyday]"), function(b){
b.onclick = function(e){
e.stopPropagation();
var key = b.getAttribute("data-copyday");
var all = (Store.d.history||[]).filter(function(o){ return stOfSafe(o) !== "cancel" && o.dlv === key; });
var text = dayPlanText(key, all);
try{ copy(text); }
catch(err){ try{ navigator.clipboard.writeText(text); toast("План дня скопирован"); }catch(e2){} }
};
});
Array.prototype.forEach.call(box.querySelectorAll("[data-toggle]"), function(b){
b.onclick = function(e){
e.stopPropagation();
var id = b.getAttribute("data-toggle");
var to = b.getAttribute("data-to");
try{ setStatus(id, to); }catch(err){}
};
});
}
document.getElementById("ocalPeriod").addEventListener("click", function(e){
var b = e.target.closest("button[data-p]"); if(!b) return;
PERIOD = b.getAttribute("data-p");
Array.prototype.forEach.call(this.querySelectorAll("button"), function(x){
x.setAttribute("aria-pressed", String(x === b));
});
build();
});
var base = window.renderHistory;
window.renderHistory = function(){
try{ base.apply(null, arguments); }catch(e){}
build();
};
build();
})();
</script>
<!-- ===== АНАЛИТИКА ПРОДАЖ ===== -->
<script>
(function(){
"use strict";
var wrap = document.querySelector(".wrap");
if(!wrap || typeof Store === "undefined") return;
/* ---------- разметка раздела ---------- */
var sec = document.createElement("section");
sec.className = "no-print";
sec.id = "anaSection";
sec.hidden = true;
sec.innerHTML =
'<div class="eyebrow">Аналитика продаж</div>'+
'<div class="ana-period" id="anaPeriod">'+
'<button data-p="w" aria-pressed="false">Неделя</button>'+
'<button data-p="m" aria-pressed="true">Этот месяц</button>'+
'<button data-p="q" aria-pressed="false">Квартал</button>'+
'<button data-p="y" aria-pressed="false">Год</button>'+
'<button data-p="all" aria-pressed="false">Всё время</button>'+
'</div>'+
'<div id="anaBody"></div>';
wrap.appendChild(sec);
var PERIOD = "m";
/* ---------- вспомогательное ---------- */
function fmtMoney(n){
try{ return money0(n); }catch(e){ return Math.round(n).toLocaleString("ru-RU")+" \u20BD"; }
}
function fmtVol(n){
return new Intl.NumberFormat("ru-RU",{minimumFractionDigits:3,maximumFractionDigits:3}).format(n)+" м³";
}
function parseDate(o){
if(o.ym){ var p=String(o.ym).split("-"); if(p.length===2) {
var dd = String(o.date||"").split(".")[0];
return new Date(+p[0], +p[1]-1, +(dd||1));
}}
var q=String(o.date||"").split(".");
return q.length===3 ? new Date(+q[2], +q[1]-1, +q[0]) : null;
}
function sold(){
return (Store.d.history||[]).filter(function(o){ return (o.status||"shipped") !== "cancel"; });
}
function rangeStart(p, base){
var d = base || new Date();
if(p==="w"){
var day = d.getDay(); // 0=вс,1=пн...6=сб
var diff = (day===0 ? 6 : day-1); // дней с начала недели (пн)
return new Date(d.getFullYear(), d.getMonth(), d.getDate()-diff);
}
if(p==="m") return new Date(d.getFullYear(), d.getMonth(), 1);
if(p==="q") return new Date(d.getFullYear(), d.getMonth()-2, 1);
if(p==="y") return new Date(d.getFullYear(), 0, 1);
return new Date(1970,0,1);
}
function inRange(o, from, to){
var d = parseDate(o); if(!d) return false;
return d >= from && (!to || d < to);
}
/* ---------- сборка ---------- */
function build(){
var all = sold();
var box = document.getElementById("anaBody");
if(!all.length){
box.innerHTML = '<div class="ana-empty">Заказов пока нет. Как только появится первый сохранённый заказ, здесь будет выручка, объёмы и что чаще всего берут.</div>';
return;
}
var now = new Date();
var from = rangeStart(PERIOD, now);
var cur = all.filter(function(o){ return inRange(o, from, null); });
/* предыдущий такой же отрезок — для сравнения */
var prevFrom, prevTo = from;
if(PERIOD==="w") prevFrom = new Date(from.getFullYear(), from.getMonth(), from.getDate()-7);
else if(PERIOD==="m") prevFrom = new Date(from.getFullYear(), from.getMonth()-1, 1);
else if(PERIOD==="q") prevFrom = new Date(from.getFullYear(), from.getMonth()-3, 1);
else if(PERIOD==="y") prevFrom = new Date(from.getFullYear()-1, 0, 1);
var prev = prevFrom ? all.filter(function(o){ return inRange(o, prevFrom, prevTo); }) : [];
var sum = function(a,f){ return a.reduce(function(s,x){ return s + (+f(x)||0); },0); };
var rev = sum(cur, function(o){ return o.total; });
var vol = sum(cur, function(o){ return o.vol; });
var cnt = cur.length;
var avg = cnt ? rev/cnt : 0;
var pRev = sum(prev, function(o){ return o.total; });
function delta(a,b){
if(!b) return "";
var k = Math.round((a-b)/b*100);
if(!isFinite(k) || k===0) return '<div class="d">без изменений</div>';
return '<div class="d '+(k>0?"up":"dn")+'">'+(k>0?"+":"")+k+'% к прошлому периоду</div>';
}
var h = '';
/* сводка */
h += '<div class="kpis">'+
'<div class="kpi"><div class="k">Выручка</div><div class="v">'+fmtMoney(rev)+'</div>'+delta(rev,pRev)+'</div>'+
'<div class="kpi"><div class="k">Объём</div><div class="v">'+fmtVol(vol)+'</div></div>'+
'<div class="kpi"><div class="k">Заказов</div><div class="v">'+cnt+'</div></div>'+
'<div class="kpi"><div class="k">Средний заказ</div><div class="v">'+fmtMoney(avg)+'</div></div>'+
'</div>';
/* выручка по месяцам — всегда последние 12 */
var MON = ["янв","фев","мар","апр","май","июн","июл","авг","сен","окт","ноя","дек"];
var months = [], key = {};
for(var i=11;i>=0;i--){
var d = new Date(now.getFullYear(), now.getMonth()-i, 1);
var k = d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0");
key[k] = months.length;
months.push({k:k, lab:MON[d.getMonth()], y:d.getFullYear(), v:0});
}
all.forEach(function(o){
var d = parseDate(o); if(!d) return;
var k = d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0");
if(key[k]!=null) months[key[k]].v += (+o.total||0);
});
var mx = Math.max.apply(null, months.map(function(m){ return m.v; })) || 1;
h += '<div class="eyebrow">Выручка по месяцам</div><div class="chart">'+
'<div class="bars">'+ months.map(function(m,idx){
var pct = Math.max(2, Math.round(m.v/mx*100));
var last = idx===months.length-1;
return '<div class="b'+(last?" now":"")+'">'+
(m.v? '<span class="t">'+Math.round(m.v/1000)+'к</span>' : '')+
'<i style="height:'+pct+'%"></i></div>';
}).join("") + '</div>'+
'<div class="xlab">'+ months.map(function(m,idx){
return '<span'+(idx===months.length-1?' class="now"':'')+'>'+m.lab+'</span>';
}).join("") + '</div></div>';
/* из чего складывается выручка */
var g = sum(cur, function(o){ return o.goods; }),
s = sum(cur, function(o){ return o.svc; }),
p = sum(cur, function(o){ return o.ship; });
var tot = g+s+p;
if(tot > 0){
var pc = function(x){ return Math.round(x/tot*100); };
h += '<div class="eyebrow">Из чего складывается</div>'+
'<div class="split">'+
'<i style="width:'+(g/tot*100)+'%;background:var(--mark)"></i>'+
'<i style="width:'+(s/tot*100)+'%;background:var(--steel)"></i>'+
'<i style="width:'+(p/tot*100)+'%;background:var(--grain-2)"></i>'+
'</div>'+
'<div class="split-leg" style="margin-bottom:26px">'+
'<span><em style="background:var(--mark)"></em>Материал <b>'+pc(g)+'%</b> · '+fmtMoney(g)+'</span>'+
'<span><em style="background:var(--steel)"></em>Услуги <b>'+pc(s)+'%</b> · '+fmtMoney(s)+'</span>'+
'<span><em style="background:var(--grain-2)"></em>Доставка <b>'+pc(p)+'%</b> · '+fmtMoney(p)+'</span>'+
'</div>';
}
/* что берут чаще всего */
var byItem = {};
cur.forEach(function(o){
(o.items||[]).forEach(function(it){
var name = (it.sz||"") + (it.kind? " · "+it.kind : "");
var r = byItem[name] || (byItem[name] = {sum:0, vol:0, pcs:0, sort:it.sort||""});
r.sum += (+it.sum||0); r.vol += (+it.vol||0); r.pcs += (+it.pcs||0);
});
});
h += rankBlock("Что берут чаще всего", byItem, function(r){
return {n: fmtMoney(r.sum), s: fmtVol(r.vol)+" · "+r.pcs+" шт", w: r.sum, sub: r.sort};
});
/* кто больше покупает */
var byCl = {};
cur.forEach(function(o){
var n = (o.fio||"").trim() || "Без имени";
var r = byCl[n] || (byCl[n] = {sum:0, cnt:0, vol:0});
r.sum += (+o.total||0); r.cnt++; r.vol += (+o.vol||0);
});
h += rankBlock("Кто больше покупает", byCl, function(r){
return {n: fmtMoney(r.sum), s: r.cnt+" "+plu(r.cnt,"заказ","заказа","заказов")+" · "+fmtVol(r.vol), w: r.sum, sub:""};
});
box.innerHTML = h;
}
function plu(n,a,b,c){
var d=n%100, e=n%10;
if(d>4&&d<21) return c;
if(e===1) return a;
if(e>1&&e<5) return b;
return c;
}
function rankBlock(title, map, shape){
var rows = Object.keys(map).map(function(k){
var v = shape(map[k]); v.name = k; return v;
}).sort(function(a,b){ return b.w - a.w; }).slice(0,8);
if(!rows.length) return '';
var mx = rows[0].w || 1;
return '<div class="eyebrow">'+title+'</div><div class="rank">'+
rows.map(function(r){
return '<div class="rr">'+
'<i class="fill" style="width:'+Math.max(3, Math.round(r.w/mx*100))+'%"></i>'+
'<div class="a">'+esc(r.name)+(r.sub? '<small>'+esc(r.sub)+'</small>':'')+'</div>'+
'<div class="n">'+r.n+'<small>'+r.s+'</small></div>'+
'</div>';
}).join("") + '</div>';
}
function esc(t){ return String(t).replace(/[&<>"]/g, function(c){
return {"&":"&","<":"<",">":">",'"':"""}[c]; }); }
window.renderAna = build;
/* ---------- переключатель периода ---------- */
document.getElementById("anaPeriod").addEventListener("click", function(e){
var b = e.target.closest("button[data-p]"); if(!b) return;
PERIOD = b.getAttribute("data-p");
Array.prototype.forEach.call(this.querySelectorAll("button"), function(x){
x.setAttribute("aria-pressed", String(x === b));
});
build();
});
/* ---------- пункт меню ---------- */
var nav = document.getElementById("railNav");
if(nav){
var btn = document.createElement("button");
btn.id = "viewAna";
btn.setAttribute("aria-pressed","false");
btn.textContent = "Аналитика";
nav.appendChild(btn);
btn.addEventListener("click", function(){ setView("ana"); });
}
/* ---------- расширяем переключение разделов ---------- */
/* в исходной вёрстке раздел заказов не спрятан при загрузке,
из-за чего на широком экране он накладывался на список прайса */
var base = window.setView;
window.setView = function(v){
try{ base(v === "ana" ? "__none" : v); }catch(e){}
var A = document.getElementById("anaSection");
if(A) A.hidden = (v !== "ana");
var B = document.getElementById("viewAna");
if(B) B.setAttribute("aria-pressed", String(v === "ana"));
if(v === "ana") build();
};
/* приводим стартовое состояние к тому, что подсвечено в меню */
try{ setView("calc"); }catch(e){}
})();
</script>
<!-- ===== КАЛЕНДАРЬ ДОСТАВОК ===== -->
<script>
(function(){
"use strict";
var wrap = document.querySelector(".wrap");
var nav = document.getElementById("railNav");
if(!wrap || !nav || typeof Store === "undefined") return;
/* ---------- раздел ---------- */
var sec = document.createElement("section");
sec.className = "no-print";
sec.id = "dlvSection";
sec.hidden = true;
sec.innerHTML =
'<div class="eyebrow">Календарь доставок</div>'+
'<div class="seg dlv-period" id="dlvPeriod">'+
'<button type="button" data-p="all" aria-pressed="true">Все</button>'+
'<button type="button" data-p="w" aria-pressed="false">Неделя</button>'+
'<button type="button" data-p="m" aria-pressed="false">Месяц</button>'+
'<button type="button" data-p="arch" aria-pressed="false">Архив</button>'+
'</div>'+
'<div id="dlvBody"></div>';
wrap.appendChild(sec);
var PERIOD = "all";
var COPY_ICON =
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" '+
'stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
'<rect x="9" y="9" width="12" height="12" rx="2"/>'+
'<path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>';
var WD = ["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"];
var MON = ["января","февраля","марта","апреля","мая","июня","июля","августа",
"сентября","октября","ноября","декабря"];
function m0(n){ try{ return money0(n); }catch(e){ return Math.round(+n||0).toLocaleString("ru-RU")+" \u20BD"; } }
function cb(n){ try{ return cube(n); }catch(e){ return (+n||0).toFixed(3); } }
function esc(t){ return String(t==null?"":t).replace(/[&<>"]/g,function(c){
return {"&":"&","<":"<",">":">",'"':"""}[c]; }); }
function plu(n,a,b,c){
var d=n%100, e=n%10;
if(d>4&&d<21) return c;
if(e===1) return a;
if(e>1&&e<5) return b;
return c;
}
function stOfSafe(o){ try{ return stOf(o); }catch(e){ return o.status||"shipped"; } }
function statName(st){
try{ return stName(st); }
catch(e){
return {new:"Согласован",paid:"Оплачен",ready:"Напилен",shipped:"Отгружен",cancel:"Отменён"}[st] || st;
}
}
/* «Собран» = заказ напилен и готов («ready»), «Не собран» — ещё в работе.
Отгруженные/отменённые заказы этой кнопкой не трогаем — просто подпись. */
function toggleHTML(o, st){
if(st === "shipped" || st === "cancel"){
return '<span class="stlabel">'+esc(statName(st))+'</span>';
}
if(st === "ready"){
return '<button type="button" class="stbtn ready" data-toggle="'+o.id+'" data-to="new">Собран</button>';
}
return '<button type="button" class="stbtn notready" data-toggle="'+o.id+'" data-to="ready">Не собран</button>';
}
/* "07.08.2026" → Date */
function parseRu(s){
var p = String(s||"").split(".");
if(p.length !== 3) return null;
var d = new Date(+p[2], +p[1]-1, +p[0]);
return isNaN(d) ? null : d;
}
function midnight(d){ var x = new Date(d); x.setHours(0,0,0,0); return x; }
function dstatusOfSafe(o){ return (o && o.dstatus) || "pending"; }
/* ---------- сборка ---------- */
function build(){
var box = document.getElementById("dlvBody");
var sec = document.getElementById("dlvSection");
var isArch = (PERIOD === "arch");
if(sec) sec.classList.toggle("arch-mode", isArch);
var all = (Store.d.history || []).filter(function(o){
var st = stOfSafe(o);
if(st === "cancel") return false;
var delivered = (dstatusOfSafe(o) === "delivered");
return isArch ? delivered : !delivered;
});
if(!all.length){
box.innerHTML = '<div class="dlv-empty">'+(isArch
? "В архиве пока пусто — сюда попадают доставки со статусом «Доставлен»."
: "Заказов пока нет. Укажите дату доставки в расчёте — заказ появится в календаре.")+'</div>';
return;
}
var today = midnight(new Date());
var to = null;
if(!isArch && PERIOD === "w"){ to = new Date(today); to.setDate(to.getDate() + 7); }
if(!isArch && PERIOD === "m"){ to = new Date(today); to.setDate(to.getDate() + 31); }
var days = {}, order = [], noDate = [];
all.forEach(function(o){
var d = parseRu(o.dlv);
if(!d){ noDate.push(o); return; }
d = midnight(d);
if(!isArch && PERIOD !== "all"){
if(d < today || (to && d >= to)) return;
}
var k = d.getTime();
if(!days[k]){ days[k] = {d:d, list:[]}; order.push(k); }
days[k].list.push(o);
});
order.sort(function(a,b){ return isArch ? (b-a) : (a-b); }); // в архиве — сначала недавние
/* сводка по периоду */
var trips = 0, vol = 0, cars = {}, sum = 0, shipSumAll = 0;
order.forEach(function(k){
days[k].list.forEach(function(o){
trips++; vol += (+o.vol||0); sum += (+o.total||0); shipSumAll += (+o.ship||0);
if((o.car||"").trim()) cars[o.car.trim()] = 1;
});
});
noDate.forEach(function(o){
trips++; vol += (+o.vol||0); sum += (+o.total||0); shipSumAll += (+o.ship||0);
if((o.car||"").trim()) cars[o.car.trim()] = 1;
});
var h = '<div class="dlv-sum stsum">'+
'<div class="c"><div class="k">'+(isArch?"Завершено":"Доставок")+'</div><div class="v">'+trips+'</div></div>'+
'<div class="c"><div class="k">Объём</div><div class="v">'+cb(vol)+' м³</div></div>'+
'<div class="c"><div class="k">Машин</div><div class="v">'+Object.keys(cars).length+'</div></div>'+
'<div class="c"><div class="k">Доставка</div><div class="v">'+m0(shipSumAll)+'</div></div>'+
'<div class="c"><div class="k">На сумму</div><div class="v">'+m0(sum)+'</div></div>'+
'</div>';
if(!order.length && !noDate.length){
h += '<div class="dlv-empty">'+(isArch ? "В архиве пока пусто." : "На этот период доставок не запланировано.")+'</div>';
}
order.forEach(function(k){
h += dayCard(days[k].d, days[k].list, today);
});
if(noDate.length){
h += '<div class="eyebrow">Без даты доставки</div>'+
'<div class="hist day nodate">'+
'<div class="day-head"><div class="dt"><b>Дата не указана</b>'+
'<span>'+noDate.length+' '+plu(noDate.length,"заказ","заказа","заказов")+
' · впишите дату в расчёте, чтобы они попали в план</span></div></div>'+
noDate.map(tripRow).join("") + '</div>';
}
box.innerHTML = h;
bind();
}
function dayCard(d, list, today){
var isToday = d.getTime() === today.getTime();
var vol = 0, cars = {}, ship = 0;
list.forEach(function(o){
vol += (+o.vol||0);
ship += (+o.ship||0);
if((o.car||"").trim()) cars[o.car.trim()] = 1;
});
var carNames = Object.keys(cars);
var sub = list.length + ' ' + plu(list.length,"доставка","доставки","доставок") +
' · ' + cb(vol) + ' м³' +
(ship > 0 ? ' · доставка ' + m0(ship) : '') +
(carNames.length ? ' · ' + esc(carNames.join(", ")) : '');
return '<div class="hist day">'+
'<div class="day-head">'+
'<div class="dt"><b>'+d.getDate()+' '+MON[d.getMonth()]+', '+WD[d.getDay()]+'</b>'+
'<span>'+sub+'</span></div>'+
(isToday ? '<span class="today">сегодня</span>' : '')+
'<button type="button" class="cp" data-day="'+d.getTime()+'" '+
'title="Скопировать план дня" aria-label="Скопировать план дня">'+COPY_ICON+'</button>'+
'</div>'+
list.map(tripRow).join("") +
'</div>';
}
function timeWindow(o){
var f = (o.twFrom||"").trim(), t = (o.twTo||"").trim();
if(!f && !t) return "";
return (f||"?") + "–" + (t||"?");
}
function tripRow(o){
var st = stOfSafe(o);
var ship = +o.ship || 0;
var tw = timeWindow(o);
var tags =
(tw ? '<i class="tw">🕐 ' + esc(tw) + '</i>' : '') +
'<i class="car">' + esc((o.car||"").trim() || "машина не выбрана") + '</i>' +
(ship > 0
? '<i class="ship">Доставка ' + m0(ship) + '</i>'
: '<i class="noship">Доставка не посчитана</i>');
var dstatusSel = (typeof window.__dstatusSelectHTML === "function") ? window.__dstatusSelectHTML(o) : '';
return '<div class="trip st-'+st+'" data-oid="'+o.id+'">'+
'<div class="triprow">'+
'<button type="button" class="dlvopen" data-open="'+o.id+'">'+
'<i class="dot"></i>'+
'<div class="a">'+
'<b>'+esc(o.addr || "адрес не указан")+'</b>'+
'<span class="meta">'+esc(o.num || "")+(o.fio ? ' · '+esc(o.fio) : '')+'</span>'+
'</div>'+
'</button>'+
'<div class="n">'+cb(o.vol||0)+' м³ · '+m0(o.total)+'</div>'+
'</div>'+
'<div class="tripbottom">'+
'<div class="tags">'+tags+'</div>'+
'<div class="dlvctl">'+dstatusSel + toggleHTML(o, st) + '</div>'+
'</div>'+
'</div>';
}
/* ---------- план дня текстом для водителя ---------- */
function dayText(ms){
var d = new Date(+ms);
var list = (Store.d.history||[]).filter(function(o){
var x = parseRu(o.dlv);
return x && midnight(x).getTime() === +ms && stOfSafe(o) !== "cancel";
});
var L = [], vol = 0;
L.push("Доставки на " + String(d.getDate()).padStart(2,"0") + "." +
String(d.getMonth()+1).padStart(2,"0") + "." + d.getFullYear());
L.push("");
list.forEach(function(o, i){
vol += (+o.vol||0);
L.push((i+1) + ") " + (o.addr || "адрес не указан") + (o.car ? " — " + o.car : ""));
L.push(" Заказ " + (o.num||"") + ", " + cb(o.vol||0) + " м³, " + (o.pcs||0) + " шт");
if(+o.ship > 0) L.push(" Доставка: " + m0(o.ship));
(o.items||[]).forEach(function(it){
L.push(" · " + (it.sz||"") + (it.kind ? " " + it.kind : "") +
(it.sort ? ", " + it.sort : "") + " — " + (it.pcs||0) + " шт");
});
if(o.fio || o.phone) L.push(" " + [o.fio, o.phone].filter(Boolean).join(", "));
L.push("");
});
L.push("Итого: " + list.length + " " + plu(list.length,"адрес","адреса","адресов") +
", " + cb(vol) + " м³");
return L.join("\n");
}
function bind(){
var box = document.getElementById("dlvBody");
if(typeof window.__bindStatusPanel === "function") window.__bindStatusPanel(box);
Array.prototype.forEach.call(box.querySelectorAll("[data-toggle]"), function(b){
b.onclick = function(e){
e.stopPropagation();
var id = b.getAttribute("data-toggle");
var to = b.getAttribute("data-to");
try{ setStatus(id, to); }catch(err){}
};
});
Array.prototype.forEach.call(box.querySelectorAll("[data-day]"), function(b){
b.onclick = function(){
var t = dayText(b.getAttribute("data-day"));
try{ copy(t); }
catch(e){
try{ navigator.clipboard.writeText(t); toast("План дня скопирован"); }catch(e2){}
}
};
});
Array.prototype.forEach.call(box.querySelectorAll("[data-open]"), function(b){
b.onclick = function(){
var id = b.getAttribute("data-open");
if(typeof window.__orderCard === "function") window.__orderCard(id);
else { try{ openOrder(id); setView("calc"); }catch(e){} }
};
});
}
document.getElementById("dlvPeriod").addEventListener("click", function(e){
var b = e.target.closest("button[data-p]"); if(!b) return;
PERIOD = b.getAttribute("data-p");
Array.prototype.forEach.call(this.querySelectorAll("button"), function(x){
x.setAttribute("aria-pressed", String(x === b));
});
build();
});
/* ---------- пункт меню ---------- */
var btn = document.createElement("button");
btn.id = "viewDlv";
btn.setAttribute("aria-pressed","false");
btn.textContent = "Доставки";
var afterOrders = document.getElementById("viewOrders");
if(afterOrders && afterOrders.nextSibling) nav.insertBefore(btn, afterOrders.nextSibling);
else nav.appendChild(btn);
btn.addEventListener("click", function(){ setView("dlv"); });
/* ---------- «Ещё» для телефона: пять вкладок вместо шести ---------- */
var more = document.createElement("button");
more.id = "viewMore";
more.type = "button";
more.setAttribute("aria-pressed","false");
more.textContent = "Ещё";
nav.appendChild(more);
var back = document.createElement("div");
back.id = "moreBack";
back.className = "no-print";
document.body.appendChild(back);
var sheet = document.createElement("div");
sheet.id = "moreSheet";
sheet.className = "no-print";
sheet.innerHTML =
'<button type="button" data-go="pay" aria-pressed="false">Долги</button>'+
'<button type="button" data-go="ana" aria-pressed="false">Аналитика</button>'+
'<button type="button" data-go="cfg" aria-pressed="false">Настройки</button>';
document.body.appendChild(sheet);
function closeMore(){ sheet.classList.remove("on"); back.classList.remove("on"); }
function openMore(){ sheet.classList.add("on"); back.classList.add("on"); }
more.addEventListener("click", function(){
if(sheet.classList.contains("on")) closeMore(); else openMore();
});
back.addEventListener("click", closeMore);
sheet.addEventListener("click", function(e){
var b = e.target.closest("button[data-go]"); if(!b) return;
closeMore();
setView(b.getAttribute("data-go"));
});
document.addEventListener("keydown", function(e){
if(e.key === "Escape") closeMore();
});
/* обновление статуса/оплаты может прийти как из этой же вкладки, так и из
«Заказов» — подписываемся на renderHistory(), чтобы всегда быть в курсе */
var baseRenderH = window.renderHistory;
window.renderHistory = function(){
try{ if(typeof baseRenderH === "function") baseRenderH.apply(null, arguments); }catch(e){}
build();
};
/* ---------- расширяем переключение разделов ---------- */
var base = window.setView;
window.setView = function(v){
try{ base(v === "dlv" ? "__none" : v); }catch(e){}
var S1 = document.getElementById("dlvSection");
if(S1) S1.hidden = (v !== "dlv");
var B1 = document.getElementById("viewDlv");
if(B1) B1.setAttribute("aria-pressed", String(v === "dlv"));
var hidden = (v === "ana" || v === "cfg");
more.setAttribute("aria-pressed", String(hidden));
Array.prototype.forEach.call(sheet.querySelectorAll("button[data-go]"), function(x){
x.setAttribute("aria-pressed", String(x.getAttribute("data-go") === v));
});
if(v === "dlv") build();
};
})();
</script>
<!-- ===== ДОЛГИ И БАЗА КОНТРАГЕНТОВ ===== -->
<script>
(function(){
"use strict";
var wrap = document.querySelector(".wrap");
var nav = document.getElementById("railNav");
if(!wrap || !nav || typeof Store === "undefined") return;
Store.d.debtors = Store.d.debtors || [];
var sec = document.createElement("section");
sec.className = "no-print";
sec.id = "paySection";
sec.hidden = true;
sec.innerHTML =
'<div class="eyebrow">Долги и контрагенты</div>'+
'<div id="payBody"></div>';
wrap.appendChild(sec);
var OPEN = {}; // id -> развёрнута ли карточка
var FORM_ON = false; // false | "new" | id редактируемого контрагента
var SEARCH = "";
var FILTER = "all"; // all | debt
var TRASH =
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" '+
'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
'<path d="M4 7h16"/><path d="M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>'+
'<path d="M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"/>'+
'<path d="M10 11v6M14 11v6"/></svg>';
var PENCIL =
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" '+
'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
'<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>';
function esc(t){ return String(t==null?"":t).replace(/[&<>"]/g,function(c){
return {"&":"&","<":"<",">":">",'"':"""}[c]; }); }
function m0(n){ try{ return money0(n); }catch(e){ return Math.round(+n||0).toLocaleString("ru-RU")+" \u20BD"; } }
function zpad(n){ return String(n).padStart(2,"0"); }
function today(){ var d=new Date(); return zpad(d.getDate())+"."+zpad(d.getMonth()+1)+"."+d.getFullYear(); }
function balance(d){
return (d.entries||[]).reduce(function(s,e){
return s + (e.kind === "debt" ? +e.amount : -e.amount);
}, 0);
}
function byId(id){ return Store.d.debtors.find(function(x){ return String(x.id)===String(id); }); }
/* ---------- контрагент: создание / редактирование карточки ---------- */
function saveDebtorFields(id, f){
var name = (f.name||"").trim();
if(!name) return null;
if(id){
var d = byId(id);
if(!d) return null;
d.name = name; d.contact = (f.contact||"").trim(); d.phone = (f.phone||"").trim();
d.inn = (f.inn||"").trim(); d.addr = (f.addr||"").trim(); d.note = (f.note||"").trim();
Store.save();
return d;
}
var nd = { id: Date.now(), name: name, contact: (f.contact||"").trim(), phone: (f.phone||"").trim(),
inn: (f.inn||"").trim(), addr: (f.addr||"").trim(), note: (f.note||"").trim(), entries: [] };
Store.d.debtors.push(nd);
Store.save();
return nd;
}
function removeDebtor(id){
var d = byId(id);
if(!d) return;
if(!window.confirm('Удалить контрагента «'+d.name+'»? История долгов и оплат будет потеряна.')) return;
Store.d.debtors = Store.d.debtors.filter(function(x){ return String(x.id)!==String(id); });
Store.save();
try{ toast("Контрагент удалён"); }catch(e){}
build();
}
function addEntry(id, kind){
var d = byId(id);
if(!d) return;
var label = kind === "debt" ? "сумму долга" : "сумму оплаты";
var val = window.prompt("Внести " + label + " для «" + d.name + "», \u20BD:", "");
if(val == null) return;
var amount = parseFloat(String(val).replace(",", "."));
if(!(amount > 0)){ try{ toast("Введите сумму больше нуля"); }catch(e){} return; }
var note = window.prompt(
kind === "debt" ? "Комментарий (например: материал без предоплаты)" : "Комментарий (необязательно)", ""
);
if(note == null) note = "";
d.entries = d.entries || [];
d.entries.push({ date: today(), kind: kind, amount: Math.round(amount*100)/100, note: note.trim() });
Store.save();
try{ toast((kind==="debt"?"Долг ":"Оплата ") + m0(amount) + " добавлена"); }catch(e){}
OPEN[id] = true;
build();
}
/* ---------- форма (новый контрагент или редактирование) ---------- */
function formHTML(){
if(FORM_ON === false){
return '<div class="dbt-add"><button type="button" class="dbt-add-btn" id="dbtOpenForm">+ Добавить контрагента</button></div>';
}
var editing = FORM_ON !== "new";
var d = editing ? byId(FORM_ON) : null;
if(editing && !d){ FORM_ON = false; return formHTML(); }
var v = function(k){ return d ? esc(d[k]||"") : ""; };
return '<div class="dbt-add"><div class="dbt-form">'+
'<div class="ft">'+(editing ? "Изменить контрагента" : "Новый контрагент")+'</div>'+
'<div class="fw"><label>Название компании</label><input id="dbtName" type="text" placeholder="ООО «Ромашка»" value="'+v("name")+'"></div>'+
'<div class="f2">'+
'<div><label>Контактное лицо</label><input id="dbtContact" type="text" placeholder="Необязательно" value="'+v("contact")+'"></div>'+
'<div><label>Телефон</label><input id="dbtPhone" type="tel" placeholder="Необязательно" value="'+v("phone")+'"></div>'+
'</div>'+
'<div class="f2">'+
'<div><label>ИНН</label><input id="dbtInn" type="text" placeholder="Необязательно" value="'+v("inn")+'"></div>'+
'<div><label>Адрес</label><input id="dbtAddr" type="text" placeholder="Необязательно" value="'+v("addr")+'"></div>'+
'</div>'+
'<div class="fw"><label>Заметка</label><input id="dbtNote" type="text" placeholder="Например: поставщик кругляка" value="'+v("note")+'"></div>'+
'<div class="acts">'+
'<button type="button" class="save" id="dbtSave">'+(editing?"Сохранить":"Добавить")+'</button>'+
'<button type="button" class="cancel" id="dbtCancel">Отмена</button>'+
'</div>'+
'</div></div>';
}
/* ---------- карточка контрагента ---------- */
function cardHTML(d){
var bal = balance(d);
var balClass = bal > .5 ? "" : bal < -.5 ? "neg" : "zero";
var balText = bal > .5 ? "Должны " + m0(bal)
: bal < -.5 ? "Переплата " + m0(-bal)
: "Долгов нет";
var open = !!OPEN[d.id];
var entries = (d.entries||[]).slice().reverse();
var sub = [d.contact, d.phone].filter(Boolean).join(" · ");
var infoRows = [];
if(d.inn) infoRows.push('<div><b>ИНН</b> '+esc(d.inn)+'</div>');
if(d.addr) infoRows.push('<div><b>Адрес</b> '+esc(d.addr)+'</div>');
if(d.note) infoRows.push('<div><b>Заметка</b> '+esc(d.note)+'</div>');
var infoHtml = infoRows.length ? '<div class="dbt-info">'+infoRows.join("")+'</div>' : '';
var entriesHtml = entries.length
? entries.map(function(e){
return '<div class="dbt-e '+e.kind+'"><span class="dt">'+esc(e.date)+'</span>'+
'<span class="nt">'+esc(e.note || (e.kind==="debt"?"Долг":"Оплата"))+'</span>'+
'<b class="am">'+(e.kind==="debt"?"+":"\u2212")+m0(e.amount)+'</b></div>';
}).join("")
: '<div class="dbt-e-empty">Записей пока нет</div>';
return '<div class="dbt-card'+(open?' open':'')+'" data-id="'+d.id+'">'+
'<div class="dbt-head" data-toggle="'+d.id+'">'+
'<div class="nm">'+esc(d.name)+(sub?'<small>'+esc(sub)+'</small>':'')+'</div>'+
'<div class="bal '+balClass+'">'+balText+'</div>'+
'<button type="button" class="ic edit" data-edit="'+d.id+'" title="Изменить данные" aria-label="Изменить данные">'+PENCIL+'</button>'+
'<button type="button" class="ic del" data-del="'+d.id+'" title="Удалить контрагента" aria-label="Удалить контрагента">'+TRASH+'</button>'+
'<div class="car">\u25BE</div>'+
'</div>'+
'<div class="dbt-body">'+
infoHtml+
'<div class="dbt-entries">'+entriesHtml+'</div>'+
'<div class="dbt-actions">'+
'<button type="button" class="add-debt" data-debt="'+d.id+'">+ Долг</button>'+
'<button type="button" class="add-pay" data-pay="'+d.id+'">+ Оплата</button>'+
'</div>'+
'</div>'+
'</div>';
}
/* ---------- сборка ---------- */
function build(){
var box = document.getElementById("payBody");
var all = Store.d.debtors || [];
if(!all.length){
box.innerHTML = formHTML() +
'<div class="pay-empty"><b>Пока пусто</b>Здесь можно вести базу контрагентов и их долги — например, если отгрузили '+
'материал без предоплаты. К заказам список не привязан, всё вносится вручную.</div>';
bind();
return;
}
var total = all.reduce(function(s,d){ return s + Math.max(balance(d),0); }, 0);
var withDebt = all.filter(function(d){ return balance(d) > .5; }).length;
var h = '<div class="kpis">'+
'<div class="kpi"><div class="k">Должны нам</div><div class="v">'+m0(total)+'</div></div>'+
'<div class="kpi"><div class="k">С долгом</div><div class="v">'+withDebt+'</div></div>'+
'<div class="kpi"><div class="k">Контрагентов</div><div class="v">'+all.length+'</div></div>'+
'</div>';
h += '<div class="dbt-tools">'+
'<div class="dbt-search"><input type="text" id="dbtSearch" placeholder="Поиск: название, телефон, ИНН…" value="'+esc(SEARCH)+'"></div>'+
'<div class="seg dbt-filter" id="dbtFilterSeg">'+
'<button type="button" data-f="all" aria-pressed="'+(FILTER==="all")+'">Все</button>'+
'<button type="button" data-f="debt" aria-pressed="'+(FILTER==="debt")+'">С долгом</button>'+
'</div>'+
'</div>';
h += formHTML();
var q = SEARCH.trim().toLowerCase();
var filtered = all.filter(function(d){
if(FILTER === "debt" && !(balance(d) > .5)) return false;
if(!q) return true;
var hay = [d.name, d.contact, d.phone, d.inn, d.addr, d.note].filter(Boolean).join(" ").toLowerCase();
return hay.indexOf(q) >= 0;
});
if(!filtered.length){
h += '<div class="pay-empty">Ничего не найдено.</div>';
box.innerHTML = h;
bind();
return;
}
var sorted = filtered.slice().sort(function(a,b){ return balance(b) - balance(a); });
h += sorted.map(cardHTML).join("");
box.innerHTML = h;
bind();
}
function bind(){
var box = document.getElementById("payBody");
var openBtn = document.getElementById("dbtOpenForm");
if(openBtn) openBtn.onclick = function(){ FORM_ON = "new"; build();
setTimeout(function(){ var n=document.getElementById("dbtName"); if(n) n.focus(); },0); };
var cancelBtn = document.getElementById("dbtCancel");
if(cancelBtn) cancelBtn.onclick = function(){ FORM_ON = false; build(); };
var saveBtn = document.getElementById("dbtSave");
if(saveBtn) saveBtn.onclick = function(){
var fields = {
name: document.getElementById("dbtName").value,
contact: document.getElementById("dbtContact").value,
phone: document.getElementById("dbtPhone").value,
inn: document.getElementById("dbtInn").value,
addr: document.getElementById("dbtAddr").value,
note: document.getElementById("dbtNote").value
};
if(!fields.name.trim()){ try{ toast("Впишите название компании"); }catch(e){} return; }
var editing = FORM_ON !== "new";
var editId = editing ? FORM_ON : null;
var d = saveDebtorFields(editId, fields);
FORM_ON = false;
if(d) OPEN[d.id] = true;
try{ toast(editing ? "Изменения сохранены" : "Контрагент добавлен"); }catch(e){}
build();
};
var search = document.getElementById("dbtSearch");
if(search){
search.oninput = function(){ SEARCH = search.value; build();
setTimeout(function(){
var n=document.getElementById("dbtSearch");
if(n){ n.focus(); n.selectionStart = n.selectionEnd = n.value.length; }
},0);
};
}
var filterSeg = document.getElementById("dbtFilterSeg");
if(filterSeg){
filterSeg.addEventListener("click", function(e){
var b = e.target.closest("button[data-f]"); if(!b) return;
FILTER = b.getAttribute("data-f");
build();
});
}
Array.prototype.forEach.call(box.querySelectorAll("[data-toggle]"), function(h){
h.onclick = function(){
var id = h.getAttribute("data-toggle");
OPEN[id] = !OPEN[id];
build();
};
});
Array.prototype.forEach.call(box.querySelectorAll("[data-edit]"), function(b){
b.onclick = function(e){ e.stopPropagation(); FORM_ON = b.getAttribute("data-edit"); build();
setTimeout(function(){
var n=document.getElementById("dbtName");
if(n){ n.scrollIntoView({block:"center"}); n.focus(); }
},0);
};
});
Array.prototype.forEach.call(box.querySelectorAll("[data-debt]"), function(b){
b.onclick = function(e){ e.stopPropagation(); addEntry(b.getAttribute("data-debt"), "debt"); };
});
Array.prototype.forEach.call(box.querySelectorAll("[data-pay]"), function(b){
b.onclick = function(e){ e.stopPropagation(); addEntry(b.getAttribute("data-pay"), "pay"); };
});
Array.prototype.forEach.call(box.querySelectorAll("[data-del]"), function(b){
b.onclick = function(e){ e.stopPropagation(); removeDebtor(b.getAttribute("data-del")); };
});
}
/* ---------- пункт меню ---------- */
var btn = document.createElement("button");
btn.id = "viewPay";
btn.setAttribute("aria-pressed","false");
btn.textContent = "Долги";
var afterDlv = document.getElementById("viewDlv") || document.getElementById("viewOrders");
if(afterDlv && afterDlv.nextSibling) nav.insertBefore(btn, afterDlv.nextSibling);
else nav.appendChild(btn);
btn.addEventListener("click", function(){ setView("pay"); });
/* ---------- расширяем переключение разделов ---------- */
var base = window.setView;
window.setView = function(v){
try{ base(v === "pay" ? "__none" : v); }catch(e){}
var S1 = document.getElementById("paySection");
if(S1) S1.hidden = (v !== "pay");
var B1 = document.getElementById("viewPay");
if(B1) B1.setAttribute("aria-pressed", String(v === "pay"));
if(v === "pay") build();
};
})();
</script>
<!-- ===== РОЛИ: ЛОГИСТ / БРИГАДИР ===== -->
<script>
(function(){
"use strict";
var nav = document.getElementById("railNav");
if(!nav) return;
/* Какие разделы разрешены каждой роли. Поле "role" читается из записи
пользователя в PocketBase (users → role, тип Select). Пусто или любое
незнакомое значение = полный доступ, как раньше. */
var PERMS = {
logist: { nav:["viewCalc","viewDlv"], view:["calc","dlv"] },
brigadir: { nav:["viewCalc","viewOrders"], view:["calc","orders"] }
};
var ALL_NAV = ["viewCalc","viewOrders","viewPay","viewDlv","viewStock","viewCfg","viewAna"];
var SHEET_MAP = { pay:"viewPay", ana:"viewAna", cfg:"viewCfg" }; // data-go -> id кнопки в панели
var CUR_ALLOWED_VIEWS = null; // null = без ограничений
function showAll(){
ALL_NAV.forEach(function(id){
var b = document.getElementById(id);
if(b) b.style.removeProperty("display");
});
var more = document.getElementById("viewMore");
if(more) more.style.removeProperty("display");
var sheet = document.getElementById("moreSheet");
if(sheet){
Array.prototype.forEach.call(sheet.querySelectorAll("button[data-go]"), function(b){
b.style.removeProperty("display");
});
}
}
function applyRole(rec){
var role = rec && rec.role;
var perm = PERMS[role];
if(!perm){ CUR_ALLOWED_VIEWS = null; showAll(); return; }
CUR_ALLOWED_VIEWS = perm.view;
ALL_NAV.forEach(function(id){
var b = document.getElementById(id);
if(!b) return;
b.style.display = (perm.nav.indexOf(id) >= 0) ? "" : "none";
});
// «Ещё» на телефоне: скрываем пункты, недоступные роли; саму кнопку —
// если внутри не осталось ни одного разрешённого пункта
var sheet = document.getElementById("moreSheet");
var anyInSheet = false;
if(sheet){
Array.prototype.forEach.call(sheet.querySelectorAll("button[data-go]"), function(b){
var goId = SHEET_MAP[b.getAttribute("data-go")];
var ok = goId && perm.nav.indexOf(goId) >= 0;
b.style.display = ok ? "" : "none";
if(ok) anyInSheet = true;
});
}
var more = document.getElementById("viewMore");
if(more) more.style.display = anyInSheet ? "" : "none";
// если открыт раздел, который этой роли не положен — уходим на расчёт
var current = null;
ALL_NAV.concat(["viewMore"]).forEach(function(id){
var b = document.getElementById(id);
if(b && b.getAttribute("aria-pressed") === "true") current = id;
});
if(current && perm.nav.indexOf(current) < 0){
try{ setView("calc"); }catch(e){}
}
}
window.__applyRole = applyRole;
// ограничиваем и сами переключения — на случай прямого вызова setView(...)
var baseSetView = window.setView;
if(typeof baseSetView === "function"){
window.setView = function(v){
if(CUR_ALLOWED_VIEWS && CUR_ALLOWED_VIEWS.indexOf(v) < 0) v = "calc";
return baseSetView(v);
};
}
// если роль уже была определена до подключения этого модуля — применяем сразу
if(window.__meRecord) applyRole(window.__meRecord);
})();
</script>
<!-- ===== ПЕРЕКЛЮЧАТЕЛЬ ОФОРМЛЕНИЯ ===== -->
<script>
(function(){
"use strict";
var cfg = document.getElementById("cfgSection");
if(!cfg || typeof window.__setTheme !== "function") return;
var box = document.createElement("div");
box.id = "themeBox";
box.innerHTML =
'<div class="eyebrow">Оформление</div>'+
'<div class="cfgbox">'+
'<div class="seg" id="themeSeg">'+
'<button type="button" data-t="auto" aria-pressed="false">Авто</button>'+
'<button type="button" data-t="light" aria-pressed="false">Светлая</button>'+
'<button type="button" data-t="dark" aria-pressed="false">Тёмная</button>'+
'</div>'+
'<div class="cfghint">В режиме «Авто» оформление следует настройкам телефона или компьютера: '+
'днём светлое, ночью тёмное. Выбор запоминается только на этом устройстве.</div>'+
'</div>';
cfg.insertBefore(box, cfg.firstChild);
var seg = document.getElementById("themeSeg");
function paint(){
var v = window.__getTheme();
Array.prototype.forEach.call(seg.querySelectorAll("button"), function(b){
b.setAttribute("aria-pressed", String(b.getAttribute("data-t") === v));
});
}
seg.addEventListener("click", function(e){
var b = e.target.closest("button[data-t]"); if(!b) return;
window.__setTheme(b.getAttribute("data-t"));
paint();
});
paint();
/* ---- название из настроек показываем и в боковой панели ---- */
(function brandSync(){
var b = document.querySelector("#rail .rb-txt b");
if(!b) return;
function put(){
var h = document.querySelector("header h1");
var name = (h && h.textContent.trim()) || "Лесопилка";
b.textContent = name;
b.title = name;
}
put();
var base = window.applyCfg;
if(typeof base === "function"){
window.applyCfg = function(){
try{ base.apply(null, arguments); }catch(e){}
put();
};
}
// на случай правки поля названия без вызова applyCfg
var f = document.getElementById("cfgBrand");
if(f) f.addEventListener("input", function(){ setTimeout(put, 0); });
new MutationObserver(put).observe(document.querySelector("header h1") || document.body,
{childList:true, characterData:true, subtree:true});
})();
})();
</script>
<!-- ===== СИНХРОНИЗАЦИЯ С POCKETBASE + ВХОД ===== -->
<div id="authGate">
<div class="agBox">
<h2>Вход в систему</h2>
<div class="agSub">Лесопилка · CRM</div>
<label for="agLogin">Почта</label>
<input id="agLogin" type="email" autocomplete="username" inputmode="email">
<label for="agPass">Пароль</label>
<input id="agPass" type="password" autocomplete="current-password">
<button id="agBtn" type="button">Войти</button>
<div id="agErr"></div>
</div>
</div>
<div id="syncBar"></div>
<script>
(function(){
/* ---- НАСТРОЙКИ. Если коллекции в PocketBase названы иначе — поменяйте здесь ---- */
var CFG = {
users: "users", // коллекция пользователей (тип auth)
state: "appstate", // коллекция для данных CRM
key: "main" // имя записи с данными
};
var SKEY = "lesopilka.v3"; // тот же ключ, что у Store в приложении
var POLL_MS = 7000; // как часто проверять изменения от других
var SAVE_DELAY = 900; // задержка перед отправкой на сервер
var token = localStorage.getItem("pb_token") || "";
var recId = null;
var lastUpd = "";
var dirty = false; // есть несохранённые изменения
var pushing = false;
var timer = null;
var ready = false;
var gate = document.getElementById("authGate");
var bar = document.getElementById("syncBar");
var out = document.getElementById("logoutBtn");
var box = document.getElementById("userBox");
var me = null; // запись текущего пользователя
function gateOn(){ document.body.classList.add("gated"); gate.style.display = "flex"; }
function gateOff(){ document.body.classList.remove("gated"); gate.style.display = "none"; }
function api(p){ return "/api/" + p; }
function H(){
var h = {"Content-Type":"application/json"};
if(token) h["Authorization"] = token;
return h;
}
function say(t, ms){
bar.textContent = t; bar.classList.add("on");
clearTimeout(bar._t);
bar._t = setTimeout(function(){ bar.classList.remove("on"); }, ms || 2200);
}
/* ---------- карточка пользователя ---------- */
function showUser(rec){
me = rec || null;
window.__meRecord = me;
try{ if(typeof window.__applyRole === "function") window.__applyRole(me); }catch(e){}
if(!me){ box.style.display = "none"; return; }
var mail = me.email || "";
var nm = (me.name && String(me.name).trim()) ? String(me.name).trim() : (mail.split("@")[0] || "Пользователь");
document.getElementById("uName").textContent = nm;
document.getElementById("uMail").textContent = mail;
document.getElementById("uMail").title = mail;
document.getElementById("uAva").textContent = nm.charAt(0).toUpperCase();
box.style.display = "flex";
}
/* ---------- вход ---------- */
function login(identity, password){
return fetch(api("collections/"+CFG.users+"/auth-with-password"), {
method:"POST",
headers:{"Content-Type":"application/json"},
body: JSON.stringify({identity:identity, password:password})
}).then(function(r){
if(!r.ok) throw new Error("Неверная почта или пароль");
return r.json();
}).then(function(d){
token = d.token;
localStorage.setItem("pb_token", token);
showUser(d.record);
});
}
function checkAuth(){
if(!token) return Promise.resolve(false);
return fetch(api("collections/"+CFG.users+"/auth-refresh"), {method:"POST", headers:H()})
.then(function(r){
if(!r.ok) throw 0;
return r.json();
})
.then(function(d){
token = d.token;
localStorage.setItem("pb_token", token);
showUser(d.record);
return true;
})
.catch(function(){
token = ""; localStorage.removeItem("pb_token");
return false;
});
}
/* ---------- чтение состояния с сервера ---------- */
function pull(){
var url = api("collections/"+CFG.state+"/records")
+ "?perPage=1&filter=" + encodeURIComponent("key='"+CFG.key+"'");
return fetch(url, {headers:H()})
.then(function(r){ if(!r.ok) throw 0; return r.json(); })
.then(function(d){
if(d.items && d.items.length){ recId = d.items[0].id; return d.items[0]; }
return null;
})
.catch(function(){ return null; });
}
/* ---------- запись состояния на сервер ---------- */
function push(data){
if(pushing){ dirty = true; return; }
pushing = true;
var body = JSON.stringify({key:CFG.key, data:data});
var req = recId
? fetch(api("collections/"+CFG.state+"/records/"+recId), {method:"PATCH", headers:H(), body:body})
: fetch(api("collections/"+CFG.state+"/records"), {method:"POST", headers:H(), body:body});
req.then(function(r){
if(!r.ok) throw 0;
return r.json();
}).then(function(d){
recId = d.id; lastUpd = d.updated;
say("сохранено на сервере", 1400);
}).catch(function(){
dirty = true;
say("нет связи с сервером — данные сохранены локально", 3500);
}).then(function(){
pushing = false;
if(dirty){ dirty = false; push(Store.d); }
});
}
/* вызывается из Store.save() */
window.__cloudSave = function(d){
if(!ready) return;
dirty = true;
clearTimeout(timer);
timer = setTimeout(function(){
dirty = false;
var copy;
try { copy = JSON.parse(JSON.stringify(d)); } catch(e){ return; }
push(copy);
}, SAVE_DELAY);
};
/* ---------- перерисовка после изменений от других ---------- */
function refreshUI(){
var fns = ["applyPrices","renderPriceDate","renderSorts","renderList","renderLens",
"renderCalc","renderClient","renderOrder","renderOrderNum","stSanitize",
"renderOrdFilters","renderOrdSummary","renderHistory","renderCustom",
"applyCfg","fillCfgForm","fillRqForm","stRender","renderStock"];
for(var i=0;i<fns.length;i++){
try{ if(typeof window[fns[i]] === "function") window[fns[i]](); }catch(e){}
}
}
function applyRemote(rec){
if(!rec || !rec.data) return;
lastUpd = rec.updated;
try{
for(var k in rec.data){ if(Object.prototype.hasOwnProperty.call(rec.data,k)) Store.d[k] = rec.data[k]; }
localStorage.setItem(SKEY, JSON.stringify(Store.d));
}catch(e){}
refreshUI();
}
/* ---------- опрос сервера ---------- */
function startPoll(){
setInterval(function(){
if(pushing || dirty) return; // не мешаем своим сохранениям
if(document.hidden) return;
pull().then(function(rec){
if(rec && rec.updated !== lastUpd){
applyRemote(rec);
/* по просьбе — фоновое обновление больше не показывает всплывашку,
сами данные при этом обновляются точно так же */
}
});
}, POLL_MS);
}
/* ---------- запуск ---------- */
function start(){
gateOff();
pull().then(function(rec){
if(rec){
applyRemote(rec);
} else {
// записи ещё нет — создаём из того, что есть локально
ready = true;
push(JSON.parse(JSON.stringify(Store.d)));
}
ready = true;
startPoll();
say("подключено", 1500);
});
}
function boot(){
checkAuth().then(function(ok){
if(ok) start();
else gateOn();
});
}
/* ---------- обработчики ---------- */
var btn = document.getElementById("agBtn");
var err = document.getElementById("agErr");
function doLogin(){
var l = document.getElementById("agLogin").value.trim();
var p = document.getElementById("agPass").value;
if(!l || !p){ err.textContent = "Заполните почту и пароль"; return; }
btn.disabled = true; err.textContent = "";
login(l,p).then(function(){
btn.disabled = false;
document.getElementById("agPass").value = "";
start();
}).catch(function(e){
btn.disabled = false;
err.textContent = e.message || "Не удалось войти";
});
}
btn.addEventListener("click", doLogin);
document.getElementById("agPass").addEventListener("keydown", function(e){
if(e.key === "Enter") doLogin();
});
out.addEventListener("click", function(){
if(!confirm("Выйти из системы?")) return;
token = ""; localStorage.removeItem("pb_token");
showUser(null);
location.reload();
});
/* ---- карточка пользователя: в шапке на телефоне, в панели на большом экране ---- */
(function placeUser(){
var head = document.querySelector("header .head-in");
var rail = document.getElementById("rail");
if(!head || !rail || !box) return;
function put(){
var target = (window.innerWidth <= 900) ? head : rail;
if(box.parentNode !== target) target.appendChild(box);
}
window.addEventListener("resize", put);
put();
})();
/* ---- бегунок на мерной ленте ---- */
(function tapeMarker(){
var nav = document.getElementById("railNav");
var mark = document.getElementById("tapeMark");
if(!nav || !mark) return;
function place(){
if(window.innerWidth <= 900) return;
var on = nav.querySelector('button[aria-pressed="true"]');
if(!on) return;
mark.style.top = (on.offsetTop + on.offsetHeight/2 - mark.offsetHeight/2) + "px";
}
new MutationObserver(place).observe(nav, {attributes:true, subtree:true, attributeFilter:["aria-pressed"]});
window.addEventListener("resize", place);
place();
})();
boot();
})();
</script>
<!-- ===== ИКОНКИ РАЗДЕЛОВ МЕНЮ ===== -->
<script>
(function(){
"use strict";
var ICONS = {
calc: '<path d="M6 3h12a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z"/><path d="M8 7h8"/><circle cx="8.5" cy="12" r=".9" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r=".9" fill="currentColor" stroke="none"/><circle cx="15.5" cy="12" r=".9" fill="currentColor" stroke="none"/><circle cx="8.5" cy="15.5" r=".9" fill="currentColor" stroke="none"/><circle cx="12" cy="15.5" r=".9" fill="currentColor" stroke="none"/><circle cx="15.5" cy="15.5" r=".9" fill="currentColor" stroke="none"/>',
orders:'<path d="M7 3h10a1 1 0 0 1 1 1v16l-3-2-2 2-2-2-2 2-3-2V4a1 1 0 0 1 1-1Z"/><path d="M9 8h6M9 11.5h6"/>',
truck: '<path d="M3 7h11v9H3z"/><path d="M14 10h4l3 3v3h-7z"/><circle cx="7" cy="18" r="1.6"/><circle cx="17.5" cy="18" r="1.6"/>',
stock: '<path d="M3.5 7 12 3l8.5 4-8.5 4-8.5-4Z"/><path d="M3.5 7v10L12 21l8.5-4V7"/><path d="M12 11v10"/>',
ana: '<path d="M4 20V10M11 20V4M18 20v-7"/>',
pay: '<path d="M3 7a2 2 0 0 1 2-2h13a1 1 0 0 1 1 1v2H5"/><path d="M3 7v11a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V9a1 1 0 0 0-1-1H5a2 2 0 0 1-2-2Z"/><circle cx="16.2" cy="14.2" r="1.15" fill="currentColor" stroke="none"/>',
cfg: '<circle cx="12" cy="12" r="2.9"/><path d="M19.4 13.5a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1 1.55V19.6a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1-1.56 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.55-1H4.4a2 2 0 1 1 0-4h.09a1.7 1.7 0 0 0 1.56-1 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34H10.5a1.7 1.7 0 0 0 1-1.55V4.4a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87V10.5a1.7 1.7 0 0 0 1.55 1h.09a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.56 1Z"/>',
more: '<circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/>'
};
var MAP = {
viewCalc:"calc", viewOrders:"orders", viewDlv:"truck",
viewStock:"stock", viewAna:"ana", viewCfg:"cfg", viewMore:"more", viewPay:"pay"
};
function svg(key){
return '<svg class="navico" viewBox="0 0 24 24" width="20" height="20" fill="none" '+
'stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" '+
'aria-hidden="true">'+ (ICONS[key]||"") +'</svg>';
}
function dress(btn, key){
if(!btn || btn.querySelector(".ico")) return;
var ico = document.createElement("span");
ico.className = "ico";
ico.innerHTML = svg(key);
var lbl = document.createElement("span");
lbl.className = "lbl";
while(btn.firstChild) lbl.appendChild(btn.firstChild);
btn.appendChild(ico);
btn.appendChild(lbl);
btn.insertBefore(ico, lbl);
}
Object.keys(MAP).forEach(function(id){
dress(document.getElementById(id), MAP[id]);
});
/* «Настройки» переносим в конец списка — все остальные пункты
нужны каждый день, настройки открывают редко. */
(function pushSettingsDown(){
var cfg = document.getElementById("viewCfg");
var railNav = document.getElementById("railNav");
if(cfg && railNav) railNav.appendChild(cfg);
})();
var sheet = document.getElementById("moreSheet");
if(sheet){
Array.prototype.forEach.call(sheet.querySelectorAll("button[data-go]"), function(b){
var go = b.getAttribute("data-go");
var key = go === "ana" ? "ana" : go === "pay" ? "pay" : "cfg";
dress(b, key);
});
}
})();
</script>
</body>
</html>
Sitemap
Кол-во: 0
XML-карта сайта для поисковиков
?
Sitemap.xml помогает поисковику быстрее находить и индексировать страницы. Особенно важен для крупных сайтов и новых страниц, на которые ещё нет входящих ссылок.
Robots.txt не содержит ссылку на карту сайта. Рекомендуется добавить карту сайта и указать ссылку на нее в robots.txt.
Внутренние ссылки
Не найдено
Ссылки на другие страницы своего сайта
?
Внутренние ссылки распределяют ссылочный вес между страницами и помогают поисковику обходить сайт. Пустые анкоры и ссылки на запрещённые robots.txt страницы — типичные ошибки.
Внутренних ссылок на странице 0 оптимально.
Внешние ссылки
Не найдено
Ссылки на сторонние сайты
?
Исходящие внешние ссылки передают часть ссылочного веса на чужие сайты. Ссылки на авторитетные ресурсы безопасны; ссылки на мусорные сайты могут навредить репутации страницы.
Внешних ссылок на странице не найдено.
Конкуренты Готовность: 0%
Конкуренты в Яндексе
Кол-во: 0
Топ сайтов-конкурентов в Яндексе
?
Сайты, чаще всего появляющиеся в ТОПе Яндекса по запросам из семантического ядра этой страницы.
Мы не нашли у вас конкурентов в Яндексе. Сайт или очень молодой или плохо продвигается.
Конкурентов в ТОП-10 Яндекса не нашлось.
Конкуренты в Google
Кол-во: 0
Топ сайтов-конкурентов в Google
?
Сайты, чаще всего появляющиеся в ТОПе Google по запросам из семантического ядра этой страницы.
Конкуренты в Google тоже не найдены. Займитесь продвижением сайта!
Конкурентов в ТОП-10 Google не нашлось.
ФЗ-152: ПД Готовность: 40%
РКН
ОК
Сайт не заблокирован
Контакты
https://prestizh-les-crm.ru/contacts/
Страница с контактами найдена
Политика ПД
https://prestizh-les-crm.ru/policy/
Страница политики обработки ПД найдена
Согласие
https://prestizh-les-crm.ru/personal/
Страница согласия на обработку ПД найдена
Чекбокс согласия
Не найден
Чекбокс согласия на обработку ПД в формах сайта не найден
Счётчики до согласия
ОК
Счётчики аналитики не собирают данные до согласия пользователя
ЗоЗПП: права потребителей Готовность: 100%
Нарушения
Не выявлены
Признаков дистанционной продажи товаров (интернет-магазина) не обнаружено — требования ЗоЗПП о раскрытии информации продавца к сайту не применяются. Нарушений нет.
ФЗ-149: рекомендательные технологии Готовность: 100%
Нарушения
Не выявлены
Рекомендательные блоки («с этим покупают», «похожие товары» и т.п.) на сайте не обнаружены — требования ст. 10.7 ФЗ-149 к сайту не применяются. Нарушений нет.
ФЗ-38: реклама Готовность: 100%
Нарушения
Не выявлены
Рекламных тематик с обязательными оговорками (медицина, БАД, кредиты и займы, новостройки) на сайте не обнаружено. Нарушений нет.
ФЗ-436: защита детей Готовность: 100%
Нарушения
Не выявлены
Признаков информационной продукции (новости, видео, книги, игры, курсы) не обнаружено — обязательная возрастная маркировка по ФЗ-436 сайту не требуется. Нарушений нет.
Вердикт
Сайт prestizh-les-crm.ru не совсем готов к продвижению (процент готовности лишь 61%). Для попадания в ТОПы поисковых систем нужно:
Исправьте ошибки в мета-тегах.
Исправьте ошибки индексации.
Поделитесь с друзьями: