:root{
  --bg:#05080c; --panel:#0a0f16; --panel2:#0d131c;
  --line:#1c2733; --line-soft:#141c26;
  --text:#dbe6ef; --dim:#5f7688; --dim2:#3d4d5c;
  /* Пунктирные рамки блоков. Отдельно от --dim намеренно: штрихи усредняются с
     фоном, и при равном цвете рамка выглядит светлее сплошной. Здесь на тон
     контрастнее, чтобы вес совпадал на глаз, а не в значениях. */
  --map-dash:#8aa3b6;
  --accent:#34d5ff; --accent-dim:#1a5f75;
  --green:#3ddc84; --green-dim:#155c37;
  --red:#ff5470; --red-dim:#5c1524;
  --orange:#ffb648; --orange-dim:#5c3d10;
  --purple:#b48cff; --purple-dim:#3a2a5c;
  /* Розовый - только под "локация не указана". Не берём --red-dim: это не ошибка
     расчёта, а незаполненные данные, и путать их не нужно. */
  --pink:#ff7ab2; --pink-dim:#5c2038;
  --mono:'JetBrains Mono',ui-monospace,Menlo,Consolas,monospace;
  /* Наименьшая ширина окон списков на карте - от неё же считается наименьшая
     ширина самой панели карты в разделённом экране. */
  --map-side-min:340px;
  /* Масштаб интерфейса. Вынесен в переменную, потому что от него зависит не только
     сам zoom: единицы vh зум не учитывают, и всякую высоту "в целый экран" внутри
     body приходится делить на этот множитель. Тот же множитель читает uiZoom(). */
  --ui-zoom:1.2;
}

/* Светлая тема. Переопределяем только переменные - вся остальная вёрстка
   опирается на них, поэтому отдельных правил под неё не требуется.
   Акценты взяты темнее исходных: на светлом фоне неоново-яркие цвета
   исходной палитры не читаются. */
body.light-theme{
  --bg:#f4f6f8; --panel:#ffffff; --panel2:#f0f3f6;
  --line:#c8d2dc; --line-soft:#dfe5eb;
  --text:#16202b; --dim:#5c7080; --dim2:#93a5b3;
  --map-dash:#3e5464;
  --accent:#0273a8; --accent-dim:#8fc9e4;
  --green:#137a45; --green-dim:#a5dcbe;
  --red:#c02040; --red-dim:#f2b8c3;
  --orange:#a35c00; --orange-dim:#f0d2a0;
  --purple:#6a3fbf; --purple-dim:#d3c2f2;
  --pink:#c9337a; --pink-dim:#ffd4e6;
}

*{box-sizing:border-box; margin:0; padding:0;}
/* overscroll-behavior-y: contain - против «потянул вниз и страница перезагрузилась».
   На карте и в длинных списках жест вверх-вниз пальцем постоянно доходил до края
   прокрутки и запускал перезагрузку прямо посреди работы. */
html{ overscroll-behavior-y: contain; }
body{
  background:var(--bg); color:var(--text); font-family:var(--mono);
  min-height:calc(100vh / var(--ui-zoom)); font-size:13px; zoom:var(--ui-zoom); overscroll-behavior-y: contain;
  background-image:
    radial-gradient(circle at 15% 0%, rgba(52,213,255,0.06), transparent 40%),
    repeating-linear-gradient(0deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 3px);
}
.hidden{display:none !important;}

/* ---------- ЛОГИН ---------- */
#login-screen{
  min-height:100vh; display:flex; align-items:center; justify-content:center; padding:20px;
}
.login-box{
  width:100%; max-width:360px; background:var(--panel); border:1px solid var(--line);
  padding:28px; position:relative;
  clip-path:polygon(18px 0,100% 0,100% calc(100% - 18px),calc(100% - 18px) 100%,0 100%,0 18px);
}
.login-box::before{
  content:''; position:absolute; inset:0; padding:1px;
  background:linear-gradient(135deg, rgba(52,213,255,0.5), rgba(52,213,255,0.05) 40%);
  clip-path:inherit; -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude; pointer-events:none;
}
.login-title{font-size:20px; font-weight:800; letter-spacing:1px; margin-bottom:24px;}
.field{margin-bottom:14px;}
.field label{display:block; font-size:10px; letter-spacing:1.5px; color:var(--dim); margin-bottom:6px; text-transform:uppercase;}
.field input{
  width:100%; background:var(--panel2); border:1px solid var(--line); color:var(--text);
  font-family:inherit; font-size:14px; padding:11px 12px; outline:none;
}
.field input:focus{border-color:var(--accent); box-shadow:0 0 0 1px var(--accent) inset;}
.login-error{color:var(--red); font-size:11px; min-height:16px; margin-bottom:8px;}
.btn-primary{
  width:100%; background:rgba(52,213,255,0.08); border:1px solid var(--accent); color:var(--accent);
  font-family:inherit; font-size:12px; font-weight:700; letter-spacing:1.5px; text-transform:uppercase;
  padding:12px; cursor:pointer; margin-top:6px;
}
.btn-primary:active{background:var(--accent); color:#04141c;}
.btn-primary:disabled{opacity:0.4; cursor:default;}
/* Пока идёт запрос - глухая заслонка поверх формы. Не просто disabled на кнопке:
   поля тоже должны быть недоступны, иначе человек правит логин, пока летит запрос
   со старым, и потом не понимает, почему ошибка не про то. */
.login-busy{
  position:absolute; inset:0; z-index:5; clip-path:inherit;
  display:flex; align-items:center; justify-content:center;
  cursor:progress;
}
/* Гасим саму форму прозрачностью, а не кладём сверху заливку: любой конкретный
   цвет верен только в одной теме, и на светлой он читался как грязно-серое пятно.
   Прозрачность работает одинаково в обеих - под ней просто фон окна. */
.login-box.busy > *:not(.login-busy){ opacity:0.25; transition:opacity 0.12s; }
.login-spinner{
  width:26px; height:26px; border-radius:50%;
  border:2px solid var(--line); border-top-color:var(--accent);
  animation:spin-loading-bar 0.7s linear infinite;
}

/* ---------- DASHBOARD ---------- */
#dashboard{min-height:calc(100vh / var(--ui-zoom)); display:flex; flex-direction:column;}

.topbar{
  display:flex; align-items:center; gap:16px; padding:12px 18px; border-bottom:1px solid var(--line);
  background:var(--panel); flex-wrap:nowrap;
  position:sticky; top:0; z-index:1000;
}
/* Единственное содержимое шапки - имя открытой вкладки. Вкладку тут не выбирают:
   выбор живёт в меню у нижнего края, до которого достаёт палец. */
.tb-tabname{
  flex-shrink:0; font-size:12px; letter-spacing:1.5px; text-transform:uppercase;
  color:var(--dim); white-space:nowrap;
}
/* При разделении экрана имена обеих вкладок стоят прямо над панелями и переключают
   их же - общая подпись в шапке только дублировала бы их. */
body.split-on .tb-tabname{ display:none; }
.status-dot{width:8px; height:8px; border-radius:50%; background:var(--dim2); flex-shrink:0;}
.status-dot.live{background:var(--green); box-shadow:0 0 8px var(--green); animation:pulse 1.6s ease-in-out infinite;}
.status-dot.err{background:var(--red); box-shadow:0 0 8px var(--red);}
@keyframes pulse{0%,100%{opacity:1;} 50%{opacity:0.35;}}
.status-text{font-size:11px; color:var(--dim);}
.tb-spacer{flex:1;}
.tb-controls{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
.tb-controls label{font-size:10px; color:var(--dim); letter-spacing:0.5px; display:flex; align-items:center; gap:6px;}
.tb-controls select, .tb-controls input[type=number]{
  background:var(--panel2); border:1px solid var(--line); color:var(--text); font-family:inherit;
  font-size:11px; padding:5px 8px;
}
.tb-btn{
  background:var(--panel2); border:1px solid var(--line); color:var(--dim); font-family:inherit;
  font-size:10px; letter-spacing:1px; text-transform:uppercase; padding:7px 12px; cursor:pointer;
}
.tb-btn:hover{border-color:var(--accent); color:var(--accent);}
.tb-btn.active{border-color:var(--accent); color:var(--accent); background:rgba(52,213,255,0.08);}

#tab-feed > .tb-controls{padding:10px 18px; border-bottom:1px solid var(--line); background:var(--panel);}
.layout{flex:1; display:flex; min-height:0;}
.stock-panel{
  width:340px; flex-shrink:0; border-right:1px solid var(--line); background:var(--panel);
  display:flex; flex-direction:column; min-height:0;
}
.feed-panel{flex:1; display:flex; flex-direction:column; min-height:0;}

.panel-header{
  padding:12px 16px; border-bottom:1px solid var(--line); font-size:10px; letter-spacing:2px;
  color:var(--dim); text-transform:uppercase; display:flex; justify-content:space-between; align-items:center;
}
.panel-header .count{color:var(--accent); font-weight:700;}

.panel-search{padding:10px 12px; border-bottom:1px solid var(--line-soft);}
.panel-search input{
  width:100%; background:var(--panel2); border:1px solid var(--line); color:var(--text);
  font-family:inherit; font-size:12px; padding:8px 10px; outline:none;
}
.panel-search input:focus{border-color:var(--accent);}

.stock-list{flex:1; overflow-y:auto; padding:6px;}
.stock-row{
  padding:9px 10px; border-bottom:1px dashed var(--line-soft); font-size:12px;
  display:flex; flex-direction:column; gap:3px; position:relative;
}
.stock-row.flash{animation:flashRow 1.4s ease-out;}
@keyframes flashRow{0%{background:rgba(52,213,255,0.18);} 100%{background:transparent;}}
.stock-row.negative{border-left:2px solid var(--red);}
.stock-row-top{display:flex; justify-content:space-between; gap:8px; align-items:baseline;}
.stock-row-name{color:var(--text); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.stock-row-qty{font-weight:800; font-size:13px; white-space:nowrap;}
.stock-row-qty.negative{color:var(--red);}
.stock-row-meta{font-size:10px; color:var(--dim);}
.stock-row-warn{font-size:10px; color:var(--red); font-weight:700;}

.feed-list{flex:1; overflow-y:auto; padding:6px 6px 40px;}
.feed-empty{color:var(--dim2); text-align:center; padding:60px 20px; font-size:12px; line-height:1.6;}

.event{
  border:1px solid var(--line); border-left-width:3px; background:var(--panel2);
  padding:10px 12px; margin-bottom:6px; cursor:pointer; animation:slideIn 0.25s ease-out;
}
@keyframes slideIn{from{opacity:0; transform:translateY(-6px);} to{opacity:1; transform:translateY(0);}}
.event.warn{background:rgba(255,84,112,0.06);}
.event-top{display:flex; justify-content:space-between; align-items:flex-start; gap:10px;}
.event-type{font-size:10px; font-weight:800; letter-spacing:1px; text-transform:uppercase; padding:2px 7px; border-radius:2px; white-space:nowrap;}
.event-time{font-size:10px; color:var(--dim); white-space:nowrap; flex-shrink:0;}
.event-body{margin-top:6px; font-size:12.5px; color:var(--text); line-height:1.5;}
.event-body b{color:#fff;}
.event-meta{margin-top:5px; font-size:10.5px; color:var(--dim); display:flex; gap:12px; flex-wrap:wrap;}
.event-warn-badge{color:var(--red); font-weight:800; margin-top:5px; font-size:11px;}
.event-raw{
  margin-top:8px; padding:8px 10px; background:var(--bg); border:1px solid var(--line-soft);
  font-size:10.5px; color:var(--dim); white-space:pre-wrap; word-break:break-all; display:none;
}
.event.open .event-raw{display:block;}

/* цвета по типам */
.event.type-delivery{border-left-color:var(--green);} .event.type-delivery .event-type{background:var(--green-dim); color:var(--green);}
.event.type-take{border-left-color:var(--orange);} .event.type-take .event-type{background:var(--orange-dim); color:var(--orange);}
.event.type-put{border-left-color:var(--accent);} .event.type-put .event-type{background:var(--accent-dim); color:var(--accent);}
.event.type-adjust{border-left-color:var(--purple);} .event.type-adjust .event-type{background:var(--purple-dim); color:var(--purple);}
.event.type-other{border-left-color:var(--dim2);} .event.type-other .event-type{background:var(--line); color:var(--dim);}
.event.type-error{border-left-color:var(--red); background:rgba(255,84,112,0.08);} .event.type-error .event-type{background:var(--red-dim); color:var(--red);}

.errbar{
  background:var(--red-dim); color:#ffd7de; font-size:11px; padding:8px 16px; display:none;
}
.errbar.show{display:block;}

::-webkit-scrollbar{width:8px;}
::-webkit-scrollbar-thumb{background:var(--line); border-radius:4px;}
::-webkit-scrollbar-track{background:transparent;}

@media (max-width:760px){
  .layout{flex-direction:column;}
  .stock-panel{width:100%; max-height:38vh; border-right:none; border-bottom:1px solid var(--line);}
  .tb-controls{width:100%; justify-content:space-between;}
}

/* ---------- ВКЛАДКИ ---------- */
/* Выпадающий список вкладок. Открывается от названия панели (.pane-title) при
   разделении экрана и меняет вкладку именно той панели, от которой открыт. Вне
   разделения вкладку выбирают в меню. */
.tab-menu{
  position:fixed; z-index:3000; min-width:210px; max-height:70vh; overflow:auto;
  background:var(--panel); border:1px solid var(--line); padding:4px 0;
  box-shadow:0 8px 26px rgba(0,0,0,0.45);
}
.tab-menu button{
  display:block; width:100%; text-align:left; background:none; border:none;
  border-left:2px solid transparent; color:var(--dim); font-family:inherit;
  font-size:12px; letter-spacing:0.5px; text-transform:uppercase;
  padding:11px 16px; cursor:pointer; white-space:nowrap;
}
.tab-menu button:hover{ background:var(--panel2); color:var(--text); }
/* Вкладка этой панели помечена ярко, вкладка соседней - бледно: так заранее видно,
   что выбор её сюда просто поменяет панели местами. */
.tab-menu button.cur{ color:var(--accent); border-left-color:var(--accent); }
.tab-menu button.cur-other{ color:var(--text); border-left-color:var(--dim); }

/* ---------- НИЖНЯЯ ПЛАШКА И МЕНЮ ----------
   Общая для всех вкладок полоса действий у нижнего края: слева то, что положила в
   неё текущая вкладка, справа вход в меню. Высоту плашки меряет bootstrap.js и
   кладёт в --appbar-h: под неё отступает содержимое вкладок и приподнимаются окна
   на карте, а зашивать цифру в стили нельзя - полоса чипов бывает выше одной строки. */
.appbar{
  display:flex; align-items:center; gap:8px;
  position:fixed; left:0; right:0; bottom:0; z-index:960;
  padding:8px 12px calc(8px + env(safe-area-inset-bottom));
  background:var(--panel); border-top:1px solid var(--line);
  box-shadow:0 -2px 14px rgba(0,0,0,0.28);
}
.appbar-actions{ flex:1; min-width:0; display:flex; align-items:center; gap:8px; }
/* Логотип и есть кнопка меню - без рамки и фона: рамка делала из знака приложения
   ещё один невнятный значок в ряду. */
.appbar-menu{
  flex-shrink:0; display:flex; align-items:center; justify-content:center;
  padding:0; background:none; border:none; cursor:pointer; opacity:0.85;
}
.appbar-menu img{ display:block; border-radius:5px; }
.appbar-menu:hover, .appbar-menu[aria-expanded="true"]{ opacity:1; }
/* Разделение экрана - соседка меню, но обычная кнопка: это действие, а не вход. */
.appbar-split{
  flex-shrink:0; width:34px; height:34px; padding:0;
  background:var(--panel2); border:1px solid var(--line); color:var(--dim);
  font-family:inherit; font-size:14px; line-height:1; cursor:pointer;
}
.appbar-split:hover{ color:var(--text); border-color:var(--accent); }
/* Действия вкладки внутри плашки: своих отступов и фона им здесь не нужно. */
.appbar .tasks-chips-bar{ flex:1; min-width:0; margin:0; }

/* Отступ снизу под плашку - сразу всем вкладкам: плашка общая и низ содержимого
   перекрывает одинаково везде. */
.panes{ padding-bottom:var(--appbar-h, 58px); }

.drawer{
  display:flex; position:fixed; top:0; right:0; bottom:0; z-index:1200;
  width:min(78vw, 300px); flex-direction:column; overflow-y:auto;
  background:var(--panel); border-left:1px solid var(--line);
  box-shadow:-2px 0 18px rgba(0,0,0,0.35);
  transform:translateX(100%); transition:transform 0.18s ease-out;
}
.drawer.open{ transform:none; }
.drawer-head{ display:flex; align-items:center; gap:10px; padding:16px 18px 10px; font-size:13px; font-weight:800; letter-spacing:1.5px; }
.drawer-head img{ border-radius:5px; }
.drawer-head span{ color:var(--accent); }
.drawer-tabs, .drawer-service{ display:flex; flex-direction:column; }
/* Общие действия отбиты от вкладок одной полосой на всю группу, а не на каждой. */
.drawer-service{ margin-top:8px; border-top:1px solid var(--line-soft); }
.drawer-tabs button, .drawer-service button{
  text-align:left; background:none; border:none; border-left:2px solid transparent;
  color:var(--dim); font-family:inherit; font-size:12px; letter-spacing:0.5px;
  text-transform:uppercase; padding:13px 18px; cursor:pointer;
}
.drawer-tabs button:hover, .drawer-service button:hover{ color:var(--text); }
/* Значок вкладки. Ширина задана жёстко, поэтому названия выстраиваются в столбик по
   левому краю - эмодзи разной ширины иначе разъезжаются, и список выглядит рваным.
   Регистр сбрасываем: у text-transform:uppercase некоторые значки на Android
   подменяются другим начертанием. Приглушать не стали - цвет у эмодзи свой, а
   opacity гасит и его, делая рисунок грязным. */
.tab-icon{
  display:inline-block; width:1.5em; margin-right:2px;
  text-align:center; text-transform:none; font-size:13px; line-height:1;
  vertical-align:-1px;
}
.drawer-service .tb-service-label{ margin-left:8px; }
/* Отпечаток сборки - служебная строка, а не кнопка: читают её редко, поэтому она
   мелкая и приглушённая, но выделяется, когда часть файлов не загрузилась. */
.drawer-build{
  padding:10px 18px 14px; color:var(--dim); font-size:10px; letter-spacing:0.5px;
  opacity:0.65; user-select:text; word-break:break-all;
}
/* В вертикальном списке подчёркивание снизу не читается - метим полосой слева. */
.drawer-tabs button.active{ color:var(--accent); border-left-color:var(--accent); background:var(--panel2); }
/* Затемнение под выехавшим меню: и подсказка, что остальное сейчас неактивно, и
   удобная зона закрытия по касанию мимо. */
.drawer-overlay{ display:none; position:fixed; inset:0; z-index:1150; background:rgba(0,0,0,0.45); }
.drawer-overlay.open{ display:block; }

.panes{ display:flex; flex-direction:column; min-height:0; }
.tab-pane{display:none;}
.tab-pane.active{display:flex;}

/* ---------- РАЗДЕЛЁННЫЙ ЭКРАН ----------
   Обычный режим не меняется вовсе: пока класса split-on нет, .panes - обычная
   колонка с одной видимой вкладкой, и страница прокручивается как раньше.
   Включили разделение - оболочка становится ростом с экран, и каждая панель
   прокручивается сама (у вкладок для этого уже есть свои .metrics-wrap/.map-wrap
   с overflow:auto). Ограничивать высоту всегда было бы рискованно: на телефоне
   100vh врёт из-за адресной строки, а разделение там и не нужно. */
/* В разделённом экране страница не прокручивается вовсе - листают сами панели.
   Место под нижнюю плашку отнимаем у всей колонки приложения, а не отступом внутри
   .panes: так высота панелей известна точно, и ни одна из них не может вытолкнуть
   вторую за нижний край окна. overflow:hidden на body - страховка от того же:
   если что-то внутри окажется выше панели, оно обрежется, а не утащит всю страницу. */
body.split-on{ overflow:hidden; }
/* min-height из базового правила обязательно сбросить: он сильнее height, и без него
   колонка приложения оставалась ростом в целый экран. Деление на --ui-zoom тоже
   обязательно: vh отсчитывается от окна и про zoom на body не знает, поэтому 100vh
   внутри body - это на 20% больше экрана, и низ панелей уходил под нижний край. */
body.split-on #dashboard{ height:calc(100vh / var(--ui-zoom) - var(--appbar-h, 58px)); min-height:0; }
body.split-on .panes{
  flex:1; min-height:0; flex-direction:row; flex-wrap:nowrap;
  padding-bottom:0; overflow:hidden;
}
/* Колонкой - чтобы подпись панели встала НАД содержимым, а не рядом с ним: часть
   вкладок (задачи, очередь, метрики) в обычном режиме выложены строкой. */
body.split-on .tab-pane.active{ flex:1 1 0; min-width:0; order:3; flex-direction:column; }
/* Имя вкладки над панелью. Оно же переключатель вкладки этой панели, поэтому это
   кнопка. Стиль тот же, что у заголовка списка задач. */
.pane-title{
  display:block; width:100%; text-align:left; font-family:inherit;
  font-size:11px; letter-spacing:2px; color:var(--dim); text-transform:uppercase;
  padding:14px 24px 8px; background:none; border:none; cursor:pointer;
  border-bottom:1px solid var(--line); flex-shrink:0;
}
.pane-title::after{ content:' ▾'; }
.pane-title:hover{ color:var(--accent); }
body.split-on .tab-pane.pane-left{ flex:0 0 var(--split-ratio, 50%); order:1; }
body.split-on .pane-split{ display:block; order:2; }
/* Вкладки-таблицы своей прокрутки не имеют - в обычном режиме их листает
   страница. В панели листать нечему, поэтому даём прокрутку самой вкладке. */
body.split-on #tab-recipes.active,
body.split-on #tab-items.active{ overflow:auto; }
.pane-split{
  display:none; flex:0 0 7px; cursor:col-resize; position:relative;
  background:var(--line-soft);
}
.pane-split::before{
  content:''; position:absolute; top:50%; left:50%; transform:translate(-50%, -50%);
  width:1px; height:28px; background:var(--dim2);
}
.pane-split:hover{ background:var(--accent-dim); }
/* Кнопка обмена панелей. Появляется не сразу: у самой границы курсор оказывается
   и просто по дороге, а всплывающая под рукой кнопка сбивает прицел. Показываем
   после секунды удержания рядом с линией - тогда это уже намерение. */
.pane-swap{
  position:absolute; top:10px; left:50%; transform:translateX(-50%) scale(0.8);
  width:26px; height:26px; padding:0; border-radius:50%;
  background:var(--panel2); border:1px solid var(--accent); color:var(--accent);
  font-family:inherit; font-size:13px; line-height:1; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  opacity:0; pointer-events:none; transition:opacity 0.15s, transform 0.15s;
}
body.split-hint .pane-swap{ opacity:1; pointer-events:auto; transform:translateX(-50%) scale(1); }
.pane-swap:hover{ background:var(--accent); color:var(--panel); }
body.split-dragging{ cursor:col-resize; user-select:none; }
/* Пока тянут границу, курсор не должен превращаться в текстовый над содержимым
   панелей, а сами панели - ловить наведения. */
body.split-dragging .tab-pane{ pointer-events:none; }
#split-toggle.active{ color:var(--accent); }
/* Порог тот же, что в SPLIT_MIN_WIDTH: узкое окно делить незачем - в панели
   шириной в пол-экрана не помещается ни карта, ни список задач с количествами. */
@media (max-width: 1099px){
  #split-toggle{ display:none; }
}

/* ---------- КАРТА СВЯЗЕЙ ---------- */
.map-wrap{flex:1; overflow:auto; padding:24px; cursor:grab; user-select:none; -webkit-user-select:none;}
.map-wrap.map-panning{ cursor:grabbing; }
.map-empty{color:var(--dim2); text-align:center; padding:60px 20px; font-size:12px; line-height:1.6;}
/* Только собственная фигура блока (прямой потомок <g>): внутри блока теперь есть и
   свой rect у метатега формы/цвета, а правила вида ".map-node-item rect" красили
   его рамку заодно с рамкой блока. */
.map-node-item > rect{fill:var(--panel2); stroke:var(--dim); stroke-width:1;}
.map-node-item text{fill:var(--text); font-size:11px;}
/* Рамка нейтральная: цвет отдан под пометки о незаполненных данных. Процесс от
   продукта отличает форма, а внутри блока - жирная подпись. */
.map-node-recipe > rect{fill:var(--panel2); stroke:var(--dim); stroke-width:1;}
.map-node-recipe text{fill:var(--text); font-size:11px; font-weight:700;}
.map-edge{stroke:var(--line); stroke-width:1.4; fill:none;}
.map-group-box{fill:none; stroke:var(--dim); stroke-dasharray:4 3;}
.map-band-title{fill:var(--dim); font-size:11px; letter-spacing:1px; text-transform:uppercase;}
.map-legend{display:flex; gap:18px; padding:12px 24px; border-bottom:1px solid var(--line-soft); font-size:10.5px; color:var(--dim);}
.map-legend span{display:inline-flex; align-items:center; gap:6px;}
.map-legend .sw{width:10px; height:10px; border-radius:2px; display:inline-block;}

/* ---------- МЕТРИКИ ---------- */
/* Своя прокрутка вкладке нужна только в разделённом экране: там панель ограничена
   по высоте. В обычном режиме страницу листает окно, а лишний контейнер прокрутки
   здесь ломал бы прилипающие заголовки списков - они прилипали бы к невидимому
   краю этого контейнера, а не к шапке (см. .tasks-group-title). */
.metrics-wrap{flex:1; overflow:visible; padding:24px; display:flex; flex-direction:column; gap:24px;}
body.split-on .metrics-wrap{ overflow:auto; padding-top:0; }
/* Обязательно, иначе прокрутки не будет вовсе: в колоночном флексе с заданной
   высотой содержимое сжимается до размера контейнера вместо того, чтобы вылезти за
   него, и полоса прокрутки не появляется - список просто обрезается на середине. */
body.split-on .metrics-wrap > *{ flex-shrink:0; }
.metric-section-title{font-size:11px; letter-spacing:2px; color:var(--dim); text-transform:uppercase; margin-bottom:10px;}
.metric-cards{display:flex; gap:14px; flex-wrap:wrap;}
.metric-card{background:var(--panel2); border:1px solid var(--line); padding:16px 20px; min-width:170px;}
.metric-card .num{font-size:28px; font-weight:800; color:var(--accent);}
.metric-card .label{font-size:10.5px; color:var(--dim); letter-spacing:0.5px; margin-top:4px;}
.metric-table{width:100%; border-collapse:collapse; font-size:12px;}
.metric-table th{text-align:left; color:var(--dim); font-size:10px; letter-spacing:1px; text-transform:uppercase; padding:6px 10px; border-bottom:1px solid var(--line);}
.metric-table td{padding:7px 10px; border-bottom:1px dashed var(--line-soft);}
.metric-table tr.warn td{color:var(--orange);}
.metric-table tr.danger td{color:var(--red);}

/* ---------- РЕЦЕПТЫ / ПОЗИЦИИ СКЛАДА (та же тема монитора, просто отдельные вкладки) ---------- */
#tab-recipes, #tab-items { padding: 18px 22px; overflow: auto; }
#tab-recipes h2, #tab-items h2 { font-size: 15px; font-weight: 800; letter-spacing: 1px; color: var(--text); margin-bottom: 12px; }
#tab-recipes h3, #tab-items h3 { font-size: 13px; color: var(--accent); margin: 14px 0 8px; }
#tab-recipes h4, #tab-items h4 { font-size: 11px; letter-spacing:1px; text-transform:uppercase; color: var(--dim); margin: 14px 0 6px; }

#tab-recipes button, #tab-items button, #column-picker button {
    background: var(--panel2); border: 1px solid var(--line); color: var(--dim);
    font-family: var(--mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase;
    padding: 7px 12px; cursor: pointer; margin: 2px 4px 2px 0;
}
#tab-recipes button:hover, #tab-items button:hover, #column-picker button:hover { border-color: var(--accent); color: var(--accent); }
#tab-recipes button.danger, #tab-items button.danger, #tab-recipes .danger, #tab-items .danger { color: var(--red); border-color: var(--red-dim); }
#tab-recipes button.danger:hover, #tab-items button.danger:hover { background: rgba(255,84,112,0.1); }

#tab-recipes input, #tab-items input, #tab-recipes select, #tab-items select, #column-picker input {
    background: var(--panel2); border: 1px solid var(--line); color: var(--text);
    font-family: var(--mono); font-size: 12px; padding: 6px 8px; outline: none;
}
#tab-recipes input:focus, #tab-items input:focus, #tab-recipes select:focus, #tab-items select:focus { border-color: var(--accent); }
#tab-recipes select option, #tab-items select option { background: var(--panel2); color: var(--text); }

#tab-recipes table, #tab-items table { border-collapse: collapse; width: 100%; margin-bottom: 16px; font-size: 12px; }
#tab-recipes td, #tab-items td, #tab-recipes th, #tab-items th { border: 1px solid var(--line-soft); padding: 7px 10px; text-align: left; }
#tab-recipes th, #tab-items th { color: var(--dim); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; background: var(--panel); }

.group-header {
    cursor: default; background: var(--panel2); border: 1px solid var(--line);
    padding: 9px 14px; margin-top: 12px; font-size: 12px; font-weight: 700; color: var(--text);
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.group-header-toggle { cursor: pointer; flex: 1 1 auto; }
.group-header:hover { border-color: var(--accent); color: var(--accent); }
.group-header-actions { flex: 0 0 auto; display: flex; gap: 4px; }
/* Восклицательный знак у позиции склада с проблемной единицей измерения. В шапке
   группы - тот же знак, если проблема есть хотя бы у одной строки внутри. */
.item-warn {
    display: inline-block; flex: 0 0 auto; margin-right: 6px; cursor: help;
    color: var(--orange); font-weight: 700;
}
.group-body { border: 1px solid var(--line-soft); border-top: none; padding: 10px; background: var(--panel); }
/* Шапка столбца кликается: сортировка внутри группы. Третий клик по тому же
   столбцу возвращает порядок по умолчанию. */
.col-sortable { cursor: pointer; user-select: none; }
.col-sortable:hover { color: var(--accent); }
.group-sub { margin-top: 3px; font-size: 10.5px; color: var(--dim); }

.cell-value { cursor: pointer; display: inline-block; min-width: 20px; padding: 2px 5px; color: var(--text); }
.cell-value:hover { background: var(--panel2); outline: 1px dashed var(--line); }
.empty-cell { color: var(--dim2); }
.cell-edit-wrap { position: relative; display: inline-block; }
/* Призрак невидим, но занимает место прежнего значения - ячейка не меняет размер. */
.cell-edit-ghost { display: inline-block; visibility: hidden; padding: 2px 5px; white-space: pre; }
.cell-edit {
    position: absolute; left: 0; top: 50%; transform: translateY(-50%); z-index: 40;
    display: inline-flex; align-items: center; gap: 4px;
    background: var(--panel2); border: 1px solid var(--accent); padding: 3px;
}
.cell-edit input, .cell-edit select { width: 220px; max-width: 52vw; min-width: 0; }
.cell-btn { padding: 3px 7px; font-size: 11px; }
/* Кнопка "×" (очистить значение ячейки) нужна редко, а видно её было всегда - в
   таблице из десятка столбцов это десяток красных крестов в каждой строке. Прячем
   прозрачностью, а не display:none: ширина ячейки не должна прыгать при наведении.
   Только там, где мышь есть: на тач-устройстве наведения не бывает, и кнопка,
   спрятанная до hover, стала бы недоступной совсем. */
@media (hover: hover) {
    td .cell-btn.danger { opacity: 0; transition: opacity .12s; }
    td:hover .cell-btn.danger,
    td .cell-btn.danger:focus-visible { opacity: 1; }
}

.drag-handle { cursor: grab; color: var(--dim2); text-align: center; width: 20px; user-select: none; touch-action: none; }
.recipe-group { margin-bottom: 4px; }
/* Столбец, у которого во всей группе одно и то же значение: он различает не строки,
   а саму группу, то есть повторяет её название. Подсветка слабая - это подсказка,
   а не ошибка: столбец может быть нужен и таким. */
.col-uniform { background: rgba(255, 182, 72, 0.07); }
th.col-uniform { background: rgba(255, 182, 72, 0.12); color: var(--orange); }
body.light-theme .col-uniform { background: rgba(163, 92, 0, 0.08); }
/* Столбец, по которому группа сейчас отсортирована вручную - перебивает и жёлтый
   "одинаковый у всех", иначе стрелку не видно. */
th.col-sorted, th.col-uniform.col-sorted { color: var(--accent); }
body.light-theme th.col-uniform { background: rgba(163, 92, 0, 0.14); }
.sortable-dragging { opacity: 0.95; box-shadow: 0 10px 28px rgba(0,0,0,0.65); background: var(--panel2); }
.sortable-placeholder { background: rgba(52,213,255,0.10); outline: 1px dashed var(--accent); outline-offset: -2px; }
.sortable-placeholder * { visibility: hidden; }
tr.role-input { background: rgba(52, 213, 255, 0.14); box-shadow: inset 3px 0 0 var(--accent); }
tr.role-output { background: rgba(61, 220, 132, 0.14); box-shadow: inset 3px 0 0 var(--green); }
tr.role-input:hover, tr.role-output:hover { filter: brightness(1.25); }

#recipe-editor { border-top: 1px solid var(--line); margin-top: 20px; padding-top: 16px; }

/* Пикер колонок - всплывает поверх всего, через document.body.appendChild */
#column-picker {
    position: fixed; top: 60px; left: 50%; transform: translateX(-50%);
    background: var(--panel2); border: 1px solid var(--line); padding: 12px; z-index: 1000;
    max-height: 70vh; overflow: auto; font-family: var(--mono); color: var(--text);
}
#column-picker input { width: 250px; }
#column-picker .cell-value { display: block; padding: 5px 6px; }
#column-picker .cell-value:hover { background: var(--panel); }


#mini-spinner {
    display: inline-block; width: 10px; height: 10px; margin-left: 4px;
    border: 2px solid #3a4a5c; border-top-color: #9fc6ff; border-radius: 50%;
    vertical-align: middle; animation: spin-loading-bar 0.7s linear infinite;
}
@keyframes spin-loading-bar { to { transform: rotate(360deg); } }
/* Спиннер внутри кнопки на время записи. Цвет берёт у самой кнопки, поэтому годится
   любой: видимая дуга - currentColor, остальное кольцо - линия рамки. */
.btn-spinner{
  display:inline-block; width:10px; height:10px; vertical-align:middle;
  border:2px solid var(--line); border-top-color:currentColor; border-radius:50%;
  animation: spin-loading-bar 0.7s linear infinite;
}


.row-flash { animation: rowFlash 1.5s ease-out; }
@keyframes rowFlash {
    0% { background: rgba(52, 213, 255, 0.35); }
    100% { background: transparent; }
}

.photo-cell { width: 40px; cursor: pointer; }
.photo-thumb { display: block; width: 32px; height: 32px; object-fit: cover; border: 1px solid var(--line); border-radius: 2px; }
.photo-thumb-empty {
    display: flex; align-items: center; justify-content: center;
    color: var(--dim2); font-size: 16px; background: var(--panel2);
}
.photo-cell:hover .photo-thumb { border-color: var(--accent); }
.photo-uploading { position: relative; opacity: 0.5; pointer-events: none; }
.photo-uploading::after {
    content: '';
    position: absolute; inset: 0; margin: auto;
    width: 16px; height: 16px;
    border: 2px solid var(--line); border-top-color: var(--accent);
    border-radius: 50%;
    animation: photo-spin 0.7s linear infinite;
}
@keyframes photo-spin { to { transform: rotate(360deg); } }

#photo-lightbox {
    position: fixed; inset: 0; z-index: 2000;
    background: rgba(5,8,12,0.85);
    display: flex; align-items: center; justify-content: center;
}
.photo-lightbox-box {
    background: var(--panel2); border: 1px solid var(--line);
    padding: 16px; max-width: 90vw; max-height: 90vh;
    display: flex; flex-direction: column; align-items: center; gap: 12px;
}
#photo-lightbox-img { max-width: 80vw; max-height: 70vh; object-fit: contain; }
#photo-lightbox-img.hidden { display: none; }
.photo-lightbox-actions { display: flex; gap: 8px; }

tr.row-new-highlight { background: rgba(52, 213, 255, 0.18); }

.item-picker-btn { width: 100%; min-width: 160px; text-align: left; }
/* Кнопка выбора фокуса на карте - группа + метатеги, без капса/разрядки tb-btn,
   иначе название и теги смотрелись бы криво. */
/* Кнопка показа/скрытия панели карты - слева, сразу под самой панелью. Место ей
   считает mapPositionControls: панель и кнопка висят поверх карты и должны стоять
   ниже шапки, а при разделении экрана - ниже названия вкладки. */
#map-controls-toggle{
  position:fixed; left:8px; z-index:941;
  width:34px; height:34px; line-height:1; font-size:16px;
  background:var(--panel2); color:var(--dim); border:1px solid var(--line);
  border-radius:6px; cursor:pointer;
}
#map-controls-toggle:hover{ color:var(--text); }
#map-controls-toggle.active{ color:var(--accent); border-color:var(--accent); }
.map-focus-btn{
  min-width:200px; text-align:left; cursor:pointer;
  background:var(--panel2); color:var(--text);
  border:1px solid var(--line); border-radius:6px; padding:6px 10px; font-size:12.5px;
  /* Группа и метатеги идут одной строкой: ужимать кнопку по остатку места нельзя -
     правый край подписи просто срезался. Растём по содержимому, а остальные
     органы управления переносятся на вторую строку (см. .map-controls). */
  flex:0 0 auto; white-space:nowrap; max-width:min(70vw, 720px);
}
.map-focus-btn:hover{ border-color:var(--accent); }
.map-focus-btn .tasks-pick-line{ display:inline-flex; }
.item-picker-panel {
    position: fixed; z-index: 2500;
    background: var(--panel2); border: 1px solid var(--line);
    min-width: 220px; max-height: 50vh; display: flex; flex-direction: column;
}
.item-picker-search { margin: 4px; flex: 0 0 auto; }
.item-picker-list { overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.item-picker-group-label { padding: 4px 8px; font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--dim); background: var(--panel); }
.item-picker-option { padding: 5px 10px; cursor: pointer; }
.item-picker-option:hover { background: var(--panel); }
.item-picker-option.active { color: var(--accent); font-weight: 700; }

.map-controls {
  display: flex; flex-wrap: wrap; gap: 10px; padding: 10px 24px; align-items: center;
  transition: opacity .2s ease; opacity: 1;
  /* Ширину и положение задаёт mapPositionControls - на всё окно панель растягивать
     нельзя, при разделении экрана она легла бы поверх соседней вкладки. */
  position: fixed; left: 0; width: 100%; z-index: 940;
  background: var(--panel); border-bottom: 1px solid var(--line-soft);
}
.map-controls select { flex: 1; max-width: 360px; }
.map-controls.map-controls-hidden { opacity: 0; pointer-events: none; }

#tab-map { position: relative; }

/* ---------- ЦВЕТА ОШИБОК НА КАРТЕ ----------
   Раньше три разные беды красились одним красным, и по картинке нельзя было понять,
   что чинить: нет производителя, не хватает на складе или не проставлено место.
   Теперь у каждой свой цвет, и он один и тот же у блока и у стрелки:
     красный   - взять неоткуда: ни процесса, ни поставки. Данные неполны;
     розовый   - не указано место. Данные есть, но считать по ним нельзя;
     оранжевый - с данными всё в порядке, просто на складе не хватает.
   Оранжевый намеренно мягче двух других: это не поломка справочника, а сегодняшнее
   состояние склада, и завтра оно само изменится. */

/* Продукт, которого не выпускает ни один процесс: получить его нечем - в справочнике
   не хватает рецепта-производителя (даже "Поставки"). */
.map-node-noproducer > rect, .map-node-noproducer > polygon { stroke: var(--red) !important; stroke-width: 2px !important; }

.map-node-insufficient > rect, .map-node-insufficient > polygon { stroke: var(--orange) !important; stroke-width: 2px !important; }
.map-node-flash > rect, .map-node-flash > polygon { fill: var(--red) !important; transition: fill 0.15s ease; }

/* Окна списков висят над картой у нижнего края - поднимаем их над общей плашкой
   действий, иначе она накрывает их нижние строки. Левый и правый края им ставит
   mapPositionControls по прямоугольнику самой вкладки: при разделении экрана окна
   должны стоять внутри карты, а не поперёк обеих панелей. */
.map-side-panel{
  position:fixed; bottom:calc(var(--appbar-h, 58px) + 14px); z-index:950;
  background:var(--panel); border:1px solid var(--line);
  border-radius:8px; width:auto; min-width:var(--map-side-min); max-width:560px; max-height:384px;
  display:flex; flex-direction:column; overflow:hidden;
  box-shadow:0 -4px 16px rgba(0,0,0,0.35);
}
#map-purchase-panel{ left:16px; }
#map-task-panel{ right:16px; }
/* Два окна и поля между ними задают, насколько узкой может стать панель карты при
   разделении экрана: уже этого они начали бы наезжать друг на друга. Ограничение
   сверху обязательно - без него на узком мониторе карта выдавливала бы соседнюю
   панель за край окна. */
body.split-on #tab-map.active{ min-width:min(calc(var(--map-side-min) * 2 + 48px), 60%); }
.map-side-panel-header{
  padding:10px 14px; font-size:11px; letter-spacing:1px; text-transform:uppercase;
  cursor:pointer; display:flex; align-items:center; gap:8px;
  color:var(--text); border-bottom:1px solid var(--line-soft); user-select:none; flex-shrink:0;
}
.map-side-panel-count{ background:var(--red); color:#fff; border-radius:10px; padding:1px 7px; font-size:10px; }
.map-side-panel-count-warn{ background:none; color:var(--orange); border:1px solid var(--orange); }
.map-side-panel.collapsed .map-side-panel-body{ display:none; }
.map-side-panel-body{ overflow:auto; max-height:340px; overscroll-behavior:contain; }
.map-side-panel table{ width:100%; border-collapse:collapse; font-size:11px; }
.map-side-panel td{ padding:6px 10px; border-bottom:1px solid var(--line-soft); white-space:normal; word-break:break-word; }
.map-side-panel td:last-child{ text-align:right; white-space:nowrap; }
.map-side-panel tr{ cursor:pointer; }
.map-side-panel tr:hover td{ background:rgba(255,255,255,0.05); color:var(--accent); }
.map-side-row-warn td{ color:var(--orange); border-top:1px dashed var(--line-soft); }
.map-side-row-warn:hover td{ background:rgba(255,255,255,0.05); color:var(--orange); }
.map-side-panel-empty{ padding:10px 14px; font-size:11px; color:var(--dim); }
.map-side-row-assignee{ font-size:9.5px; color:var(--dim); }

.session-row { border-bottom: 1px solid var(--line-soft); padding: 8px 0; }
.session-row-top { display: flex; align-items: center; gap: 10px; cursor: pointer; flex-wrap: wrap; }
.session-recipe-name { font-weight: 700; }
.session-employee, .session-time, .session-op-count { color: var(--dim); font-size: 11px; }
.session-ops-list { margin: 6px 0 6px 20px; border-left: 2px solid var(--line-soft); padding-left: 10px; }
.op-row { display: flex; align-items: center; gap: 10px; padding: 4px 0; font-size: 11px; flex-wrap: wrap; }
.op-item { font-weight: 600; }
.op-loc, .op-date, .op-type { color: var(--dim); }
.op-qty-value { cursor: pointer; border-bottom: 1px dotted currentColor; color: var(--accent); }
.op-qty-value:hover { color: var(--text); }
.op-row-incoming { background: rgba(61, 220, 132, 0.1); border-left: 2px solid var(--green); padding-left: 6px; }
.op-row-incoming .op-item, .op-row-incoming .op-type { color: var(--green); }

.op-row-incoming { background: rgba(61, 220, 132, 0.1); border-left: 2px solid var(--green); padding-left: 6px; }
.op-row-incoming .op-item, .op-row-incoming .op-type { color: var(--green); }

.session-subrow { margin-left: 16px; border-left: 2px solid var(--line-soft); padding-left: 8px; border-bottom: none; }

.tb-input { background:var(--panel2); border:1px solid var(--line); color:var(--text); padding:4px 6px; font-family:inherit; font-size:12px; width:100%; }

/* ---------- РЕЦЕПТЫ / ПОЗИЦИИ СКЛАДА - мобильная адаптация ---------- */
@media (max-width:760px){
  #tab-recipes table, #tab-items table { font-size: 14px; }
  #tab-recipes td, #tab-items td, #tab-recipes th, #tab-items th { padding: 5px 6px; }
  #tab-recipes th, #tab-items th { font-size: 9px; }
  .cell-value { padding: 4px 5px; }
  .cell-btn { padding: 6px 9px; font-size: 12px; }
  .drag-handle { width: 24px; }
  #tab-recipes input, #tab-items input, #tab-recipes select, #tab-items select { font-size: 14px; padding: 7px 9px; }
}

.map-edge-back { stroke: var(--dim); stroke-dasharray: 6 5; opacity: 0.1; transition: opacity 0.15s; }
/* Галочка "Показывать обратные циклы" - петли видны наравне с основными ветками */
svg.show-back-cycles .map-edge-back { opacity: 1; }
.map-edge-back:hover, .map-edge-back.map-edge-hovered { opacity: 1 !important; }
/* Точки изгиба лежат прямо на линии, поэтому мелкие и чёрные с тонкой светлой
   обводкой - иначе на самой линии их не видно. */
.map-edge-handle { fill: #000; stroke: rgba(255,255,255,0.55); stroke-width: 0.8; opacity: 0; pointer-events: none; cursor: grab; touch-action: none; }
/* !important - у приглушённых связей (побочные, петли) точки не должны наследовать
   прозрачность самой линии, иначе в режиме правки их не найти. */
svg.map-edit-mode .map-edge-handle { opacity: 0.7 !important; pointer-events: auto; }
svg.map-edit-mode .map-edge-handle:hover,
svg.map-edit-mode .map-edge-handle.edge-handle-active { opacity: 1 !important; fill: var(--accent); }
/* Точка, предложенная наведением: пока за неё не взялись, она бледнее настоящих -
   видно, что это приглашение согнуть линию, а не уже заданный изгиб. */
svg.map-edit-mode .map-edge-handle.map-edge-handle-new { opacity: 0.5 !important; fill: var(--accent); stroke: var(--panel); stroke-width: 1.2; }

/* ---------- ЗАДАЧИ ---------- */
.tasks-layout{ display:flex; }
.tasks-main{ flex:1; min-width:0; }
/* Очередь переехала на свою вкладку. Рядом с задачами её теперь ставят разделением
   экрана (кнопка ◫ в панели вкладок) - это работает с любой парой вкладок, а не
   только с этой одной, ради которой раньше держали двухколоночную раскладку. */
.tasks-queue-panel{ flex:1; min-width:0; }
/* Заголовок очереди - та же синяя полоса, что у разделов списка задач
   (.tasks-group-title): выбиваться из общего ряда ей незачем. Сворачиваться больше
   нечему, поэтому ни стрелки, ни курсора-руки здесь нет. */
.tasks-priority-summary{
  font-size:12px; letter-spacing:1.2px; color:var(--text); text-transform:uppercase; font-weight:700;
  background:var(--accent-dim); border-radius:6px; padding:7px 12px; margin-bottom:10px;
  display:flex; align-items:center; gap:6px;
  /* Заголовки очереди прилипают к верху так же, как разделы списка задач: списки
     здесь такие же длинные, и без этого непонятно, что именно сейчас на экране. */
  position:sticky; top:var(--topbar-h, 0px); z-index:5;
}
body.split-on .tasks-priority-summary{ top:0; }



/* Срок заказа. Просроченный и горящий метим рамкой слева, а не заливкой: строки
   в очереди узкие, заливка перекрыла бы номер приоритета и метатеги. */
/* Срок - одна нажимаемая строка. Поле ввода лежит под ней прозрачным слоем: убирать
   его из разметки нельзя, системный календарь открывается именно от него и к его
   месту на экране привязывается. pointer-events:none - чтобы клик доставался кнопке,
   а не полю: по самому полю браузер открывает календарь только с иконки. */
.tasks-order-deadline{ position:relative; display:flex; margin-top:4px; }
.tasks-order-deadline-btn{
  align-self:flex-start; max-width:100%;
  font-family:inherit; font-size:11px; letter-spacing:0.3px; text-align:left;
  color:var(--dim); background:var(--panel); border:1px solid var(--line);
  border-radius:4px; padding:3px 9px; cursor:pointer;
}
.tasks-order-deadline-btn:hover{ color:var(--text); border-color:var(--accent); }
/* Цвет срочности здесь дублирует рамку карточки намеренно: рамка ведёт взгляд по
   списку сверху вниз, заливка отвечает на вопрос "а когда именно". */
.tasks-order-deadline-btn.overdue,
.tasks-order-deadline-btn.urgent{ background:rgba(220,80,80,0.28); color:var(--text); border-color:var(--red); font-weight:700; }
.tasks-order-deadline-btn.soon{ background:rgba(230,198,46,0.24); color:var(--text); border-color:#e6c62e; }
.tasks-order-deadline-btn.ok{ background:rgba(76,175,80,0.22); color:var(--text); border-color:#4caf50; }
/* Срок не задан - заливка красная и держится, пока дату не выберут. Мигание после
   перемотки к форме отсюда убрано: вспышку видит только тот, кто смотрел на экран в
   эту секунду, а к форме возвращаются и позже. Красным, а не жёлтым: заказ без срока
   не «скоро», он вообще не встаёт в очередь по приоритету. */
.tasks-order-deadline-btn.none{
  background:rgba(220,80,80,0.28); color:var(--text); border-color:var(--red); font-weight:700;
}
.tasks-order-deadline-input{
  position:absolute; left:0; bottom:0; width:100%; height:100%;
  opacity:0; pointer-events:none; border:none; padding:0; background:none;
}
.tasks-deadline-tag{ font-size:10px; letter-spacing:0.5px; text-transform:uppercase; }
.tasks-deadline-tag.urgent{ color:var(--orange); }
.tasks-deadline-tag.overdue{ color:var(--red); font-weight:700; }

.tasks-priority-list{ display:flex; flex-direction:column; gap:4px; }
/* touch-action тут не ставим. Он остался от перетаскивания заказов, которое давно
   убрали, а на телефоне запрещал браузеру считать касание жестом: карточки занимают
   почти всю панель, палец всегда попадает в них, и очередь просто не прокручивалась. */
.tasks-priority-row, .tasks-order-row{
  display:flex; align-items:flex-start; gap:8px; padding:8px 10px;
  background:var(--panel2); border:1px solid var(--line-soft); border-radius:6px;
  font-size:12.5px;
}

/* Номер в очереди стоит столбиком вместе с кнопками заказа и вдвое крупнее прежнего:
   по нему называют заказ вслух, и разглядывать его среди мелкого текста не должно
   приходиться. */
.tasks-priority-index{ color:var(--dim); font-size:21px; font-weight:700; line-height:1; text-align:center; }
/* Строка очереди: сверху название группы, снизу метатеги и управление.
   Ручка перетаскивания и номер стоят слева и выровнены по верху. */
/* Слева столбиком: ручка перетаскивания и корзина */
.tasks-order-side{ display:flex; flex-direction:column; align-items:center; gap:8px; flex-shrink:0; }
/* Миниатюра продукта - по ней заказ узнают раньше, чем прочитают название. */
.tasks-order-photo{
  width:44px; height:44px; flex-shrink:0; border-radius:6px; object-fit:cover;
  background:var(--panel2); border:1px solid var(--line-soft); display:block;
}
/* Предложения «что ещё можно сделать». Не задачи и не заказы - подсказка, поэтому
   без рамок и цвета: строка, число и место. */
/* Каждый блок предложений - в своей рамке. Их два подряд, и без границ строки
   одного перетекали в другой: непонятно, где кончается «выходит без закупок» и
   начинается «выходит, если докупить». */
.tasks-spare{
  margin-top:14px; padding:10px 12px;
  border:1px solid var(--line-soft); border-radius:6px; background:var(--panel2);
}
/* Полоса заголовка идёт от края до края блока, а не с отступом внутри него. */
.tasks-spare > .tasks-spare-title{ margin:-10px -12px 8px; border-radius:6px 6px 0 0; }
.tasks-spare:not([open]) > .tasks-spare-title{ margin-bottom:-10px; border-radius:6px; }
/* Та же синяя полоса, что у заголовка очереди (.tasks-priority-summary) и у разделов
   списка задач: это такой же раздел, просто сворачиваемый. */
.tasks-spare-title{
  font-size:12px; color:var(--text); text-transform:uppercase; letter-spacing:1.2px; font-weight:700;
  background:var(--accent-dim); border-radius:6px; padding:7px 12px;
  cursor:pointer; user-select:none; list-style:none; display:flex; align-items:center; gap:6px;
}
.tasks-spare-title::-webkit-details-marker{ display:none; }
/* Своя стрелка: стандартный маркер съезжает от заглавных букв с межбуквенным
   интервалом и на телефоне попадает мимо. */
.tasks-spare-title::before{ content:'▸'; transition:transform 0.15s; }
.tasks-spare[open] > .tasks-spare-title::before{ transform:rotate(90deg); }
.tasks-spare-title:hover{ color:var(--accent); }
/* Прилипает внутри своего блока - как шапки свёрнутых разделов в списке задач. */
.tasks-spare-title{ position:sticky; top:var(--topbar-h, 0px); z-index:4; }
body.split-on .tasks-spare-title{ top:0; }
.tasks-spare:not([open]){ padding-bottom:10px; }
.tasks-spare-hint{ font-size:10px; color:var(--dim2); margin:2px 0 6px; }
/* Предложение - такая же карточка, как заказ в очереди: рамка, скругление, название
   группы жирным сверху. Раньше это был ряд строк без границ, и десяток предложений
   сливался в сплошной текст, в котором не видно, где кончается одно и начинается
   следующее. Классы тела берём от карточки заказа - вид должен совпадать буквально,
   а не «примерно так же». */
.tasks-spare-row{
  display:flex; align-items:flex-start; gap:8px; padding:8px 10px; margin-top:6px;
  background:var(--panel); border:1px solid var(--line-soft); border-radius:6px;
  font-size:12.5px; min-width:0;
}
.tasks-spare-body{ flex:1; min-width:0; display:flex; flex-direction:column; gap:6px; }
/* Строка блока «Учёт врёт». Не карточка задачи: делать тут нечего, пока не
   пересчитают, - только посмотреть и пойти считать. */
/* Переучёты рисуются обычными карточками задач - своей разметки у них больше нет. */

/* Список докупки - таблица без рамок: имя слева, количество справа. Строк бывает
   пять-шесть, и через запятую они читались как сплошной текст. */
/* Обещание держится на позициях, которых в карте нет. Не ошибка, но человек должен
   видеть, на чём стоит число. */
.tasks-buy-table{ width:100%; border-collapse:collapse; font-size:10px; color:var(--orange); }
.tasks-buy-table td{ padding:1px 0; vertical-align:top; }
.tasks-buy-qty{ text-align:right; white-space:nowrap; padding-left:10px !important; font-family:var(--mono); }
/* Подпись позиции здесь та же, что в очереди: группа плюс метатеги. Обычный
   .tasks-pick-line - блочный flex, а в ячейке перед ним стоит значок 🛒/🏭, и строка
   ломалась пополам. inline-flex оставляет их в одной строке. */
.tasks-buy-table .tasks-pick-line,
.tasks-spare-assumed .tasks-pick-line{ display:inline-flex; vertical-align:middle; gap:6px; min-width:0; }
.tasks-buy-table .tasks-pick-line > span:first-child{ min-width:0; }
/* Заголовок списка недостающего. Раньше таблица начиналась сразу, без подписи, и
   читалась как продолжение карточки. */
.tasks-buy-missing-title{ font-size:11px; font-weight:700; color:var(--text); }
/* Позицию, которую взять неоткуда, метим цветом. Слово «Обеспечить» со значком
   занимало полстроки и повторялось у каждой такой строки - цвета достаточно. */
.tasks-buy-nosupply{ color:var(--red); }
/* Отметка "уже есть на месте" прямо в строке недостающего: у такой позиции нет
   карточки задачи, и поставить отметку больше негде. Снимается там же, где все
   остальные, - в блоке "Скоро понадобится купить". */
.tasks-buy-act{ width:1%; padding-left:8px !important; }
.tasks-buy-have{
  background:none; border:1px solid var(--line); border-radius:4px; color:var(--dim);
  font-family:inherit; font-size:10px; line-height:1; padding:2px 6px; cursor:pointer;
}
.tasks-buy-have:hover{ color:var(--accent); border-color:var(--accent); }
/* Общий список допущений под строкой предложения. Пунктами, а не через запятую:
   у каждого своя кнопка, и в строку они слипались в кашу. Заголовок у списка тот же,
   что у таблицы расходов - «Что понадобится», - поэтому и пункты зачёркнуты так же:
   иначе список под таким заголовком читался бы как «сходи возьми это». */
.tasks-spare-assumed{ flex:0 0 100%; font-size:10px; color:var(--dim); margin-top:4px; }
.tasks-spare-assumed ul{ margin:2px 0 0; padding-left:16px; }
/* Зачёркивание вешаем на текст названия (ниже), а не на всю строку: с родителя оно
   протягивается сквозь потомков и не отменяется - кнопка ✕ выходила перечёркнутой,
   а text-decoration:none на ней не помогал. */
/* Подпись позиции - inline-flex, а сквозь flex-контейнер text-decoration с родителя
   не проходит: черта обрывалась на пустом месте перед названием. Вешаем её на сам
   текст названия. Метатеги не зачёркиваем - на фигурках черта читается как дефект
   печати, а не как «эту позицию не покупаем». */
.tasks-spare-assumed li .tasks-pick-line{ text-decoration:none; }
.tasks-spare-assumed li .tasks-pick-line > span:first-child{ text-decoration:line-through; }
/* Кнопки ✕ стоят столбиком у правого края, как колонка .tasks-buy-act в таблице
   соседнего блока. Раньше кнопка шла сразу за текстом и у каждой строки оказывалась
   на своём месте - попасть пальцем можно было только прицелившись. Маркеры списка
   при display:flex пропадают, и это к лучшему: в таблице их тоже нет, а блоки должны
   выглядеть одинаково. */
.tasks-spare-assumed ul{ padding-left:0; list-style:none; }
.tasks-spare-assumed li{ display:flex; align-items:baseline; gap:8px; }
.tasks-spare-assumed li .tasks-pick-line{ flex:1 1 auto; min-width:0; }
.tasks-spare-assumed li .tasks-buy-have{ flex:0 0 auto; margin-left:auto; }
/* Допущение - вычеркнутая строка в той же таблице: видно, что позиция в расходе была
   бы, но её решили считать имеющейся. Кнопка ✕ возвращает её обратно в расход. */
.tasks-buy-assumed td{ text-decoration:line-through; color:var(--dim); }
.tasks-buy-assumed .tasks-buy-act{ text-decoration:none; }
/* То же, что в списке допущений: через inline-flex черта не наследуется. */
.tasks-buy-assumed td .tasks-pick-line{ text-decoration:none; }
.tasks-buy-assumed td .tasks-pick-line > span:first-child{ text-decoration:line-through; }
/* Работа на стороне стоит в общей таблице расходов вместе с докупкой - отличает её
   только значок. Он же на вкладке себестоимости, чтобы не заводить второй язык. */
.tasks-buy-work-mark{ font-size:9px; }
/* Деньги в строке недостающего и в строке платной операции. Цвет не оранжевый, как
   у самой строки: сумма - это факт из себестоимости, а не предупреждение, и глаз
   должен цеплять её отдельно от «чего не хватает». */
.tasks-buy-money{ font-family:var(--mono); white-space:nowrap; color:var(--text); }
.tasks-buy-money-none{ color:var(--dim); font-family:inherit; }
.tasks-buy-money-cell{ text-align:right; white-space:nowrap; padding-left:10px !important; }
/* Итог стоит в заголовке списка, а не отдельной строкой под ним: вопрос «во что
   обойдётся» задают до того, как читать состав, а не после. */
/* Приписка «ещё N без цены» идёт рядом с суммой - и в заголовке строки, и в списке.
   Тише самой суммы: это оговорка к числу, а не второе число. */
.tasks-buy-total{ font-weight:400; color:var(--dim); font-family:var(--mono); font-size:10px; }
.tasks-spare-assumed li{ margin:1px 0; }
/* Снятые допущения показываем в подсказке блока, а не в строке предложения: строка
   вместе с предложением исчезает, а вернуть допущение надо уметь и после этого. */
.tasks-spare-nostock{ color:var(--orange); }
/* Объём работы за предложением - по нему и выстроен порядок списка, поэтому он
   стоит в строке, а не в подсказке. */
.tasks-spare-work{ font-size:10px; color:var(--dim); }
/* Количество - подчёркнутый текст, а не поле: правят его редко, а места поле с двумя
   кнопками шага занимало полстроки в каждой карточке. Нажатие открывает системное
   окно ввода - тем же способом, что количество и срок в заказе. */
.tasks-spare-qty-btn{
  background:none; border:none; padding:0; cursor:pointer;
  font-family:var(--mono); font-size:12.5px; color:var(--accent);
  text-decoration:underline; text-underline-offset:2px;
}
.tasks-spare-qty-btn:hover{ color:var(--text); }
/* Полосы-футера «Добавить в заказы» больше нет: подстановка в форму переехала в ряд
   квадратных кнопок раскрытой строки, к остальным действиям. Отдельная кнопка во всю
   ширину занимала место в каждой строке списка ради действия, которым пользуются,
   уже выбрав строку. */
/* Количество стоит в одном ряду с местом и кнопкой - как в карточке заказа. */
.tasks-spare-qty{
  font-family:var(--mono); color:var(--accent);
  display:flex; align-items:center; gap:4px; flex-wrap:wrap;
}
/* Сам ряд тегов не переносим - это общее правило проекта. Если он не влез,
   на новую строку уходит ряд целиком, вместе с локацией. */
.tasks-spare-row .tasks-tags{ flex:0 0 auto; }
.tasks-spare-loc{ flex:0 0 auto; color:var(--dim); font-size:10px; }

/* Отмена и пауза переехали в ряд действий раскрытой строки (.tasks-card-act): в
   свёрнутой они стояли у самого края и ловились пальцем при прокрутке, а нужны
   раз в неделю. В боковой колонке осталась только ✅ - она про заказ, который
   прямо сейчас просит подтверждения. */
.tasks-order-done-btn{
  background:var(--panel); border:1px solid var(--line); border-radius:4px;
  padding:4px 6px; cursor:pointer;
  font-size:13px; line-height:1; opacity:0.65; transition:opacity 0.15s, border-color 0.15s;
}
.tasks-order-done-btn:hover{ border-color:var(--accent); opacity:1; }
/* Выполненный заказ фоном не выделяем: он не требует действий прямо сейчас, а
   цветная карточка в очереди читается как тревога. Хватает галочки и строки. */
.tasks-order-done{ margin-top:6px; font-size:11px; color:var(--green); }
.tasks-order-progress{ margin-top:6px; font-size:11px; color:var(--dim); }
.tasks-order-done-count{ margin-left:8px; font-size:11px; color:var(--green); }
/* Приостановленный - серый целиком, но остаётся на месте и читаемым. */
.tasks-order-paused{ opacity:0.5; filter:grayscale(1); }
.tasks-order-body{ flex:1; min-width:0; display:flex; flex-direction:column; gap:6px; }
/* Название группы - главное в строке: по нему заказ и находят глазами. */
.tasks-order-title{ font-size:12.5px; font-weight:700; }
/* Метатеги держим строго в один ряд - иначе размеры и форма разъезжаются
   по строкам и перестают читаться как единое описание позиции. */
.tasks-order-tags .tasks-tags{ flex-shrink:0; }
.tasks-order-controls{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:4px; }
/* Количество - такая же нажимаемая строка, как срок: правят его редко, а смотрят
   постоянно, поэтому в покое это текст, а не поле ввода. */
.tasks-order-qty-btn{
  align-self:flex-start; font-family:inherit; font-size:11px; letter-spacing:0.3px;
  color:var(--text); background:var(--panel); border:1px solid var(--line);
  border-radius:4px; padding:3px 9px; cursor:pointer;
}
.tasks-order-qty-btn:hover{ border-color:var(--accent); }
/* Локация - продолжение фразы о количестве, поэтому в той же строке и тем же
   приглушённым тоном, что и остальные пояснения. */
.tasks-order-place{ font-size:10.5px; color:var(--dim); }
.tasks-location-link{ font-size:10.5px; color:var(--dim); text-decoration:underline; cursor:pointer; }
.tasks-location-link:hover{ color:var(--accent); }
/* Форма добавления: по умолчанию только кнопка, поля разворачиваются по клику */
/* Кнопка добавления заказа живёт в нижней плашке - одним знаком, как и остальные
   действия там. Внутри панели её больше нет: форма разворачивается на месте. */
.tasks-appbar-add{
  background:var(--panel2); border:1px solid var(--line); color:var(--dim);
  font-family:inherit; font-size:18px; line-height:1; width:34px; height:34px;
  cursor:pointer; flex-shrink:0;
}
.tasks-appbar-add:hover{ color:var(--accent); border-color:var(--accent); }
/* Форма заказа - единственное место вкладки, где что-то вводят руками, поэтому
   рамка заметная: на фоне списков блок в тонкую линию терялся. */
.tasks-add-form{
  margin-top:10px; padding:10px; display:flex; flex-direction:column; gap:8px;
  background:var(--panel2); border:1px solid var(--accent-dim); border-radius:6px;
  box-shadow:0 0 0 1px rgba(52,213,255,0.08), 0 2px 10px rgba(0,0,0,0.25);
}
.tasks-add-row{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
/* Подпись над полем: у поля срока без неё не понять, что за дата - её вводят
   заранее, когда заказа ещё нет и подсказать некому. */
.tasks-add-picker-btn{
  width:100%; text-align:left; cursor:pointer;
  background:var(--panel); color:var(--text);
  border:1px solid var(--line); border-radius:6px; padding:6px 8px; font-size:12.5px;
}
/* Группа и метатеги в пункте списка - одной строкой, теги не переносим */
.tasks-pick-line{ display:flex; align-items:center; gap:8px; }
/* Пункт списка выбора: описание сверху, метатеги под ним. */
.tasks-pick-option{ display:flex; flex-direction:column; align-items:flex-start; gap:3px; }
.tasks-pick-option .tasks-tags{ margin:0; }
.tasks-pick-desc{ font-size:12px; color:var(--text); }
.tasks-pick-line .tasks-tags{ flex-shrink:0; margin:0; }

/* Фильтр по сотрудникам: одна строка с прокруткой и стрелками по краям. */
/* Полоса фильтра: прокручиваемая лента чипов и прижатая к правому краю кнопка
   обновления. Стрелки прокрутки лежат внутри .tasks-chips-scroll, а не всей полосы -
   иначе правая стрелка накрывала бы кнопку. */
.tasks-chips-bar{ display:flex; align-items:center; gap:8px; margin-bottom:16px; }
/* Счётчик задач-затычек «Обеспечить». Оранжевый, а не красный: это не поломка -
   красным на этой карте помечены места без источника вовсе и без места процесса, -
   а честное «карту тут ещё не дописали». Кегль и обводка как у прочих значков-
   счётчиков в верхних панелях, чтобы вкладки выглядели одним приложением. */
.tasks-placeholder-warn{
  flex:0 0 auto; font-size:11px; font-weight:700; white-space:nowrap;
  color:var(--orange); border:1px solid var(--orange); border-radius:10px;
  padding:1px 8px; line-height:1.6;
}
.tasks-chips-scroll{ position:relative; flex:1; min-width:0; }
.tasks-chips{
  display:flex; flex-wrap:nowrap; gap:6px; overflow-x:auto; overflow-y:hidden;
  scrollbar-width:none; -webkit-overflow-scrolling:touch;
}
.tasks-chips::-webkit-scrollbar{ display:none; }
.tasks-chip{
  background:var(--panel2); border:1px solid var(--line); color:var(--dim);
  border-radius:14px; padding:4px 12px; font-size:11px; cursor:pointer; font-family:inherit;
  flex-shrink:0; white-space:nowrap;
}
/* Стрелки лежат поверх крайних чипсов с растушёвкой к краю - так видно, что
   список продолжается, и при этом не теряется место под сами чипсы. */
.tasks-chips-arrow{
  position:absolute; top:0; bottom:0; z-index:2; display:none;
  align-items:center; justify-content:center; width:26px;
  background:none; border:none; color:var(--dim); font-size:17px; line-height:1;
  cursor:pointer; padding:0;
}
.tasks-chips-arrow:hover{ color:var(--accent); }
.tasks-chips-arrow.left{ left:0; background:linear-gradient(to right, var(--panel) 45%, transparent); }
.tasks-chips-arrow.right{ right:0; background:linear-gradient(to left, var(--panel) 45%, transparent); }
.tasks-chips-bar.can-left .tasks-chips-arrow.left{ display:flex; }
.tasks-chips-bar.can-right .tasks-chips-arrow.right{ display:flex; }
/* Выбранный чип красим той же заливкой, что и шапки списков: бледная подсветка
   терялась на ленте из полутора десятков чипов, и было не видно, кто выбран. */
.tasks-chip.active{ border-color:var(--accent); color:var(--text); background:var(--accent-dim); font-weight:700; }
.tasks-chips-empty{ font-size:11px; color:var(--dim); }

.tasks-refresh{
  background:none; border:1px solid var(--line); color:var(--dim); font-family:inherit;
  font-size:13px; line-height:1; padding:0; width:26px; height:26px;
  display:flex; align-items:center; justify-content:center;
  border-radius:4px; cursor:pointer; flex-shrink:0;
}
.tasks-refresh:hover{ border-color:var(--accent); color:var(--accent); }
.tasks-refresh:disabled{ cursor:default; opacity:0.5; }

.tasks-list{ display:flex; flex-direction:column; gap:8px; }
.tasks-card{
  display:flex; align-items:center; gap:14px; padding:12px 16px;
  background:var(--panel2); border:1px solid var(--line-soft); border-radius:8px;
  cursor:pointer; flex-wrap:wrap;
}
.tasks-card:hover{ border-color:var(--line); }
/* Верх карточки - отдельный ряд с flex-wrap:nowrap. Раньше подпись и количество
   лежали прямо в .tasks-card, а он переносящийся (это нужно блоку подробностей с
   flex-basis:100%), и при нехватке места флексбокс сбрасывал колонку количества
   на новую строку целиком, вместо того чтобы её сжать. */
.tasks-card-row{ display:flex; align-items:center; gap:14px; flex-wrap:nowrap; flex:1 1 100%; min-width:0; }
/* flex-basis:auto (а не 0) - чтобы флексбокс знал реальную ширину подписи в одну
   строку. Пока места хватает, ничего не происходит; как только не хватает,
   дефицит сначала целиком уходит в колонку количества (см. её flex-shrink), и
   только когда та упёрлась в свой минимум, начинает ужиматься сама подпись. */
.tasks-card-main{ flex:1 1 auto; min-width:0; }
/* Подпись идёт одной строкой: название группы, описание, прочие параметры, метатеги.
   Переносим только когда действительно не влезло. */
.tasks-card-headline{ display:flex; align-items:center; gap:8px 8px; flex-wrap:wrap; }
/* Метатеги переносятся ЦЕЛИКОМ, а не по одному: ряд тегов - это флекс-элемент,
   он уходит на новую строку сразу весь. Раньше здесь стояла распорка flex-basis:100%,
   которая гнала теги под заголовок всегда, даже когда до края оставалось полстроки.
   flex-wrap:wrap внутри самого ряда - последняя ступенька: она срабатывает, только
   если ряд шире карточки и не помещается даже на отдельной строке. */
.tasks-card-headline > .tasks-tags{ flex:0 1 auto; flex-wrap:wrap; max-width:100%; margin-top:0; }
.tasks-card-active{ display:inline-flex; align-items:center; gap:4px; font-size:11px; color:var(--green); font-weight:600; }
.tasks-card-name{ font-size:13px; font-weight:600; }
.tasks-card-subtitle{ font-size:12px; color:var(--text); }
/* Описание - то, что различает карточки внутри одной группы, когда метатегов нет.
   Чуть тише подзаголовка, но не в один тон с подсказками: это часть подписи. */
.tasks-card-desc{ font-size:11.5px; color:var(--text); opacity:0.8; }
.tasks-card-assumed-done .tasks-card-desc{ text-decoration:line-through; }

/* Вид самих метатегов задаётся в visual-tags.js - общем файле с терминалом цеха.
   Здесь остаются только правила про то, как ряд тегов встраивается в конкретные
   места админки: их в терминале нет. */
/* Огромный flex-shrink - это приоритет переносов, а не размер: при нехватке ширины
   первой сдаётся колонка количества ("ХВАТИТ СЫРЬЯ" / "НА 5 ШТ" в две строки), и
   лишь когда она сжалась до своего min-content, очередь доходит до метатегов и
   описания. nowrap перенесён с колонки на сами слова: сам ряд теперь переносим. */
.tasks-card-qty{ font-size:12px; color:var(--accent); flex-shrink:1000; font-weight:600; display:flex; flex-direction:column; gap:2px; align-items:flex-end; text-align:right; margin-left:auto; }
/* Ломаться пополам ("ХВАТИТ СЫРЬЯ" / "НА 5 ШТ") разрешено только помеченным строкам.
   Остальные - "срочно", "сделано", голое число - остаются в одну строку. */
.tasks-qty-row{ display:flex; align-items:baseline; justify-content:flex-end; flex-wrap:nowrap; gap:0 6px; }
.tasks-qty-row-split{ flex-wrap:wrap; }
/* Две строки количества - это два взгляда на одно число ("хватит сырья на" против
   "всего нужно", "срочно" против "всего нужно"). Тире посередине связывает их в пару,
   иначе читаются как два независимых числа. Псевдоэлемент вынесен из потока и
   центрирован по границе строк, поэтому расстояния между ними не прибавляет. */
.tasks-qty-row + .tasks-qty-row{ position:relative; }
.tasks-qty-row + .tasks-qty-row::before{
  content:'–'; position:absolute; left:50%; top:-1px;
  transform:translate(-50%, -50%);
  font-size:10px; font-weight:400; line-height:1; color:var(--dim2); pointer-events:none;
}
/* Миниатюра позиции у закупок. Размер по высоте двух строк количества, чтобы
   карточка не подрастала из-за картинки. */
.tasks-card-photo{
  width:38px; height:38px; flex-shrink:0; border-radius:6px; object-fit:cover;
  background:var(--panel2); border:1px solid var(--line-soft); display:block;
}
/* Прогресс задачи в работе идёт в три строки - выравниваем их по правому краю,
   как и остальные количества в карточке. */
.tasks-qty-val{ text-align:right; line-height:1.35; white-space:nowrap; }
.tasks-qty-tag{ font-size:10px; font-weight:400; color:var(--dim); text-transform:uppercase; letter-spacing:0.5px; white-space:nowrap; }
/* Время выравниваем по правому краю и фиксируем ширину - так две строки читаются
   столбиком, а не лесенкой. */
.tasks-qty-time{ font-size:11.5px; font-weight:400; color:var(--dim); min-width:74px; text-align:right; }
.tasks-card-duration{ font-size:11.5px; color:var(--dim); white-space:nowrap; flex-shrink:0; }
.tasks-card-actions{ display:flex; gap:6px; flex-shrink:0; }
.tasks-card-recount{ }
.tasks-card-supply .tasks-card-qty{ color:var(--accent); }

/* Галочка "считать выполненным" - поправка к остаткам, когда материал по факту
   есть на рабочем месте, а склад об этом не знает. */
/* Галочки "считать выполненным" на карточке больше нет: действие убирает
   потребность из расчёта всей цепочки, а снаружи оно ловилось случайным пальцем.
   Теперь это кнопка внутри раскрытой карточки, .tasks-why-btn. */
.tasks-card-assumed-done{ opacity:0.5; }
.tasks-card-assumed-done .tasks-card-name,
.tasks-card-assumed-done .tasks-card-subtitle{ text-decoration:line-through; }
.tasks-card-assumed-done .tasks-card-qty{ color:var(--green); }
.tasks-done-hint{ font-size:10.5px; color:var(--dim); margin-bottom:8px; line-height:1.6; }

.tasks-detail-warn{ color:var(--orange); }
/* Красным - только то, что означает ошибку в данных, а не просто ожидание.
   Оранжевый тут уже занят предупреждениями, которые в порядке вещей. */
.tasks-detail-alert{ color:var(--red); }
.tasks-card-alert{ color:var(--red); font-size:11px; margin-top:2px; }
.tasks-detail-quickwin{ color:var(--purple); }

/* Пачка подряд идущих задач одной группы - общая рамка с заголовком.
   Заголовок оформлен как имя карточки, чтобы не путаться с "ДЕЛАТЬ СЕЙЧАС"
   (тот - мелкая капслок-метка раздела, а это название конкретной работы). */
.tasks-run{
  display:flex; flex-direction:column; gap:6px;
  border:1px solid var(--line); border-radius:8px; padding:10px 12px;
}
.tasks-run-title{ font-size:13px; font-weight:600; color:var(--text); }
/* Карточки внутри пачки занимают её ширину целиком: роль отступа играет padding
   самой рамки. Раньше здесь были линии иерархии (вертикаль от заголовка + ветка
   вправо) и margin-left под них. Они говорили то же, что и рамка, - "эти карточки
   вместе", - но не показывали, где группа кончается, и были размечены фиксированными
   числами (top:20px, height:26px) под карточку одной высоты: у раскрытой или
   двухстрочной ветка уезжала мимо. Остались рамка и ровный левый край. */
.tasks-run .tasks-card{
  background:var(--bg); border-color:var(--line-soft);
  position:relative;
}

/* Задача, на которую сырья хватает не на весь объём. Рыжей рамки у неё больше нет -
   в этом списке рыжий читается как "горит", а тут ровно наоборот. Вместо этого
   приглушаем: делать её можно прямо сейчас, но придётся возвращаться, поэтому среди
   полностью обеспеченных работ она вторым эшелоном. Гасим слабее, чем блок "Ждут
   материалов" (там 0.6): та работа не начинается вовсе, а эта начинается.
   Под курсором и в раскрытом виде возвращаем полный цвет - читать подробности
   сквозь дымку неудобно. */
.tasks-card.tasks-card-partial{ opacity:0.45; }
.tasks-card.tasks-card-partial:hover,
.tasks-card.tasks-card-partial.tasks-card-expanded{ opacity:1; }
/* Задачи из блока "Ждут материалов": сырья нет вовсе, поэтому глушим сильнее -
   взгляд не должен цепляться за них при просмотре списка. */
.tasks-waiting-materials .tasks-card{ opacity:0.45; }
.tasks-waiting-materials .tasks-card:hover,
.tasks-waiting-materials .tasks-card.tasks-card-expanded{ opacity:1; }
.tasks-card.tasks-card-partial .tasks-card-qty{ color:var(--dim); }
/* Окно «почему столько». Таблица нарочно плотная: в ней сравнивают три числа в
   строке, и воздух между ними мешает больше, чем помогает. */
.tasks-why-btn{
  display:block; width:100%; text-align:left; margin-bottom:6px;
  background:rgba(52,213,255,0.06); border:1px solid var(--accent-dim); color:var(--accent);
  border-radius:4px; padding:6px 8px; font-family:var(--mono); font-size:11px; cursor:pointer;
}
.tasks-why-btn:hover{ background:rgba(52,213,255,0.14); }
.tasks-explain{ width:min(720px, 94vw); max-height:88vh; overflow-y:auto; }
.tasks-explain-lead{ color:var(--dim); font-size:11px; margin-bottom:10px; }
.tasks-explain-sub{ margin:12px 0 4px; color:var(--dim); font-size:11px; }
/* Цепочка расчёта текстом. Абзацы короткие, между ними воздух: читают их подряд,
   и слитная простыня сводит на нет весь смысл затеи.

   Подпись объекта внутри предложения ведёт себя как обычные слова: никаких своих
   переносов, никаких колонок - иначе фраза «Для процесса X нужно…» рвётся на три
   строки и перестаёт читаться. Метатеги тоже переносятся по месту, как слова.
   Правило действует только в этом окне: на карте и в карточках подпись остаётся
   вертикальной, там она заголовок блока, а не часть фразы. */
.tasks-explain-chain{ margin-bottom:10px; font-size:12px; line-height:1.5; }
.tasks-explain-chain p{ margin:0 0 7px; }
.tasks-explain-chain p:last-child{ margin-bottom:0; }
.tasks-explain-chain b{ color:var(--accent); font-weight:600; white-space:nowrap; }
/* Подпись объекта внутри предложения: она сама по себе flex-строка, поэтому
   выравниваем её по базовой линии текста, иначе съезжает вниз. */
/* Подпись объекта внутри предложения собирается в tasks.js обычными строчными
   элементами (tasks-explain-name), поэтому ей хватает пары правил: метатеги должны
   вести себя как слова и переноситься по месту, а не выстраиваться в неразрывный ряд. */
.tasks-explain-name{ color:var(--text); }
.tasks-explain-name .tasks-tags{ display:inline; }
.tasks-explain-name .tasks-tag-model,
.tasks-explain-name .tasks-tag-size,
.tasks-explain-name .tasks-tag-inspection{ display:inline-flex; vertical-align:middle; margin:0 2px; }
.tasks-explain-name .tasks-tag-shape{ vertical-align:middle; margin:0 2px; }
/* Красным - только сам ярлык заказа: он делит таблицу на цепочки, и по нему глаз
   находит начало каждой. Красить всю строку значит превратить её в тревогу. */
.tasks-explain-order{ color:var(--red); font-weight:600; }
.tasks-explain-hint{ color:var(--dim2); font-size:10px; margin-top:2px; }
/* Служебные слова между подписями объектов: «Для работы», «нужно N на единицу».
   Тусклые и мелкие - в ячейке главное сами объекты и числа. */
.tasks-explain-step{ color:var(--dim2); font-size:10px; margin:2px 0; }
.tasks-explain .entity-label{ display:flex; }
.tasks-explain-notes{ margin-top:10px; display:flex; flex-direction:column; gap:6px; font-size:11px; }
/* Лента операций в окне переучёта. Моноширинные цифры - чтобы столбик остатка
   читался сверху вниз как столбик, а не как рваный текст. */
/* Число на блоке - норма, а не потребность: спроса на процесс сейчас нет.
   Помечаем пунктиром снизу, чтобы не путать с посчитанной цифрой. */
.map-norm-only{ border-bottom:1px dotted currentColor; opacity:0.75; }
.tasks-recount-ops{ width:100%; border-collapse:collapse; font-size:11px; }
.tasks-recount-ops td{ padding:4px 8px 4px 0; border-bottom:1px solid var(--line-soft); vertical-align:top; }
.tasks-recount-op-date{ color:var(--dim); white-space:nowrap; }
.tasks-recount-op-qty, .tasks-recount-op-run{ font-family:var(--mono, monospace); text-align:right; white-space:nowrap; }
.tasks-recount-op-run{ color:var(--dim); }
.tasks-recount-op-neg .tasks-recount-op-run{ color:var(--red); font-weight:700; }
.tasks-recount-op-deleted td{ opacity:0.45; text-decoration:line-through; }
.tasks-recount-op-flag{ font-size:9.5px; color:var(--dim); text-decoration:none; display:inline-block; }
.tasks-explain-close{ margin-top:12px; }
/* Скрыть закупки. Значок один, состояние показывает перечёркивание: две разные
   картинки на одной кнопке читались бы как две разные кнопки. */
/* Значок выгрузки для ИИ - в нижней плашке и в строке заказа. Оформление общее:
   это одно и то же действие, отличается только объём выгрузки. */
.tasks-prompt-btn{ background:none; border:0; padding:0 6px; font-size:16px; line-height:1; cursor:pointer; opacity:.8; }
.tasks-prompt-btn:hover{ opacity:1; }
.tasks-order-prompt{ font-size:13px; padding:0 2px; }
/* Ряд действий в раскрытой карточке: слева направо, с запасом на будущие кнопки.
   Рамка и отступы обязательны - без них значок не читается как кнопка и по нему
   тяжело попасть пальцем. */
.tasks-card-actions{ display:flex; gap:6px; flex-wrap:wrap; margin:0 0 8px; }
.tasks-card-act{
  min-width:32px; height:30px; padding:0 9px;
  display:inline-flex; align-items:center; justify-content:center;
  border:1px solid var(--line); border-radius:6px; background:var(--card2, transparent);
  color:var(--text); font-size:15px; line-height:1; cursor:pointer;
}
.tasks-card-act:hover{ border-color:var(--accent-dim); }
.tasks-card-act:active{ transform:scale(.96); }
/* Отмена заказа - действие необратимое, поэтому единственная кнопка в ряду, которая
   краснеет при наведении. Обычным видом она стоит специально: красная с самого
   начала притягивала бы палец. */
.tasks-card-act-danger:hover{ border-color:var(--red); color:var(--red); }
/* Сообщение о результате: кнопки под рукой нет, а знать, сработало ли, надо. */
.tasks-prompt-toast{ position:fixed; left:50%; bottom:72px; transform:translateX(-50%) translateY(8px);
  background:var(--card,#222); color:var(--fg,#eee); border:1px solid var(--line,#444);
  border-radius:8px; padding:8px 14px; font-size:13px; z-index:9999;
  opacity:0; pointer-events:none; transition:opacity .18s, transform .18s; max-width:90vw; text-align:center; }
.tasks-prompt-toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }
.tasks-supply-toggle{ position:relative; background:none; border:0; padding:0 6px; font-size:17px; line-height:1; cursor:pointer; opacity:.85; }
.tasks-supply-toggle.active{ opacity:.45; }
.tasks-supply-toggle.active::after{ content:''; position:absolute; left:4px; right:4px; top:50%; height:2px; background:var(--danger,#e5484d); transform:rotate(-20deg); }

/* Подробности - скрыты, раскрываются кликом (tasksToggleCardDetails). Один и тот же
   блок у карточки задачи, строки очереди и обоих предложений «можно сделать»: жест
   везде одинаковый, значит и разметка должна быть одна. */
.tasks-card-details{ display:none; }
.tasks-card-expanded .tasks-card-details{
  display:block; flex-basis:100%; margin-top:8px; padding-top:8px;
  border-top:1px dashed var(--line-soft); font-size:11px; color:var(--dim); line-height:1.7;
}
/* Раскрывается вся строка целиком, поэтому курсор ставим на неё, а не на заголовок:
   иначе непонятно, куда именно нажимать. У кнопок внутри свой курсор, он перебивает. */
.tasks-order-row, .tasks-spare-row{ cursor:pointer; }
/* Внутри подробностей ряд кнопок и так отбит снизу - двойной отступ не нужен. */
.tasks-card-details > .tasks-card-actions:first-child{ margin-top:0; }

/* Заголовки блоков - синие полосы во всю ширину. Раньше это была строчка того же
   роста и цвета, что подписи внутри карточек, и в потоке из полусотни строк переход
   между блоками просто не читался. Заливка делает из заголовка полосу, а не текст, -
   проскочить её глазом уже нельзя.
   Синий взят из --accent-dim, а не --accent: сплошная полоса неонового #34d5ff на
   почти чёрном фоне режет глаза, а приглушённый вариант держит контраст с текстом
   в обеих темах (на светлой он, наоборот, светлее текста).
   Иерархия осталась, но тише: у "В работе" и "Делать сейчас" акцентная грань слева -
   они всегда раскрыты и есть главное на экране. */
.tasks-group-title{
  display:flex; align-items:center; gap:8px;
  font-size:12px; text-transform:uppercase; letter-spacing:1.2px; font-weight:700;
  color:var(--text); background:var(--accent-dim);
  border-radius:6px; padding:7px 12px; margin:22px 0 8px;
  border-left:3px solid transparent;
}
.tasks-group-title-main{ border-left-color:var(--accent); }
/* Цвет шапки = состояние блока, и по нему список читается с одного взгляда, не
   вчитываясь в подписи: зелёное уже идёт, красное надо брать, жёлтое стоит и ждёт,
   красное перечёркнутое выключено руками. Фон приглушённый (--*-dim), а грань слева
   в полный цвет: сплошная заливка ярким на полсотни карточек рябит, а тонкая полоса
   держит различие даже боковым зрением. Значок дублирует цвет - на светлой теме
   разница между зелёным и жёлтым фоном меньше, чем хотелось бы, да и дальтонику
   цвет не подсказка. */
.tasks-group-go{ background:var(--green-dim); border-left-color:var(--green); }
.tasks-group-now{ background:var(--red-dim); border-left-color:var(--red); }
.tasks-group-wait{ background:var(--orange-dim); border-left-color:var(--orange); }
.tasks-group-off{ background:var(--red-dim); border-left-color:var(--red); }
.tasks-group-idle{ background:var(--panel2); border-left-color:var(--dim2); }

/* Число в кружке: глаз цепляется за него раньше, чем читает подпись. На синей полосе
   кружок берёт фон панели - он контрастен и в тёмной теме, и в светлой. */
.tasks-group-count{
  font-size:10px; font-weight:700; letter-spacing:0; color:var(--text);
  background:var(--panel); border-radius:10px; padding:1px 7px; line-height:1.5;
}
.tasks-list > .tasks-group-title:first-child{ margin-top:0; }
/* Шапка прилипает к верху прокрутки: в списке на полсотни карточек иначе не видно,
   куда именно смотришь - в "Делать сейчас" или уже в "Ждут материалов". У каждого
   свёрнутого блока свой контейнер (сам <details>), поэтому его шапка стоит только
   пока виден блок, и заголовки сменяют друг друга сами. Заголовки блоков идут выше
   главного: главный держится через весь список, и на стыке они накладываются. */
.tasks-group-title{ position:sticky; top:var(--topbar-h, 0px); z-index:4; }
/* В разделённом экране прокручивается сама панель, а не страница: её верх уже под
   шапкой, и поправка на её высоту увела бы заголовок вниз на пустое место. */
body.split-on .tasks-group-title{ top:0; }
.tasks-waiting-block > summary.tasks-group-title{ z-index:6; }
/* Своя стрелка вместо системного маркера: у flex-контейнера он не рисуется, да и
   от заглавных с межбуквенным интервалом системный съезжает - тем же приёмом
   сделаны заголовки в очереди заказов (.tasks-spare-title). */
.tasks-waiting-block > summary.tasks-group-title{ cursor:pointer; user-select:none; list-style:none; }
.tasks-waiting-block > summary.tasks-group-title::-webkit-details-marker{ display:none; }
.tasks-waiting-block > summary.tasks-group-title::before{ content:'▸'; font-size:10px; transition:transform 0.15s; }
.tasks-waiting-block[open] > summary.tasks-group-title::before{ transform:rotate(90deg); }
/* Свёрнутый блок раскрывают кликом, поэтому полоса реагирует на наведение. Меняем
   яркость, а не цвет: подбирать второй оттенок синего под обе темы незачем. */
.tasks-waiting-block > summary.tasks-group-title:hover{ filter:brightness(1.12); }
.tasks-waiting-block[open] > summary{ margin-bottom:8px; }
.tasks-waiting-block .tasks-card{ opacity:0.6; }
/* Пачку одинаковых работ гасим целиком, вместе с её заголовком: иначе название
   группы оставалось в полном цвете и выглядело ярче самих карточек под ним.
   Внутренним карточкам прозрачность возвращаем, чтобы она не умножилась дважды. */
.tasks-waiting-block .tasks-run{ opacity:0.6; }
.tasks-waiting-block .tasks-run .tasks-card{ opacity:1; }
.tasks-waiting-block .tasks-card + .tasks-card{ margin-top:8px; }

/* ---------- МОБИЛЬНАЯ ВЕРСТКА ---------- */
@media (max-width: 760px){
  /* Шапка только показывает, где ты находишься: всё управление в меню у нижнего края. */
  .topbar{ gap:10px; padding:10px 12px; }

  .appbar{ padding-left:10px; padding-right:10px; }
  .tasks-main{ width:100%; min-width:0; }
  .tasks-priority-row, .tasks-order-row{ font-size:13px; }

  /* Заслонки у стрелок прокрутки должны совпадать с фоном полосы, а не со страницей. */
  .tasks-chips-arrow.left{ background:linear-gradient(to right, var(--panel) 45%, transparent); }
  .tasks-chips-arrow.right{ background:linear-gradient(to left, var(--panel) 45%, transparent); }

  /* Карта: меньше полей вокруг холста - на узком экране они съедают полезное место. */
  .map-wrap{ padding:12px; }
  .map-legend{ flex-wrap:wrap; gap:10px; padding:10px 14px; }

  /* Окно нормы: фиксированная ширина не влезала, поля уезжали за край. */
  .map-qty-dialog{ min-width:0; width:92vw; max-width:92vw; padding:16px 14px 12px; }
  .map-qty-row{ flex-wrap:wrap; }
  .map-qty-label{ width:100%; }
  /* 16px - меньше этого Safari зумит страницу при фокусе на поле. */
  .map-qty-input{ font-size:16px; width:96px; }
  .map-qty-btn{ padding:6px 14px; }

  .metric-cards{ gap:10px; }
  .metric-card{ min-width:0; flex:1 1 140px; padding:12px 14px; }
  .metrics-wrap{ padding:14px; gap:16px; }

  /* Окно «почему столько» на телефоне остаётся таблицей: три числа в строке
     сравнивают глазами, и разложенные в столбик они перестают сравниваться.
     Ужимаем шрифт и отступы, числам разрешаем переносы, подпись объекта в первой
     колонке и так идёт в столбик - её ширина минимальная. */
  .tasks-explain{ width:96vw; padding:12px 10px; max-height:92vh; }
  .tasks-explain-chain{ font-size:11.5px; }
  .tasks-explain .entity-label{ font-size:10px; }
  .tasks-explain .tasks-explain-step{ font-size:9px; }
}

/* Меню правого клика и выбор продукта на карте связей. Стили держим здесь, а не
   в <style> внутри SVG: сами элементы живут в body, и правила из SVG до них
   доходили не всегда - меню оказывалось в потоке страницы, ниже и правее курсора. */
.map-context-menu{
  position:fixed; z-index:9999; background:var(--panel); border:1px solid var(--line);
  border-radius:6px; padding:4px; box-shadow:0 4px 14px rgba(0,0,0,0.35);
}
.map-context-menu button{
  background:transparent; border:none; color:var(--text); text-align:left;
  padding:7px 12px; cursor:pointer; border-radius:4px; font-family:var(--mono); font-size:12px;
}
.map-context-menu button:hover{ background:rgba(127,127,127,0.15); color:var(--accent); }

.map-picker-backdrop{
  position:fixed; inset:0; z-index:9999; background:rgba(0,0,0,0.45);
  /* Высокое окно (карточка блока со сводкой) при центрировании по вертикали уезжало
     верхушкой за край экрана - и до заголовка с полями было не добраться. Прижимаем
     к верху с отступом и прокручиваем сам фон. */
  display:flex; align-items:flex-start; justify-content:center;
  padding:16px; overflow-y:auto; overscroll-behavior:contain;
}
.map-picker{
  position:relative; background:var(--panel); border:1px solid var(--line);
  border-radius:8px; padding:16px; width:min(420px, 92vw); margin:auto;
  font-family:var(--mono); font-size:12px; color:var(--text);
  box-shadow:0 10px 40px rgba(0,0,0,0.5);
}
.map-picker-title{ margin-bottom:10px; margin-right:20px; }
/* Карточка блока карты: та же форма, что при создании, плюс фото и сводка снизу -
   в узкое окно выбора это уже не помещается. */
.map-picker-wide{ width:min(560px, 94vw); }
.map-edit-photo{ display:flex; flex-direction:column; gap:6px; align-items:flex-start; margin-bottom:8px; }
.map-edit-photo-img{ max-width:100%; max-height:180px; border:1px solid var(--line); border-radius:6px; }
.map-edit-details{ margin-top:14px; padding-top:12px; border-top:1px solid var(--line); }
/* Вид самой подписи (группа, описание, строки параметров, метатеги) задаётся в
   visual-tags.js - общем файле с терминалом цеха. */

/* Колонка метатегов в справочниках: только для чтения, не растягиваем таблицу. */
.tags-cell{ white-space:nowrap; }
.tags-cell .tasks-tags{ flex-shrink:0; }
.map-edit-add-panel{ border:1px solid var(--line); border-radius:6px; padding:6px; margin-bottom:8px; }
.map-edit-add-list{ max-height:220px; overflow-y:auto; margin-top:6px; }
.map-picker-search{ width:100%; font-family:var(--mono); font-size:12px; padding:5px 8px; margin-bottom:8px; }
.map-picker-list{ max-height:50vh; overflow-y:auto; }
.map-picker-option{ padding:6px 8px; cursor:pointer; border-radius:4px; }
/* Пункт "создать" стоит над списком и не должен теряться среди сотен позиций -
   отделяем чертой и подсвечиваем акцентом. */
.map-picker-create{ color:var(--accent); border:1px dashed var(--line); margin-bottom:6px; width:100%; text-align:left;
  background:transparent; font-family:inherit; font-size:inherit; }
/* Строка списка: название кликается целиком, кнопка дублирования прижата вправо. */
.map-picker-row{ display:flex; align-items:center; gap:4px; }
.map-picker-row .map-picker-option{ flex:1 1 auto; min-width:0; }
/* Описание - второй строкой, мельче и приглушённо: оно уточняет выбор, но не должно
   спорить с названием и метатегами. */
.map-picker-desc{ display:block; font-size:10px; color:var(--dim); margin-top:2px; }
.map-picker-dup{ flex:0 0 auto; cursor:pointer; background:transparent; border:1px solid var(--line);
  color:var(--dim); border-radius:4px; padding:2px 7px; font-size:12px; line-height:1.4; }
.map-picker-dup:hover{ color:var(--accent); border-color:var(--accent); }
/* Заголовок группы внутри списка выбора - те же правила, что у групп в пикере полей. */
.map-picker-group-label{ padding:6px 8px 2px; font-size:10px; letter-spacing:1px; text-transform:uppercase;
  color:var(--dim); position:sticky; top:0; background:var(--panel2); }
.map-picker-create:hover{ border-color:var(--accent); background:rgba(127,127,127,0.10); }
.map-newline-primary{ color:var(--accent); }
.map-picker-option:hover{ background:rgba(127,127,127,0.12); color:var(--accent); }

/* Окно создания связи на карте. */
.map-newline-body{ display:flex; flex-direction:column; gap:8px; }
.map-newline-arrow{ font-size:13px; line-height:1.5; }
.map-newline-note{ font-size:11px; color:var(--dim); line-height:1.5; }
.map-newline-label{ font-size:11px; color:var(--dim); margin-top:4px; }
.map-newline-select{ font-family:var(--mono); font-size:12px; padding:5px 8px; width:100%; }
.map-newline-actions{ display:flex; justify-content:flex-end; gap:8px; margin-top:14px; }

/* Пункт списка выбора фокуса на карте: название сверху, локация под ним. */
.map-pick-item{ display:flex; flex-direction:column; align-items:flex-start; gap:2px; }
.map-pick-loc{ font-size:10px; color:var(--dim); }

/* "Скоро понадобится купить" - позиции, которых нет на складе, но материал лежит
   у станка. Приглушены: это не дело на сегодня, а предупреждение на будущее. */
.tasks-wip-watch{ margin-top:18px; opacity:0.75; }
.tasks-wip-note{ font-size:11px; color:var(--dim); margin-bottom:8px; line-height:1.5; }
.tasks-wip-row{
  display:flex; align-items:baseline; gap:12px; padding:6px 0;
  border-bottom:1px dashed var(--line-soft); font-size:12px;
}
.tasks-wip-name{ flex:1; min-width:0; }
.tasks-wip-loc{ font-size:10px; color:var(--dim); }
.tasks-wip-qty{ color:var(--accent); white-space:nowrap; }
.tasks-wip-recipe{ font-size:10px; color:var(--dim); max-width:40%; text-align:right; }

/* Расшифровка расчёта количества в подробностях задачи. */
.tasks-detail-calc{ color:var(--dim); }

/* Цвет карточки заказа = запас времени. Правила стоят в конце файла намеренно:
   базовое .tasks-order-row задаёт свой фон, специфичность у них одинаковая, и
   побеждает то, что ниже. Кроме заливки - полоса слева: при ярком свете в цеху
   бледная заливка почти не видна. */
/* Заливкой красим не карточку, а только подпись срока: цветной прямоугольник во всю
   строку заслонял и номер, и метатеги, а срочность - свойство даты, а не заказа
   целиком. Цвет уходит в рамку целиком, слева она толще - по этой полосе список
   читается сверху вниз, не вчитываясь в сами карточки. */
.tasks-order-row.tasks-order-overdue,
.tasks-order-row.tasks-order-urgent{ border:1px solid var(--red); border-left:3px solid var(--red); }
.tasks-order-row.tasks-order-soon{ border:1px solid #e6c62e; border-left:3px solid #e6c62e; }
.tasks-order-row.tasks-order-ok{ border:1px solid #4caf50; border-left:3px solid #4caf50; }

/* Название группы - оно же переключатель фильтра. Пунктир снизу, а не цвет:
   заголовок карточки должен читаться как заголовок, а не как ссылка. */
.tasks-card-name-group{ cursor:pointer; border-bottom:1px dashed var(--line); }
/* Заголовок пачки - блочный элемент в колоночном флексе, и по умолчанию его
   растягивает на всю ширину списка (align-items:stretch), вместе с пунктиром.
   Прижимаем к левому краю, чтобы подчёркивание шло только под текстом. */
.tasks-run-title.tasks-card-name-group{ display:inline-block; align-self:flex-start; max-width:100%; }
.tasks-card-name-group:hover{ color:var(--accent); border-bottom-color:var(--accent); }
/* Сколько всего таких же работ можно делать прямо сейчас. Намеренно мелко:
   это подсказка к решению «ехать сейчас или подождать», а не часть названия. */
.tasks-card-name-count{ font-size:10px; font-weight:400; color:var(--dim); margin-left:4px; }

/* Плашка активного фильтра по группе работ. */
/* Плашка активного фильтра прибита к экрану прямо над нижней полосой действий -
   там же, где и кнопка, которой фильтр снимают. Фильтр действует на весь список,
   поэтому при прокрутке она не уезжает. */
.tasks-group-filter{
  display:flex; align-items:center; gap:10px;
  font-size:11px; color:var(--accent);
  position:fixed; left:0; right:0; bottom:var(--appbar-h, 58px); z-index:899;
  margin:0; padding:7px 12px;
  background:var(--panel2); border-top:1px solid var(--line);
}
/* Пока фильтр включён, нижние карточки не должны прятаться под ним. В разделённом
   экране отступ не нужен: там панель прокручивается сама и её низ и так выше плашки. */
body:has(.tasks-group-filter) .panes{ padding-bottom:calc(var(--appbar-h, 58px) + 38px); }
body.split-on:has(.tasks-group-filter) .panes{ padding-bottom:0; }
.tasks-group-filter button{
  background:transparent; border:1px solid var(--line); color:var(--dim);
  border-radius:4px; padding:3px 10px; cursor:pointer; font-family:var(--mono); font-size:10px;
}
.tasks-group-filter button:hover{ border-color:var(--accent); color:var(--accent); }

/* Пробел зажат - холст готов к перетаскиванию: показываем это курсором. */
.map-wrap.map-panning, .map-wrap.map-panning *{ cursor:grab !important; }
/* Курсор подсказывает, что сейчас сделает левая кнопка по пустому месту: ладонь -
   потащит холст, крестик - начнёт рамку выделения. Над блоками курсор свой. */
.map-wrap{ cursor:grab; }
.map-wrap svg.map-edit-mode{ cursor:crosshair; }
.map-wrap svg.map-edit-mode .map-node{ cursor:move; }
/* Подсказка про закреплённые поставки: они видны и с выключенной галочкой, и без
   этой строки поведение выглядит поломкой. */
.map-pinned-supply{ font-size:11px; color:var(--orange); display:inline-flex; align-items:center; gap:6px; }
.map-pan-hint{ font-size:10px; color:var(--dim); white-space:nowrap; }

/* ===== СТИЛИ КАРТЫ СВЯЗЕЙ =====
   Жили внутри <style> прямо в разметке SVG - 172 строки посреди функции отрисовки,
   которая и без них самая длинная в проекте. SVG вставляется в документ инлайном и
   наследует его стили, так что правила работают отсюда точно так же, а перерисовка
   карты больше не тащит их за собой в каждую сборку строки. */
    .map-node { transition: opacity 0.2s; cursor: pointer; touch-action: none; }
    .map-edge { transition: opacity 0.2s, stroke-width 0.2s, stroke 0.2s; fill: none; stroke: var(--dim); stroke-width: 1.5px; marker-end: url(#arrow); }
    .map-node.dimmed { opacity: 0.15; }
    .map-edge.dimmed { opacity: 0.08; marker-end: none; }
    /* Промежуточный отрезок длинной связи: наконечник нужен только там, где линия
       действительно упирается в блок, иначе стрелки сыплются по всему пути. */
    .map-edge-mid { marker-end: none; }
    .map-edge.highlighted { stroke: var(--accent) !important; stroke-width: 3px !important; opacity: 1 !important; marker-end: url(#arrow-hi) !important; }
    .map-node.highlighted > rect, .map-node.highlighted > polygon { stroke: var(--accent) !important; stroke-width: 2px !important; }
    /* ---------- ЧТО ЧЕМ ОБОЗНАЧЕНО ----------
       Форма  - что это: прямоугольник со скруглением - процесс, параллелограмм и
                терминатор - продукт. Одного признака достаточно, дублировать его
                цветом незачем.
       Рамка  - только состояние данных: красная «взять неоткуда», розовая «нет
                места», оранжевая «не хватает на складе». Любая цветная рамка на
                карте значит ровно одно - здесь что-то не заполнено. Раньше сюда же
                шли фиолетовый у процессов и зелёный у продуктов, и покрасневшую
                рамку было не отличить от обычной.
       Линия  - роли и маршруты: побочный выход, запасной вход, запасной маршрут,
                разбор запасов, возвратная петля. Это свойства связи, а не блока.
       Яркость - в основном ли потоке узел относительно текущего фокуса. */
    /* Рамка того же цвета, что и стрелки (--dim): блок и связь - части одного
       рисунка, и разная серость читалась как разница по смыслу, которой нет. */
    .map-node-recipe > rect { fill: var(--panel2); stroke: var(--dim); stroke-width: 1.5px; }
    .map-node-branch > rect { fill: var(--panel2); stroke: var(--map-dash); stroke-width: 1.6px; stroke-dasharray: 4 3; }
    .map-node-item > rect, .map-node-item > polygon { fill: var(--panel2); stroke: var(--dim); stroke-width: 1px; }
    /* Терминатор (сырьё без производителя) и параллелограмм (обычный продукт) - это
       формы, а не состояния. Зелёную обводку у них убрал вместе с остальными
       цветами сущностей. */
    .map-terminator-raw, .map-parallelogram { stroke: var(--dim); }
    .map-node-byproduct > rect, .map-node-byproduct > polygon { stroke: var(--map-dash) !important; }
    .map-node-byproduct { opacity: 0.1; transition: opacity 0.15s; }
    .map-edge-defect { opacity: 0.1; transition: opacity 0.15s; }
    .map-edge-branch { opacity: 0.1; transition: opacity 0.15s; }
    .map-edge-branch:hover, .map-edge-branch.map-edge-hovered { opacity: 1 !important; }
    .map-node-byproduct:hover, .map-edge-defect:hover { opacity: 1 !important; }
    .map-edge-hovered { stroke: var(--dim) !important; opacity: 1 !important; marker-end: url(#arrow) !important; }
    /* Подпись - это текст в рамке, а не линия: обводка и стрелка ей не нужны,
       от подсветки требуется только проявиться вместе со своей связью. */
    .map-edge-label.map-edge-hovered { stroke: none !important; marker-end: none !important; }
    /* Побочный узел тоже метим пунктиром, а не цветом - по той же причине.
       Галочку «Показывать тупики» включают ровно затем, чтобы их рассмотреть,
       поэтому при ней - полная яркость. Пунктир и так отличает их от основного
       потока, притушивать сверх этого незачем. */
    svg.show-defects .map-node-byproduct { opacity: 1 !important; }
    /* Штрих тот же, что у связи побочного выхода (7 5): блок и ведущая к нему линия
       должны выглядеть одинаково - по рамке сразу видно, каким путём узел связан
       с основным потоком. Было 5 4, и рисунки не совпадали. */
    .map-node-byproduct > rect, .map-node-byproduct > polygon { stroke-dasharray: 7 5; }
    /* Побочка, брак и всё, что не ведёт к фокусу - на карте таких блоков быть
       не должно. Если всё же попались (включена галочка побочных) - красный
       фон, чтобы сразу было видно. */
    /* Заливку побочных блоков не трогаем: тёмно-красный фон читался как ошибка и
       спорил с текстом. Их и так отличают приглушённость и жёлтая рамка. */
    svg.show-defects .map-edge-defect { opacity: 1 !important; }
    /* Не в основном потоке - альтернативный путь и возвратная петля. Раньше они
       метились цветом рамки (жёлтым и фиолетовым), но рамка отдана под поломки:
       узел на запасной ветке - не ошибка, и спорить с красным ему незачем.
       Теперь это приглушённость плюс пунктир, как у всего второстепенного. */
    .map-node-cat-alt > rect, .map-node-cat-alt > polygon,
    .map-node-cat-back > rect, .map-node-cat-back > polygon {
        fill: var(--panel2) !important; stroke: var(--map-dash) !important;
        stroke-width: 1.6px !important; stroke-dasharray: 5 4;
    }
    /* Приглушены, пока их не попросили показать. Включена своя галочка - видно в
       полную силу: иначе выходит, что человек нажал «показать», а блоки всё равно
       бледные. */
    /* Приглушение лёгкое: цвет рамки у них тот же, что у стрелок, и гасить его
       прозрачностью значит снова делать их бледно-серыми - ровно от этого и уходили.
       Отличать неосновную ветку должен пунктир, а не выцветание. */
    .map-node-cat-alt, .map-node-cat-back { opacity: 0.85 !important; }
    svg.show-alt-paths .map-node-cat-alt { opacity: 1 !important; }
    svg.show-back-cycles .map-node-cat-back { opacity: 1 !important; }
    .map-node-cat-alt:hover, .map-node-cat-back:hover { opacity: 1 !important; }
    .map-node-insufficient > rect, .map-node-insufficient > polygon { stroke: var(--orange) !important; stroke-width: 2px !important; }
    .map-node-flash > rect, .map-node-flash > polygon { fill: var(--red) !important; transition: fill 0.15s ease; }
    .node-text-wrap {
        display: flex; align-items: center; justify-content: center; height: 100%;
        text-align: center; font-family: var(--mono); font-size: 11px; line-height: 1.25;
        word-break: break-word; overflow: hidden; pointer-events: none; user-select: none; padding: 0 6px;
    }
    /* Подпись блока в четыре яруса: название, группа, описание, метатеги. Название
       серое и всегда в одну строку - длинные имена раздували блок и вытесняли всё
       остальное; полное имя остаётся в подсказке. */
    .node-text-lines { flex-direction: column; gap: 1px; }
    .node-text-lines > * { max-width: 100%; }
    /* Группа - главная подпись блока: тот же стиль, что был у названия, только
       жирный. Не сокращаем - переносим по словам, места под неё считает nodeHeightOf. */
    /* Подпись блока - та же разметка, что и везде (см. visual-tags.js). На карте
       она крупнее и по центру; наведение на группу показывает полное название. */
    .node-text-lines .entity-label { align-items: center; text-align: center; max-width: 100%; }
    /* Текст блока по умолчанию прозрачен для мыши - иначе он перехватывал бы клики,
       которыми блок тащат и открывают. Но подсказку с полным названием браузер
       показывает только тому, на что можно навести, поэтому группе события
       возвращаем: клик по ней всё равно всплывает до самого блока. */
    .node-text-lines .entity-label-group { pointer-events: auto; cursor: pointer; }
    .node-text-lines .entity-label-group,
    .node-text-lines .entity-label-desc {
        display: block; white-space: normal; word-break: break-word;
        color: var(--text); font-size: 1.2em; font-weight: 700;
    }
    .node-text-lines .entity-label-param { display: block; white-space: normal; word-break: break-word; }
    .node-text-lines .entity-label-tags { justify-content: center; }
    .map-node-loc { display: block; color: var(--dim); font-size: 9px; }
    .node-text-lines .map-node-cost-label { display: block; color: var(--accent); font-size: 10px; }
    .node-text-wrap .map-node-cost-label, .node-text-wrap .map-node-details-btn,
    .node-text-wrap .map-cost-fillin, .node-text-wrap input, .node-text-wrap button {
        pointer-events: auto;
    }
    .mc-table { width: auto; margin: 0 auto; border-collapse: collapse; }
    .mc-table td { padding: 0; white-space: nowrap; }
    .mc-table .mc-label { text-align: left; opacity: 0.85; }
    .mc-table .mc-int { text-align: right; padding-left: 6px; }
    .mc-table .mc-frac { text-align: left; }
    .mc-table .mc-raw { text-align: right; padding-left: 6px; }
    .mc-table .mc-icon { text-align: left; padding-left: 3px; width: 12px; }
    .mc-table .mc-total td { border-top: 1px solid var(--line-soft); font-weight: 700; padding-top: 2px; }
    .map-node-details-btn {
        background: transparent; border: 1px solid var(--line); color: var(--dim);
        border-radius: 4px; cursor: pointer; font-family: var(--mono);
    }
    .map-node-details-btn:hover { border-color: var(--accent); color: var(--text); }
    .mc-warn { color: var(--orange); }
    .mc-editable { cursor: pointer; border-bottom: 1px dotted currentColor; }
    .mc-editable:hover { color: var(--text); background: rgba(255,255,255,0.1); }
    .mc-override { color: var(--red) !important; }
    .mc-inline-input { width: 4.2em; font-size: 10px; padding: 0 2px; font-family: var(--mono); }
    .mc-inline-btns { display: block; margin-top: 2px; }
    .mc-inline-btn { font-size: 9px; padding: 0 4px; margin-right: 3px; cursor: pointer; }
    .map-node-recipe .node-text-wrap { color: var(--text); }
    .map-edge-label-bg { fill: var(--panel); stroke: var(--line); }
    .map-edge-label-manual { stroke: var(--orange); }
    /* Значки у нормы: жёлтый - нужна ваша правка, красный - факт выше нормы,
       бледная галочка - проверку замерами отключили вручную */
    .map-edge-remove { cursor: pointer; }
    .map-edge-remove circle { fill: var(--panel); stroke: var(--red); stroke-width: 1.2; }
    .map-edge-remove text { fill: var(--red); font-size: 10px; font-family: var(--mono); pointer-events: none; }
    .map-edge-remove:hover circle { fill: var(--red); }
    .map-edge-remove:hover text { fill: var(--panel); }
    /* Связь с побочным выходом: красная, чтобы отличать от основного потока.
       Прячется той же галочкой, что и боковые ветки - смотреть на брак нужно
       не всегда, а места он занимает много. */
    /* Побочный выход - роль, которую поставил человек, а не ошибка. Красным он спорил
       с настоящими пометками о поломках; пунктир и приглушённость говорят то же самое
       («не основной путь»), никого не пугая. */
    /* Пунктирные связи - того же цвета и той же плотности, что сплошные: их
       отличает рисунок штриха, а не бледность. Приглушение оставлено небольшим,
       только чтобы основной поток читался первым. */
    .map-edge-byproduct { stroke: var(--dim); stroke-dasharray: 7 5; opacity: 0.8; }
    /* Побочные связи гаснут, пока не включены тупики. Но возврат своих отходов -
       не тупик: узел и связь подчинены галочке обратных циклов, и гасить их второй
       галочкой значит показывать блок без видимых стрелок. Класс map-edge-defect
       карта на такие связи больше не вешает - по нему их и отличаем. */
    /* Гасим только связи к настоящим тупикам. Класс map-edge-defect карта вешает
       лишь тогда, когда позицию никто не берёт в дело; у побочного с потребителем
       его нет, и связь видна наравне с прочими. */
    svg:not(.show-defects) .map-edge-byproduct.map-edge-defect { opacity: 0.08; }
    /* Альтернативный вход - пунктиром и приглушённо, чтобы не путать его с
       основным путём. Показывается галочкой "Альтернативные пути". */
    .map-edge-altinput { stroke: var(--dim); stroke-dasharray: 5 4; opacity: 0.8; }
    /* Запасной маршрут: путь живой, просто пробовать его будут не первым. Пунктир
       длинный и редкий - линия читается как сплошная на расстоянии и как прерывистая
       вблизи, в отличие от побочного выхода (частый пунктир) и запасного входа
       (мелкий). Приглушать сильно нельзя: это рабочий путь, а не отход. */
    .map-edge-secondary { stroke: var(--dim); stroke-dasharray: 12 6; opacity: 0.85; }
    /* Разбор запасов: планировать через него нельзя вовсе. Точки вместо штрихов -
       линия есть, но она не про производство под заказ. */
    .map-edge-salvage-route { stroke: var(--orange); stroke-dasharray: 2 5; opacity: 0.75; }
    .map-edge-role { cursor: pointer; }
    .map-edge-role circle { fill: var(--panel); stroke: var(--dim); stroke-width: 1.2; }
    .map-edge-role text { fill: var(--dim); font-size: 10px; font-family: var(--mono); pointer-events: none; }
    /* Кружок «разбор запасов» - только у обычных выходов. Отдельным цветом от роли:
       это не роль, а способ появления работы, и путать их нельзя. Включённый
       заливаем, как и побочный, чтобы состояние читалось без наведения. */
    .map-edge-salvage { cursor: pointer; }
    /* Сплошной кружок, как у переключателя роли. Пунктирный и бледный терялся на
       светлом фоне: рядом три ярких значка, и четвёртый просто не находили. */
    .map-edge-salvage circle { fill: var(--panel); stroke: var(--dim); stroke-width: 1.2; }
    .map-edge-salvage text { fill: var(--dim); font-size: 10px; pointer-events: none; }
    .map-edge-salvage.is-on circle { fill: var(--orange); stroke: var(--orange); stroke-dasharray: none; }
    .map-edge-salvage.is-on text { fill: var(--panel); }
    /* Предпочтение задано - кружок сплошной, но не залитый: это не особый режим,
       а обычный путь, у которого просто указан порядок среди равных. */
    .map-edge-salvage.has-pref circle { stroke: var(--accent); stroke-dasharray: none; }
    .map-edge-salvage.has-pref text { fill: var(--accent); font-family: var(--mono); }
    .map-edge-salvage:hover circle { stroke: var(--accent); }
    .map-edge-role.is-byproduct circle { fill: var(--dim); }
    .map-edge-role.is-byproduct text { fill: var(--panel); }
    .map-edge-role:hover circle { stroke: var(--accent); }
    /* Кнопка локации на стрелке. Пустая - розовая, как и блоки без локации: один цвет
       на одну проблему, чтобы взгляд не искал два разных значения. */
    .map-edge-loc { cursor: pointer; }
    .map-edge-loc circle { fill: var(--panel); stroke: var(--dim); stroke-width: 1.2; }
    .map-edge-loc text { font-size: 9px; pointer-events: none; }
    .map-edge-loc.is-empty circle { fill: var(--pink-dim); stroke: var(--pink); }
    .map-edge-loc:hover circle { stroke: var(--accent); }
    .map-edge-role:hover text { fill: var(--accent); }
    /* Порты процесса: слева вход, справа выход. Появляются только в режиме
       редактирования - в обычном виде они бы только мешали читать карту. */
    /* touch-action: none - иначе на телефоне первое же движение пальцем с порта
       уходит браузеру на прокрутку, pointercancel убивает перетаскивание, и связь
       провести невозможно. Радиус попадания больше видимого кружка: пальцем в
       семь пикселей не попасть. */
    .map-port { cursor: crosshair; touch-action: none; }
    .map-port circle { fill: var(--panel); stroke: var(--accent); stroke-width: 1.2; }
    .map-port-hit { fill: transparent; stroke: none; }
    .map-port text { fill: var(--accent); font-size: 9px; font-family: var(--mono); pointer-events: none; }
    .map-port:hover circle { fill: var(--accent); }
    .map-port:hover text { fill: var(--panel); }
    .map-port-line { stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 4 3; }
    .map-lasso { fill: rgba(52,213,255,0.10); stroke: var(--accent); stroke-width: 1; stroke-dasharray: 4 3; pointer-events: none; }
    .map-node-droptarget > rect, .map-node-droptarget > polygon { stroke: var(--accent) !important; stroke-width: 2.5px !important; }
    .map-node-draft > rect { fill: var(--panel); stroke: var(--accent); stroke-dasharray: 5 3; }
    .map-undo-toast {
        position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 9998;
        display: flex; align-items: center; gap: 12px;
        background: var(--panel); border: 1px solid var(--line); border-radius: 6px;
        padding: 10px 14px; font-family: var(--mono); font-size: 12px; color: var(--text);
        box-shadow: 0 6px 24px rgba(0,0,0,0.4);
    }
    .map-undo-toast button {
        background: transparent; border: 1px solid var(--line); color: var(--accent);
        border-radius: 4px; padding: 4px 12px; cursor: pointer; font-family: var(--mono); font-size: 11px;
    }
    .map-undo-toast button:hover { border-color: var(--accent); }
    .map-undo-hint { color: var(--dim); font-size: 10px; }
    .map-norm-warn { color: #e6c62e; margin-left: 4px; cursor: pointer; font-size: 0.8em; }
    .map-norm-alert { color: var(--red); margin-left: 4px; cursor: pointer; font-size: 0.9em; font-weight: bold; }
    .map-qty-dialog-backdrop {
        position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,0.45);
        display: flex; align-items: center; justify-content: center;
    }
    .map-qty-dialog {
        position: relative; background: var(--panel); border: 1px solid var(--line);
        border-radius: 8px; padding: 18px 20px 14px; min-width: 340px; max-width: 470px;
        font-family: var(--mono); font-size: 12px; color: var(--text);
        box-shadow: 0 10px 40px rgba(0,0,0,0.5);
    }
    .map-qty-close {
        position: absolute; top: 6px; right: 8px; background: transparent; border: none;
        color: var(--dim); cursor: pointer; font-size: 14px; padding: 2px 4px;
    }
    .map-qty-close:hover { color: var(--text); }
    .map-qty-title { margin-right: 18px; line-height: 1.5; }
    .map-qty-title div + div { margin-top: 2px; }
    .map-qty-title-gap { margin-bottom: 14px; }
    .map-qty-alert {
        border-left: 2px solid var(--red); background: rgba(220,80,80,0.10);
        padding: 7px 10px; margin: 0 18px 12px 0; border-radius: 0 4px 4px 0; line-height: 1.45;
    }
    .map-qty-tabs { display: flex; flex-direction: column; gap: 2px; margin: 0 0 10px; }
    .map-qty-tab {
        background: transparent; border: none; border-left: 2px solid transparent;
        color: var(--dim); padding: 6px 10px; cursor: pointer; text-align: left;
        font-family: var(--mono); font-size: 11px; border-radius: 0 4px 4px 0;
    }
    .map-qty-tab:hover { color: var(--text); background: rgba(127,127,127,0.10); }
    .map-qty-tab.active { color: var(--accent); border-left-color: var(--accent); background: rgba(127,127,127,0.08); }
    .map-qty-pane[hidden] { display: none; }
    .map-qty-hint { color: var(--dim); font-size: 11px; margin-top: -2px; }
    .map-qty-check { display: flex; align-items: center; gap: 6px; color: var(--dim); cursor: pointer; font-size: 11px; }
    .map-qty-check:hover { color: var(--text); }
    .map-qty-check input { cursor: pointer; }
    .map-qty-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
    .map-qty-label { width: 128px; color: var(--dim); flex: none; }
    .map-qty-value { min-width: 76px; }
    .map-qty-empty { color: var(--dim); font-style: italic; }
    .map-qty-input { width: 76px; font-family: var(--mono); font-size: 12px; padding: 3px 5px; }
    .map-qty-unit { color: var(--dim); }
    .map-qty-btn {
        margin-left: auto; background: transparent; border: 1px solid var(--line); color: var(--text);
        border-radius: 4px; padding: 3px 12px; cursor: pointer; font-family: var(--mono); font-size: 11px;
    }
    .map-qty-btn:hover { border-color: var(--accent); color: var(--accent); }
    .map-edge-label { cursor: pointer; }
    .map-edge-label-text { fill: var(--dim); font-size: 11px; font-family: var(--mono); pointer-events: none; }
    .map-edge-label:hover .map-edge-label-bg { stroke: var(--accent); }
    .map-edge-label:hover .map-edge-label-text { fill: var(--accent); }
    .node-item .node-text-wrap { color: var(--text); opacity: 0.9; }

/* Блок, у которого хоть одна строка рецепта заведена без локации. Розовая заливка -
   это про незаполненные данные, а не про ошибку расчёта: остаток по такой строке
   считается сразу по всем складам, и задача может проситься туда, где материала нет.
   Правило идёт последним, чтобы перебивать заливку категорий. */
/* Локация принадлежит СВЯЗИ, а не блоку: у процесса шесть строк, и каждая может
   брать материал со своего склада. Красим только саму стрелку - обводка на блоке
   говорила лишь "где-то тут не заполнено" и висела на пол-карты, не показывая где. */
.map-edge-noloc { stroke: var(--pink) !important; stroke-width: 2 !important; }
/* Счётчик строк без места - в одной строке с галочками панели. Красный, а не серый:
   это не настройка вида, а поломка данных, из-за которой рецепт выпадает из расчёта
   работ целиком. Прячется сам, когда таких строк не осталось. */
.map-noloc-warn{
  display:inline-flex; align-items:center; gap:6px;
  font-size:11px; color:var(--red); white-space:nowrap;
}
.map-noloc-warn.hidden{ display:none; }
.map-noloc-warn b{ font-family:var(--mono); }
/* Варианты в окне «как получают продукт». Радиокнопка с пояснением в две-три
   строки: без пояснения разницу между «разбором запасов» и «предпочтением» по одним
   названиям не понять, а ошибка тут молча отрезает товар от планирования. */
.map-route-opt{
  display:flex; gap:8px; align-items:flex-start;
  padding:8px 0; border-bottom:1px solid var(--line-soft);
  font-size:11px; line-height:1.5; color:var(--dim); cursor:pointer;
}
.map-route-opt:last-of-type{ border-bottom:none; }
.map-route-opt b{ color:var(--text); font-size:12px; }
.map-route-opt input[type=radio]{ margin-top:3px; flex:0 0 auto; }
.map-route-opt input[type=number]{ display:block; margin-top:6px; width:90px; }

/* Обязательное поле в карточке блока. Помечаем словом, а не звёздочкой: звёздочку
   рядом с подписью читают как сноску и ищут пояснение внизу формы, которого нет. */
/* Включённая кнопка в ряду действий - например, ручная срочность заказа. Заливка,
   а не рамка: состояние должно читаться в свёрнутой строке краем глаза, без
   наведения и без раскрытия карточки. */
.tasks-card-act.is-on{ background:var(--red-dim); border-color:var(--red); }

.map-field-required{
  margin-left:6px; font-size:9px; letter-spacing:0.5px; text-transform:uppercase;
  color:var(--red); border:1px solid var(--red); border-radius:3px; padding:0 4px;
}
/* Предупреждение под незаполненным обязательным полем: то же место, что у обычной
   пояснительной строки формы, но красным - это не подсказка, а несделанное дело. */
.map-newline-note-warn{ color:var(--red); }

/* Узел с непроставленным местом - у самого процесса или у любой его строки. Розовым,
   как и стрелки без места: это одна и та же поломка, и глаз должен связывать их
   между собой. Рамка, а не заливка: внутри узла уже есть свои цвета состояния. */
/* Рамка побочного блока повторяет штрих связи, которая к нему ведёт (7 5, как у
   .map-edge-byproduct): один смысл - один вид, и по рамке сразу ясно, каким путём
   узел связан с основным потоком.

   Правило намеренно перекрывает оранжевую пометку о нехватке остатка. «Не хватает»
   про побочное бессмысленно: добрать его нельзя ни закупкой, ни запуском процесса,
   оно получается попутно. Я трижды пробовал не ставить эту пометку в расчёте и
   трижды промахивался мимо нужной ветки - здесь же перекрытие работает независимо
   от того, откуда взялся класс.

   Настоящие поломки исключены и перекрывают рамку по-прежнему: розовая «нет места»
   и красная «взять неоткуда» - это дыры в данных, и молчать о них нельзя. */
svg .map-node-byproduct:not(.map-node-noloc):not(.map-node-noproducer) > rect,
svg .map-node-byproduct:not(.map-node-noloc):not(.map-node-noproducer) > polygon{
  stroke: var(--map-dash) !important;
  /* Толще сплошных намеренно. Пунктир при равной толщине и равном цвете читается
     светлее: глаз усредняет штрихи с просветами, и рамка выглядит выцветшей, хотя
     цвет тот же. Прибавка возвращает ей тот же вес, что у соседних блоков. */
  stroke-width: 1.6px !important;
  stroke-dasharray: 7 5 !important;
}

/* Ветка вне планирования: через неё нельзя построить цепочку под заказ, и всё, что
   её кормит, существует ради неё одной. Пунктир тот же, что у побочного блока, -
   смысл один: это не основной путь. Поломки по-прежнему главнее и рамку перекрывают. */
svg .map-node-unplanned:not(.map-node-noloc):not(.map-node-noproducer) > rect,
svg .map-node-unplanned:not(.map-node-noloc):not(.map-node-noproducer) > polygon{
  stroke: var(--map-dash) !important;
  stroke-width: 1.6px !important;
  stroke-dasharray: 7 5 !important;
}

.map-node-noloc rect{ stroke: var(--pink) !important; stroke-width: 2 !important; }

/* Пояснение к цветам - прямо в панели карты. Без него цвет остаётся загадкой: он
   виден постоянно, а расшифровка живёт в голове того, кто это писал. */
.map-legend{ display:inline-flex; align-items:center; gap:12px; font-size:11px; color:var(--dim); }
.map-legend span{ display:inline-flex; align-items:center; gap:5px; white-space:nowrap; }
.map-legend i{ width:14px; height:0; border-top:2px solid currentColor; display:inline-block; }
.map-legend .lg-nosrc{ color:var(--red); }
.map-legend .lg-noloc{ color:var(--pink); }
.map-legend .lg-stock{ color:var(--orange); }
/* Связь чернового блока: настоящая строка рецепта, но блок ещё не вошёл в подграф
   фокуса - показываем её приглушённо, как и сам черновик. */
.map-edge-draft { opacity: 0.75; }

/* Галочка "Можно по частям" в карточке заказа - мелкая служебная строка под сроком,
   она не должна спорить с количеством и датой. */
/* Заказ с дырами в составе: зачёркнут и не участвует в расчёте. Приглушаем весь
   ряд, но кнопки оставляем живыми - количество и срок менять можно, это часто и
   есть способ обойти проблему. */
.tasks-order-broken .tasks-order-title,
.tasks-order-broken .tasks-order-controls{ text-decoration:line-through; opacity:0.6; }
.tasks-order-problems{
  margin-top:6px; padding:6px 8px; border-radius:4px; font-size:11px; line-height:1.45;
  background:rgba(255,84,112,0.07); border:1px solid var(--red-dim); color:var(--text);
}
.tasks-order-problems b{ color:var(--red); }
.tasks-order-problems ul{ margin:4px 0 6px; padding-left:16px; }
.tasks-order-problems li{ margin-bottom:2px; }
.tasks-order-problems-more{ color:var(--dim); margin-bottom:6px; }
/* Замечания по цепочке заказа: столько же, сколько показала бы панель проблем, если
   встать на этот продукт фокусом. Не ошибка - считать они не мешают, - поэтому
   только значок с числом, а сам перечень в подсказке. */
.tasks-order-warn{
  margin-left:6px; color:var(--orange); font-size:11px; white-space:nowrap; cursor:help;
}
/* Переключатель режима расчёта. Два состояния подписаны словами, а не галочкой:
   «Всё сразу» - обычный режим, «Можно по частям» - послабление, поэтому включённое
   состояние подсвечено акцентом, а выключенное выглядит нейтрально. */
.tasks-order-partial{
  align-self:flex-start; margin-top:4px; padding:3px 9px;
  background:var(--panel); border:1px solid var(--line); border-radius:4px;
  font-family:inherit; font-size:10px; letter-spacing:0.5px; color:var(--dim);
  cursor:pointer; user-select:none; white-space:nowrap;
}
.tasks-order-partial:hover{ color:var(--text); border-color:var(--accent); }
.tasks-order-partial.on{ color:var(--accent); border-color:var(--accent); }
.tasks-order-partial:disabled{ cursor:default; }

/* Закупка, временно отмеченная выполненной: в списке "скоро понадобится" она стоит
   по другой причине, чем остальные, - помечаем, чтобы не путали с расчётным остатком
   у станка. */
.tasks-wip-assumed{ opacity: 0.75; }
.tasks-wip-assumed .tasks-wip-recipe{ font-style: italic; }

/* Галочка снятия отметки прямо в строке списка - иначе отметку по исчезнувшей задаче
   можно было убрать только сбросом всех сразу. */
.tasks-wip-uncheck{ display:inline-flex; align-items:center; margin-right:5px; cursor:pointer; }
.tasks-wip-uncheck input{ margin:0; cursor:pointer; }

/* ---------- ПРОЕКТЫ И КОНТАКТЫ ----------
   Список с поиском и карточка. Ширина ограничена: длинные строки на широком
   мониторе читать тяжело, а на телефоне всё в одну колонку. */
#projects-wrap, #contacts-wrap{ gap:14px; max-width:980px; width:100%; margin:0 auto; box-sizing:border-box; }
.prj-top{ display:flex; flex-direction:column; gap:10px; }
.prj-top .tb-input, .prj-card .tb-input{ font-size:13px; padding:8px 10px; box-sizing:border-box; }
.prj-chips{ display:flex; flex-wrap:wrap; gap:6px; }
.prj-new{ display:flex; gap:8px; }
.prj-new .tb-input{ flex:1; min-width:0; }
.prj-new .tb-btn{ flex-shrink:0; }
.prj-list{ display:flex; flex-direction:column; border-top:1px solid var(--line-soft); }
.prj-row{
  display:flex; align-items:center; gap:12px; width:100%; text-align:left;
  background:none; border:none; border-bottom:1px solid var(--line-soft);
  color:var(--text); font-family:inherit; padding:10px 4px; cursor:pointer;
}
.prj-row:hover{ background:var(--panel2); }
.prj-row.closed{ opacity:0.55; }
.prj-row-main{ flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; }
.prj-row-name{ font-size:13px; overflow-wrap:anywhere; }
.prj-row-sub{ font-size:11px; color:var(--dim); overflow-wrap:anywhere; }
/* В списке проектов каждая строка - ровно одна, с «…»: строки одной высоты, длинное
   описание не раздувает список. Целиком - в подсказке при наведении и в карточке. */
.prj-row .prj-line{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; overflow-wrap:normal; }
.prj-row-desc{ font-size:12px; color:var(--dim); color:color-mix(in srgb, var(--text) 72%, var(--bg)); }
.prj-row .prj-row-sub{ font-size:10.5px; color:var(--dim); }
.prj-row-side{ flex-shrink:0; display:flex; flex-direction:column; align-items:flex-end; gap:3px; }
.prj-row-sum{ font-size:11px; color:var(--dim); white-space:nowrap; }
/* Раскрытая строка: название целиком, а описание и мелкая строка уходят в
   подробности под ней - строка и подробности читаются одним блоком. */
.prj-row.open{ background:var(--panel2); border-bottom-color:transparent; }
.prj-row.open .prj-row-name{ white-space:normal; overflow:visible; overflow-wrap:anywhere; }
.prj-row.open .prj-row-desc, .prj-row.open .prj-row-sub{ display:none; }
.prj-peek{
  display:flex; flex-direction:column; gap:8px; padding:0 10px 12px 36px;
  background:var(--panel2); border-bottom:1px solid var(--line-soft); font-size:12px; color:var(--text);
}
.prj-peek-row{ display:flex; flex-wrap:wrap; align-items:baseline; gap:4px 8px; }
.prj-peek-label, .prj-peek-title{ font-size:10px; letter-spacing:0.5px; color:var(--dim); text-transform:uppercase; }
.prj-peek-label{ flex-shrink:0; }
.prj-peek-val{ min-width:0; overflow-wrap:anywhere; }
.prj-peek-block{ display:flex; flex-direction:column; gap:4px; }
/* текст - как записан, с переносами строк */
.prj-peek-text{ white-space:pre-wrap; overflow-wrap:anywhere; line-height:1.45; }
.prj-peek-comment{ padding-left:8px; border-left:2px solid var(--line); }
.prj-peek-photos{ display:flex; flex-wrap:wrap; gap:6px; }
.prj-peek .prj-thumb{ width:72px; height:72px; }
.prj-peek-kids{ overflow-wrap:anywhere; }
.prj-peek-foot{ display:flex; flex-wrap:wrap; align-items:center; justify-content:flex-end; gap:10px; }
.prj-peek-foot .prj-muted{ margin-right:auto; }
/* вид «Сейчас»: путь мелко над названием, справа - когда было последнее событие */
.prj-views .tb-btn{ flex:1; }
.prj-now-row{ padding-left:10px; }
.prj-now-path{ font-size:10.5px; color:var(--dim); }
.prj-now-when{ font-size:11px; color:var(--dim); white-space:nowrap; }
/* статус в строке «Сейчас» нажимается - окно смены статуса */
.prj-now-row .prj-status{ cursor:pointer; }
.prj-status-list .map-picker-option{ justify-content:flex-start; }
.prj-status-confirm{ margin-top:10px; display:flex; flex-direction:column; gap:4px; font-size:12px; overflow-wrap:anywhere; }
.prj-status-change{ font-size:14px; }
/* Окно выбора (prjPick): имя и путь в две строки, галочка при выборе нескольких */
.prj-pick .prj-pick-name{ flex:1; min-width:0; display:flex; flex-direction:column; gap:1px; text-align:left; }
.prj-pick .prj-pick-name .prj-muted{ font-size:10.5px; }
.prj-pick-check{ flex:none; width:16px; color:var(--accent); }
.prj-pick-foot{ margin-right:auto; }
/* Окно «Новый проект»: поля-кнопки, файлы, новый заказчик */
.prj-new-media{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.prj-new-media .tb-btn{ font-size:16px; padding:4px 10px; }
.prj-new-files{ display:flex; flex-wrap:wrap; gap:6px; }
.prj-new-files:empty{ display:none; }
.prj-new-file{ position:relative; display:inline-flex; align-items:center; gap:4px; max-width:100%; border:1px solid var(--line); padding:2px 4px; font-size:12px; }
.prj-new-file img{ width:44px; height:44px; object-fit:cover; display:block; }
.prj-new-field{ display:flex; flex-direction:column; gap:3px; }
.prj-new-label{ font-size:12px; color:var(--dim); }
.prj-new-val{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; min-width:0; }
.prj-new-pick{ max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-transform:none; }
.prj-new-from{ font-size:11px; }
.prj-new-chip{ display:inline-flex; align-items:center; gap:4px; border:1px solid var(--accent-dim); color:var(--accent); padding:2px 6px; font-size:12px; }
.prj-new-chip-x{ background:none; border:none; color:var(--dim); cursor:pointer; padding:0 2px; font:inherit; }
.prj-new-contact{ display:flex; flex-direction:column; gap:6px; padding-left:10px; border-left:2px solid var(--accent-dim); }
.prj-new-dup .tb-btn{ margin-left:6px; padding:2px 8px; }
/* окно длинное - «Создать» всегда видна внизу, прокручиваются поля */
.prj-new-dialog .prj-dialog-btns{ position:sticky; bottom:-16px; background:var(--panel); padding:8px 0; margin-bottom:-8px; z-index:1; }
/* Окно проекта (§34) - вместо карточки, поверх списка. Шапка с «Закрыть» прилипает к верху. */
.prj-card-dialog{ width:min(760px, 100%); box-sizing:border-box; padding-top:0; }
.prj-card-dialog .prj-card-head{ position:sticky; top:-16px; z-index:2; background:var(--panel); padding:12px 0 8px; }
.prm-group.empty .prm-row.prm-none{ font-size:12px; }
.prm-btn.prm-add-one{ margin-left:6px; vertical-align:middle; }
.prm-btn[data-act="edit"]:hover{ color:var(--accent); border-color:var(--accent-dim); }
.prj-more{ border-top:1px solid var(--line); padding-top:8px; }
.prj-more > summary{ cursor:pointer; font-size:11px; letter-spacing:0.5px; text-transform:uppercase; color:var(--dim); padding:4px 0; }
.prj-more.warn > summary{ color:var(--red); }
.prj-more[open] > summary{ margin-bottom:8px; }
.prj-ask{ width:min(420px, 100%); }
.prj-ask-text{ font-size:14px; line-height:1.4; overflow-wrap:anywhere; }
.prj-now-totals{ font-size:12px; color:var(--accent); }
.prj-now-ev{ display:flex; gap:10px; align-items:baseline; padding:2px 0; }
.prj-now-ev-when{ flex-shrink:0; width:92px; font-size:11px; color:var(--dim); white-space:nowrap; }
.prj-now-ev-what{ min-width:0; overflow-wrap:anywhere; }
.prj-status{
  font-size:10px; letter-spacing:0.5px; text-transform:uppercase; white-space:nowrap;
  padding:2px 6px; border:1px solid var(--line); color:var(--dim);
}
.prj-status.s-5105{ color:var(--accent); border-color:var(--accent-dim); }
.prj-status.s-5333{ color:var(--orange); border-color:var(--orange-dim); }
.prj-status.s-541{ color:var(--green); border-color:var(--green-dim); }
.prj-status.s-5104{ color:var(--green); border-color:var(--green-dim); }
.prj-status.s-5920{ color:var(--purple); border-color:var(--purple-dim); }
.prj-status.s-5106, .prj-status.s-9572{ color:var(--dim2); }
.prj-status.s-zp{ color:var(--pink); border-color:var(--pink-dim); }
.prj-unnamed{ color:var(--dim); font-style:italic; }
.prj-tree-mark{ color:var(--dim2); flex-shrink:0; }
.prj-overdue{ font-size:10px; color:var(--red); text-transform:uppercase; letter-spacing:0.5px; }
.prj-more{ padding:12px 4px; }
.prj-muted{ font-size:11px; color:var(--dim); }

.prj-card{ display:flex; flex-direction:column; gap:12px; }
.prj-card-head{ display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }
.prj-title-row{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
.prj-title{ font-size:18px; line-height:1.3; overflow-wrap:anywhere; }
.prj-section{ display:flex; flex-direction:column; gap:8px; padding-top:12px; border-top:1px solid var(--line-soft); }
.prj-section-title{ font-size:11px; letter-spacing:2px; color:var(--dim); text-transform:uppercase; }
.prj-section > .tb-btn{ align-self:flex-start; }
/* Заголовок блока с действиями справа - мелкими словами, без рамок: кнопки не
   спорят с содержимым. */
.prj-section-head{ display:flex; align-items:baseline; gap:14px; flex-wrap:wrap; }
.prj-section-head .prj-section-title{ margin-right:auto; }
.prj-head-act{ background:none; border:none; padding:2px 0; font-family:inherit; font-size:11px; color:var(--accent); cursor:pointer; }
.prj-head-act:hover{ text-decoration:underline; text-underline-offset:3px; }
.prj-textarea{ resize:vertical; min-height:56px; }
.prj-thumb{
  width:96px; height:96px; display:flex; align-items:center; justify-content:center; overflow:hidden;
  background:var(--panel2); border:1px solid var(--line); color:var(--dim); font-size:11px; text-decoration:none;
}
.prj-thumb img, .prj-thumb video{ width:100%; height:100%; object-fit:cover; display:block; }
button.prj-thumb{ position:relative; padding:0; font-family:inherit; cursor:zoom-in; }
.prj-thumb[data-kind="video"], .prj-thumb[data-kind="file"]{ cursor:pointer; }
.prj-play{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  font-size:26px; color:#fff; text-shadow:0 1px 6px rgba(0,0,0,0.8); pointer-events:none;
}
.prj-thumb-text{ font-size:10px; line-height:1.3; padding:4px; text-align:center; overflow-wrap:anywhere; }
/* Миниатюры в строках «Сейчас»: полосой под названием (на узком экране переносится), лишнее -
   «+N», документы - 📄 N.
   Плитка - не кнопка (строка сама кнопка), поэтому курсор и размер - здесь. */
.prj-row-media{ display:flex; align-items:center; gap:4px; margin-top:6px; flex-wrap:wrap; max-width:100%; }
.prj-row .prj-row-thumb{ width:56px; height:56px; flex:none; position:relative; cursor:zoom-in; }
.prj-row .prj-row-thumb .prj-play{ font-size:18px; }
.prj-row-more, .prj-row-docs{ flex:none; font-size:11px; color:var(--dim); padding:0 4px; white-space:nowrap; }
.prj-row-docs[hidden]{ display:none; }
/* Просмотр фото и видео поверх страницы. */
.prj-viewer{
  position:fixed; inset:0; z-index:10001; background:rgba(0,0,0,0.88);
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; padding:12px;
}
.prj-viewer-body{ flex:1; min-height:0; width:100%; display:flex; align-items:center; justify-content:center; }
.prj-viewer-body img, .prj-viewer-body video{ max-width:100%; max-height:100%; object-fit:contain; background:#000; }
.prj-viewer-note{ color:#dbe6ef; font-size:13px; text-align:center; line-height:1.6; }
.prj-viewer-bar{ display:flex; gap:10px; padding-bottom:env(safe-area-inset-bottom); }
.prj-viewer-bar .tb-btn{ text-decoration:none; }
.prj-quick-btns{ display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
.prj-quick-file{ cursor:pointer; }
.prj-field{ display:flex; gap:8px; }
.prj-field select{ flex:1; min-width:0; }
.prj-field .tb-btn{ flex-shrink:0; min-width:64px; }
.prj-row-field{ font-size:11px; color:var(--orange); overflow-wrap:anywhere; }

/* Все параметры объекта: имя шаблона из базы, под ним значения. */
.prm-list{ display:flex; flex-direction:column; }
.prm-group{ padding:7px 0; border-bottom:1px solid var(--line-soft); display:flex; flex-direction:column; gap:4px; }
.prm-name{ font-size:10px; letter-spacing:0.5px; color:var(--dim); text-transform:uppercase; }
.prm-id{ font-size:10px; color:var(--dim2); text-transform:none; letter-spacing:0; }
.prm-count{ font-size:10px; color:var(--orange); text-transform:none; letter-spacing:0; }
.prm-row{ display:flex; align-items:flex-start; gap:8px; }
.prm-val{ flex:1; min-width:0; font-size:13px; line-height:1.45; white-space:pre-wrap; overflow-wrap:anywhere; padding:3px 0; }
.prm-val.editable{ cursor:pointer; }
.prm-val.editable:hover{ color:var(--accent); }
.prm-btn{
  flex-shrink:0; width:30px; height:30px; padding:0; background:none; border:1px solid transparent;
  color:var(--dim2); font-family:inherit; font-size:16px; line-height:1; cursor:pointer;
}
.prm-btn:hover{ color:var(--red); border-color:var(--red-dim); }
.prm-link{
  background:none; border:none; padding:0; font:inherit; color:var(--accent); cursor:pointer;
  text-decoration:underline; text-decoration-color:var(--accent-dim); text-underline-offset:3px; text-align:left;
}
.prm-warn{ font-size:11px; color:var(--orange); }
/* Альбом - плиткой, а не столбиком: фото бывает десяток. */
.prm-group.media{ flex-direction:row; flex-wrap:wrap; gap:8px; }
.prm-group.media .prm-name{ width:100%; }
.prm-group.media .prm-row{ gap:2px; }
.prm-group.media .prm-val{ flex:none; padding:0; }
.prm-row.editing{ display:block; }
.prm-edit{ display:flex; flex-direction:column; gap:8px; padding:4px 0; }
.prm-edit .tb-input{ font-size:13px; padding:8px 10px; box-sizing:border-box; }
.prm-edit-btns{ display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
.prm-add{ display:flex; flex-direction:column; gap:8px; padding-top:10px; }
.prm-add > .tb-btn{ align-self:flex-start; }
.prm-add-panel{ display:flex; flex-direction:column; gap:6px; }
.prm-add-panel .tb-input{ font-size:13px; padding:8px 10px; box-sizing:border-box; }
.prm-add-list{ display:flex; flex-direction:column; max-height:320px; overflow:auto; border:1px solid var(--line-soft); }
.prm-pick{
  text-align:left; background:none; border:none; border-bottom:1px solid var(--line-soft);
  color:var(--text); font-family:inherit; font-size:12px; padding:9px 10px; cursor:pointer;
}
.prm-pick:hover{ background:var(--panel2); color:var(--accent); }
/* Основное / дополнительное название и кнопка «основным». */
.prm-tag{ font-size:9px; letter-spacing:0.5px; text-transform:uppercase; color:var(--dim2); border:1px solid var(--line); padding:1px 4px; margin-right:4px; vertical-align:middle; }
.prm-btn.prm-btn-text{ width:auto; padding:0 6px; font-size:10px; letter-spacing:0.5px; text-transform:uppercase; }
.prm-btn.prm-btn-text:hover{ color:var(--accent); border-color:var(--accent-dim); }
.prm-ref{ display:flex; flex-wrap:wrap; align-items:baseline; gap:6px; font-size:12px; padding:4px 2px; border-bottom:1px solid var(--line-soft); }

/* Подсказки: в какой вид привести. Только текст и кнопки - решает человек. */
.prj-hints{ display:flex; flex-direction:column; gap:8px; padding:10px 12px; border:1px solid var(--orange-dim); background:rgba(255,182,72,0.05); }
.prj-hint{ font-size:12px; line-height:1.45; display:flex; flex-direction:column; gap:6px; }
.prj-hint + .prj-hint{ padding-top:8px; border-top:1px solid var(--line-soft); }
.prj-hint-btns{ display:flex; flex-wrap:wrap; gap:6px; }
/* в кнопках подсказок - названия проектов: верхний регистр их искажал бы */
.prj-hint-btns .tb-btn{ text-align:left; text-transform:none; letter-spacing:0; font-size:12px; }
/* пустой Заказчик/Исполнитель у части дела: рекомендация родительского - красным */
.prj-hint-warn{ color:var(--red); }

/* «Сейчас»: фильтры по Заказчику и Исполнителю. В кнопке - имя контакта, без верхнего регистра. */
.prj-sides .tb-btn{ text-transform:none; letter-spacing:0; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.prj-side-on{ display:inline-flex; min-width:0; max-width:100%; }
.prj-side-on .tb-btn:first-child{ min-width:0; border-top-right-radius:0; border-bottom-right-radius:0; }
.prj-side-on .prj-side-x{ flex:none; border-left:none; border-top-left-radius:0; border-bottom-left-radius:0; }
.prj-side-opt{ display:flex; justify-content:space-between; align-items:baseline; gap:10px; width:100%; text-align:left;
  background:transparent; border:none; color:var(--text); font-family:inherit; font-size:inherit; }
.prj-side-opt.active{ color:var(--accent); font-weight:700; }
.prj-side-opt .prj-line{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.prj-side-note{ padding:4px 2px 8px; }
.prj-side-empty{ padding:8px; }

/* Родитель и подпроекты: выбор через поиск. */
.prj-parent{ display:flex; flex-wrap:wrap; align-items:center; gap:8px; font-size:13px; }
.prj-picker{ display:flex; flex-direction:column; gap:6px; }
.prj-picker .tb-input{ font-size:13px; padding:8px 10px; box-sizing:border-box; }
.prj-pick-list{ display:flex; flex-direction:column; max-height:360px; overflow:auto; }
.prj-pick-list:empty{ display:none; }
.prj-pick-row{
  display:flex; flex-direction:column; gap:2px; text-align:left; background:none; border:none;
  border-bottom:1px solid var(--line-soft); color:var(--text); font-family:inherit; font-size:13px; padding:9px 6px; cursor:pointer;
}
/* только с мышью: на телефоне :hover «залипает» на строке, куда пришёлся тап */
@media (hover: hover){ .prj-pick-row:hover{ background-color:var(--panel2); color:var(--accent); } }
.prj-pick-row:disabled{ opacity:0.5; cursor:default; }
.prj-kid{ display:flex; align-items:center; gap:4px; }
.prj-kid .prj-row{ flex:1; min-width:0; }

/* Окно «новый проект / контакт» (кнопка «+» в нижней плашке). */
/* Ширина - в процентах от фона окна, а не в vw: vw про общий zoom не знает. */
.prj-dialog{ width:min(720px, 100%); box-sizing:border-box; }
/* Окно выбора проекта - почти во весь экран: дереву нужно место. Высота делится
   на --ui-zoom по той же причине, что и у #dashboard; 32px - отступы фона. */
.prj-dialog-tall{
  height:calc(100vh / var(--ui-zoom) - 32px); height:calc(100dvh / var(--ui-zoom) - 32px); max-height:960px;
  display:flex; flex-direction:column; margin:0 auto;
}
.prj-dialog-tall .prj-dialog-body{ flex:1; min-height:0; }
.prj-dialog-tall .prj-tree-picker{ flex:1; min-height:0; }
.prj-dialog-tall .prj-tree{ flex:1; min-height:0; max-height:none; }
.prj-dialog-tall .prj-dialog-btns{ margin-top:10px; }
/* Дерево «папок» в окне выбора родителя. */
.prj-tree-picker{ display:flex; flex-direction:column; gap:6px; text-transform:none; letter-spacing:0; }
.prj-tree-picker .tb-input{ font-size:13px; padding:8px 10px; box-sizing:border-box; }
.prj-tree-legend{ color:var(--green); }
.prj-tree-legend:empty{ display:none; }
.prj-tree{ position:relative; border:1px solid var(--line-soft); border-radius:6px; }
.prj-dialog-field .prj-tree{ max-height:calc(40vh / var(--ui-zoom)); max-height:calc(40dvh / var(--ui-zoom)); }
.prj-tree-row{
  flex-direction:row; align-items:baseline; gap:8px;
  padding-left:calc(8px + var(--depth, 0) * 18px);
  background-image:repeating-linear-gradient(to right, var(--line) 0 1px, transparent 1px 18px);
  background-size:calc(var(--depth, 0) * 18px) 100%; background-position:8px 0; background-repeat:no-repeat;
}
.prj-tree-name{ flex:1; min-width:0; overflow-wrap:anywhere; }
.prj-tree-n{ font-size:11px; color:var(--dim); white-space:nowrap; }
.prj-tree-here{ font-size:10.5px; color:var(--dim); white-space:nowrap; }
.prj-tree-top{ color:var(--dim); }
.prj-pick-row.sugg{ background-color:color-mix(in srgb, var(--green-dim) 55%, transparent); box-shadow:inset 3px 0 0 var(--green); }
.prj-pick-row.chosen, .prj-pick-row.chosen:hover{ background-color:var(--accent-dim); color:var(--text); box-shadow:inset 3px 0 0 var(--accent); }
.prj-dialog-body{ display:flex; flex-direction:column; gap:10px; }
.prj-dialog-body label, .prj-dialog-field{ display:flex; flex-direction:column; gap:4px; font-size:10px; letter-spacing:0.5px; color:var(--dim); text-transform:uppercase; }
.prj-dialog-body .tb-input{ font-size:13px; padding:8px 10px; box-sizing:border-box; text-transform:none; letter-spacing:0; }
.prj-dialog-field .prj-parent{ text-transform:none; letter-spacing:0; color:var(--text); }
.prj-dialog-field .prj-pick-list{ max-height:200px; text-transform:none; letter-spacing:0; }
.prj-dialog-btns{ display:flex; align-items:center; justify-content:flex-end; flex-wrap:wrap; gap:8px; margin-top:14px; }
.prj-dialog-btns .prj-muted{ flex:1 1 100%; text-align:right; }
.prj-dialog-btns .prj-muted:empty{ display:none; }
.prj-quick-file.disabled{ opacity:0.5; pointer-events:none; }

/* Выделение в списке проектов: долгий тап включает, панель - над нижней плашкой,
   как у фильтра групп в «Задачах» и как в Платежах. */
#prj-list .prj-row{ -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
.prj-check{
  flex-shrink:0; width:20px; height:20px; border:1px solid var(--dim2); border-radius:3px;
  display:flex; align-items:center; justify-content:center; font-size:13px; color:var(--accent);
}
.prj-row.selected{ background:rgba(52,213,255,0.08); }
/* в выделении галочку ставит тап по квадратику - попасть в него должно быть легко */
.prj-check{ position:relative; }
.prj-check::after{ content:''; position:absolute; left:-10px; right:-10px; top:-14px; bottom:-14px; }
.prj-row.selected .prj-check{ border-color:var(--accent); }
.prj-selbar{
  position:fixed; left:0; right:0; bottom:var(--appbar-h, 58px); z-index:899;
  display:flex; flex-wrap:wrap; align-items:center; gap:8px; padding:8px 12px;
  background:var(--panel2); border-top:1px solid var(--accent-dim);
  box-shadow:0 -2px 12px rgba(0,0,0,0.25);
}
.prj-selbar-count{ font-size:12px; color:var(--accent); margin-right:auto; }
.prj-selbar .tb-btn{ padding:7px 10px; }
.prj-selbar-exit{ font-size:13px; letter-spacing:0; }
.prj-selbar-note{ flex:1 1 100%; font-size:11px; color:var(--orange); }
.prj-selbar .tb-btn:disabled{ opacity:0.45; cursor:default; }
/* пока панель видна, низ списка не должен прятаться под ней */
body:has(#tab-projects.active #prj-selbar:not(.hidden)) .panes{ padding-bottom:calc(var(--appbar-h, 58px) + 96px); }
body.split-on:has(#tab-projects.active #prj-selbar:not(.hidden)) .panes{ padding-bottom:0; }
body.split-on #projects-wrap:has(#prj-selbar:not(.hidden)){ padding-bottom:96px; }
.prj-note{ font-size:12px; color:var(--green); padding:8px 10px; border:1px solid var(--green-dim); }
/* Удаление проекта - внизу карточки, отдельно и красным: действие необратимое. */
.prj-danger{ border-top-color:var(--red-dim); }
.prj-del-btn{ color:var(--red); border-color:var(--red-dim); align-self:flex-start; }
.prj-del-btn:hover:not(:disabled){ color:var(--red); border-color:var(--red); background:rgba(255,84,112,0.08); }
.prj-del-btn:disabled{ opacity:0.4; cursor:default; }
/* Ручка перетаскивания и стрелка группы в дереве. Ручка не прокручивает страницу
   пальцем (touch-action) - иначе тянуть было бы нельзя. */
.prj-drag{
  flex-shrink:0; align-self:stretch; display:flex; align-items:center; justify-content:center;
  width:24px; margin-left:-4px; color:var(--dim2); font-size:16px; cursor:grab; touch-action:none;
}
.prj-drag:hover{ color:var(--accent); }
.prj-toggle, .prj-toggle-sp{ flex-shrink:0; width:20px; align-self:stretch; display:flex; align-items:center; justify-content:center; }
/* ручка и стрелка - вплотную, без общего зазора строки */
.prj-drag + .prj-toggle, .prj-drag + .prj-toggle-sp{ margin-left:-10px; }
.prj-toggle{ color:var(--accent); font-size:13px; cursor:pointer; }
/* Заголовок хэштега в дереве: не строка проекта - не выделяется, не принимает перетаскивание. */
.prj-tag-head{
  display:flex; align-items:baseline; gap:6px; width:100%; text-align:left; cursor:pointer;
  background:var(--panel2); border:none; border-bottom:1px solid var(--line-soft);
  color:var(--text); font-family:inherit; font-size:13px; padding:9px 6px;
}
.prj-tag-head:hover{ color:var(--accent); }
.prj-tag-arrow{ flex-shrink:0; width:14px; color:var(--accent); font-size:13px; }
.prj-tag-name{ min-width:0; overflow-wrap:anywhere; }
.prj-tag-n{ flex-shrink:0; color:var(--dim); font-size:12px; }
.prj-row.dragging{ opacity:0.4; }
.prj-row.drop-ok{ outline:2px dashed var(--accent); outline-offset:-2px; background:rgba(52,213,255,0.10); }
body.prj-dragging, body.prj-dragging *{ cursor:grabbing !important; user-select:none; }
.prj-drag-ghost{
  position:fixed; z-index:10000; pointer-events:none; max-width:260px;
  padding:6px 10px; font-size:12px; color:var(--text); background:var(--panel2);
  border:1px solid var(--accent); box-shadow:0 6px 20px rgba(0,0,0,0.45);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.prj-drop-top{
  position:fixed; top:0; left:0; right:0; z-index:9999; padding:16px 12px; text-align:center;
  font-size:12px; color:var(--accent); background:var(--panel2); border-bottom:2px dashed var(--accent-dim);
}
.prj-drop-top.over{ background:rgba(52,213,255,0.18); border-bottom-color:var(--accent); }
.prj-kpis{ display:grid; grid-template-columns:repeat(auto-fill, minmax(140px, 1fr)); gap:8px; }
.prj-kpis > div{ background:var(--panel2); border:1px solid var(--line-soft); padding:8px 10px; display:flex; flex-direction:column; gap:3px; }
.prj-kpis b{ font-size:15px; font-weight:normal; white-space:nowrap; }
.prj-kpis b.in{ color:var(--green); }
.prj-kpis b.out{ color:var(--red); }
.prj-kpis span{ font-size:10px; color:var(--dim); }
.prj-sub{ font-size:10px; letter-spacing:1px; color:var(--dim); text-transform:uppercase; margin-top:6px; }
.prj-pay{
  display:grid; grid-template-columns:auto minmax(0, 1fr) auto; gap:10px; align-items:baseline;
  font-size:12px; padding:5px 2px; border-bottom:1px solid var(--line-soft);
}
.prj-pay > span:first-child{ color:var(--dim); white-space:nowrap; }
.prj-pay-who{ overflow-wrap:anywhere; }
.prj-pay-sum{ white-space:nowrap; }
.prj-pay.in .prj-pay-sum{ color:var(--green); }
.prj-pay.out .prj-pay-sum{ color:var(--red); }
.prj-now{ font-weight:normal; color:var(--accent); }

@media (max-width: 760px){
  #projects-wrap, #contacts-wrap{ padding:12px 10px; }
  .prj-kpis{ grid-template-columns:repeat(2, minmax(0, 1fr)); }
  .prj-kpis b{ font-size:14px; }
  .prj-row{ padding:11px 2px; }
  .prj-thumb{ width:84px; height:84px; }
  .prj-row .prj-row-thumb{ width:44px; height:44px; }
  /* 14px с общим зумом 1.2 - больше 16: меньше этого Safari зумит страницу при фокусе. */
  .prj-top .tb-input, .prj-card .tb-input, .prm-edit .tb-input, .prm-add-panel .tb-input, .prj-picker .tb-input, .prj-dialog-body .tb-input{ font-size:14px; }
  /* на телефоне окно почти во весь экран */
  .prj-dialog-backdrop{ padding:8px; }
  .prj-dialog{ padding:12px; }
  .prj-dialog-tall{ height:calc(100vh / var(--ui-zoom) - 16px); height:calc(100dvh / var(--ui-zoom) - 16px); }
  .prj-tree-row{ padding-top:11px; padding-bottom:11px; }
}

/* ---------- ВКЛАДКА «ВРЕМЯ» ----------
   Стили экрана графиков из приложения «Время» (style.css), каждый под #tab-time,
   тёмные константы заменены переменными админки - светлая тема работает и здесь.
   Масштаб вкладки - 1:1, без общего зума админки: шкала считает касания и
   размеры в пикселях экрана, а шрифт - тот же, что во «Времени», под него
   рассчитана ширина колонок. */
#tab-time{
  zoom:calc(1 / var(--ui-zoom));
  --panel-2:var(--panel2); --muted:var(--dim); --danger:var(--red); --warn:var(--orange);
  font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size:13px; color:var(--text); background:var(--panel);
}
#tab-time.active{ display:flex; flex-direction:column; min-height:0; }
body.split-on #tab-time.active{ height:auto; }
/* Подпись панели при разделении экрана - общая для всех вкладок: возвращаем ей
   масштаб и шрифт админки, иначе у «Времени» она мельче соседней. */
#tab-time > .pane-title{ zoom:var(--ui-zoom); font-family:var(--mono); }
#tab-time .time-bar{
  display:flex; align-items:center; gap:8px; flex-shrink:0;
  padding:6px 10px; background:var(--panel); border-bottom:1px solid var(--line);
}
#tab-time .time-bar-title{ color:var(--dim); font-size:12px; }
#tab-time #dayPanel{ flex:1; min-height:0; overflow:auto; border-bottom:none; }
#tab-time #dayPanelBody{ max-height:none; overflow:visible; padding-bottom:16px; }
#tab-time .reply-box{ margin:6px 10px; }
/* Нижняя плашка админки лежит поверх всех вкладок: окно правки встаёт над ней
   (высоту плашки кладёт time.js в --time-appbar-h), календарь и так сверху. */
#tab-time .modal.sheet{ bottom:var(--time-appbar-h, 0px); }
/* Приглушение блоков при правке сделано затемнением - на светлой теме из блоков
   получались тёмные пятна. Здесь гасим в сторону фона. */
body.light-theme #tab-time .bd-seg.dim, body.light-theme #tab-time .bd-pause.dim{ filter:saturate(0.35) brightness(1.3); }
@keyframes time-spin { to { transform: rotate(360deg); } }
#tab-time .spinner-small {
  display: inline-block; width: 14px; height: 14px;
  border: 2px solid rgba(14,26,34,0.35);
  border-top-color: var(--panel);
  border-radius: 50%;
  animation: time-spin 0.7s linear infinite;
  vertical-align: -2px;
}
#tab-time .topbar-date { display: flex; align-items: center; gap: 5px; flex-shrink: 0; margin: 0 4px; }
#tab-time .topbar-date.warn #shiftsDateInput { border-color: var(--danger); color: var(--danger); font-weight: 700; background: var(--red-dim); }
#tab-time .topbar-seg {
  background: none; border: 1px solid var(--line); border-radius: 8px; color: var(--accent);
  font-size: 12px; padding: 4px 6px; cursor: pointer;
}
#tab-time .reply-box {
  flex-shrink: 0; margin: 6px 10px 0; padding: 8px 10px; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line); font-size: 13px;
  max-height: 30vh; overflow-y: auto; white-space: pre-wrap; position: relative;
}
#tab-time .reply-box-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 6px; }
#tab-time .reply-box-actions button { background: none; border: none; color: var(--accent); font: inherit; cursor: pointer; padding: 0; }
#tab-time .reply-box-item + .reply-box-item { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 6px; }
#tab-time #dayPanel { flex: 1; min-height: 0; overflow-y: auto; border-bottom: none; }
#tab-time .day-panel-body { max-height: none; overflow: visible; padding-bottom: 16px; }
#tab-time .day-panel { background: var(--panel); border-bottom: 1px solid var(--line); flex-shrink: 0; font-size: 13px; }
#tab-time .day-panel-body { max-height: 40vh; overflow-y: auto; padding: 2px 12px 6px; }
#tab-time .modal.sheet { align-items: flex-end; padding: 0; background: none; pointer-events: none; }
#tab-time .modal.sheet .modal-card { pointer-events: auto; box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.45); }
#tab-time /* окно всегда влезает в экран: карточка не выше экрана, #tab-time длинный список
   проектов прокручивается внутри себя, #tab-time кнопки снизу остаются на виду */
.modal.sheet .modal-card {
  border-radius: 16px 16px 0 0; max-width: 560px; padding-bottom: env(safe-area-inset-bottom, 0px);
  max-height: 38dvh; display: flex; flex-direction: column; border-top: 1px solid var(--line);
}
#tab-time .modal.sheet .modal-body { padding: 8px 12px 12px; }
#tab-time .modal.sheet .modal-header { padding: 6px 12px; }
#tab-time .modal.sheet .modal-body { flex: 1; min-height: 0; overflow-y: auto; }
#tab-time .modal.sheet h2 { font-size: 15px; }
#tab-time .sheet-btn {
  display: block; width: 100%; text-align: left; margin: 6px 0; padding: 12px;
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 10px; font-size: 16px; cursor: pointer;
}
#tab-time .sheet-btn.danger { color: var(--danger); }
#tab-time .sheet-btn.primary { background: var(--accent); color: #111; border-color: var(--accent); text-align: center; font-weight: 600; }
#tab-time /* шапка окна: закрыть слева, #tab-time "Сохранить" справа */
.sheet-header { align-items: center; gap: 8px; }
#tab-time .sheet-title { flex: 1; }
#tab-time .sheet-save {
  background: var(--accent); color: var(--panel); border: none; border-radius: 8px;
  padding: 8px 14px; font-size: 14px; font-weight: 600; cursor: pointer;
}
#tab-time .sheet-save:disabled { opacity: 0.4; }
#tab-time .sheet-btn:disabled { opacity: 0.45; }
#tab-time .sheet-actions { display: flex; gap: 8px; }
#tab-time .sheet-actions .sheet-btn { text-align: center; }
#tab-time .sheet-label { color: var(--muted); font-size: 13px; margin: 10px 0 4px; }
#tab-time .sheet-time-row { position: relative; display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 4px; }
#tab-time /* список часов/минут — вертикально над полем времени */
.time-drop {
  position: fixed; transform: translateX(-50%);
  width: 168px; z-index: 1200; display: flex; gap: 4px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 12px; padding: 4px;
}
#tab-time .time-col { flex: 1; overflow-y: auto; scrollbar-width: none; }
#tab-time .time-col::-webkit-scrollbar { width: 0; }
#tab-time .time-opt {
  display: block; width: 100%; padding: 8px; background: none; border: none; cursor: pointer;
  color: var(--text); font-size: 18px; font-variant-numeric: tabular-nums; border-radius: 8px;
}
#tab-time .time-opt.on { background: var(--accent); color: var(--panel); font-weight: 700; }
#tab-time /* Поле времени — главный элемент формы: крупнее и цветом, #tab-time чтобы не путалось
   с чипами времени над ним. .modal-body input задаёт width:100% — перебиваем. */
/* время как две кнопки: часы и минуты */
.sheet-time .time-part {
  background: none; border: none; color: inherit; font: inherit; cursor: pointer; padding: 0 2px;
}
#tab-time .sheet-time .time-colon { opacity: 0.7; }
#tab-time .modal-body .sheet-time, #tab-time .modal-body input.sheet-time {
  font-size: 24px; font-weight: 700; letter-spacing: 1px; padding: 6px 12px;
  background: var(--panel-2); color: var(--orange);
  border: 2px solid var(--orange); border-radius: 10px;
  flex: none; text-align: center; font-variant-numeric: tabular-nums;
  display: inline-flex; align-items: center;
}
#tab-time .modal-body input.sheet-time:focus { outline: none; box-shadow: 0 0 0 3px rgba(232, 163, 62, 0.25); }
#tab-time .sheet-rel { color: var(--muted); font-size: 13px; }
#tab-time /* Оба ряда чипов времени (точные времена над полем и сдвиги под ним) —
   строго одна строка с прокруткой вбок. */
.sheet-chips.sheet-time-chips, #tab-time .sheet-chips.sheet-nudge {
  display: flex; flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch; max-width: 100%;
}
#tab-time /* Полосы прокрутки у рядов чипов не показываем: они съедают высоту и мелькают */
.sheet-chips, #tab-time .tla-btns, #tab-time .bd, #tab-time .day-panel { scrollbar-width: none; }
#tab-time .sheet-chips::-webkit-scrollbar, #tab-time .tla-btns::-webkit-scrollbar { width: 0; height: 0; }
#tab-time .sheet-nudge { margin-top: 6px; }
#tab-time .sheet-time-chips .chip, #tab-time .sheet-nudge .chip, #tab-time .sheet-proj-chips .chip { flex: 0 0 auto; white-space: nowrap; font-size: 14px; padding: 6px 10px; }
#tab-time .sheet-chips { display: flex; flex-wrap: wrap; gap: 6px; }
#tab-time .sheet-chips .chip { font-size: 16px; }
#tab-time .sheet-emp-chips { margin-bottom: 4px; flex-wrap: nowrap; overflow-x: auto; }
#tab-time .sheet-chips.sheet-time-chips { margin-bottom: 6px; padding-bottom: 2px; }
#tab-time /* переключатель СТОП / ПРОЕКТ */
.seg-switch { display: flex; gap: 0; margin: 10px 0 6px; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
#tab-time .seg-switch .seg {
  flex: 1; padding: 7px; background: var(--panel-2); color: var(--muted);
  border: none; font: inherit; font-weight: 600; cursor: pointer;
}
#tab-time .seg-switch .seg.seg-on { background: var(--accent); color: var(--panel); }
#tab-time .seg-switch .seg.stop.seg-on { background: var(--danger); color: #1a0f0c; }
#tab-time .sheet-search {
  width: 100%; box-sizing: border-box; margin-bottom: 6px; padding: 8px 10px; font-size: 16px;
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 10px;
}
#tab-time /* Чипы проектов — тоже одна строка с прокруткой вбок (класс задан вместе с
   .sheet-chips, #tab-time поэтому селектор из двух классов: иначе перенос переопределял) */
.sheet-chips.sheet-proj-chips { flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; }
#tab-time .chip.chip-new { border-color: var(--accent); color: var(--accent); }
#tab-time .chip.chip-stop { border-color: var(--danger); color: var(--danger); }
#tab-time .chip.chip-stop.chip-selected { background: var(--danger); color: #fff; }
#tab-time .sheet-preview { margin: 12px 0 6px; font-size: 14px; line-height: 1.5; }
#tab-time .sheet-preview.err { color: var(--danger); }
#tab-time .sheet-preview:empty { margin: 0; }
#tab-time .dp-empty, #tab-time .dp-warn { color: var(--muted); padding: 6px 0; }
#tab-time /* сотрудник без записей за день — приглушён, #tab-time чтобы работавшие выделялись */
.bd-head.empty { color: var(--muted); font-weight: 500; }
#tab-time .dp-warn { color: var(--danger); }
#tab-time #shiftsDateInput {
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 8px; color: var(--text); padding: 4px 6px; font-size: 13px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
#tab-time .shifts-unclosed-warn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--red); color: #fff; font-weight: 700; font-size: 13px; line-height: 1;
  flex-shrink: 0;
}
#tab-time .shifts-unclosed-warn.short { background: var(--orange); }
#tab-time /* Прижат к верху, #tab-time а не по центру: когда закрывается клавиатура (тап по
   кнопке из поля причины), #tab-time центрированное окно съезжало бы между нажатием
   и отпусканием пальца, #tab-time и тап попадал бы в соседнюю кнопку или другой день. */
.cal-backdrop {
  position: fixed; inset: 0; z-index: 1000;
  background: rgba(0, 0, 0, 0.55);
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: calc(12px + env(safe-area-inset-top, 0px));
}
#tab-time .cal-box {
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 12px; width: min(320px, calc(100vw - 24px));
}
#tab-time .cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; font-weight: 600; }
#tab-time .cal-nav {
  background: var(--panel-2); border: none; color: var(--text);
  width: 34px; height: 34px; border-radius: 8px; font-size: 20px; cursor: pointer;
}
#tab-time .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
#tab-time .cal-dow span { text-align: center; font-size: 11px; opacity: 0.6; padding-bottom: 2px; }
#tab-time .cal-day {
  aspect-ratio: 1; border: none; border-radius: 8px; cursor: pointer;
  background: var(--panel-2); color: var(--text); font-size: 14px;
}
#tab-time .cal-day.unclosed { background: var(--red); color: #fff; font-weight: 600; }
#tab-time .cal-day.short { background: var(--orange); color: #fff; font-weight: 600; }
#tab-time .cal-day.acked { box-shadow: inset 0 0 0 2px var(--green); }
#tab-time .cal-day.today { box-shadow: inset 0 0 0 1px var(--accent); }
#tab-time .cal-day.selected { box-shadow: inset 0 0 0 2px var(--accent); font-weight: 700; }
#tab-time .cal-day.detail { outline: 2px solid var(--text); outline-offset: -2px; }
#tab-time .cal-legend { margin-top: 8px; font-size: 12px; opacity: 0.8; display: flex; flex-wrap: wrap; gap: 4px 12px; }
#tab-time .cal-legend span { display: inline-flex; align-items: center; gap: 6px; }
#tab-time .cal-legend i { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
#tab-time .cal-legend .lg-unclosed { background: var(--red); }
#tab-time .cal-legend .lg-short { background: var(--orange); }
#tab-time .cal-legend .lg-acked { box-shadow: inset 0 0 0 2px var(--green); }
#tab-time .cal-box { max-height: calc(100dvh - 24px); overflow-y: auto; }
#tab-time .cal-detail { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
#tab-time .cal-detail-title { font-weight: 600; }
#tab-time .cal-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
#tab-time .cal-row span { flex: 1; min-width: 0; }
#tab-time .cal-act {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 8px; padding: 6px 10px; font-size: 12px; cursor: pointer; flex-shrink: 0;
}
#tab-time .cal-act:disabled { opacity: 0.5; }
#tab-time .cal-act.wide { width: 100%; padding: 8px; }
#tab-time .cal-act.primary { background: var(--accent); color: var(--panel); border-color: var(--accent); font-weight: 600; }
#tab-time .cal-reason {
  width: 100%; box-sizing: border-box; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 8px; color: var(--text); padding: 6px 8px; font-size: 13px;
}
#tab-time .cal-err { color: var(--danger); font-size: 12px; }
#tab-time .cal-err:empty { display: none; }
#tab-time .chip {
  flex-shrink: 0;
  background: var(--panel-2); border: 1.5px solid var(--line); color: var(--text);
  border-radius: 21px; padding: 4px 10px; font-size: 19px; cursor: pointer; white-space: nowrap;
  transition: background .12s, border-color .12s, transform .08s;
}
#tab-time .chip:active { transform: scale(0.96); }
#tab-time .chip.chip-now { border-color: var(--accent); color: var(--accent); font-weight: 600; }
#tab-time .chip.chip-selected {
  background: var(--accent); color: var(--panel); border-color: var(--accent); font-weight: 700;
  box-shadow: 0 0 0 2px rgba(46,166,160,0.35);
}
#tab-time .modal {
  position: fixed; inset: 0; background: rgba(0,0,0,0.5);
  display: flex; align-items: center; justify-content: center;
  z-index: 50; padding: 16px;
}
#tab-time .modal-card {
  background: var(--panel); border-radius: 16px;
  width: 100%; max-width: 460px; max-height: 82vh;
  display: flex; flex-direction: column; overflow: hidden;
}
#tab-time .modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--line);
}
#tab-time .modal-header h2 { margin: 0; font-size: 16px; }
#tab-time .modal-header button { background: none; border: none; color: var(--muted); font-size: 18px; cursor: pointer; }
#tab-time .modal-body { padding: 14px 16px; overflow-y: auto; font-size: 14px; line-height: 1.6; }
#tab-time .modal-body label { display: block; margin: 12px 0 4px; font-size: 13px; color: var(--muted); }
#tab-time .modal-body input, #tab-time .modal-body textarea {
  width: 100%; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 8px; color: var(--text); padding: 8px 10px; font-size: 14px; font-family: inherit;
}
#tab-time .bd { --bd-axis: 44px; padding-bottom: 8px; width: max-content; min-width: 100%; }
#tab-time #dayPanel { overflow: auto; }
#tab-time .bd-heads {
  display: grid; grid-template-columns: var(--bd-axis) var(--bd-grid);
  position: sticky; top: 0; z-index: 3; background: var(--panel); padding: 4px 0;
  border-bottom: 1px solid var(--line);
}
#tab-time .bd-head { font-weight: 600; font-size: 13px; line-height: 15px; padding: 0 6px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#tab-time .bd-head b { display: block; color: var(--muted); font-weight: 500; font-size: 12px; }
#tab-time .bd-body { position: relative; margin-top: 8px; }
#tab-time /* отступ — чтобы первая метка не пряталась под шапкой */
/* тонкие линии часов через всю ширину, #tab-time получасы — ещё бледнее */
.bd-line { position: absolute; left: 0; border-top: 1px solid var(--line); }
#tab-time .bd-line.half { border-top-style: dotted; border-top-color: var(--line-soft); }
#tab-time .bd-line span {
  position: absolute; left: 0; top: -9px; height: 18px; width: calc(var(--bd-axis) - 6px);
  display: flex; align-items: center; justify-content: flex-end;
  font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; background: var(--panel); padding-right: 2px;
}
#tab-time .bd-line.draft { border-top: 2px dashed var(--accent); z-index: 2; }
#tab-time .bd-line.draft span { color: var(--accent); font-weight: 700; }
#tab-time .bd-now { position: absolute; left: var(--bd-axis); right: 0; border-top: 2px solid var(--accent); z-index: 1; }
#tab-time .bd-now span {
  position: absolute; left: calc(var(--bd-axis) * -1); top: -8px; width: calc(var(--bd-axis) - 6px);
  text-align: right; font-size: 11px; font-weight: 700; color: var(--accent);
  background: var(--panel); padding-right: 2px;
}
#tab-time .bd-cols {
  position: absolute; top: 0; bottom: 0; left: var(--bd-axis); right: 0;
  display: grid; grid-template-columns: var(--bd-grid);
}
#tab-time .bd-col { position: relative; }
#tab-time /* блоки — сплошная заливка цветом проекта, #tab-time без рамок; тёмный текст читается
   на всех цветах палитры */
.bd-seg {
  position: absolute; left: 3px; right: 3px; border-radius: 5px; overflow: hidden; cursor: pointer; z-index: 1;
  background: var(--tl-color);
  padding: 1px 5px; font-size: 11px; line-height: 13px; color: rgba(0, 0, 0, 0.85);
  display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center;
}
#tab-time /* выделенный блок и его метки времени */
.bd-seg.sel { outline: 2px solid var(--text); outline-offset: -2px; }
#tab-time /* во время правки линии и подписи приглушаем вместе с блоками */
.bd-line.dim { border-top-color: var(--line-soft); }
#tab-time .bd-line.dim span { opacity: 0.4; }
#tab-time .bd-line.hot { border-top-color: var(--text); border-top-width: 2px; }
#tab-time /* участок до колонки: линия обычная, #tab-time подпись — белая и жирная */
.bd-line.hot-label { border-top-color: var(--text); border-top-width: 2px; }
#tab-time .bd-line.hot-label span, #tab-time .bd-line.hot span { color: var(--text) !important; font-weight: 700; }
#tab-time .bd-line.hot span { color: var(--text); font-weight: 700; top: -17px; height: 34px; }
#tab-time .bd-line.hot-label span { top: -17px; height: 34px; }
#tab-time /* черновик новой записи: пока не выбран проект, #tab-time он только на экране */
.bd-draft-grip {
  position: absolute; left: 0; right: 0; top: -6px; height: 14px;
  border-top: 3px solid var(--accent); border-radius: 3px;
}
#tab-time /* Гасим яркостью, #tab-time а не прозрачностью: при opacity сквозь блок просвечивали
   линии времени, #tab-time которые должны оставаться под ним. */
.bd-seg.dim, #tab-time .bd-pause.dim { filter: brightness(0.45) saturate(0.75); }
#tab-time /* черновик всегда тёмный, #tab-time цвет проекта — в рамке и подписи */
.bd-draft.colored { border-color: var(--tl-color); color: var(--tl-color); }
#tab-time .bd-draft {
  position: absolute; left: 3px; right: 3px; z-index: 1; border-radius: 5px;
  border: 2px dashed var(--accent); color: var(--accent); font-size: 10px; line-height: 11px;
  display: flex; align-items: center; justify-content: center; text-align: center;
  padding: 0 3px; overflow: hidden; background: var(--bg);
}
#tab-time /* панель действий над выделенным блоком — внизу экрана */
.tl-actions {
  flex-shrink: 0; background: var(--panel); border-top: 1px solid var(--line);
  padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px));
  display: flex; flex-direction: column; gap: 6px;
}
#tab-time .tla-title { font-size: 12px; color: var(--muted); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#tab-time .tla-btns { display: flex; gap: 6px; overflow-x: auto; }
#tab-time .tla-btn {
  flex: 0 0 auto; white-space: nowrap; background: var(--panel-2); color: var(--text);
  border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; font-size: 14px; cursor: pointer;
}
#tab-time .tla-btn.danger { color: var(--danger); }
#tab-time /* окно во время сохранения */
.modal-card.sheet-busy { position: relative; pointer-events: none; }
#tab-time .modal-card.sheet-busy::after {
  content: ""; position: absolute; inset: 0; background: rgba(14, 26, 34, 0.45);
  border-radius: inherit;
}
#tab-time .bd-projedit { text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
#tab-time .bd-edit {
  display: inline-flex; align-items: center; justify-content: flex-end;
  min-height: 34px; padding-right: 4px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
}
#tab-time .bd-seg.jt { border-top-left-radius: 0; border-top-right-radius: 0; }
#tab-time .bd-seg.jb { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
#tab-time /* пауза — слабо-серая заливка без рамок, #tab-time стыкуется с отрезками без зазора */
.bd-pause {
  position: absolute; left: 3px; right: 3px;
  /* Штриховка поверх непрозрачного фона: линии времени сквозь паузу не видны,
     и она не путается ни с блоками работы, ни с сеткой. */
  background-color: var(--panel);
  /* штриховка и боковые границы — один и тот же сплошной цвет, без прозрачности */
  background-image: repeating-linear-gradient(45deg, var(--line) 0 5px, transparent 5px 10px);
  border-left: 5px solid var(--line); border-right: 5px solid var(--line);
  color: var(--muted); font-size: 11px; line-height: 12px;
  padding: 0 5px; overflow: hidden; box-sizing: border-box; text-align: center;
  display: flex; align-items: center; justify-content: center;
}
#tab-time .bd-pause.tl-tap { cursor: pointer; }
#tab-time .bd-add.stop { color: var(--danger); }
#tab-time .bd-add.tall { height: 40px; font-size: 20px; }
#tab-time /* завершить / сменить проект — крупная цель для пальца */
/* Лаконичный "+" без рамки и фона. На цветных блоках (штриховка стопа) —
   с лёгкой подложкой, #tab-time иначе не читается. */
.bd-add {
  position: absolute; left: 3px; right: 3px; height: 20px; z-index: 2; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--dim); font-size: 18px; border-radius: 5px;
  background: var(--panel);
}
#tab-time /* пустое место над первым и под последним блоком — вся область тапается */
.bd-zone {
  position: absolute; left: 3px; right: 3px; z-index: 1; cursor: pointer;
  display: flex; justify-content: center;
  color: var(--dim); font-size: 20px; line-height: 1; /* незаметный серый "+" */
}
#tab-time .bd-zone.top { align-items: flex-end; padding-bottom: 2px; }
#tab-time .bd-zone.bottom { align-items: flex-start; padding-top: 2px; }
#tab-time .bd-zone:active { background: rgba(46, 166, 160, 0.10); border-radius: 6px; }
#tab-time .bd-seg.open { background: linear-gradient(to bottom, var(--tl-color), transparent); }
#tab-time .bd-seg.pending {
  background: repeating-linear-gradient(135deg, var(--danger) 0 4px, transparent 4px 7px);
  color: var(--danger); font-weight: 700; text-align: center; padding: 0;
}
#tab-time .bd-name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-weight: 600; max-width: 100%; }
#tab-time .bd-seg.tiny { padding: 0 3px; font-size: 9px; line-height: 9px; }
#tab-time .bd-dur { opacity: 0.85; }
/* Окно «Слить проекты». */
.prj-merge{ gap:14px; }
.prj-merge-sec{ display:flex; flex-direction:column; gap:6px; }
.prj-merge-title{ font-size:10px; letter-spacing:0.5px; color:var(--dim); text-transform:uppercase; }
.prj-dialog-body label.prj-merge-opt{ flex-direction:row; align-items:flex-start; gap:8px; font-size:13px; color:var(--text); text-transform:none; letter-spacing:0; cursor:pointer; }
.prj-merge-opt input[type=radio], .prj-merge-opt input[type=checkbox]{ margin-top:3px; flex-shrink:0; accent-color:var(--accent); }
.prj-merge-opt > span{ display:flex; flex-direction:column; gap:2px; min-width:0; overflow-wrap:anywhere; }
.prj-merge-opt .tb-input{ margin-top:4px; }
.prj-merge-name{ font-size:13px; }
.prj-dialog-body label.prj-merge-chk{ color:var(--dim); font-size:12px; }
.prj-merge-sum{ margin:0; padding-left:18px; display:flex; flex-direction:column; gap:4px; font-size:12.5px; color:var(--text); text-transform:none; letter-spacing:0; }
.prj-merge-block{ font-size:12px; color:var(--red); }