/* Полная пересборка по Home Redesign.dc.html (хендофф Claude Design, авг. 2026).
   Ничего не унаследовано из старого дизайна — каждое правило сверено с
   конкретной строкой прототипа. Бэкенд/функциональная JS-логика не менялись,
   меняется только эта разметка/стили и DOM-рендеринг в app.js. */

/* Urbanist — локально, НЕ с fonts.googleapis.com. Раньше в index.html висел
   <link> на Google Fonts, а это рендер-блокирующий CSS: в России без VPN домен
   не открывается, запрос висит до таймаута, и первая отрисовка страницы
   откладывается на секунды. За это время нативная шторка Telegram успевает
   развернуться, поверхность webview рассинхронизируется — и нижние блоки
   (meals-card, nav-pill) не отрисовываются. Отсюда и подсказка пользователя:
   БЕЗ VPN баг был всегда, С VPN — изредка (гонка). Локальные файлы убирают
   и блокировку, и саму внешнюю зависимость.
   Файлов два, а не по одному на вес: Onest вариативный, один файл покрывает
   весь диапазон 100-900 — отсюда font-weight: 100 900. Второй файл не
   «расширенная латиница», а КИРИЛЛИЦА: ради неё и меняли Urbanist, у которого
   её нет вовсе, из-за чего русский текст рисовался системным шрифтом, а
   цифры — Urbanist'ом. Две гарнитуры на экране никто не выбирал.

   ВЕСА НИЖЕ ПО ФАЙЛУ — те же 500/600/700, что были в прототипе на Urbanist,
   без сдвига. Сдвигали вниз (20.08.2026, до 400/500/600) ради совпадения
   толщины штриха с Urbanist — формально получилось точно, но пользователю
   результат показался тонким: ему нравилась именно бо́льшая плотность.
   Тот же числовой вес у разных шрифтов даёт разную физическую толщину штриха
   (замер стойки буквы «I» на 1000 ед. кегля: Urbanist@600=104, Onest@600=125) —
   Onest на тех же цифрах прототипа сам по себе плотнее Urbanist, и в этот раз
   это то, что нужно. Меняете шрифт ещё раз — сверьтесь с пользователем, что
   именно должно остаться неизменным: буквальные значения веса или ощущаемая
   плотность, — это не одно и то же. */
@font-face {
  font-family: 'Onest';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/onest-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Onest';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/onest-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* Интерлиньяж тремя ступенями. Раньше он был задан в 13 правилах из 112, а
     у body стоял normal — то есть высоту строки определял шрифт, а не мы.
     Замер: у Onest normal = 1.27 на всех кеглях, поэтому база 1.3 почти не
     сдвигает вёрстку. Но normal зависит от шрифта: сменим гарнитуру — поедет
     весь вертикальный ритм, ровно как сегодня поехала плотность из-за весов. */
  /* Разрядка капса по кеглю: чем мельче, тем больше — иначе прописные слипаются.
     Правило в стилях уже угадывалось, но соблюдалось через раз: на 11px стояли
     сразу 0.04em, 0.07em и 0.08em на одной и той же роли. */
  --track-caps-sm: 0.08em;   /* до 10.5px */
  --track-caps-md: 0.07em;   /* 11–11.5px */
  --track-caps-lg: 0.06em;   /* от 12px */
  /* ШКАЛА КЕГЛЕЙ — семь ступеней, других значений в файле быть не должно:
       10  микроподписи, оси графиков
       11  капсовые подписи блоков
       13  вторичный текст, значения в списках
       15  основной текст, кнопки
       17  заголовки экранов и разделов
       20  крупные значения статистики
       28  акцентные числа второго плана (вода, донат, вес)
       38  главный счётчик калорий
     Было двадцать кеглей от 9 до 38 с шагом в полпикселя — не шкала, а пандус:
     один смысловой уровень «вторичный текст» жил сразу на пяти кеглях, а
     половинные значения не ложатся в пиксельную сетку на экранах 1x. */
  --lh-tight: 1.05;   /* крупные числа: цифрам не нужен воздух над и под */
  --lh-ui: 1.3;       /* база: подписи, кнопки, короткие строки интерфейса */
  --lh-text: 1.5;     /* сплошной текст: инсайты, описания, будущие тренировки */
  /* Поля экрана. Было 24 — с ним содержимое занимало 87% ширины, стало 89%.
     Ниже 20 не опускаем: карточки со скруглением подходят вплотную к скруглению
     самого экрана, и это читается как ошибка вёрстки, а не как плотная вёрстка.
     Панель навигации берёт этот же токен, поэтому выравнивание с карточками
     держится само при любом значении. */
  --card-pad-x: 20px;
  --modal-pad-x: 18px;
  /* Шеврон «дальше» — Iconly Bold. Одной переменной, чтобы путь не
     расползался копиями по файлу. */
  --chevron-right: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M16.5725 12c0 .2926-.112.5851-.3348.808l-6.8569 6.8569c-.4469.4468-1.1691.4468-1.616 0-.4468-.4469-.4468-1.1691 0-1.616L13.8137 12 7.7648 5.9511c-.4468-.4469-.4468-1.1691 0-1.616.4469-.4468 1.1691-.4468 1.616 0l6.8569 6.8569c.2228.2229.3348.5154.3348.808z'/%3E%3C/svg%3E");
  --nav-item: clamp(35px, 9.8vw, 44px);
  --fab-size: clamp(42px, calc(9.5vw + 8px), 54px);
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);

  /* БЕЗОПАСНЫЕ ЗОНЫ. Внутри Telegram env(safe-area-inset-*) отдаёт НУЛИ — это
     их давняя недоработка (Telegram-iOS#1377), и в обычном режиме она не
     мешала: вебвью и так сидит ниже шапки клиента. В полноэкранном режиме
     мешает сразу — сверху оказываются и вырез устройства, и кнопки Telegram
     «закрыть» и «...».
     Считаем из ДВУХ величин клиента: --tg-safe-area-inset-* это зона самого
     устройства, --tg-content-safe-area-inset-* — сколько поверх неё занимает
     интерфейс Telegram. Их надо складывать, а не выбирать одну.
     max с env() оставляет правильное поведение вне Telegram: там env работает,
     а переменных клиента нет. В обычном режиме обе величины нулевые, поэтому
     правило одно на оба режима — отдельной ветки не нужно. */
  --safe-top: max(env(safe-area-inset-top),
    calc(var(--tg-safe-area-inset-top, 0px) + var(--tg-content-safe-area-inset-top, 0px)));
  --safe-bottom: max(env(safe-area-inset-bottom),
    calc(var(--tg-safe-area-inset-bottom, 0px) + var(--tg-content-safe-area-inset-bottom, 0px)));
  --safe-left: max(env(safe-area-inset-left), var(--tg-safe-area-inset-left, 0px));
  --safe-right: max(env(safe-area-inset-right), var(--tg-safe-area-inset-right, 0px));

  /* Свой отступ НАД содержимым, поверх безопасной зоны. В обычном режиме над
     карточкой стоит шапка Telegram, и запас в 26px отделяет от неё содержимое.
     В полноэкранном режиме этот запас уже входит в --tg-content-safe-area-inset-top
     (клиент сообщает, сколько занимают его кнопки), и наши 26px ложатся вторым
     слоем — отсюда ощущение лишнего места. Оставляем 8: зона клиента кончается
     ровно по краю кнопок, и вплотную к ним содержимое прижимать не стоит.
     Величины вырезов НЕ трогаем: их сообщает клиент, и только поэтому оно
     работает на любом телефоне с любой чёлкой — что на iPhone, что на Android. */
  --top-gap: 26px;
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);

  /* Мелкое зерно против бандинга: размытие даёт очень пологие переходы, а
     8 бит на канал на них дают видимые ступеньки. Зерно их разбивает.
     Идёт первым слоем в --blur-bg, т.е. поверх фона.

     Частота и число октав ЗАМЕРЕНЫ на устройстве 07.09.2026 (iPhone 17 Pro,
     3x), а не подобраны: на снимке экрана считали три величины — среднюю
     разницу соседних пикселей (это зерно), самый длинный участок с одинаковым
     значением по вертикали (это ступенька) и среднюю яркость.

       0.85 / 3 октавы / 0.05 (было)   зерно 0.615   ступень 9px   ярк. 34.0
       2.5  / 1 октава  / 0.05 (стало) зерно 0.597   ступень 6px   ярк. 34.4
       2.5  / 1 октава  / 0.09         зерно 0.734   ступень 6px   ярк. 37.6
       без зерна                       зерно 0.355   ступень 11px  ярк. 32.2

     Три октавы добавляли низкочастотную составляющую — крупные пятна, которые
     видно как зернистость, — а мелкой, которая и разбивает ступеньки, не
     хватало: 9px против 11px без зерна, то есть зерно почти не работало.
     Одна октава на 2.5 убирает ступеньку до 6px и при этом сама заметна
     МЕНЬШЕ. Поднимать непрозрачность дальше смысла нет: ступенька ниже 6px не
     опускается, а яркость и зерно растут.

     Подъём яркости от зерна учтён в тонировке клавиатуры (app.js, поправка
     [2,4,2]); смена зерна сдвинула его на 0.4 из 255 — в пределах погрешности
     того замера, трогать не нужно. */
  --bg-base: #14190d;
  --bg-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='2.5' numOctaves='1' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");

  /* Ночь — дефолт :root (line 909-924 прототипа).

     Фон-подложка — ровно то, что в хендоффе: градиенты из README + blur(46px).
     Но в прототипе блюр давал CSS-фильтр на отдельном слое
     (`filter:blur(46px)` у div на line 49 `Home Redesign.dc.html`), а нам
     фильтр нельзя: фон приходится рисовать ещё и внутри скролл-контейнера
     (см. .scroll-y.fade-* ниже), а фильтр там не воспроизвести — получается
     шов. Поэтому блюр вынесен внутрь картинки: SVG с feGaussianBlur
     stdDeviation='46' (CSS blur(<len>) по спеке = гауссиана с σ=<len>).

     Это по-прежнему обычный background-image — композитного слоя не создаёт и
     побайтово одинаково рисуется в обоих местах. Размывается только группа
     пятен: линейная подложка — гладкий градиент, блюр её не меняет, зато так
     не нужно раздувать SVG ради краёв (в прототипе ту же роль играли
     inset:-60px и scale(1.05)).

     Геометрия пересчитана в скрипте: радиусы эллипсов = размер градиента ×
     позиция стопа `transparent NN%`, концы linear — из угла по формуле CSS.
     viewBox 400x800 + preserveAspectRatio='none' → растягивается по вьюпорту,
     блюр масштабируется вместе с ним. */
  --blur-bg: var(--bg-noise),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 800' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='l' gradientUnits='userSpaceOnUse' x1='48.0' y1='-17.5' x2='352.0' y2='817.5'%3E%3Cstop offset='0' stop-color='%230f1609'/%3E%3Cstop offset='.45' stop-color='%231c2810'/%3E%3Cstop offset='1' stop-color='%230a0d05'/%3E%3C/linearGradient%3E%3CradialGradient id='r0'%3E%3Cstop offset='0' stop-color='%232d5a14' stop-opacity='0.55'/%3E%3Cstop offset='1' stop-color='%232d5a14' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='r1'%3E%3Cstop offset='0' stop-color='%23000000' stop-opacity='0.68'/%3E%3Cstop offset='1' stop-color='%23000000' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='r2'%3E%3Cstop offset='0' stop-color='%23000000' stop-opacity='0.75'/%3E%3Cstop offset='1' stop-color='%23000000' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='r3'%3E%3Cstop offset='0' stop-color='%2383ec1a' stop-opacity='0.4'/%3E%3Cstop offset='1' stop-color='%2383ec1a' stop-opacity='0'/%3E%3C/radialGradient%3E%3Cfilter id='b' x='-40%25' y='-40%25' width='180%25' height='180%25' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='46'/%3E%3C/filter%3E%3C/defs%3E%3Crect width='400' height='800' fill='url(%23l)'/%3E%3Cg filter='url(%23b)'%3E%3Cellipse cx='328' cy='624' rx='169' ry='286' fill='url(%23r0)'/%3E%3Cellipse cx='200' cy='528' rx='115' ry='115' fill='url(%23r1)'/%3E%3Cellipse cx='200' cy='256' rx='115' ry='138' fill='url(%23r2)'/%3E%3Cellipse cx='88' cy='96' rx='132' ry='216' fill='url(%23r3)'/%3E%3C/g%3E%3C/svg%3E");
  --text-primary: rgba(255, 255, 255, 0.97);
  --text-secondary: rgba(255, 255, 255, 0.68);
  --text-tertiary: rgba(255, 255, 255, 0.42);
  --glass-bg: linear-gradient(160deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.05));
  --glass-border: rgba(255, 255, 255, 0.26);
  --chip-bg: rgba(255, 255, 255, 0.14);
  --input-bg: rgba(10, 16, 4, 0.32);
  --track-color: rgba(255, 255, 255, 0.3);
  --dot-color: #c8f24a;
  --fab-bg: linear-gradient(135deg, #c8f24a, #8fbf2c);
  --fab-icon: #16210a;
  /* Не плоская заливка: бегунок активной вкладки — такая же стеклянная
     поверхность, как карточки, и светится сверху по тому же 160deg, что
     и --glass-bg. Плоский прямоугольник выдавал в нём чужеродную деталь. */
  --nav-active-pill: linear-gradient(160deg, rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0.10));
  --modal-bg: #1a2410;
  --on-accent: #16210a;
  /* Недобор воды. Не красный: не дотянуть до нормы — это промах, а не
     ошибка. Янтарный отличим от лаймового --dot-color и в обеих темах. */
  --miss-color: #e8a33d;
  --macro-fat: #e8c468;
  --macro-carbs: #7ecfc0;
}

:root[data-theme="day"] {
  --miss-color: #f0b661;
  /* Средний тон линейной подложки дневной темы (#93aa9a -> #7b9280 -> #5f7865).
     Виден только пока не растрирован SVG-фон; с тёмным #14190d в этот момент
     мелькала бы чёрная заливка. */
  --bg-base: #7b9280;
  /* Тот же приём, что и в ночной теме выше: градиенты хендоффа + blur(46px),
     запечённые в SVG. */
  --blur-bg: var(--bg-noise),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 800' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='l' gradientUnits='userSpaceOnUse' x1='11.1' y1='-5.2' x2='388.9' y2='805.2'%3E%3Cstop offset='0' stop-color='%2393aa9a'/%3E%3Cstop offset='.45' stop-color='%237b9280'/%3E%3Cstop offset='1' stop-color='%235f7865'/%3E%3C/linearGradient%3E%3CradialGradient id='r0'%3E%3Cstop offset='0' stop-color='%23000000' stop-opacity='0.53'/%3E%3Cstop offset='1' stop-color='%23000000' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='r1'%3E%3Cstop offset='0' stop-color='%23000000' stop-opacity='0.6'/%3E%3Cstop offset='1' stop-color='%23000000' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='r2'%3E%3Cstop offset='0' stop-color='%2393aa9a' stop-opacity='0.35'/%3E%3Cstop offset='1' stop-color='%2393aa9a' stop-opacity='0'/%3E%3C/radialGradient%3E%3Cfilter id='b' x='-40%25' y='-40%25' width='180%25' height='180%25' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='46'/%3E%3C/filter%3E%3C/defs%3E%3Crect width='400' height='800' fill='url(%23l)'/%3E%3Cg filter='url(%23b)'%3E%3Cellipse cx='200' cy='528' rx='115' ry='115' fill='url(%23r0)'/%3E%3Cellipse cx='200' cy='256' rx='115' ry='138' fill='url(%23r1)'/%3E%3Cellipse cx='60' cy='64' rx='99' ry='154' fill='url(%23r2)'/%3E%3C/g%3E%3C/svg%3E");
  --text-primary: rgba(255, 255, 255, 0.98);
  --text-secondary: rgba(255, 255, 255, 0.75);
  --text-tertiary: rgba(255, 255, 255, 0.5);
  --glass-bg: linear-gradient(160deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.07));
  --glass-border: rgba(255, 255, 255, 0.32);
  --chip-bg: rgba(255, 255, 255, 0.18);
  --input-bg: rgba(0, 0, 0, 0.22);
  --track-color: rgba(255, 255, 255, 0.35);
  --dot-color: #f2f4ee;
  --fab-bg: #eef1ea;
  --fab-icon: #3a4a3c;
  --nav-active-pill: linear-gradient(160deg, rgba(255, 255, 255, 0.30), rgba(255, 255, 255, 0.13));
  --modal-bg: #5f7865;
}

@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes backdropIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes modalPopIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes navBounce { 0% { transform: scale(1); } 45% { transform: scale(1.32); } 100% { transform: scale(1); } }

@media (prefers-reduced-motion: reduce) {
  .sheet-anim, .backdrop-anim, .modal-pop { animation: none !important; }
  /* Подсветку вкладки оставляем видимой (это состояние, а не украшение), но
     без переезда и без подскока иконки. */
  .nav-indicator.positioned, .mode-indicator { transition: opacity 0.2s ease; }
  .nav-item.active svg { animation: none; }
}



button { -webkit-appearance: none; appearance: none; font-family: inherit; }

/* Высота: НЕ 100dvh, а собственная переменная Telegram. На мобильных клиентах
   мини-аппа живёт в нативном BottomSheet (выдвижная шторка с анимацией
   разворачивания), про который CSS-юнит dvh ничего не знает и разрешается в
   высоту БОЛЬШЕ реально отрисованной области. Всё, что оказалось ниже
   фактической границы (у нас это meals-card и nav-pill — последние по потоку),
   в DOM есть и кликается, но не нарисовано. Это официальный баг клиента:
   github.com/TelegramMessenger/Telegram-iOS/issues/1548 («на первой загрузке
   видна только часть webview, при этом весь контент под ней интерактивен»).
   Проверено 2026-08-14: в обычном Safari по тому же адресу бага нет вообще —
   только внутри Telegram, и только на мобильном (на десктопе шторки нет).
   --tg-viewport-stable-height — высота последнего УСТОЙЧИВОГО состояния, т.е.
   без дёрганья во время анимации шторки и при открытии клавиатуры (так же
   делает официальный бойлерплейт Telegram). Фолбэк 100dvh — для обычных
   браузеров, где переменной нет.
   Пробовали и откатили (2026-08-14, по ощущениям стало хуже): промежуточный
   фолбэк на --tg-viewport-height (текущая высота вместо устойчивой) и отложенный
   через двойной rAF вызов tg.expand() в app.js. */
html, body {
  /* Найдено разбором 20.08.2026: одна и та же вёрстка выглядела заметно жирнее
     в настольном Safari/Telegram на macOS 26.5.2, чем в мобильном Safari-движке
     iOS 18.7 — не кеш (замерено логами ngrok: мак честно скачивал ту же свежую
     версию style.css) и не Telegram-специфика (воспроизвелось в чистом Safari
     мимо приложения). Причина — старое различие рендеринга шрифтов между
     платформами: macOS исторически включает субпиксельное сглаживание, из-за
     которого штрихи выглядят толще на тех же весах, iOS всегда использовал
     серое сглаживание. Без явного antialiased macOS утяжеляет весь текст —
     ровно на ту разницу, что мы и наблюдали между вебом и айфоном. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: var(--lh-ui);
  height: 100vh;
  height: 100dvh;
  height: var(--tg-viewport-stable-height, 100dvh);
  font-family: 'Onest', -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif;
  /* Фон здесь НЕ задаём: он живёт на html (ниже) и оттуда распространяется на
     канву. Если покрасить ещё и body, его непрозрачная заливка ляжет поверх
     градиента канвы и вернёт тёмную полосу, ради которой всё и затевалось. */
  touch-action: manipulation;
  /* Гасим родную подсветку нажатия WebKit — тёмный прямоугольник поверх кнопки,
     который на стеклянных карточках выглядит как артефакт. Свойство
     наследуется, поэтому одного объявления на корне хватает на всё приложение.
     Обратную связь на нажатие даёт transform: scale() у :active — она из
     хендоффа и остаётся. */
  -webkit-tap-highlight-color: transparent;
}

/* S1 (2026-08-15). overflow: hidden НЕ на body — только на контейнере
   приложения (.page/.card ниже). Это прямая рекомендация из разбора багов
   рендеринга Safari 26: «body { overflow: hidden } часто делает только хуже
   для полноэкранных приложений — держите документ скроллируемым, а блокировку
   жестов вешайте на контейнер приложения». У нас было ровно то, что там
   названо антипаттерном.
   Документ при этом на 1px выше вьюпорта (приём комьюнити мини-аппов) — так
   скролл-слой webview с самого старта в нормальном состоянии, а не ждёт жеста
   пользователя. Контент не разъезжается: .page и .card держат overflow: hidden
   и высоту во вьюпорт, поэтому скроллиться нечему, кроме этого пикселя. */
html { height: calc(var(--tg-viewport-stable-height, 100dvh) + 1px); }
/* Тот самый лишний пиксель делает документ прокручиваемым — а значит, iOS даёт
   его резиново оттянуть в обе стороны. Под открытой шторкой это и видно как
   «фон едет вверх-вниз и пружинит обратно»: тянется не элемент внутри
   страницы, а сам документ. Разбор 29.08.2026: touch-action на подложке и
   disableVerticalSwipes у клиента Telegram этот случай не закрывают — оба про
   другое. Гасит его только запрет чейнинга/баунса на КОРНЕ; сам пиксель
   остаётся на месте, вместе с тем, ради чего он заведён. */
html, body { overscroll-behavior: none; }

/* Тот же фон, что рисует .card::after, продублирован на канве.
   Зачем: когда клавиатура уходит, Telegram возвращает webview прежнюю высоту
   одним шагом, и на пару кадров контейнер уже вырос, а градиентный слой ещё
   не перерисовался — на видео в этот момент внизу мелькает тёмная полоса с
   нав-пиллом (это проглядывает плоский background-color).
   Фон корневого элемента распространяется на канву и рисуется на весь вьюпорт
   независимо от высоты html, поэтому подложка есть всегда и полоса не видна.
   Геометрия — как у .card::after и .scroll-y.fade-*, иначе будет стык. */
html {
  background-color: var(--bg-base);
  background-image: var(--blur-bg);
  background-attachment: fixed;
  /* Высота градиента ЗАМОРОЖЕНА (--bg-h, ставит app.js) и намеренно не следует
     за вьюпортом. SVG растягивается preserveAspectRatio='none', поэтому любая
     смена высоты пересчитывает всю картинку: при выезде клавиатуры окно
     укорачивается на ~40%, пятна свечения съезжают вверх, при уходе — обратно.
     Это и есть «гармошка»/«лесенка» на фоне (разбор 05.09.2026 по кадрам
     видео). Клавиатура высоту только уменьшает, поэтому --bg-h держит
     наибольшую виденную: при короткой раскладке картинка просто обрезается
     снизу, как в нативном экране, и не двигается.
     ПРОБОВАЛИ в тот же день и откатили: 100dvh -> --tg-viewport-stable-height.
     Мимо — переменная Telegram при клавиатуре меняется точно так же (замер:
     824 -> 479 за 82мс), менялась мера, а не факт пересчёта. */
  background-size: auto, 100vw var(--bg-h, 100dvh);
  background-repeat: repeat, no-repeat;
}

.page {
  position: relative;
  height: 100vh;
  height: 100dvh;
  height: var(--tg-viewport-stable-height, 100dvh);
  /* Потолок ширины по самому широкому телефону (iPhone Pro Max, 430). Без него
     на планшете и в десктопном Telegram колонка растягивается на всё окно, а
     графика и текст расходятся: дуга тянется за карточкой, кегли фиксированные.
     Замер на окне 1024 — высота дуги в кеглях счётчика: телефон 390 → 3.63
     (как задумано), потолок 430 → 3.92, без потолка → 11.31. Ни один телефон
     потолком не затронут (430 ≤ 430). Фон-свечение живёт на html и остаётся
     во всю ширину окна, навигация в обычном потоке и едет за колонкой. */
  max-width: 430px;
  margin-inline: auto;
  display: flex;
  overflow: hidden;
  /* Пробовали и откатили (2026-08-14): отступы через переменные Telegram
     (--tg-safe-area-inset-* + --tg-content-safe-area-inset-*) вместо env() —
     на баг с невидимыми нижними блоками не повлияло. */
  /* Снизу безопасную зону НЕ учитываем (03.10.2026): внизу у главного экрана
     только пилюля навигации, и она по образцу владельца стоит в 20px от края
     окна, заходя в зону полоски «домой», как плавающий таб-бар нативных
     приложений. Telegram сообщает эту зону нулём, MAX - честными 34px, и с
     ней пилюля в MAX стояла почти вдвое выше. Оверлеи считают свою зону сами. */
  padding-block: var(--safe-top) 0;
  padding-inline: var(--safe-left) var(--safe-right);
}

/* Дуо-мокап прототипа (390x800, "рамка телефона" на веб-странице для демонстрации) —
   не часть самого экрана; здесь экран занимает весь вьюпорт. */
.card {
  position: relative;
  isolation: isolate;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  /* Низ: пилюля навигации ровно в 20px от края окна в Telegram и в MAX (см.
     .page - безопасная зона снизу намеренно не прибавляется). */
  padding: var(--top-gap) var(--card-pad-x) 20px;
  overflow: hidden;
  color: var(--text-primary);
  transition: color 0.4s ease;
}

/* Фон экрана. Блюр из хендоффа на месте, но живёт ВНУТРИ картинки (SVG
   feGaussianBlur, см. --blur-bg), а не в CSS-свойстве filter. Два повода:
   1) скролл-контейнер держит КОПИЮ этого фона (см. .scroll-y.fade-* ниже), и
      копия обязана совпадать пиксель в пиксель, иначе на границе контейнера
      виден стык; filter в копии не воспроизвести;
   2) filter создал бы композитный слой, а они у нижней границы экрана в
      мини-аппе iOS не отрисовываются (тот самый баг с невидимыми блоками).
   inset:-60px/scale(1.05) из прототипа не нужны — они прятали растушёванные
   края фильтра, у SVG эту роль играет область фильтра. */
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* Размеры заданы явно и повторяют .scroll-y.fade-* один в один: SVG-подложка
     имеет пропорцию, но не имеет собственного размера, и при background-size:
     auto легла бы как contain — с полями. */
  background-image: var(--blur-bg);
  background-attachment: fixed;
  background-size: auto, 100vw var(--bg-h, 100dvh);
  background-repeat: repeat, no-repeat;
  transition: background 0.4s ease;
}

/* Не из хендоффа (там нет табов реального приложения, каждый экран открывается
   отдельно) — обёртка вкладок нужна, чтобы .view лежали в одной grid-ячейке
   и не складывались друг под другом по высоте (тот же приём, что и у
   .stats-panels ниже, просто на уровень выше — для Главной/Статистики/
   Меню/Профиля). */
.views-wrap { flex: 1; min-height: 0; display: grid; }
.view { grid-area: 1 / 1; min-height: 0; }
.panel-hidden { visibility: hidden; pointer-events: none; }

.scroll-y {
  overflow-y: auto;
  overscroll-behavior: contain;
  margin-inline: calc(-1 * var(--card-pad-x));
  padding-inline: var(--card-pad-x);
}

/* Копия фона страницы ВНУТРИ скролл-контейнера — снимает конфликт маски и
   backdrop-filter (2026-08-15).
   Суть конфликта: маска (см. .fade-top/.fade-bottom ниже) отправляет содержимое
   контейнера в отдельный буфер, и карточкам с backdrop-filter внутри нечего
   сэмплить — за ними в этом буфере пусто, отсюда тусклый цвет до тех пор, пока
   что-нибудь не заставит буфер пересобраться (растягивание окна).
   Решение: даём контейнеру собственный фон — тот же --blur-bg. Теперь он лежит
   ВНУТРИ буфера позади карточек, и им снова есть что размывать.
   background-attachment: fixed — чтобы градиент разворачивался по вьюпорту, как
   у .card::after, а не по коробке контейнера: иначе он был бы сжат по-другому и
   на краю фейда стал бы виден стык с настоящим фоном. Маска гасит эту копию
   вместе с контентом, а под ней оказывается тот же градиент — поэтому переход
   остаётся незаметным. */
.scroll-y.fade-top,
.scroll-y.fade-bottom {
  /* Свойства разнесены намеренно: --blur-bg это несколько слоёв, и в сокращённой
     записи `var(--blur-bg) fixed` привязка досталась бы только последнему.
     Геометрия обязана совпадать с .card::after (теперь это ровно вьюпорт),
     иначе на границе контейнера виден стык с настоящим фоном — проверено.
     Первый слой (шум) повторяется в своём натуральном размере, остальные
     растягиваются на вьюпорт. */
  background-image: var(--blur-bg);
  background-attachment: fixed;
  background-size: auto, 100vw var(--bg-h, 100dvh);
  background-repeat: repeat, no-repeat;
}

/* Затухание у краёв скролл-контейнера — реализация из предыдущего дизайна,
   она же самая удачная из перепробованных.
   Тускнеет САМ КОНТЕНТ через mask, то есть прозрачностью, а не краской.
   Накладку градиентом мы пробовали дважды (и в старом дизайне, и в этой
   сессии) — она всегда мимо: фон под ней вертикальный градиент, поэтому
   «цвет края» не совпадает с тем, что реально лежит снизу, и ночью получался
   тёмный прямоугольник, а днём накладка была практически не видна. С маской
   любая тема и любая точка градиента выглядят правильно сами собой.
   Одна маска на оба края, глубина каждого — своя переменная: классы вешает JS
   независимо (сверху — когда есть прокрученное вверх, снизу — когда есть
   непрокрученное вниз), и у края, которого нет, глубина остаётся 0.
   ВНИМАНИЕ на будущее: маска заставляет отрисовывать содержимое контейнера в
   отдельный буфер, и в промежуточной версии этой сессии это ломало цвет у
   карточек с backdrop-filter внутри (проявлялось при изменении высоты
   вьюпорта — растягивание шторки, появление клавиатуры). Если такое вернётся,
   первый подозреваемый — именно эта маска. */
.fade-top,
.fade-bottom {
  --fade-top: 0px;
  --fade-bottom: 0px;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent, #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)), transparent);
  mask-image: linear-gradient(to bottom,
    transparent, #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)), transparent);
}

.fade-top { --fade-top: 28px; }
.fade-bottom { --fade-bottom: 28px; }

.view-home-scroll-wrap { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#view-home-scroll { flex: 1; min-height: 0; }

/* Главная: день-навигация вынесена из скролл-контейнера отдельным блоком
   (не sticky внутри него) — иначе маска затухания гасила бы саму
   закреплённую "Сегодня" при скролле вниз, ровно когда она нужнее всего
   видимой. */
#view-home { display: flex; flex-direction: column; min-height: 0; }
/* То же самое для Статистики. Без этого #view-stats оставался блочным, .stats-
   panels-wrap разрастался под контент (631px внутри 482px), панель «Сводки» не
   получала ограниченной высоты — и потому НЕ скроллилась, а раз нет прокрутки,
   не включался и фейд краёв (он вешается по факту переполнения). */
#view-stats { display: flex; flex-direction: column; min-height: 0; }

/* Радиус снижен с 22/18px до 12/10px (2026-08-15). Рекомендация для мобильных —
   8-16px: чем больше радиус, тем дороже слой для GPU и тем вероятнее, что он не
   отрисуется (наш баг с невидимыми нижними блоками в мини-аппе Telegram на iOS).
   Визуально потеря близка к нулю: за карточками лежит плавный градиент
   (.card::after), а размытие плавного градиента на 12px и на 22px даёт почти
   одинаковый результат — резких деталей, которые размытие могло бы «смазать»,
   там просто нет. */
.glass {
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: blur(12px) saturate(160%);
  -webkit-backdrop-filter: blur(12px) saturate(160%);
}

/* Мелкие элементы (пиллы, диапазон дат, строки настроек) — радиус ещё меньше. */
.pill.glass,
.range-trigger.glass,
.settings-row.glass,
.profile-block-sm.glass,
.food-sheet-block.glass,
.insight-ai-block.glass {
  backdrop-filter: blur(10px) saturate(160%);
  -webkit-backdrop-filter: blur(10px) saturate(160%);
}

/* Причина бага «карточка/нав-пилл невидимы, но кликабельны» на iOS 26
   (найдено 2026-08-14 замерами в браузере): скрытые вкладки остаются в DOM
   через visibility:hidden (не display:none — их размеры нужны, чтобы графики
   отрисовывались заранее, до перехода на вкладку), но WebKit НЕ снимает их
   backdrop-filter с композитора (bugs.webkit.org/show_bug.cgi?id=149318).
   Замер: на Главной реально видимы 6 .glass-элементов, а blur-слоёв WebKit
   держит 29 — впятеро больше нужного. На этом объёме GPU иногда не может
   выделить текстуру слоя при загрузке, и слой не рисуется вовсе (элемент при
   этом остаётся кликабельным, а его вычисленные стили корректны — проверено
   через Web Inspector на устройстве). Пока панель не видна, блюр ей не нужен;
   при снятии .panel-hidden он возвращается сам.
   ВАЖНО: правило должно идти ПОСЛЕ обоих блоков blur выше — у .settings-row
   .glass и т.п. та же специфичность, выигрывает то, что объявлено позже. */
.panel-hidden .glass,
.panel-hidden.glass {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}


.glass-shadow {
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

/* ===== Заголовок экрана (Статистика/Рацион питания/Профиль) — line 166,432,496 ===== */
.screen-title { font-size: 20px; font-weight: 700; margin-bottom: 14px; }
.screen-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.screen-title-row .screen-title { margin-bottom: 0; }

/* ===== День-навигация (line 58-62) ===== */
/* Стрелки стоят вплотную к дате, а не разъезжаются по краям. Это отход от
   прототипа: там justify-content: space-between и зазор 119px до слова.
   По просьбе пользователя 27.08.2026 — разнесённые по краям стрелки читаются
   как два одиноких значка, не связанных с датой между ними.
   Зазор 4px, а не больше: кнопки 38x38 со значком по центру, поэтому между
   самим значком и словом визуально остаётся около 16px. */
.day-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

/* Зона нажатия расширена до 44x44 невидимым слоем, а сама кнопка осталась
   38x38. Почему не увеличить кнопку: значок в ней стоит по центру, и рост до 44
   развёл бы стрелки от даты на 3px в каждую сторону — ровно против того, ради
   чего их сюда придвинули. Apple просит минимум 44x44, и после сближения со
   словом это стало важнее: соседние цели ближе, промахнуться легче.
   Псевдоэлемент наследует обработку нажатий от кнопки, отдельный обработчик не
   нужен. Внешне не меняется ничего. */
.day-arrow {
  position: relative;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: none;
  background: transparent;
  /* Яркость правилась трижды: 0.4 -> 0.6 -> 0.85 по opacity, и всё равно было
     тускло. Причина в перемножении: --text-tertiary сам по себе rgba с alpha
     0.42-0.5, opacity накладывалась сверху, и итог выходил ~0.36 в дневной
     теме. Теперь яркость задаётся ОДНИМ токеном без множителя — стрелка
     сравнялась по весу с подписью дня, которой она управляет, и дальнейшие
     правки предсказуемы. */
  color: var(--text-secondary);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  transition: opacity 0.2s ease, transform 0.15s ease;
}
.day-arrow::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.day-arrow:active { transform: scale(0.88); }
.day-arrow:disabled { opacity: 0.2; cursor: default; }

.day-label {
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track-caps-lg);
  cursor: pointer;
  transition: transform 120ms var(--ease-out);
}
.day-label:active { transform: scale(0.95); }

/* ===== Главная: пиллы стрик/вес (line 64-70) ===== */
/* Без отступа сверху: пиллы идут сразу под переключателем даты. Тот стоит на
   той же строке, что заголовки остальных вкладок (замер 10.09.2026: у всех
   четырёх верх на 26px), поэтому лишний зазор опускал содержимое Главной ниже
   всех прочих экранов без всякой на то причины. */
.home-pills { display: flex; align-items: center; gap: 10px; }
.pill {
  display: flex;
  align-items: center;
  padding: 6px 10px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  /* Не --text-secondary: на стекле пиллы читались тускло. --text-primary даёт
     ту же белизну, что и остальной основной текст, в обеих темах. */
  color: var(--text-primary);
}
#streak-badge { gap: 5px; }
#weight-pill { margin-left: auto; }
/* Огонёк — опознавательный значок пилла, а не значение (тот же довод, что у
   капли на карточке воды). Рисуется через currentColor, поэтому без этого
   правила он белел бы вместе с числом. */
#streak-badge svg { color: var(--text-secondary); }
/* «кг» — единица при значении, её место на той же ступени, что «/ 140» у БЖУ
   (.kcal-macro-of), а не на ступени самого веса. */
.pill-unit { margin-left: 4px; color: var(--text-secondary); }
/* Пилл веса — кнопка, ведёт на вкладку «Вес» в Статистике. Отклик на нажатие
   по конвенции хендоффа: scale(0.95) + transform 120ms var(--ease-out).
   font/font-size здесь НЕ задаём: шорткат `font: inherit` сбрасывает
   font-size из .pill на унаследованные 16px, и пилл становится выше соседнего
   стрика. Семейство шрифта кнопкам и так задано глобально (button {...}).
   border тоже НЕ трогаем: рамку даёт .glass, а `border: none` здесь перебивал
   её по специфичности (id сильнее класса) — пилл оставался без рамки. Нативная
   рамка кнопки и так не появляется: .glass задаёт свою.
   line-height тоже не нужен. Он был добавлен, чтобы компенсировать 2px разницы
   со стриком, но разницу давала как раз убранная рамка: без неё пилл был 27+0,
   у стрика 27+2. С вернувшейся рамкой оба ровно 29 при `normal`.
   text-align возвращаем принудительно: браузер сбрасывает его у кнопок.
   Раньше здесь же возвращался text-shadow — тень текста ушла из интерфейса
   целиком 23.08.2026, наследовать больше нечего. */
#weight-pill {
  cursor: pointer;
  text-align: inherit;
  transition: transform 120ms var(--ease-out);
}
#weight-pill:active { transform: scale(0.95); }

/* ===== Карточка "Калории" — дуга (line 72-113) ===== */
.kcal-card {
  margin-top: 18px;
  border-radius: 28px;
  padding: 20px 20px 22px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
/* Молния — опознавательная иконка карточки, не акцент: см. правило над
   .water-card-head. Цвет задан на шапке, чтобы иконка шла в вес подписи;
   кнопка перехода имеет свой --text-primary и остаётся яркой. */
.kcal-card-head { display: flex; align-items: center; gap: 6px; color: var(--text-secondary); }
/* Было опробовано: капсовые подписи на две ступени легче чисел (иерархия
   «обстановка отступает, содержание выходит вперёд»). На практике вместе с
   ростом общего веса шрифта это дало подписям заметно тонкий вид — пользователь
   попросил вернуть как было. Сейчас подписи на одной ступени с обычным
   текстом (--track-caps-* достаточно, чтобы роль подписи читалась и без
   лишней разницы в весе). */
.kcal-card-title {
  margin-right: auto;
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: var(--track-caps-md); color: var(--text-secondary);
}
.kcal-card-link {
  border: 1px solid var(--glass-border);
  cursor: pointer; width: 28px; height: 28px; border-radius: 50%;
  background: var(--chip-bg); color: var(--text-primary);
  display: flex; align-items: center; justify-content: center;
  transition: transform 120ms var(--ease-out);
}
.kcal-card-link:active { transform: scale(0.9); }

.kcal-arc-wrap { position: relative; margin-top: -6px; }
/* overflow:visible — точка на дуге (cx/cy) в концах дуги (0% и 100%) садится
   ровно на нижнюю границу viewBox (cy=100, high=8+92=100), дефолтный клиппинг
   SVG обрезает половину глоу-круга (r=9). В прототипе это не проявлялось —
   демо-данные (eaten=1450/2200≈66%) никогда не давали точке дойти до края. */
.kcal-arc { width: 100%; display: block; overflow: visible; }
.kcal-arc-track { fill: none; stroke: var(--track-color); stroke-width: 2.5; }
.kcal-arc-fill {
  fill: none; stroke: var(--dot-color); stroke-width: 2.5;
  stroke-dasharray: 0 100; opacity: 0.75;
  transition: stroke-dasharray 1.1s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Home Redesign.dc.html:84-85 — точка НЕ едет по cx/cy (это была моя добавка
   сверх хендоффа: transition на cx/cy — линейная интерполяция по прямой, не
   по дуге, плюс без начальных cx/cy circle стоит в (0,0) — дуга "прилетала"
   из угла экрана на первой отрисовке). В референсе позиция всегда мгновенная,
   анимируется только opacity (проявление после отрисовки дуги, 0.4s ease
   0.9s delay). */
/* По просьбе пользователя точка теперь едет по дуге вместе с заливкой
   (JS rAF-цикл в renderKcalArc, не CSS-transition — см. app.js), поэтому
   задержка на opacity больше не нужна: точка видна с самого начала
   движения, а не проявляется после того как дуга уже дорисовалась. */
.kcal-arc-dot-glow, .kcal-arc-dot {
  fill: var(--dot-color);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.kcal-arc-center {
  position: absolute; left: 0; right: 0; top: 48%;
  display: flex; flex-direction: column; align-items: center;
}
.kcal-arc-status { font-size: 11px; font-weight: 600; color: var(--text-secondary); min-height: 1em; }
.kcal-arc-value {
  font-size: 38px; font-weight: 700; letter-spacing: -0.02em;
  line-height: var(--lh-tight); margin-top: 2px;
  /* ТАБЛИЧНЫХ ЦИФР В ПРОЕКТЕ НЕТ — и это проверенное решение, а не упущение.
     Ставились ради «дрожания счётчика на 50px», прошли путь 14 мест -> 5 -> 0.
     Замер, который всё решил: tabular выравнивает ширину ОДНОЙ ЦИФРЫ, но не
     число цифр. На равной разрядности он работает идеально (1111/8888/1450:
     разброс 26.3px -> 0), но у нас нигде нет чисел фиксированной длины —
     калории 0…2517, вода 0…2500, проценты 0…100. На разной разрядности он
     делает ХУЖЕ, потому что каждая цифра становится шире:
         вода:      разброс 49.7 -> 54.9
         донат:     44.3 -> 54.9
         показания: 3.4 -> 8.8
         подписи:   16.4 -> 20.2
     Плюс цена: у Onest глиф one.tf это тот же рисунок в широкой ячейке (пусто
     по бокам 391 единица против 138 у восьмёрки) — отсюда дырки вокруг единиц,
     которые и заметил пользователь. Возвращать имеет смысл только если появится
     место с числами гарантированно одинаковой длины. */
}
.kcal-arc-goal { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-top: 2px; }
/* Отступ сверху разводит подписи шкалы с точкой на дуге. Концы дуги лежат
   ровно на нижней границе viewBox, поэтому при 0% и 100% точка (r=4.5) и её
   глоу (r=9) свисают НИЖЕ svg — на экране это 6 и 12.5px. При прежних -2px
   подпись начиналась выше нижнего края дуги, и достигнутая цель закрывалась
   собственным маркером: замер дал перекрытие и по горизонтали, и по вертикали.
   Развести их по горизонтали нельзя — подпись цели и так прижата к краю
   карточки. */
/* Подписи шкалы привязаны к КОНЦАМ ДУГИ, а не к краям блока.
   Зачем вообще разводить: концы дуги лежат ровно на нижней границе viewBox,
   поэтому при 0% и 100% точка (r=4.5) и её глоу (r=9) свисают ниже svg и
   накрывают собственную подпись — замер при достигнутой цели дал точку
   295–308 против подписи с 303. Вниз уводить нельзя, вырастет карточка;
   уводим вбок, в её собственные поля (по 20px) — габариты не меняются.
   Почему не flex со space-between: подписи прижимались к краям блока, а они
   разной ширины («0» против «2517»), и зазор до дуги выходил разный — слева
   31px, справа 15. Дуга занимает 10%..90% ширины (viewBox 0 200, концы в
   x=20 и x=180), поэтому привязка к 90% даёт равные зазоры по построению и не
   зависит ни от длины числа, ни от ширины карточки. */
.kcal-arc-axis {
  position: relative; height: 13px; margin-top: -2px;
  font-size: 11px; font-weight: 500; line-height: 13px; color: var(--text-tertiary);
}
.kcal-arc-axis span { position: absolute; white-space: nowrap; }
.kcal-arc-axis span:first-child { right: 90%; margin-right: 15px; }
.kcal-arc-axis span:last-child { left: 90%; margin-left: 15px; }

.kcal-macros-row { display: flex; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--glass-border); }
/* Разделители между колонками — псевдоэлементы фиксированной высоты (не
   border на всю колонку): border тянулся до самого низа, включая полосу
   БЖУ, и визуально утыкался в неё. bottom:10px — высота зоны полосы
   (margin-top:7px + high:3px), разделитель останавливается перед ней.
   Паддинг 0 8px — одинаковый у всех трёх колонок (не только у средней),
   иначе расстояние от текста до разделителя было разным по обе стороны. */
/* min-width:0 — переопределяет дефолт браузера у флекс-элементов
   (min-width:auto фактически равен min-content сам по себе, даже без
   явного указания), из-за которого колонка с более длинной подписью
   ("Углеводы") раздвигалась шире остальных на узких экранах. В хендоффе
   min-width вообще не упомянут (line 98/103/108: просто flex:1), но и
   дефолтное поведение браузера туда же не годится — нужен именно 0. */
.kcal-macro { flex: 1; min-width: 0; text-align: center; padding: 0 8px; }
.kcal-macro-mid { position: relative; }
.kcal-macro-mid::before, .kcal-macro-mid::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 10px;
  width: 1px;
  background: var(--glass-border);
}
.kcal-macro-mid::before { left: 0; }
.kcal-macro-mid::after { right: 0; }
/* Цвет подписи задан здесь, а не токеном --text-tertiary: осветление нужно
   только в ДНЕВНОЙ теме, где белый на светлой карточке даёт 1.74:1. Ночью тот
   же токен даёт 3.67:1 и в осветлении не нуждается — правка через токен
   задрала бы и её. Замер: 0.5 -> 0.85 поднимает контраст 1.74 -> 2.40 (+38%).
   Потолок здесь 2.74 (чисто белый) — порог 4.5 на светлом фоне белым текстом
   недостижим в принципе, но 0.85 забирает больше половины доступного запаса.
   Иерархию это не ломает: значение «82 / 140» рядом даёт всего 2.69 — число и
   подпись на светлой теме и так почти неразличимы по контрасту, их разводят
   кегль, капс и разрядка. */
.kcal-macro-label {
  font-size: 10px; font-weight: 600; text-transform: uppercase;
  letter-spacing: var(--track-caps-sm); color: rgba(255, 255, 255, 0.5);
}
:root[data-theme="day"] .kcal-macro-label { color: rgba(255, 255, 255, 0.85); }
.kcal-macro-value { font-size: 15px; font-weight: 700; margin-top: 5px; }
.kcal-macro-of { font-size: 13px; font-weight: 500; color: var(--text-secondary); }

/* Тонкий прогресс-бар — общий для мини-баров БЖУ на Главной, ВСЕ одним
   цветом (dotColor), без разбивки по цвету на этом экране (line 101-111). */
.progress-track, .macro-bar-track {
  height: 10px; border-radius: 999px; background: var(--track-color); overflow: hidden;
}
.macro-bar-track { height: 3px; margin-top: 7px; }
/* Тайминг заливки — из хендоффа: бар воды, `Home Redesign.dc.html:126`
   (`transition:width 0.4s ease`). Это единственное значение для полосок в
   прототипе; у остальных баров там transition нет вовсе, потому что прототип
   статичный и заполнять им нечего. Держим одно значение на все заливки. */
.progress-fill, .macro-bar-fill {
  height: 100%; width: 0%; border-radius: 999px; background: var(--dot-color);
  transition: width 0.4s ease;
}

/* ===== Карточка "Трекер воды" (line 116-134) ===== */
.water-card { margin-top: 12px; border-radius: 22px; padding: 16px 18px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); }
/* Капля — опознавательная иконка карточки, а не значение. Акцентный
   --dot-color в этом приложении означает данные и прогресс: точка на дуге,
   заливки полос, сегменты бублика, столбцы, активный чип. На карточке воды
   он законно занят заливкой прогресса, и вторая лаймовая деталь рядом сбивала
   это правило. Шапка карточки калорий устроена так же и акцента не несёт. */
.water-card-head { display: flex; align-items: center; gap: 6px; color: var(--text-secondary); }
.water-card-title { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: var(--track-caps-md); color: var(--text-secondary); }
.water-amount-row { display: flex; align-items: baseline; gap: 6px; margin-top: 8px; }
.water-amount { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; line-height: var(--lh-tight); }
.water-of { font-size: 13px; font-weight: 500; color: var(--text-secondary); }
.water-card .progress-track { margin-top: 12px; height: 6px; }
.water-buttons { display: flex; gap: 8px; margin-top: 12px; }
.chip-btn.water-add-btn { flex: 1; text-align: center; padding: 10px 0; border-radius: 14px; font-size: 13px; opacity: 1; }
.water-custom-row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.water-custom-row[hidden] { display: none; }
.water-custom-row .text-input { flex: 1; min-width: 44px; width: auto; padding: 10px 4px; text-align: center; }
.water-custom-row .chip-btn { padding: 8px 9px; font-size: 11px; }
/* Нативный спиннер числового инпута убран — вместо него свои круглые
   стрелки по бокам (тот же стиль, что у day-arrow на Главной). */
.water-custom-row .text-input { -moz-appearance: textfield; }
.water-custom-row .text-input::-webkit-inner-spin-button,
.water-custom-row .text-input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.water-step {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--text-tertiary);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: opacity 0.2s ease, transform 0.15s ease;
}
.water-step:active { transform: scale(0.88); }

/* ===== Карточка "Приёмы пищи" (line 136-158) ===== */
.meals-card {
  display: flex; flex-direction: column; gap: 10px; margin-top: 12px; margin-bottom: 24px;
  border-radius: 22px; padding: 13px 18px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
/* В свёрнутом виде .meals-list имеет max-height: 0, но остаётся flex-элементом,
   и gap: 10px продолжает действовать — снизу от заголовка получалось 23px
   против 13px сверху. В прототипе тот же перекос (там та же связка gap + max-height),
   но это явный недосмотр, а не приём. Гасим gap, пока карточка свёрнута. */
.meals-card:not(.expanded) { gap: 0; }
.meals-toggle {
  display: flex; align-items: center; justify-content: space-between; border: none;
  background: transparent; padding: 0; cursor: pointer; font: inherit; color: inherit;
  text-align: left; transition: transform 120ms var(--ease-out);
}
.meals-toggle:active { transform: scale(0.98); }
.meals-toggle-title { font-size: 15px; font-weight: 700; }
.meals-toggle-summary { font-size: 11px; font-weight: 500; color: var(--text-secondary); margin-top: 2px; }
/* Здесь НЕТ transform. Любой transform (даже тождественный rotate(0deg))
   создаёт отдельный композитный слой, а такие слои у нижней границы экрана не
   отрисовываются в мини-аппе Telegram на iOS — та же причина, что у .nav-pill
   и .meals-card выше. Проявлялось в два захода: сначала стрелка пропадала
   через раз (из-за базового rotate(0deg)), потом — исчезала при разворачивании
   списка (из-за rotate(90deg)).
   Поэтому вместо поворота одной иконки — подмена глифа: две готовые стрелки
   переключаются через display, композитных слоёв не создаётся вообще.
   Цена: пропала плавная анимация поворота (без transform её не сделать).
   Переключение мгновенное, на иконке 12px это не читается. */
.meals-chevron {
  width: 32px; height: 32px; border-radius: 50%; background: var(--chip-bg);
  border: 1px solid var(--glass-border); display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.meals-chevron-open { display: none; }
.meals-card.expanded .meals-chevron-closed { display: none; }
.meals-card.expanded .meals-chevron-open { display: block; }
/* Раскрытие через grid-template-rows, а не max-height. Прежние max-height: 160px
   означали «показать сколько влезет, остальное прокруткой внутри карточки» — на
   семи блюдах пользователь видел три и не догадывался, что список скроллится.
   Дать max-height с запасом (999px) нельзя: переход бежит до ЗАДАННОГО потолка,
   и на коротком списке анимация схлопывается в мгновенный рывок.
   0fr → 1fr доезжает ровно до высоты содержимого, сколько бы блюд ни было.
   min-height: 0 обязателен — без него grid-элемент не сжимается до нуля. */
.meals-list-wrap {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows 0.35s ease;
}
.meals-card.expanded .meals-list-wrap { grid-template-rows: 1fr; }
.meals-list { overflow: hidden; min-height: 0; opacity: 0; transition: opacity 0.3s ease; }
.meals-card.expanded .meals-list { opacity: 1; }
/* .meal-row теперь кнопка (тап открывает правку позиции), поэтому сбрасываем
   то, что браузер задаёт кнопкам: рамку, фон, выравнивание текста и шрифт. */
.meal-row {
  display: flex; align-items: center; padding: 9px 0; width: 100%;
  border: none; border-top: 1px solid var(--glass-border);
  background: transparent; color: inherit; font: inherit; text-align: left;
  cursor: pointer;
  transition: transform 120ms var(--ease-out);
}
/* 0.98, как у остальных строк-списков (.settings-row, .diary-entry-row) —
   конвенция хендоффа для строк, кнопки жмутся до 0.95. */
.meal-row:active { transform: scale(0.98); }
.meal-row:first-child { border-top: none; }
.meal-photo { width: 36px; height: 36px; border-radius: 10px; object-fit: cover; margin-right: 10px; flex-shrink: 0; }
.meal-info { flex: 1; min-width: 0; }
.meal-name { font-size: 13px; font-weight: 600; }
/* Модель на фото возвращает названия строчными, на текст — с заглавной. Ровняем
   на выводе. Именно ::first-letter, а не text-transform: capitalize — тот поднял
   бы каждое слово и сделал «Пицца 4 Сыра». */
.meal-name::first-letter,
.diary-entry-name::first-letter { text-transform: uppercase; }
.meal-macros { font-size: 11px; font-weight: 500; color: var(--text-secondary); margin-top: 2px; }
.meal-kcal { font-size: 13px; font-weight: 600; flex-shrink: 0; margin-left: 12px; }

/* Модалка правки позиции. Ширина как у модалки воды — они одного семейства. */
.meal-edit-modal { max-width: 340px; }
.meal-edit-field { margin-bottom: 12px; }
.meal-edit-label {
  display: block; margin-bottom: 6px; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-caps-md); color: var(--text-secondary);
}
.meal-edit-field .water-custom-row { margin-top: 0; }
.meal-edit-modal .text-input { font-size: 15px; padding: 11px 12px; }
.meal-edit-modal .water-custom-row .text-input { text-align: center; }
.meal-edit-macros { font-size: 13px; color: var(--text-secondary); margin-bottom: 12px; }
/* Пересчитанные значения подсвечиваем: видно, что это новые числа, а не те,
   что были до правки. */
.meal-edit-macros.updated { color: var(--text-primary); }
.meal-edit-recalc { width: 100%; margin-bottom: 12px; }
.meal-edit-recalc[hidden] { display: none; }
.meal-edit-actions { display: flex; gap: 8px; }
.meal-edit-actions .chip-btn { flex: 1; text-align: center; padding: 11px 0; font-size: 13px; }
/* Удаление — единственное необратимое действие в этой модалке, поэтому оно
   подкрашено и требует подтверждения (класс .confirm ставит JS). */
.meal-edit-delete.confirm { background: #e0894f; color: var(--on-accent); border-color: transparent; }

/* ===== Общие мелкие компоненты: чипы, инпуты, тумблеры ===== */
/* Рамки нет: в прототипе чипы — сплошная заливка без обводки (замер живого
   рендера 27.08.2026: border 0px none и у чипов ограничений, и у чипов темы).
   Убрана по просьбе пользователя сразу для обеих тем — --glass-border светлый
   и в дневной, и в ночной, поэтому контур был виден в обеих. */
.chip-btn {
  border: none;
  background: var(--chip-bg);
  color: var(--text-primary);
  font-size: 13px;
  font-weight: 600;
  padding: 9px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease, transform 120ms var(--ease-out);
}
.chip-btn:active { transform: scale(0.95); }
.chip-btn.active { background: var(--dot-color); color: var(--on-accent); }

.text-input {
  width: 100%;
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid var(--glass-border);
  background: var(--chip-bg);
  color: var(--text-primary);
}
/* Браузерное кольцо фокуса синее и в палитру не попадает. Убираем его, но
   взамен подсвечиваем рамку — совсем без индикации поле в фокусе неотличимо
   от соседних, и с клавиатуры по форме не пройти. */
.text-input:focus { outline: none; border-color: var(--dot-color); }
.text-input::placeholder { color: currentColor; opacity: 0.55; }

.field-label {
  display: block;
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: var(--track-caps-md); color: var(--text-secondary); margin-bottom: 6px;
}

/* Переключатель. Стили написаны под РАЗМЕТКУ, КОТОРАЯ ЕСТЬ: label с чекбоксом
   внутри и .switch-track рядом. Раньше здесь лежал набор под кнопку с
   .switch-thumb и классом .on — как в прототипе, — но в HTML такой разметки
   нет нигде, класс .on никто не ставил, а .switch-track не имел правил вовсе.
   Итог: системный чекбокс поверх пилюли, и так на двух вкладках сразу.
   Состояние держит :checked, а не класс из JS: у чекбокса оно уже есть, и
   существующие обработчики (e.target.checked) продолжают работать как были. */
.switch { position: relative; display: inline-block; width: 42px; height: 24px; flex-shrink: 0; cursor: pointer; }
.switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.switch-track {
  position: absolute; inset: 0; border-radius: 999px; background: var(--chip-bg);
  transition: background 180ms var(--ease-out); pointer-events: none;
}
.switch-track::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px;
  border-radius: 50%; background: #fff; transition: left 180ms var(--ease-out);
}
.switch input:checked + .switch-track { background: var(--dot-color); }
/* Включённый бегунок — зелёный самой темы, --modal-bg. Белый не годился: в
   дневной теме --dot-color почти белый (#f2f4ee), и бегунок сливался с
   дорожкой. Тёмные токены (--on-accent, --fab-icon) контраст давали, но к
   палитре темы не относятся — в дневной читались чёрным.
   Из двух зелёных палитры выбран не --bg-base, хотя это ровно фон темы:
   на дорожке он даёт 3.02:1, то есть впритык к порогу 3:1 для графических
   элементов. --modal-bg — тот же сажевый, на шаг темнее, 4.35:1.
   В ночной теме тот же токен даёт тёмно-зелёный на лайме, 12.47:1.
   Выключенный бегунок остаётся белым: он лежит на приглушённом --chip-bg,
   там контраст даёт как раз светлое пятно. */
.switch input:checked + .switch-track::after { left: 21px; background: var(--modal-bg); }
/* Блок с одной строкой-переключателем. Используется и на «Весе», и в
   «Профиле»; правил не было ни здесь, ни там. */
.profile-block { border-radius: 18px; padding: 14px 16px; margin-top: 12px; }
.toggle-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  font-size: 12.5px; font-weight: 500; line-height: 1.35;
}

/* ===== Нав-бар (line 619-642) ===== */
.nav-row { display: flex; align-items: center; flex-shrink: 0; margin-top: 12px; }
/* Здесь НЕЛЬЗЯ overflow: hidden. В сочетании с border-radius это обрезка
   (clip), а обрезка элемента с backdrop-filter — известный баг WebKit:
   bugs.webkit.org/show_bug.cgi?id=142662. Обрезка была нужна только для
   бегунка активной вкладки (.nav-indicator), но у него своё border-radius, а
   JS держит его внутри границ пилла — за края он не вылезает. */
.nav-pill {
  position: relative; display: flex; align-items: center; justify-content: space-between;
  flex: 1; gap: 2px; border-radius: 999px;
  /* Боковой отступ 3, а не 14, и он же по смыслу равен ПОЛЕ_ПОДСВЕТКИ в app.js.
     Отсюда вся симметрия нав-бара: крайние кнопки встают ровно в 4px от края
     бара (3 от padding-box плюс рамка) — там же, где проходит подсветка сверху
     и снизу. Значит подсветку можно делать просто РАВНОЙ кнопке: она у всех
     вкладок одного размера, кнопка в ней по центру, а у крайних сама собой
     оказывается вровень с оправой и концентрична её скруглению.
     Раньше отступ был 14, и чтобы дотянуть подсветку до края, её приходилось
     растягивать у первой и последней вкладки — отчего размеры разъезжались, а
     кнопка уезжала из центра.
     Вертикальные 6 не трогаем: они задают высоту кнопок, а высоту подсветки
     считает JS от высоты пилла. */
  padding: 6px 3px;
}
.nav-indicator {
  /* border-radius 999px, а не 14px: бегунок лежит внутри капсулы нав-бара, и
     скруглённый прямоугольник в овале читался как чужая деталь. Высоту задаёт
     JS от высоты ПИЛЛА (ПОЛЕ_ПОДСВЕТКИ в app.js), поэтому при полном скруглении
     подложка и оправа почти концентричны: 25.75 против 28.75 на 375px. */
  position: absolute; left: 0; top: 0; border-radius: 999px;
  pointer-events: none; z-index: 0;
  background: var(--nav-active-pill);
  /* Верхняя светлая грань — тот же приём, что у .kcal-card (inset 0 1px 0), плюс
     волосяной контур по всему периметру. Обычной рамкой не делаем: контур
     повторяет border-radius без шва на стыке с градиентом. Внешнюю тень не
     добавляем — тени ушли из интерфейса 23.08.2026. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.30),
              inset 0 0 0 1px rgba(255, 255, 255, 0.09);
  opacity: 0;
  transition: opacity 0.2s ease;
}
/* Едем через transform, а не left: ширина кнопки дробная (63.3px на 375),
   и покадровая интерполяция left пересчитывает раскладку и округляет позицию,
   тогда как transform считается композитором. Переход объявлен только у
   .positioned: первая установка и есть добавление класса, поэтому бегунок
   встаёт на место мгновенно, а не выезжает из левого края при открытии. */
.nav-indicator.positioned {
  opacity: 1;
  transition: transform 320ms var(--ease-out), width 320ms var(--ease-out),
              opacity 0.2s ease;
}

.nav-item {
  position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px; border: none; background: transparent;
  color: var(--text-tertiary); padding: 6px 2px; border-radius: 14px; cursor: pointer;
  transition: transform 100ms var(--ease-out), color 220ms var(--ease-out);
}
.nav-item:active { transform: scale(0.92); }
.nav-item svg { width: 19px; height: 19px; }
/* line-height задан явно и ЦЕЛЫЙ. При `normal` шрифт даёт строку 12.5, из-за
   чего кнопка выходит 45.5, пилл 59.5, и все горизонтальные края садятся на
   полпикселя: браузер размазывает их сглаживанием на две строки, тогда как
   боковые края стоят на целых и остаются чёткими. Расстояние до подсветки при
   этом всюду одно (4), но размытый край читается ближе, и сверху просвет
   кажется ýже, чем слева. С целой строкой вся стопка становится целой.
   13 вместо 12.5 — это 1.3 против 1.25, на глаз подпись не меняется. */
.nav-item-label { font-size: 10px; font-weight: 600; line-height: 13px; }
/* Ниже ~335px «Статистика» упирается в собственную ширину и перестаёт
   ужиматься вместе с остальными: flex: 1 делит место поровну, но ниже
   содержимого флекс-элемент не сжимается. Кнопки расходились (замер на 320:
   53.88 / 60.35 / 53.88 / 53.88), а с ними и подсветка активной вкладки.
   На 9px подпись перестаёт упираться, и все четыре снова равны.
   line-height не трогаем: он задан в пикселях, высота стопки остаётся целой. */
@media (max-width: 339px) {
  .nav-item-label { font-size: 9px; }
}
.nav-item.active { color: var(--text-primary); }
.nav-item.active svg { animation: navBounce 280ms var(--ease-out); }
.fab {
  /* Боковые поля — только у «+», а не через gap у пилла: gap развёл бы и
     соседние вкладки, а разводить нужно именно кнопку. Итого просвет до
     подсветки = gap пилла (2) + это поле. Вкладки при этом остаются равными
     между собой: flex: 1 делит остаток поровну. */
  margin: 0 3px;
  position: relative; flex-shrink: 0; width: var(--fab-size); height: var(--fab-size);
  border-radius: 50%; border: none; cursor: pointer; display: flex; align-items: center;
  justify-content: center; background: var(--fab-bg); color: var(--fab-icon);
  transition: transform 140ms var(--ease-out);
}
.fab:active { transform: scale(0.9); }
/* 23px, а не 19: значок занимает чуть больше половины кнопки и читается
   увереннее. Дальше 23 поля вокруг становятся тоньше просвета между
   палочками плюса, и круг перестаёт читаться как круг. */
.fab svg { width: 23px; height: 23px; }

/* ===== Статистика: переключатель вкладок (line 173-177) ===== */
.range-trigger {
  display: flex; align-items: center; gap: 5px; padding: 6px 10px; border-radius: 999px;
  border: 1px solid var(--glass-border); background: var(--glass-bg); font-size: 11px;
  font-weight: 600; color: var(--text-secondary); cursor: pointer;
  transition: transform 120ms var(--ease-out);
}
.range-trigger:active { transform: scale(0.95); }
.range-trigger svg { opacity: 0.85; flex-shrink: 0; }

.mode-switch {
  position: relative; display: flex; gap: 2px; padding: 4px; border-radius: 999px; margin-bottom: 14px;
  overflow: hidden;
}
.mode-indicator {
  position: absolute; top: 4px; left: 4px; height: calc(100% - 8px); border-radius: 999px;
  pointer-events: none; z-index: 0; background: var(--nav-active-pill);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.30),
              inset 0 0 0 1px rgba(255, 255, 255, 0.09);
  /* 320ms var(--ease-out) — значение из хендоффа для того же паттерна
     «пилл-подложка скользит под активным пунктом» (.nav-highlight, прототип
     `:621`). У самого сегментного переключателя в прототипе скользящей
     подложки нет — там просто пиллы, — поэтому взято по аналогии, а не
     дословно. */
  transition: transform 320ms var(--ease-out), width 320ms var(--ease-out);
}
.mode-btn {
  position: relative; z-index: 1; border: none; background: transparent; color: var(--text-primary);
  /* `flex: 1 1 auto`, а НЕ `flex: 1` (то есть `1 1 0%`). При нулевой базе всё
     место делится поровну ВМЕСТО текста: все кнопки выходят одной ширины, и
     короткое «Вес» получает такой же слот, как «Инсайты» — между словами
     возникают разные зазоры. Замер на 390: 17 / 12.8 / 12.1 / 27.7, разброс
     15.6px. С базой auto поровну делится только ОСТАТОК после текста, каждое
     слово получает одинаковые поля, зазоры становятся 20.3 / 20.3 / 20.3 /
     20.3, разброс 0 — и так на любой ширине, без пересчёта.
     Тот же дефект есть в прототипе (там кнопки по 65.6px, зазоры 20.8 / 16.8 /
     15.9 / 30.5) — это осознанное отступление от хендоффа. */
  flex: 1 1 auto;
  /* Кегль зависит от ширины экрана, а не фиксирован: 11px это ступень мелких
     подписей, а тут основной интерактивный контрол (нативный сегментный
     контрол iOS набран 13pt). После перехода на `flex: 1 1 auto` кегль
     ограничивает не обрезка, а ЗАЗОР между словами — обрезки нет даже при
     14px. Замер зазоров на 320: 11px → 13.1, 12px → 9.5, 13px → 5.9,
     14px → 2.3, то есть на узком экране слова слипаются задолго до того, как
     перестают влезать. Отсюда пол 11px. Проверенные зазоры: 13.1 при 320,
     20.3 при 390. Верхнюю границу не поднимать без перезамера зазоров. */
  font-size: clamp(11px, 3.3vw, 13px); font-weight: 600; padding: 9px 0; border-radius: 999px; cursor: pointer;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: background 180ms ease, transform 120ms var(--ease-out);
}
.mode-btn:active { transform: scale(0.96); }

.stats-panels-wrap { position: relative; flex: 1; min-height: 0; display: flex; }
.stats-panels { display: grid; grid-template-rows: minmax(0, 1fr); flex: 1; min-height: 0; }
.stats-panels > div { grid-area: 1 / 1; min-height: 0; }
#panel-insights, #panel-nutrition, #panel-weight { position: absolute; inset: 0; }
#panel-summary, #panel-charts { display: flex; flex-direction: column; }

/* ===== Сводка: "Дней в цели / Вода / Вес" (line 181-194) ===== */
/* Итог периода — сводная СТРОКА, а не карточка (23.08.2026). Коробка сообщает
   «внутри отдельный объект», а здесь три числа с подписями, то есть шапка
   экрана. Ниже идут кольцо калорий и полосы БЖУ — вот они объекты, и карточки
   у них остаются. Разделители между колонками уже были внутри строки
   (.period-stat-mid), снаружи ничего добавлять не пришлось; волосок снизу
   отделяет шапку от карточек. Тот же разбор, что и на «Инсайтах»: рамка
   оправдана вокруг объекта или когда несёт функцию, но не вокруг подписи. */
.period-stats-row {
  display: flex; padding: 2px 0 14px; margin-bottom: 14px;
  border-bottom: 1px solid var(--glass-border);
}
.period-stat { flex: 1; text-align: center; }
.period-stat-mid { border-left: 1px solid var(--glass-border); border-right: 1px solid var(--glass-border); }
.period-stat-value { font-size: 20px; font-weight: 700; line-height: var(--lh-tight); }
.period-stat-label { font-size: 10px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--track-caps-sm); margin-top: 4px; }

/* ===== Сводка: "Калории за период" — кольцо Б/Ж/У (line 196-226) ===== */
/* margin-top: 0 — сбрасываем 18px из базового .kcal-card: они нужны на
   Главной (карточка под пиллами), а здесь давали зазор 30px вместо 12,
   как в эталоне. */
#period-kcal-card { margin-top: 0; border-radius: 22px; padding: 18px 18px 6px; margin-bottom: 12px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); }
.donut-row { display: flex; align-items: center; gap: 24px; margin-top: 14px; padding-bottom: 8px; }
.donut-wrap { position: relative; width: 136px; height: 136px; flex-shrink: 0; }
.donut-svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.donut-track { fill: none; stroke: var(--track-color); stroke-width: 10; }
.donut-seg { fill: none; stroke-width: 10; stroke-linecap: round; transition: stroke-dasharray 1s cubic-bezier(0.22, 1, 0.36, 1); }
.donut-seg-protein { stroke: var(--dot-color); }
.donut-seg-fat { stroke: var(--macro-fat); }
.donut-seg-carbs { stroke: var(--macro-carbs); }
.donut-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.donut-center-value { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; line-height: var(--lh-tight); }
.donut-center-goal { font-size: 11px; font-weight: 500; color: var(--text-secondary); }
.donut-legend { flex: 1; display: flex; flex-direction: column; gap: 14px; }
.donut-legend-item { display: flex; align-items: center; gap: 14px; }
.donut-legend-label { display: flex; align-items: center; gap: 9px; width: 96px; flex-shrink: 0; font-size: 15px; font-weight: 500; }
.donut-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.donut-legend-pct { font-size: 15px; font-weight: 700; }

/* ===== Сводка: "БЖУ и микронутриенты за период" (line 228-249) ===== */
.micro-block { border-radius: 22px; padding: 18px 18px 16px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); margin-bottom: 12px; }
.micro-block .kcal-card-title { margin-bottom: 16px; }
/* Тот же знак калорий, что в шапке одноимённой карточки на главной. Здесь
   заголовок стоит сам по себе, без .kcal-card-head, поэтому строку со
   значком собираем прямо в нём. */
#period-kcal-card .kcal-card-title,
#micro-block .kcal-card-title { display: flex; align-items: center; gap: 6px; }
.period-bar-row { margin-bottom: 14px; }
.period-bar-row:last-child { margin-bottom: 0; }
.period-bar-head { display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
.period-bar-head span:last-child { color: var(--text-secondary); font-weight: 500; }
.period-bar-track { height: 6px; border-radius: 999px; background: var(--track-color); overflow: hidden; }
/* Тот же тайминг, что у .progress-fill — экрана отчётов за период в прототипе
   нет, но это такая же полоска-заливка. */
.period-bar-fill { height: 100%; border-radius: 999px; background: var(--dot-color); transition: width 0.4s ease; }
/* Отступы равные: у .period-bar-row:last-child обнулён margin-bottom, поэтому
   сверху от линии оставались только собственные 4px разделителя против 14px
   снизу — линия липла к полосе над ней. 14/14 держит и симметрию, и общий
   ритм блока (строки разделены теми же 14px). */
.micro-divider { border-top: 1px solid var(--glass-border); margin: 14px 0; }

/* ===== Питание: аккордеон по дням (line 253-281) ===== */
.diary-day-group {
  margin-bottom: 10px; border-radius: 20px; padding: 4px 16px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.diary-day-toggle {
  width: 100%; display: flex; align-items: center; justify-content: space-between; border: none;
  background: transparent; padding: 12px 0; cursor: pointer; font: inherit; color: inherit;
  text-align: left; transition: transform 120ms var(--ease-out);
}
.diary-day-toggle:active { transform: scale(0.98); }
/* Три строки дня — одной колонкой слева, шеврон отдельным элементом справа
   (прототип: шапка — flex row / space-between, слева блок 124.6px из даты,
   ккал и БЖУ, справа круг 28×28 по вертикальному центру). Раньше ккал лежал
   в одной обёртке с шевроном справа, и колонка разваливалась. */
.diary-day-toggle-main {
  display: flex; flex-direction: column; align-items: flex-start; min-width: 0;
}
.diary-day-toggle-date { font-size: 13px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--track-caps-lg); }
.diary-day-toggle-kcal { font-size: 13px; font-weight: 700; margin-top: 2px; }
.diary-day-toggle-macros { font-size: 11px; font-weight: 500; color: var(--text-secondary); margin-top: 2px; }
.diary-day-chevron {
  width: 28px; height: 28px; border-radius: 50%; background: var(--chip-bg); border: 1px solid var(--glass-border);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  transform: rotate(0deg); transition: transform 220ms var(--ease-out);
}
/* 11px — размер стрелки в прототипе. Раньше класс висел на самой svg, и
   стрелка растягивалась на все 28px круга. */
.diary-day-chevron svg { width: 11px; height: 11px; }
.diary-day-toggle.expanded .diary-day-chevron { transform: rotate(90deg); }
/* Раскрытие дня — через grid-template-rows 0fr→1fr, а НЕ max-height.
   Прототип считает точную высоту (items * 70 + 20), в CSS так нельзя, и
   наивная замена «max-height: 2000px» даёт кривую скорость: при длительности
   0.35s день с одним блюдом доезжает за долю этого времени, а с десятью — за
   всё. 0fr→1fr анимирует до ЕСТЕСТВЕННОЙ высоты, поэтому темп одинаковый при
   любом числе блюд и магического числа не нужно.
   Внутренняя обёртка обязательна: анимируется строка грид-контейнера, а
   overflow: hidden должен быть на её содержимом. */
.diary-day-entries {
  display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows 0.35s ease, opacity 0.3s ease;
}
.diary-day-entries.expanded { grid-template-rows: 1fr; opacity: 1; }
.diary-day-entries > .diary-day-entries-inner { overflow: hidden; min-height: 0; }
.diary-entry-row {
  width: 100%; display: flex; align-items: center; justify-content: space-between; padding: 11px 0;
  border-top: 1px solid var(--glass-border); border: none; background: transparent; font: inherit; color: inherit; text-align: left; cursor: pointer;
  transition: transform 120ms var(--ease-out);
}
/* 0.98, как у остальных строк-списков (.settings-row, .diary-day-toggle,
   .meals-toggle) — это конвенция хендоффа для строк, кнопки жмутся до 0.95. */
.diary-entry-row:active { transform: scale(0.98); }
/* Единственным намёком на нажимаемость было :active — то есть обратная связь
   ПОСЛЕ тапа. До него строка читалась как обычный текст, тем более рядом с
   заголовком дня, у которого шеврон в круге есть. Здесь стрелка без круга и
   приглушённая: сигнал «строка открывается» без веса кнопки.
   Правки веса в прототипе нет вовсе — это наша добавка, и сигнал к ней тоже
   наш, сверять не с чем. */
.diary-entry-chevron {
  width: 14px; height: 14px; flex-shrink: 0; margin-left: 8px;
  color: var(--text-tertiary);
  transition: color 180ms ease;
}
.diary-entry-name { font-size: 13px; font-weight: 600; }
.diary-entry-meta { font-size: 11px; font-weight: 500; color: var(--text-secondary); margin-top: 2px; }
.diary-entry-kcal { font-size: 13px; font-weight: 600; flex-shrink: 0; margin-left: 12px; }
.diary-entry-photo { width: 36px; height: 36px; border-radius: 10px; object-fit: cover; margin-right: 10px; flex-shrink: 0; }
.diary-entry { display: flex; align-items: center; border-top: 1px solid var(--glass-border); }
.diary-entry-info { flex: 1; min-width: 0; }
/* Вес между кнопками — БЕЗ коробки: ни рамки, ни заливки, ни разделителей.
   Версия с обводкой на 154px читалась инородным блоком в стеклянном
   интерфейсе; здесь «тело» только у кнопок, число просто стоит между ними.
   Фиксированная ширина — под четырёхзначный вес: иначе кнопки разъезжались бы
   по горизонтали от строки к строке и дёргались на переходе 99 → 100 → 1000. */
.weight-history-empty { padding: 14px 0; font-size: 13px; color: var(--text-secondary); text-align: center; }

/* ===== Графики (line 283-361) ===== */
.chart-block { border-radius: 20px; padding: 16px 16px 12px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); }
.chart-block + .chart-block { margin-top: 12px; }
.chart-head { display: flex; align-items: baseline; justify-content: space-between; }
.chart-title { font-size: 13px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--track-caps-lg); }
.chart-readout { font-size: 13px; font-weight: 700; }
/* margin-top 24px, а не 12: подпись значения теперь висит абсолютом НАД
   столбцом и при полной высоте выходит на 17px выше области графика. */
/* gap 5px между колонкой чисел и линией оси: 8+8 по обе стороны линии давали
   17px чистого воздуха при карточке в 342 — заметная доля ширины, съеденная
   ни на что. Зазор нужен, чтобы числа не липли к линии, а данные не росли из
   неё, но пяти пикселей для этого достаточно. */
.chart-canvas { display: flex; gap: 5px; height: 100px; margin-top: 24px; margin-bottom: 19px; min-width: 0; }
.chart-axis { display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; height: 100%; padding-bottom: 2px; }
.chart-axis span { font-size: 10px; font-weight: 600; color: var(--text-tertiary); }
/* min-width: 0 по ВСЕЙ цепочке flex-контейнеров, а не только у кнопки-столбца.
   У .chart-plot тоже flex: 1 с min-width: auto, поэтому он раздувался под
   содержимое: на месяце область столбцов вырастала до 777px при карточке 342.
   Правило «flex-элемент не сжимается уже контента» действует на каждом уровне
   вложенности, и чинить надо каждый. */
.chart-plot { position: relative; flex: 1; min-width: 0; display: flex; align-items: flex-end; height: 100%; border-left: 1px solid var(--glass-border); padding-left: 5px; touch-action: pan-y; }
/* touch-action: pan-y — вертикаль остаётся браузеру (страница скроллится),
   горизонталь достаётся нам под протяжку по графику. Без этого браузер
   съедал бы горизонтальные движения как начало скролла. */
.chart-bars { display: flex; align-items: flex-end; gap: 8px; width: 100%; min-width: 0; height: 100%; touch-action: pan-y; }
.chart-bars.dense { gap: 2px; }
/* min-width: 0 — иначе flex-элемент не сжимается уже своего содержимого
   (по умолчанию min-width: auto). Подпись значения внутри шириной ~23px, и на
   месячном периоде 31 столбец распирал контейнер до 768px при карточке в 342:
   график уезжал за края блока. Значение видно только у выбранного столбца, так
   что вылет самой подписи за узкую колонку никому не мешает. */
.chart-bar-btn { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 2px; height: 100%; justify-content: flex-end; border: none; background: transparent; cursor: pointer; padding: 0; font: inherit; }
/* Текст подписи рисуется ТОЛЬКО у выбранного столбца, у остальных — пустой.
   Раньше подпись была у всех и пряталась прозрачностью — но в плотном режиме
   она шире своей кнопки (текст 22.7px против кнопки 6.5px) и вылезала на
   16–18px за её пределы. Прозрачный, но занимающий место элемент поверх
   стеклянной карточки с backdrop-filter оставлял на iOS залипшие куски
   прежнего значения над невыбранными столбцами.
   Высота задана жёстко: пустой div схлопнулся бы в ноль, и столбцы без
   подписи стали бы выше выбранного. */
/* flex-shrink: 0 — иначе подпись в колоночном флексе сжимается у высоких
   столбцов (замер: от 13px до 11.27px), и место под неё резервируется разное.
   Из-за этого заливки одинаковых значений отличались на пиксель. */
/* Подпись значения — АБСОЛЮТОМ над столбцом, а не флекс-элементом.
   В потоке она съедала 15px (13 строка + 2 зазор) из 100px области, и столбец
   с максимумом дорастал только до 85px. Хуже того, шкала получалась
   НЕЛИНЕЙНОЙ: значение 50% рисовалось как 50px (ровно половина области), а
   100% упиралось в те же 85px — то есть всё выше ~85% от максимума выглядело
   одинаково. Заодно верхняя отметка оси Y стояла на 15px выше реального
   максимума столбцов. Позицию по вертикали задаёт JS: bottom привязан к
   высоте своего столбца. */
.chart-bar-value {
  position: absolute; left: 0; right: 0; text-align: center;
  font-size: 10px; font-weight: 700; line-height: 13px; color: var(--text-secondary);
  white-space: nowrap; pointer-events: none;
}

/* transform из перехода убран вместе со scaleX(1.25): ширину выбранного
   столбца теперь меняет flex-grow у кнопки, а трансформа тут не осталось. */
.chart-bar-fill { width: 100%; border-radius: 5px; background: var(--dot-color); opacity: 0.55; transition: opacity 160ms ease, height 300ms var(--ease-out); }
.chart-bars.dense .chart-bar-fill { border-radius: 2px; opacity: 0.45; }
/* Линия дневной цели по калориям. Лежит в .chart-plot, а не в столбцах:
   она общая для всего графика и должна быть непрерывной, а не рваться в
   зазорах между столбцами. Шкала у неё та же, что у столбцов — обе меряются
   в процентах от высоты области, и это верно только потому, что столбец
   теперь занимает её целиком (см. .chart-bar-value). */
.chart-goal-line {
  position: absolute; left: 0; right: 0; height: 0; pointer-events: none;
  border-top: 1px dashed var(--text-tertiary);
}
/* Выбранный столбец расширяется ЗА СЧЁТ остальных — через flex-grow, а не
   transform: scaleX(1.25), как было в прототипе. Две причины. Первая:
   transform не меняет раскладку, выбранный столбец наезжал на соседей вместо
   того, чтобы их потеснить. Вторая: на месячном периоде столбец шириной 6.7px
   при масштабе 1.25 становится 8.4px — разницу не видно. Коэффициент 1.4: при 2 выбранный
   столбец на коротком периоде получался вдвое шире соседей и перекашивал
   картинку, а 1.25 из прототипа на месяце уже неразличим. 1.4 читается на
   обоих масштабах. Сумма ширин при любом коэффициенте остаётся прежней. */
.chart-bar-btn.selected { flex-grow: 1.4; }
/* На плотном периоде множитель больше. Единое число не работает в обе
   стороны: при 5 столбцах (43px) 1.4 уже хорошо видно, а при 31 (6.6px) та же
   пропорция даёт прибавку в 2.6px — глазу не за что зацепиться. Коэффициент
   2.2 на dense выравнивает восприятие, и он ограничен сверху: даже на 62
   столбцах выбранный получается 5px против 2.3px, а не выламывается из ряда,
   как было бы с фиксированной минимальной шириной. */
.chart-bars.dense .chart-bar-btn.selected { flex-grow: 2.2; }
.chart-bar-btn { transition: flex-grow 220ms var(--ease-out); }
.chart-bar-btn.selected .chart-bar-fill { opacity: 1; }
/* Подпись дня — абсолютом ПОД столбцом, а не отдельным рядом под графиком.
   Абсолют, а не обычный поток: иначе подпись съела бы 19px высоты у самого
   столбца, и высоты перестали бы совпадать с прототипом. Место под неё
   резервирует margin-bottom у .chart-canvas — ровно те же 19px (6 отступ +
   13 строка), что занимал прежний ряд подписей. */
.chart-bar-x {
  position: absolute; top: 100%; left: 0; right: 0; margin-top: 6px;
  text-align: center; font-size: 10px; font-weight: 600; line-height: 13px;
  color: var(--text-tertiary); pointer-events: none;
}

/* Подписи осей на светлой теме — 0.85, как у названий БЖУ на главной.
   Замер контраста на фоне карточки rgb(141,161,145): третичный 0.5 давал
   1.74:1, что мало даже для разметки; 0.85 даёт 2.42:1, то есть +39%.
   Потолок на этом фоне — 2.71:1 (белый без прозрачности), порог WCAG 4.5
   недостижим в принципе, поэтому берём больше половины доступного запаса.
   Только на дневной теме: на ночной фон тёмный и 0.42 читается нормально —
   то же решение, что было принято для .kcal-macro-label.
   Размен: иерархия «разметка против данных» становится мягче, но при 1.74
   подписи просто терялись. */
:root[data-theme="day"] .chart-axis span,
:root[data-theme="day"] .chart-bar-x,
:root[data-theme="day"] .area-x-label {
  color: rgba(255, 255, 255, 0.85);
}
/* ===== График-область (вес) ===== */
/* Точка, вертикаль и всплывашка — обычный HTML поверх SVG, а не элементы
   внутри него: у svg preserveAspectRatio="none", и круг в растянутом viewBox
   стал бы эллипсом, а текст — сплющенным. */
.chart-area-plot { padding-left: 5px; overflow: visible; cursor: pointer; }
/* Общая система координат для пути и маркеров: проценты считаются от ширины
   этой обёртки. Пока SVG отступал от оси через inset, а маркеры — своим
   margin-left, отступ учитывался дважды и точка не лежала на линии. */
.area-inner { position: relative; flex: 1; height: 100%; }
/* overflow: visible — линия в максимуме проходит по y=0, и её обводка
   толщиной 2px наполовину оказывается выше границы SVG. По умолчанию svg
   обрезает содержимое, и верх кривой срезало ровно на пиксель. */
.area-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* Вертикаль от выбранной точки вниз к подписи даты. top задаётся инлайном
   по координате точки, поэтому высота — просто «до низа области». */
.area-vline {
  position: absolute; bottom: 0; width: 1px;
  background: var(--glass-border); transform: translateX(-50%);
}
.area-dot {
  position: absolute; width: 8px; height: 8px; border-radius: 50%;
  background: var(--dot-color); border: 2px solid var(--modal-bg, rgba(0, 0, 0, 0.25));
  transform: translate(-50%, -50%); pointer-events: none;
}
.area-bubble {
  position: absolute;
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
  background: var(--fab-bg); color: var(--fab-icon);
  font-size: 11px; font-weight: 700; pointer-events: none;
}
/* Подписи дат — абсолютом по X-координате своей точки, как у столбцов внутри
   кнопки: два независимых ряда снова разъехались бы (см. .chart-bar-x). */
.area-x-labels { position: absolute; left: 0; right: 0; top: 100%; margin-top: 6px; height: 13px; }
.area-x-label {
  position: absolute; transform: translateX(-50%);
  font-size: 10px; font-weight: 600; line-height: 13px; color: var(--text-tertiary);
  white-space: nowrap;
}

/* ===== Вода: тренд + полоса дней =====
   Два слоя, и ни у одного число элементов не растёт с периодом: линия
   непрерывна, у полосы число ячеек упирается в измеренную ширину. Почему
   именно так — в комментарии к renderWaterTrend. Всё, кроме самой линии, на
   HTML-элементах: у SVG здесь preserveAspectRatio="none", и круг в нём стал
   бы эллипсом (уже ловили это на графике веса). */
.water-stats { font-size: 11px; font-weight: 600; color: var(--text-secondary); margin-top: 14px; }
/* Высоту задаёт содержимое: колонка справа выше самого графика — под ней ещё
   полоса дней и подпись. Ось при этом меряет высоту ГРАФИКА, не колонки. */
.water-canvas { height: auto; margin-bottom: 8px; }
.water-canvas .chart-axis { height: 100px; }
.water-col { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.chart-trend-plot { height: 100px; flex: none; }
.dev-goal {
  position: absolute; left: 0; right: 0; height: 0;
  border-top: 1px dashed var(--text-tertiary);
}
/* Вертикаль на всю высоту области: одна точка на плотном ряду не сообщает, к
   какой дате она относится, — курсор сообщает. Он же связывает выбор на линии
   с выбранной ячейкой полосы под ней. */
.trend-cursor {
  position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--text-secondary); transform: translateX(-50%);
}
.trend-dot {
  position: absolute; width: 9px; height: 9px; border-radius: 50%;
  background: var(--dot-color); border: 2px solid var(--modal-bg, rgba(0, 0, 0, 0.25));
  transform: translate(-50%, -50%);
}
/* Точка недобора — цветом недобора: иначе выбранный плохой день выглядел бы
   так же, как выбранный хороший. Линия при этом может быть средним, а цвет
   точки берётся от ФАКТА дня. */
.trend-dot.miss { background: var(--miss-color); }
/* Отступ сверху разводит полосу с подписями дат: они висят абсолютом на
   top:100% графика и занимают 19px (6 отступ + 13 высота), поток о них не
   знает. Слева полоса повторяет инсет графика — рамку и padding: без этого
   день N в полосе стоял бы не под днём N на линии, и курсор бы врал. */
.water-strip {
  display: flex; gap: 1px; height: 11px; margin-top: 25px;
  border-left: 1px solid transparent; padding-left: 5px;
  cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: pan-y;
}
.strip-cell { flex: 1; min-width: 0; border-radius: 2px; background: var(--dot-color); }
.strip-cell.miss { background: var(--miss-color); }
.strip-cell.empty { background: none; border: 1px solid var(--text-tertiary); opacity: 0.5; }
/* z-index обязателен: outline рисуется ЗА пределами элемента, а соседняя
   ячейка идёт следующей в разметке и без слоя закрашивала рамку справа. */
.strip-cell.selected {
  outline: 1.5px solid var(--text-primary); outline-offset: 1px;
  border-radius: 3px; opacity: 1;
  position: relative; z-index: 1;
}

/* Иконка-капля в заголовке карточки. Это ЗНАК раздела, данных в ней нет:
   каплю как способ показать уровень убрали 22.08.2026 — заливка задаётся по
   высоте, а глаз читает площадь, и у этой формы они расходятся (50% по высоте
   выглядят как 64% площади). Здесь она ничего не кодирует и врать не может. */
.chart-title-icon {
  width: 14px; height: 14px; margin-right: 6px; vertical-align: -3px;
  color: var(--text-secondary);
}
/* ===== Инсайты (line 363-378) ===== */
/* Вкладка без карточек (23.08.2026). Коробка сообщает «внутри отдельный
   объект» — на «Графиках» это правда, там три разных графика, а здесь один
   короткий отчёт из нескольких фраз, и две стеклянные рамки вокруг текста
   только утомляли. Роль контейнера взяла на себя иерархия: главный вывод
   крупно, остальные — строками на волосяных разделителях. Разделители и
   вертикальный акцент уже есть в языке проекта (строки настроек, дневник),
   поэтому вкладка не выбивается, хотя карточек на ней нет.
   Первый инсайт крупнее не для красоты: compute_insights отдаёт выводы в
   порядке важности для цели человека и самый важный ставит первым — до этого
   информация о ранге просто пропадала, все строки выглядели одинаково. */
.insights-kicker {
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: var(--track-caps-md); color: var(--text-secondary); margin-bottom: 8px;
}
.insights-lead { font-size: 19px; font-weight: 600; line-height: 1.35; letter-spacing: -0.01em; }
.insights-rest { margin-top: 14px; }
.insight-line { display: flex; gap: 10px; padding: 10px 0; font-size: 13px; font-weight: 500; line-height: var(--lh-text); }
/* Волосок и НАД первой строкой тоже: он отделяет остальные выводы от главного.
   Базовое правило списков (первому разделитель не нужен) здесь как раз мешает. */
.insights-rest .insight-line { border-top: 1px solid var(--glass-border); color: var(--text-secondary); }
.insight-line::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--dot-color); margin-top: 6px; flex-shrink: 0; }
/* Комментарий ИИ — цитатой, а не карточкой: вертикальный акцент отделяет
   машинный текст от собственных выводов приложения, не заводя ещё одну рамку. */
.ai-quote { margin-top: 18px; padding-left: 12px; border-left: 2px solid var(--dot-color); }
.ai-comment-label { display: block; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: var(--track-caps-md); color: var(--text-secondary); margin-bottom: 6px; }
.ai-comment-text { font-size: 13px; font-weight: 500; line-height: var(--lh-text); }
/* Пока комментарий едет с сервера, текст-заглушка приглушён: это состояние
   ожидания, а не содержимое. Класс раньше вешался, но правила под него не
   было — то есть не делал ничего. */
.ai-quote.ai-comment-loading .ai-comment-text { color: var(--text-tertiary); }
/* Оба блока могут оказаться пустыми (мало данных за период, ИИ не ответил), и
   вкладка превращалась бы в пустой экран. В хендоффе это состояние не
   нарисовано — формулировка своя. */
.insights-empty {
  padding: 18px 16px; text-align: center;
  font-size: 13px; font-weight: 500; color: var(--text-secondary);
}

/* ===== Вес (line 382-427) ===== */
/* Вкладка без карточек, кроме одной (23.08.2026). Тот же разбор, что на
   «Инсайтах» и в «Сводке»: рамка оправдана вокруг объекта или когда несёт
   функцию. Здесь заголовок экрана, настройка и плоский список — не объекты.
   Коробка остаётся только у предложения пересчитать цель: это единственное,
   что требует решения, и появляется оно редко. Раньше оно было одной рамкой
   из четырёх и ничем не выделялось — теперь его нельзя пропустить. */
.weight-hero {
  display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between;
  /* row-gap меньше: 12px разносят колонку и кнопку по горизонтали, а по
     вертикали такой зазор оторвал бы строку про цель от самого веса. */
  column-gap: 12px; row-gap: 6px;
  /* Без margin-bottom: зазор до следующей строки задаёт её собственный
     padding-top. Когда стояли оба, они складывались — над текстом
     переключателя выходило 26px против 13 под ним. */
  padding: 2px 0 14px;
  border-bottom: 1px solid var(--glass-border);
}
/* Подпись и число — одной колонкой слева, кнопка справа. Без этой обёртки все
   три элемента были прямыми детьми flex-контейнера, и space-between разносил
   их по строке: подпись у левого края, число посередине, кнопка справа. */
.weight-hero-main { min-width: 0; }
.weight-hero-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: var(--track-caps-md); color: var(--text-secondary); }
.weight-hero-value { font-size: 36px; font-weight: 700; letter-spacing: -0.02em; line-height: var(--lh-tight); margin-top: 6px; }
/* Строка про цель — подпись к весу, а не отдельное сообщение: тот же размер,
   что у вторичного текста в карточках, и никакой рамки. Рамка сделала бы из
   ориентира обещание. */
.weight-hero-eta { flex-basis: 100%; font-size: 13px; font-weight: 500; color: var(--text-secondary); line-height: var(--lh-text); }
.weight-hero-unit { font-size: 16px; font-weight: 500; color: var(--text-secondary); }
/* Не круг с плюсом, а чип со словом. Круглая кнопка с плюсом в этом
   приложении уже занята: это FAB в нижней навигации, главное «добавить
   запись», и он виден на том же экране. Сначала я развёл их заливкой, но
   силуэт остался прежним — круг с плюсом читался тем же FAB. Форма и слово
   разводят надёжнее: чип принадлежит языку проекта (кнопки воды, «Сохранить»,
   «Применить»), а «Записать» прямо называет действие, чего плюс в кружке
   никогда не делал. */
.weight-add-btn { flex-shrink: 0; margin-bottom: 6px; }
/* Объяснение, почему пересчёт цели сейчас не предлагают. Тихая строка внутри
   того же блока, что и переключатель: причина относится именно к нему. */
/* Своя обёртка, а не .profile-block: тот же класс держит строки настроек в
   «Профиле», и рамки там нужны — это отдельные разделы со своими экранами. */
.weight-toggle-block .toggle-row {
  padding: 12px 0; border-bottom: 1px solid var(--glass-border);
}
.calibration-hint {
  padding: 12px 0; border-bottom: 1px solid var(--glass-border);
  /* Тот же токен, что у даты в истории веса (.weight-history-date) — цвет
     задан переменной, поэтому дневная тема подхватывает его сама. */
  font-size: 11.5px; font-weight: 500; line-height: 1.4; color: var(--text-secondary);
}
/* Размер иконки задаём здесь: svg без width/height получает размер на
   усмотрение браузера, и на устройстве пользователя иконка не отрисовалась
   вовсе, хотя в отладочном рендере была видна. У .nav-item и .fab такое
   правило есть, у микрофона не было. */
/* Строка ввода веса внутри карточки убрана 11.09.2026: ввод переехал в модалку
   у нижнего края (#weight-overlay), как вода и правка позиции. Причины те же —
   прокрутка скролл-контейнера под клавиатуру и раскрытая строка, остававшаяся
   висеть, если человек передумал. */
/* Отступ только сверху: снизу зазор даёт padding-top следующей строки. Если
   задать оба, они сложатся — ровно та ошибка, что была у заголовка. */
.weight-suggestion { margin: 14px 0 0; border-radius: 18px; padding: 14px 16px; }
.weight-suggestion-text { font-size: 13px; font-weight: 500; line-height: var(--lh-text); margin-bottom: 10px; }
.weight-suggestion-actions { display: flex; gap: 8px; }
.weight-suggestion-actions .chip-btn { flex: 1; text-align: center; padding: 10px 0; font-size: 13px; border-radius: 12px; }
.weight-calibrate-row { margin-top: 12px; border-radius: 18px; padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.weight-calibrate-row span { font-size: 13px; font-weight: 500; line-height: 1.35; }
.weight-history-label { font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--track-caps-lg); margin: 16px 0 8px; }
.weight-history { padding: 0; }
.weight-history-row { cursor: pointer; -webkit-tap-highlight-color: transparent; display: flex; justify-content: space-between; align-items: center; padding: 11px 0; border-top: 1px solid var(--glass-border); font-size: 13px; font-weight: 500; }
.weight-history-row:first-child { border-top: none; }
.weight-history-date { color: var(--text-secondary); }
.weight-history-value { font-weight: 700; }
/* scale(0.85), а не 0.95 как у остальных кнопок: здесь нажимается один глиф
   15px, на нём 5% сжатия не читается. */
.weight-history-delete { border: none; background: transparent; color: var(--text-tertiary); font-size: 15px; cursor: pointer; padding: 0 0 0 10px; line-height: 1; transition: transform 120ms var(--ease-out); }
.weight-history-delete:active { transform: scale(0.85); }
/* Очистка истории — функция бота, в хендоффе её нет, поэтому вид свой:
   тихая текстовая кнопка, чтобы не спорить с «Сохранить» и «Применить». */
/* Список внутри модалки: ограничиваем высоту, дальше прокрутка — полсотни
   записей в окно не влезут. */
.weight-history-modal { max-width: 360px; }
.weight-history-full {
  max-height: 52vh; overflow-y: auto; margin-top: 4px;
  /* Полоса прокрутки на iOS рисуется поверх контента и не занимает места в
     раскладке — она наезжала на текст и на поле поиска. Растягиваем блок до
     краёв модалки и возвращаем отступ паддингом: полоса едет по внешнему
     краю, текст стоит там же, где стоял. Так же устроен .scroll-y. */
  margin-inline: calc(-1 * var(--modal-pad-x));
  padding-inline: var(--modal-pad-x);
}
.weight-history-full .weight-history-row:first-child { border-top: none; }
.weight-clear-btn {
  display: block; width: 100%; margin-top: 14px; padding: 10px 0;
  border: none; background: transparent; color: var(--text-tertiary);
  font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  transition: color 160ms ease;
}
.weight-clear-btn:active { color: var(--text-secondary); }

/* ===== Меню (line 430-489) ===== */
/* Классу .scroll-y положены отрицательные поля по бокам: он должен доходить до
   краёв экрана, иначе полоса прокрутки висит в воздухе. Но эти два блока —
   сами КАРТОЧКИ, класс им нужен только ради прокрутки. С отрицательными полями
   они торчали на 20px шире соседних блоков с каждой стороны (поймано 05.09.2026
   на «Составить меню»). Гасим поля, прокрутка остаётся. */
#menu-setup, #menu-gate { margin-inline: 0; }
#menu-setup { border-radius: 22px; padding: 18px 18px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); }
.period-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
#menu-meals-chips.period-chips { margin-bottom: 18px; }
.menu-wide-btn { width: 100%; border: none; cursor: pointer; font: inherit; padding: 13px 0; border-radius: 14px; background: var(--dot-color); color: var(--on-accent); font-size: 13px; font-weight: 700; transition: transform 140ms var(--ease-out); }
.menu-wide-btn:active { transform: scale(0.97); }
/* Две нижние кнопки в хендоффе — призрачные: на экране, где меню уже собрано,
   это вторичные действия, а заливкой они спорили с акцентом «Составить меню». */
.menu-wide-btn.ghost { background: transparent; border: 1px solid var(--glass-border); color: var(--text-secondary); font-size: 12.5px; font-weight: 600; padding: 11px 0; border-radius: 12px; }
#menu-new-btn { margin-bottom: 8px; }
#menu-day-nav { margin-bottom: 14px; }
/* .view по умолчанию block, поэтому #menu-content ничем не ограничивался по
   высоте и уезжал под навигацию — последняя кнопка пряталась на ~100px.
   Колонка + min-height:0 отдаёт остаток высоты именно #menu-scroll. */
#view-menu { display: flex; flex-direction: column; }
#menu-content:not([hidden]) { flex: 1; min-height: 0; display: flex; flex-direction: column; }  /* :not([hidden]) обязателен: авторский display сильнее браузерного [hidden]{display:none}, и скрытый блок остался бы на экране. */
#menu-scroll { flex: 1; min-height: 0; }
/* Блюдо — не карточка, а блок с волоском: тот же язык, что на «Весе»,
   «Инсайтах» и «Сводке», где стекло осталось только за действием. Паддинг
   только вертикальный, поэтому текст выравнивается по полю экрана — за счёт
   этого название получает +34px ширины и перестаёт переноситься на вторую
   строку (замер: 293 -> 327px, три названия из четырёх свернулись в строку). */
.menu-dish {
  padding: 16px 0 14px;
  border-top: 1px solid var(--glass-border);
}
#menu-dishes .menu-dish:first-child { border-top: none; padding-top: 2px; }
/* Подпись слота теперь заголовок раздела, а не метка внутри коробки —
   те же 11px и трекинг, что у «ТЕКУЩИЙ ВЕС» и «ИСТОРИЯ». */
.menu-dish-meal { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: var(--track-caps-md); color: var(--text-secondary); }
.menu-dish-name { font-size: 15px; font-weight: 700; margin-top: 6px; }
.menu-dish-macros { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-top: 4px; }
.menu-dish-actions { display: flex; gap: 8px; margin-top: 12px; }
.menu-dish-actions .chip-btn { flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 5px; border-radius: 10px; padding: 8px 0; font-size: 11px; }
.menu-dish-actions svg { width: 13px; height: 13px; }
/* Итог — строка таблицы между двумя волосками, не ещё одна панель. */
.menu-totals {
  padding: 15px 0; margin-bottom: 12px; display: flex;
  justify-content: space-between; gap: 10px; font-size: 13px; font-weight: 600;
  border-top: 1px solid var(--glass-border);
  border-bottom: 1px solid var(--glass-border);
}
.menu-totals-label { flex: none; white-space: nowrap; font-weight: 500; color: var(--text-secondary); }
.menu-totals-value { text-align: right; }
.menu-shop-row { display: flex; gap: 8px; margin-bottom: 8px; }
.menu-shop-row .chip-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; border-radius: 12px; padding: 10px 0; font-size: 13px; }
.menu-shop-row svg { width: 14px; height: 14px; }

/* Три правила ниже потерялись при вайпе редизайна — классы в JS остались, а
   стилей под них не стало. Последствия были видны только в живом рендере:
   SVG без размеров раскрывался в дефолтные 300x150 (часы во весь экран), а
   без pre-wrap переносы строк из ответа Gemini схлопывались, и список
   покупок превращался в сплошной абзац. */
/* Анимация живёт внутри самой иконки (SMIL, перенесено из Lottie) — здесь
   только размер. 18px, а не 15: обводка в исходнике задана как 29.5 при холсте
   512, то есть на 15px это меньше пикселя, и пересыпание не читалось вовсе.
   Единого размера иконок в приложении нет, он подбирается под место, — ниже
   исключение для кнопки замены блюда. */
.icon-hourglass {
  display: inline-block;
  vertical-align: middle;
  width: 18px;
  height: 18px;
}
/* Часы подменяют собой иконку кнопки на время запроса. У неё 17px — берём
   столько же, иначе картинка скачет в размере при нажатии. */
.recipe-fav .icon-hourglass { width: 17px; height: 17px; }
/* Рецепты и список покупок приходят от Gemini готовым текстом с переносами —
   pre-wrap сохраняет разбиение на строки без разметки. */
/* white-space: pre-wrap больше не нужен: рецепт разбирается на списки в
   recipeHtml (app.js), переносы задаёт разметка. */
/* ВЫДЕЛЕНИЕ ТЕКСТА РАЗРЕШАЕМ ЯВНО. В мини-аппе список покупок и рецепт читают,
   чтобы утащить в заметки, а долгое нажатие в WebView по умолчанию ничего не
   выделяло: у слоя стоит touch-action: none (он гасит протаскивание фона), и
   вместе с ним пропадает и жест выделения (11.09.2026 — «не могу скопировать
   список продуктов»).
   Возвращаем ровно тексту: подложке и кнопкам выделение по-прежнему не нужно,
   иначе на каждом промахе пальцем будет вылезать лупа. */
.menu-recipe, #menu-overlay-body, .dish-card-body {
  -webkit-user-select: text; user-select: text;
  -webkit-touch-callout: default; touch-action: pan-y;
}
.menu-recipe { font-size: 14px; line-height: 1.5; margin-top: 8px; }
.menu-recipe .recipe-head {
  font-size: 11px; font-weight: 700; letter-spacing: var(--track-caps-md);
  text-transform: uppercase; color: var(--text-secondary);
  margin: 16px 0 8px;
}
.menu-recipe .recipe-head:first-child { margin-top: 0; }
.menu-recipe .recipe-p { margin: 8px 0; }
.menu-recipe .recipe-items { list-style: none; margin: 0; padding: 0; }
.menu-recipe .recipe-items li {
  position: relative; padding-left: 16px; margin: 5px 0;
}
/* Точка рисуется псевдоэлементом, а не list-style: маркер браузера нельзя
   выровнять по первой строке при переносе длинного ингредиента. */
.menu-recipe .recipe-items li::before {
  content: ""; position: absolute; left: 4px; top: 8px;
  width: 4px; height: 4px; border-radius: 50%; background: var(--text-secondary);
}
.menu-recipe .recipe-steps { list-style: none; margin: 0; padding: 0; }
.menu-recipe .recipe-steps li {
  display: grid; grid-template-columns: auto 1fr; gap: 8px;
  align-items: start; margin: 8px 0;
}
/* Просто «1.» перед шагом — без подложки и кружка. Номер той же величины и
   цвета, что текст шага, колонка узкая: список читается как обычный
   пронумерованный, а не как набор значков. */
.menu-recipe .recipe-num { font-weight: 700; }
/* Пока картинка генерируется, блок держит своё место — иначе рецепт прыгал бы
   вниз в момент загрузки. aspect-ratio задаёт высоту до появления картинки. */
.menu-recipe-photo {
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 3 / 2; margin-bottom: 12px; border-radius: 14px;
  overflow: hidden; background: var(--chip-bg); color: var(--text-secondary);
}
.menu-recipe-photo.ready { background: none; }
.menu-recipe-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Поиск по избранному. font-size обязателен и обязательно >= 16px: iOS Safari
   зумит всю страницу при фокусе на инпуте с меньшим кеглем. */
.fav-search { width: 100%; font-size: 16px; margin-bottom: 4px; }
.menu-fav { padding: 12px 0; border-bottom: 1px solid var(--glass-border); }
/* Превью слева, как в дневнике (.meal-photo, .diary-entry-photo) — единый приём
   для всех списков с едой. Само изображение показывается только у блюд с уже
   готовой картинкой (флаг has_photo в /api/favorites): открытие списка не
   должно запускать пачку генераций. Остальным ставится заглушка, а не пустота,
   иначе колонка текста прыгала бы на 68px от строки к строке. */
.menu-fav-top { display: flex; align-items: center; gap: 12px; }
.menu-fav-main { min-width: 0; flex: 1; }
.menu-fav-thumb { width: 56px; height: 56px; border-radius: 12px; object-fit: cover; flex-shrink: 0; display: block; }
.menu-fav-thumb-empty {
  display: flex; align-items: center; justify-content: center;
  background: var(--chip-bg); color: var(--text-secondary); opacity: 0.5;
}
.menu-fav-thumb-empty svg { width: 24px; height: 24px; }
.menu-fav:last-child { border-bottom: none; }
.menu-secondary-btn {
  width: 100%; border: none; cursor: pointer; font: inherit; padding: 11px 0; border-radius: 12px;
  background: transparent; border: 1px solid var(--glass-border); color: var(--text-secondary);
  margin-bottom: 8px; font-size: 13px; font-weight: 600; transition: transform 120ms var(--ease-out);
}
.menu-secondary-btn:active { transform: scale(0.97); }

/* ===== Профиль: список настроек (line 494-506) ===== */
#profile-list { margin-bottom: 24px; }
/* ===== Профиль: шапка, блок «о пользователе», кнопка анкеты ===== */
/* Шестерёнка без подложки — по тому же правилу, что и стрелки: значок читается
   сам. Зона нажатия 44x44 невидимым слоем, как у остальных мелких кнопок. */
.profile-gear {
  position: relative;
  border: none; background: none; cursor: pointer; padding: 0;
  color: var(--text-secondary); display: flex; align-items: center;
  transition: transform 120ms var(--ease-out);
}
.profile-gear::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}
.profile-gear:active { transform: scale(0.9); }

/* Производные значения. Отдельной карточкой над списком, потому что их нельзя
   править — они пересчитываются. Смешивать их со строками-настройками значило
   бы предлагать нажать на то, что не нажимается. */
.profile-summary { padding: 0; margin-bottom: 4px; }
.profile-summary[hidden] { display: none; }
.profile-summary-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  font-size: 13px; font-weight: 500; color: var(--text-secondary);
  padding: 5px 0;
}
.profile-summary-row + .profile-summary-row { border-top: 1px solid var(--glass-border); }
.profile-summary-row b { color: var(--text-primary); font-weight: 600; text-align: right; }

/* Пояснение под полями подэкрана — тихая строка, не заголовок. */
/* Вторичный, а не третичный: третичный (0.42-0.5 белого) на нашем фоне даёт
   около 1.4 контраста — такую подпись видно, только если знать, что она там. */
.profile-hint {
  margin-top: 14px; font-size: 12px; line-height: var(--lh-text);
  color: var(--text-secondary);
}
.profile-hint:empty { display: none; }

.profile-rewizard {
  width: 100%; margin-top: 16px; border: none; cursor: pointer; font: inherit;
  font-size: 13px; font-weight: 600; padding: 12px 0; border-radius: 12px;
  background: var(--chip-bg); color: var(--text-primary);
  transition: transform 120ms var(--ease-out);
}
.profile-rewizard:active { transform: scale(0.98); }

/* Одна карточка на весь список, строки внутри разделены волосяной линией —
   тот же приём, что на вкладке «Вес» (.weight-history-row). Было: отдельная
   стеклянная карточка на каждую строку, пять подряд читались как пять
   независимых блоков, хотя это один список. Правило то же, по которому убирали
   рамки и круги: подложка оправдана у поверхности, а поверхность здесь одна —
   сам список, а не каждый его пункт.
   Без flex и без gap намеренно: строки разделяет линия, а не пустота. */
/* Без карточки: строки лежат прямо на фоне, разделённые волосяной линией —
   как история взвешиваний на вкладке «Вес», где этот приём уже работает.
   Замер контраста подтвердил, что так лучше и по читаемости: белый текст на
   голом фоне даёт 1.92 (вторичный) и 3.18 на тёмной части, а на стеклянной
   подложке — 1.72 и 2.60. Стекло подсвечивает фон, а текст у нас светлый,
   поэтому подложка не помогала, а мешала. */
.settings-rows { padding: 0; }
.settings-row {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 13px 0; color: inherit; font: inherit; text-align: left; cursor: pointer;
  background: none; border: none;
  border-top: 1px solid var(--glass-border);
  -webkit-tap-highlight-color: transparent;
  transition: opacity 120ms var(--ease-out);
}
/* Верхняя линия у первой строки не нужна: у карточки уже есть свой край. */
/* Без этого правила атрибут hidden на строке не работает: у .settings-row свой
   display: flex, и он перебивает [hidden] { display: none } из стилей браузера
   по специфичности. Ровно на это уже наступали с замком на карточке аккаунта. */
.settings-row[hidden] { display: none; }
.settings-row:first-child { border-top: none; }
/* Нажатие показываем прозрачностью, а не сжатием: сжимать одну строку внутри
   общей карточки некуда — она не отдельный объект, как было раньше. */
.settings-row:active { opacity: 0.55; }
.settings-row-title { font-size: 14px; font-weight: 600; color: var(--text-primary); flex-shrink: 0; }
/* Удаление данных красным: строка необратимая, и выглядеть как
   соседние настройки она не должна. */
/* Строка удаления НЕ выделена цветом, и это вывод из замеров, а не лень.
   Обе темы стоят на зелёном фоне: у красного низкая светимость, и на дневной
   теме даже чистый #ff0000 даёт 1,48:1, а потолок для любого светлого цвета
   там 2,7 — столько даёт белый, которым набран весь экран. Один красный на
   обе темы упирается в 1,81:1, то есть бледнее соседних строк; разные цвета
   по темам читаются бордовыми; плашка с собственным фоном контраст решает, но
   кричит громче всего остального. Перебрано всё три раза.

   Поэтому предупреждает не цвет, а слова и порядок: строка стоит последней,
   называется прямо, и за ней идёт подтверждение с перечислением того, что
   будет стёрто. Возвращать красный не надо — он здесь не работает. */
/* margin-left: auto, а не space-between у родителя: со шевроном в строке три
   элемента, и space-between развешивал значение ровно посередине между
   заголовком и стрелкой. Автоотступ съедает весь свободный зазор слева, и
   значение встаёт вплотную к шеврону — как в строках без него («Пришлю в
   чат»), и как в системных настройках iOS. */
.settings-row-right { display: flex; align-items: center; gap: 6px; min-width: 0; margin-left: auto; font-size: 12.5px; font-weight: 500; color: var(--text-secondary); }
.settings-row-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ШЕВРОН ВЕРНУЛСЯ (18.09.2026). Его убирали с доводом «значение справа само
   говорит, что строка ведёт дальше», но довод держится только когда значение
   есть: у пустой или короткой строки («Тело», «Документы») не видно, что она
   вообще нажимается. Прежняя претензия — вторая вертикаль стрелок у края —
   снимается тем, что шеврон бледный (45% от цвета текста) и мелкий: так он
   работает признаком, а не украшением. Ровно так же устроены системные
   настройки iOS, откуда человек и приносит эту привычку.

   Только у строк С ПЕРЕХОДОМ (data-target). У «Экспорт CSV» шеврона нет
   намеренно: это действие, а не проваливание, и стрелка обещала бы экран,
   которого не будет.

   Рисуем маской, а не картинкой: цвет тогда берётся у текста и одинаково
   работает в обеих темах. Разметку не трогаем — одиннадцать строк правились
   бы вручную, а правило одно. */
.settings-row[data-target]::after {
  content: ""; flex: none; width: 16px; height: 16px;
  background: currentColor; opacity: 0.45;
  -webkit-mask: var(--chevron-right) center / contain no-repeat;
  mask: var(--chevron-right) center / contain no-repeat;
}

/* ===== Профиль: подэкраны (line 508-612) ===== */
/* Шапка экрана профиля закреплена сверху: экраны бывают длинные («Откуда
   цифры», список часовых поясов), и кнопка «назад» уезжала вместе с текстом —
   чтобы выйти, приходилось мотать до самого верха.

   ФОН — КОПИЯ ФОНА СТРАНИЦЫ, тем же приёмом, что у .scroll-y.fade-* выше:
   background-attachment: fixed и та же геометрия. Поэтому закреплённая шапка
   выглядит просто куском страницы, а текст уезжает под неё незаметно. Обычная
   заливка одним цветом тут не годится: фон вертикальный градиент, и любой
   выбранный цвет совпадёт с ним ровно в одной точке экрана.

   Отступ снизу переехал из margin в padding: под margin фон не рисуется, и в
   этом зазоре был бы виден проезжающий текст. */
.detail-header { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }

/* Закрепляем только шапки, которые лежат прямо в постраничном скролл-контейнере.
   Шапка внутри стеклянной карточки (#menu-setup) сюда НЕ попадает намеренно:
   там за ней фон карточки, а не фон страницы, и копия страничного фона выдала
   бы прямоугольник другого цвета. */
#view-profile > .profile-detail > .detail-header,
#recipes-scroll > .recipes-back {
  position: sticky; top: 0; z-index: 3;
  /* Полоса затухания под шапкой позиционируется от неё. */
  isolation: isolate;
  /* Ширина обязательна: .recipes-back это кнопка, а кнопка ужимается по
     содержимому даже с display: flex. Без этого фон закрывал бы только слово
     заголовка, а справа от него проезжал бы список. */
  width: 100%;
  padding-block: 2px 14px; margin-bottom: 0;
  background-image: var(--blur-bg);
  background-attachment: fixed;
  background-size: auto, 100vw var(--bg-h, 100dvh);
  background-repeat: repeat, no-repeat;
}

/* Пока открыт экран с закреплённой шапкой, верхнее затухание выключаем.
   Иначе маска гасила бы саму шапку — ровно та причина, по которой день-
   навигация на Главной вынесена из скролл-контейнера, а не сделана sticky
   (см. комментарий у #view-home). Гасить тут больше нечего: под верхним краем
   стоит непрозрачная шапка, а не уезжающий текст. */
.scroll-y:has(> .profile-detail:not([hidden]) > .detail-header),
.scroll-y:has(> .recipes-back:not([hidden])) { --fade-top: 0px; }

/* Затухание переезжает ПОД шапку. Верхнее у контейнера отключено (иначе гасило
   бы саму шапку), но без него текст выныривал из-под неё резко, обрезанный по
   живому.
   Полоса под шапкой — снова копия фона страницы с fixed-привязкой, только
   погашенная маской сверху вниз. Именно копия, а не градиент выбранного цвета:
   накладку сплошным цветом в этом проекте пробовали дважды и оба раза мимо, фон
   тут вертикальный градиент и «цвет края» не совпадает ни с чем. Копия
   совпадает с тем, что под ней, в каждой точке экрана по определению. */
/* ТОЛЬКО когда контейнер реально прокручен: класс .fade-top вешает JS при
   scrollTop > 4, тем же условием, что и у остальных затуханий в приложении.
   Безусловная полоса закрывала первую строку текста в статике — то есть там,
   где выныривать из-под шапки нечему. */
.scroll-y.fade-top > .profile-detail:not([hidden]) > .detail-header::after,
.scroll-y.fade-top > .recipes-back:not([hidden])::after {
  content: ""; position: absolute; left: 0; right: 0; top: 100%;
  height: 28px; pointer-events: none;
  background-image: var(--blur-bg);
  background-attachment: fixed;
  background-size: auto, 100vw var(--bg-h, 100dvh);
  background-repeat: repeat, no-repeat;
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent);
  mask-image: linear-gradient(to bottom, #000, transparent);
}
/* Без круглой подложки — только значок, как у стрелок месяца в календаре.
   Правило, по которому убрали: подложка оправдана у поверхности или у поля,
   которое приглашает ввод. Шеврон читается сам, круг добавлял ему веса и делал
   самым тяжёлым пятном в лёгкой шапке подэкрана. Он был на всех пяти
   подэкранах профиля и в двух шагах модалки добавления еды — семь кнопок.
   Это отход от прототипа: там круг есть (замер: 32x32, радиус 50%, чип-фон).
   Решение пользователя 27.08.2026, второй такой отход после стрелок.
   Цвет остаётся --text-primary, а не приглушённый как у стрелок месяца:
   «назад» — основной способ уйти с подэкрана, гасить его вместе с подложкой
   значило бы убрать сразу оба признака, по которым его находят.
   Зона нажатия 44x44 невидимым слоем: кнопка 32px, а без круга не видно даже,
   куда целиться. */
.profile-back {
  position: relative;
  border: none; cursor: pointer; width: 32px; height: 32px;
  background: none; display: flex; align-items: center; justify-content: center;
  color: var(--text-primary); font-size: 15px; transition: transform 120ms var(--ease-out);
}
.profile-back::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.profile-back:active { transform: scale(0.9); }
.detail-title { font-size: 16px; font-weight: 700; }

.profile-block-sm { border-radius: 20px; padding: 16px 18px; }

/* Внутри подэкранов профиля карточек нет: там ровно один блок на весь экран,
   он ничего не группирует — только добавляет слой. Тот же вывод, что и для
   списка настроек, и подтверждён тем же замером: белый текст на голом фоне
   контрастнее, чем на стеклянной подложке.
   Правило узкое, через .profile-detail: те же классы носят блоки на вкладке
   «Меню» (menu-gate, menu-setup), там карточка на месте и нужна. */
.profile-detail .profile-block,
.profile-detail .profile-block-sm { padding: 0; border-radius: 0; }
/* Экран «Уведомления» — три отдельные настройки подряд. Раз карточек больше
   нет, границу между ними держит линия, как между строками списка. */
/* Ритм подэкранов: раньше отступ был только под подписью (6px), а поле снизу
   ничего не отдавало — следующая подпись прилипала к нему вплотную, и блоки
   читались как сплошная лента. Теперь между парами «подпись + поле» 18px,
   у первой пары отступа сверху нет. */
.profile-detail .field-label:not(:first-child) { margin-top: 18px; }
/* Раздел «Документы». Заглушка на время, пока документов нет, и реквизиты
   исполнителя: они многострочные и приходят готовым текстом из legal.py,
   поэтому переносы держит pre-line, а не разметка. */
.docs-note {
  font-size: 14px; line-height: 1.5; color: var(--text-secondary);
}
.docs-requisites {
  font-size: 14px; line-height: 1.5; white-space: pre-line;
  color: var(--text-primary);
}

/* «Откуда цифры». Текст приходит из method.py абзацами, разделёнными пустой
   строкой, а внутри абзаца заголовок отделён одним переносом — его и держит
   pre-line, как у реквизитов выше. Разметки в тексте ровно одна, <b>, и она
   разбирается в DOM, а не вставляется как innerHTML. */
.method-p {
  font-size: 14px; line-height: 1.55; white-space: pre-line;
  color: var(--text-secondary); margin: 0 0 14px;
}
.method-p:last-child { margin-bottom: 0; }
.method-p strong { color: var(--text-primary); font-weight: 600; }
/* Иконка подзаголовка. Инлайн, а не флексом: абзац живёт на pre-line, заголовок
   и текст лежат в одном <p> и разделены переносом — флекс их разорвал бы.
   Размер и подъём те же, что у .chart-title-icon, чтобы иконки раздела не
   спорили с иконками заголовков карточек. */
.method-icon {
  width: 15px; height: 15px; margin-right: 7px; vertical-align: -2px;
  color: var(--text-secondary);
}
.icon-sprite { display: none; }

/* Встроенные стрелки числового поля убраны: на телефоне ими не пользуются,
   а в строке они читались как «здесь есть переход дальше». */
.profile-detail input[type="number"] { -moz-appearance: textfield; }
.profile-detail input[type="number"]::-webkit-inner-spin-button,
.profile-detail input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none; margin: 0;
}
/* Поле сразу после списка чипов — своё ограничение под «Ограничениями».
   Без этого отступа оно прилипало к последнему чипу. */
.profile-detail .chip-list + .text-input { margin-top: 10px; }

.profile-detail .text-input,
.profile-detail .chip-list { margin-bottom: 0; }

/* Списки-переключатели: одинаковая ширина у всех вариантов вместо рваной
   обёртки по содержимому. Пять вариантов активности с разной длиной подписи
   раскладывались 2+2+1 со ступеньками — симметрии не было ни на одной строке. */
/* Список взаимоисключающих вариантов — строками во всю ширину. Плитки тут не
   годятся: подписи от «Похудение» до «Похудеть и подкачаться (вес держим)»
   ложатся в разное число строк, ряды получаются разной высоты. Выбранную
   помечаем галочкой, а не заливкой: заливка на всю ширину — тяжёлое пятно. */
.option-row {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 13px 0; color: inherit; font: inherit; font-size: 14px;
  font-weight: 500; text-align: left; cursor: pointer; background: none; border: none;
  border-top: 1px solid var(--glass-border);
  -webkit-tap-highlight-color: transparent;
  transition: opacity 120ms var(--ease-out);
}
.option-row:first-child { border-top: none; }
.option-row:active { opacity: 0.55; }
.option-row.active { font-weight: 700; }
.option-mark { flex: none; font-size: 15px; color: var(--dot-color); opacity: 0; }
.option-row.active .option-mark { opacity: 1; }

/* grid-auto-rows: 1fr тянет все ряды под самый высокий — иначе подпись в две
   строки («Похудеть и подкачаться») делает свой ряд выше соседних. */
.chip-list { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; grid-auto-rows: 1fr; }
.chip-list .chip-btn {
  text-align: center; padding: 11px 10px;
  display: flex; align-items: center; justify-content: center;
}
/* Нечётный последний вариант тянем на всю ширину, чтобы не висел половинкой. */
.chip-list .chip-btn:last-child:nth-child(odd) { grid-column: 1 / -1; }

.profile-detail .profile-block + .profile-block {
  margin-top: 0; padding-top: 14px; border-top: 1px solid var(--glass-border);
}
/* Линию рисует border-top СЛЕДУЮЩЕГО блока, а padding блоков в подэкране
   обнулён выше — поэтому сверху от линии было 14px, а снизу ноль, и она
   притиралась к тумблеру предыдущей строки. Отдаём столько же снизу. */
.profile-detail .profile-block:has(+ .profile-block) { padding-bottom: 14px; }
.goal-summary { font-size: 13px; font-weight: 500; line-height: 1.7; }
#goal-edit-btn { width: 100%; margin-top: 12px; border: none; cursor: pointer; font: inherit; font-size: 13px; font-weight: 600; padding: 11px 0; border-radius: 12px; background: var(--chip-bg); color: var(--text-primary); transition: transform 120ms var(--ease-out); }
#goal-edit-btn:active { transform: scale(0.97); }

.notif-rows { display: flex; flex-direction: column; gap: 10px; }
.notif-row { border-radius: 18px; padding: 14px 16px; }
.notif-row-head { display: flex; align-items: center; justify-content: space-between; }
.notif-row-head span { font-size: 13px; font-weight: 600; }
.notif-row-sub { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-top: 8px; }

/* Строки выбора пояса в модалке. Правило досталось от прототипа, где это был
   один статичный див «UTC+3 · Москва — Изменить»; список же рисует JS, и
   каждая строка там <button> — без сброса браузерного вида кнопки выходили
   белыми пилюлями по ширине текста, налезающими друг на друга. Вид берём от
   .weight-history-row: тот же список в той же модалке.
   Заодно убраны .tz-value и .tz-change — остатки той же статичной строки, в
   разметке их не было нигде. */
.tz-row {
  width: 100%; border: none; background: none; color: inherit; font: inherit;
  text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent;
  display: flex; align-items: center; justify-content: space-between;
  padding: 11px 0; border-top: 1px solid var(--glass-border);
  font-size: 13px; font-weight: 500;
}
.tz-row:first-child { border-top: none; }
.tz-row:active { opacity: 0.55; }
/* Выбранный пояс — жирным, как активная строка в списках профиля
   (.option-row.active). Галочка не нужна: список открывается прокрученным
   к текущему поясу. */
.tz-row.active { font-weight: 700; }

.diet-block { border-radius: 20px; padding: 16px 18px; display: flex; flex-direction: column; gap: 12px; }

#theme-chips { display: flex; gap: 8px; margin-bottom: 0; }
#theme-chips .chip-btn { flex: 1; text-align: center; padding: 10px 0; border-radius: 12px; }

/* ===== Профиль: элементы, у которых после вайпа не осталось правил вовсе ===== */

/* Подстрока приёма настройки (время напоминания под тумблером). В референсе
   это тихая строка под заголовком: 12px, вторичный цвет, отступ сверху 8px.
   У нас в отличие от прототипа она не текст, а строка с полем ввода —
   поэтому раскладка остаётся от .toggle-row, а типографика берётся отсюда. */
.toggle-row-sub { margin-top: 8px; font-size: 12px; color: var(--text-secondary); }

/* Правило потерялось при вайпе редизайна, из-за чего строка со временем
   оставалась видна при выключенном тумблере. Класс вешает
   updateNotifTimeRowVisibility() в app.js. В прототипе строку не прячут, но
   там время — статичный текст, а у нас поле ввода: показывать поле для
   выключенного напоминания незачем. Решение пользователя, 27.08.2026. */
.toggle-row-sub.toggle-hidden { display: none; }

/* Поле времени. 13px — как у .text-input и всех прочих полей приложения.
   Про зум iOS (Safari увеличивает страницу при фокусе на поле мельче 16px)
   помним, но чинить это надо разом для всех полей, а не для одного Профиля,
   и сперва проверить на устройстве: мета-тег maximum-scale=1 уже стоит.
   Решение пользователя, 27.08.2026. */
.time-input {
  font: inherit; font-size: 12px; font-weight: 500; padding: 3px 8px;
  border-radius: 8px; border: 1px solid var(--glass-border);
  background: var(--chip-bg); color: var(--text-primary);
}
.time-input:focus { outline: none; border-color: var(--dot-color); }

/* Часовой пояс. В референсе это строка «MSK, UTC+3 … Изменить ›».
   Подпись справа выводится через ::after, а не в разметке: значение в кнопку
   пишет JS через textContent (app.js, formatTzOffset), и любой вложенный
   элемент он бы затёр при первом же обновлении. */
.tz-trigger {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: 8px; border: none; background: none; cursor: pointer; text-align: left;
  font: inherit; font-size: 14px; font-weight: 600; color: var(--text-primary);
  transition: transform 120ms var(--ease-out);
}
.tz-trigger::after {
  content: "Изменить \203A"; font-size: 12.5px; font-weight: 500;
  color: var(--text-secondary); flex-shrink: 0;
}
.tz-trigger:active { transform: scale(0.98); }

/* Нативные селекты — тот же размер, что у остальных полей. */
.select-native { font-size: 13px; }

/* ===== Модалка добавления еды (line 646-708) ===== */
/* fixed по той же причине, что и у .calendar-overlay: слой лежит внутри .page
   с потолком ширины 430px, и на экранах шире (16 Pro Max — 440pt) по бокам
   оставались незатемнённые полосы во всю высоту. Разбор — у .calendar-overlay. */
/* ВЫСОТА ОТ TELEGRAM, А НЕ ОТ РАСКЛАДКИ. Замер на устройстве 07.09.2026: при
   тапе по полю Telegram обновляет свои переменные через 158 мс, а сам вьюпорт
   (innerHeight) ужимается только через 575. Пока слой держался на inset: 0, он
   ждал вторую цифру — и лист поднимался над клавиатурой с задержкой в 420 мс,
   отдельным вторым шагом. Переменная приходит первой, поэтому берём её.
   Это РАЗМЕР, а не сдвиг: лист прижат к низу слоя и выше видимой области уйти
   не может. Прошлая попытка двигала лист своим смещением и складывалась с
   компенсацией Telegram — верх уезжал за экран (RENDERING-NOTES, раздел 10).
   В обычном Safari переменной нет — работает фолбэк, поведение прежнее. */
.food-overlay {
  position: fixed; inset: 0;
  z-index: 5; background: rgba(10, 13, 6, 0.55);
  backdrop-filter: blur(6px); display: flex; align-items: flex-end;
  animation: backdropIn 200ms var(--ease-out);
}
.food-overlay[hidden] { display: none; }
/* Высота слоя от Telegram — ТОЛЬКО внутри Telegram. Замер на устройстве
   07.09.2026: при тапе по полю Telegram обновляет свои переменные через 158 мс,
   а сам вьюпорт (innerHeight) ужимается только через 575 — эти 420 мс и видны
   вторым шагом, когда лист догоняет клавиатуру. Переменная приходит первой,
   поэтому высоту берём из неё, а не из раскладки.
   Это РАЗМЕР, а не сдвиг: лист прижат к низу слоя и выше видимой области уйти
   не может. Прошлая попытка двигала лист смещением и складывалась с
   компенсацией Telegram — верх уезжал за экран (RENDERING-NOTES, раздел 10).
   Вне Telegram правило не действует: SDK ставит переменную и там, но она
   замирает на высоте при загрузке, и слой переставал ужиматься под клавиатуру. */
.in-telegram .food-overlay { height: var(--tg-viewport-height); }
/* ШОВ У КЛАВИАТУРЫ. Цвет, который мы отдаём Telegram под клавиатуру, один и
   плоский, а фон страницы — диагональный градиент: у нижней кромки слева он
   светлее, чем справа, примерно на пять единиц после подложки. Поэтому плоский
   цвет совпадал с левым вырезом скругления и мазал в правом.
   Растушёвка снимает задачу целиком: в самых нижних пикселях страница ровно
   нашего цвета — значит оба выреза совпадают, — а выше она сходит на нет и
   сливается с градиентом, каким бы он ни был.
   Только при открытой клавиатуре: без неё этот шов не виден, а лишний слой
   поверх листа не нужен. Цвет ставит app.js той же формулой, что отдаёт
   Telegram, — два источника правды тут развалились бы молча. */
/* ПОЛОСА ПОД СЛОЕМ, пока едет клавиатура. Слой ужимается по переменной
   Telegram — она приходит через 158 мс, а сам вебвью ужимается только через 575
   (замер 07.09.2026, RENDERING-NOTES раздел 10). Эти 420 мс полоса под слоем
   ещё принадлежит нам, но мы её не красим: там видно канву страницы, а она
   светлее тонировки. Отсюда обе жалобы разом — клавиатура едет по светлому и
   «перекрашивается» только после того, как вышла (это вебвью наконец ужался, и
   показался фон Telegram), а при уходе снизу мелькает светлая полоска.
   Красим полосу тем же цветом, что отдаём Telegram. Отменить резайз нельзя, но
   на глаз получается ровно то, что нужно: клавиатура едет поверх нашего фона.
   Это декорация вне потока — сдвинуть лист она не может, в отличие от попытки
   из раздела 10. Когда вебвью ужался, полоса оказывается за его краем и просто
   не видна.
   Класс СВОЙ, не общий с растушёвкой шва: тот снимается по первому событию
   visualViewport, а оно при уходе клавиатуры может прийти в самом начале
   спуска — и заливка гасла ровно там, где полоска и появляется. Здесь держим
   фиксированные 900 мс, чтобы перекрыть весь спуск целиком.
   */
.in-telegram .food-overlay.kb-strip::before,
.in-telegram .onb.kb-strip::before,
.in-telegram .calendar-overlay.overlay-bottom.kb-strip::before,
/* MAX (03.10.2026): тот же светлый просвет под клавиатурой. Цвет фона под
   клавиатурой MAX задать нельзя (в его библиотеке такой команды нет), поэтому
   здесь заливка - единственное средство; --kb-seam app.js считает и в MAX. */
.in-max .food-overlay.kb-strip::before,
.in-max .onb.kb-strip::before,
.in-max .calendar-overlay.overlay-bottom.kb-strip::before {
  content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 100vh;
  background: var(--kb-seam, transparent);
  pointer-events: none;
}
/* Тот же шов, но для полей БЕЗ слоя (поиск по рецептам, поиск в избранном):
   растушёвка у нижней кромки экрана, чтобы плоский цвет под клавиатурой не
   спорил с диагональным градиентом страницы. Класс ставит app.js по фокусу. */
.kb-page body::after {
  content: ""; position: fixed; left: 0; right: 0; bottom: 0; height: 26px;
  background: linear-gradient(to bottom, transparent, var(--kb-seam, transparent));
  pointer-events: none; z-index: 4;
}
.food-overlay.keyboard::after,
.onb.keyboard::after,
.calendar-overlay.overlay-bottom.keyboard::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 26px;
  background: linear-gradient(to bottom, transparent, var(--kb-seam, transparent));
  pointer-events: none;
}
/* Экран добавления еды занимает слой целиком: фон здесь не подложка модалки, а
   сама камера. Заливки у слоя по-прежнему нет (см. комментарий у
   .calendar-modal) — если камеру не дали, сквозь него видно затемнённую
   страницу, и экран остаётся рабочим: затвор откроет системную камеру. */
.food-modal {
  position: relative; width: 100%; height: 100%; overflow: hidden;
  border-radius: 0; padding: 0; color: var(--text-primary);
  display: flex; flex-direction: column;
  animation: sheetUp 320ms var(--ease-drawer);
}
.food-step[hidden] { display: none; }

/* Видео растянуто на весь слой. object-fit: cover обрезает поток одинаково по
   обеим осям, поэтому центр картинки совпадает с центром экрана — на этом
   держится вырезка кадра по рамке (см. обработчик затвора в app.js). */
/* Размер задаёт JS (см. подогнатьВидеоПодЭкран): элементу даётся ровно та
   форма, что у потока, и лишнее срезает overflow: hidden у листа. Здесь —
   запасной путь на то время, пока размер потока ещё не известен. */
.food-camera-video {
  position: absolute; top: 0; left: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
}
/* Пока в потоке нет кадра, видео ПРОЗРАЧНО. Первым делом WebKit рисует его по
   натуральному размеру кадра, и только потом применяет object-fit: cover — в
   портретном экране это выглядело так: сначала картинка с тёмными полосами
   сверху и снизу, через миг она растягивается. Ждём первый нарисованный кадр
   (класс снимает app.js) и показываем уже готовую картинку.
   Появление БЕЗ перехода намеренно: плавность тут стоила бы риска остаться с
   невидимой камерой, если переход почему-то не доиграет. */
.food-camera-video.is-warming { opacity: 0; }
.food-close {
  position: absolute; z-index: 4; top: calc(var(--safe-top) + 10px); right: 16px;
  border: none; cursor: pointer; width: 34px; height: 34px; border-radius: 50%;
  background: rgba(20, 24, 16, 0.45); color: #fff; font-size: 19px; line-height: 1;
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  transition: transform 120ms var(--ease-out);
}
.food-close:active { transform: scale(0.9); }

/* Рамка видоискателя. Это не декорация: в неё целится вырезка кадра, поэтому
   квадрат (сеть ждёт квадратное фото) и поэтому она по центру свободной
   области, а не экрана — так пользователь видит ровно то, что уйдёт на
   распознавание. Схлопнуться ниже кромок ей не даёт flex-shrink свободной
   области: на низком окне (десктопный Telegram) кадр просто станет меньше. */
.food-step-capture {
  position: relative; z-index: 2; flex: 1; min-height: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px;
  /* Верхний отступ компенсирует нижнюю полосу ровно на её прибавку: свободная
     область теряет столько же, сколько та прибавила, а содержимое здесь
     центрируется — иначе рамка уходила бы вверх на половину каждой правки.
     162px складываются из зазора у переключателя (82), подъёма всей полосы (24)
     и исходных 56.
     На НИЗКОМ окне компенсация отключается сама: держать её там незачем (рамке
     и так тесно), а платил бы за неё размер кадра. Доля от высоты видимой
     области, а не от vh: внутри мини-аппа vh больше видимой области. На
     телефоне 0.2 от 812 это ровно те же 162px — мобильная раскладка не
     меняется. */
  padding: calc(var(--safe-top)
    + min(162px, calc(var(--tg-viewport-stable-height, 100dvh) * 0.2))) 20px 4px;
}
.food-frame {
  /* Третье слагаемое — потолок от ВЫСОТЫ видимой области: без него на низком
     окне (десктопный Telegram) высоты не хватало и рамка расплывалась в
     прямоугольник, ширина-то оставалась прежней. Считаем от
     --tg-viewport-stable-height, а не от vh: внутри мини-аппа vh больше
     видимой области. Доля пересчитана под выросшую нижнюю полосу: на окне 600px
     свободной области остаётся 359px, из них 156 занимают отступ (уже
     ужатый), подсказка и зазоры — на рамку около 203. На телефоне потолок
     по-прежнему не срабатывает: 0.33 от 812 это 268px при естественных 261. */
  width: min(78%, 340px, calc(var(--tg-viewport-stable-height, 100dvh) * 0.33));
  aspect-ratio: 1; min-height: 0;
  border: 1.5px solid rgba(255, 255, 255, 0.92); border-radius: 22px;
}
.food-hint {
  font-size: 13px; font-weight: 500; color: rgba(255, 255, 255, 0.88);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
}
.food-camera-loading {
  position: absolute; inset: 0; z-index: 5; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 12px; text-align: center;
  background: rgba(10, 13, 6, 0.55); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  color: #fff; font-size: 13px; font-weight: 500;
}
.food-camera-loading[hidden] { display: none; }

/* Нижняя кромка: переключатель режимов и затвор поверх картинки. Затвор
   показываем только на съёмке — на других режимах снимать нечего. */
/* Зазор между пилюлей и затвором держит пилюлю ближе к рамке: затвор прижат к
   нижней кромке, поэтому весь зазор уходит вверх. Число связано с верхним
   отступом .food-step-capture — там оно скомпенсировано, чтобы рамка от этого
   не уехала (свободная область и эта полоса делят одну высоту). */
.food-bottom {
  position: relative; z-index: 3; flex-shrink: 0;
  display: flex; flex-direction: column; align-items: center; gap: 82px;
  padding: 12px 20px calc(var(--safe-bottom) + 40px);
}
/* На проверке блюда полоса ГАСНЕТ, а не убирается: место под неё остаётся
   занятым, и «Сохранить» встаёт ровно туда, где на прошлом шаге была
   «Проанализировать». Иначе положение главной кнопки зависело бы от того,
   показываем мы переключатель или нет, — замер до правки: 218px от нижней
   кромки на тексте против 37px на проверке, прыжок в 181px между двумя
   соседними шагами одного действия. */
.food-bottom.is-off { visibility: hidden; }
/* С ОТКРЫТОЙ КЛАВИАТУРОЙ место под затвор не держим и зазор схлопываем. На
   съёмке клавиатуры не бывает, а на текстовом режиме эти 124px оказывались
   между полем ввода и клавиатурой и задирали поле высоко вверх — ровно то, за
   что держится пилюля в остальное время, здесь мешает. Класс вешает app.js по
   фокусу в поле (см. тонироватьКлавиатуру). */
/* Отступ снизу больше высоты растушёвки шва (26px, .food-overlay.keyboard::after):
   она гасится в цвет под клавиатурой, и пилюля, стоящая прямо на кромке,
   нижним краем тонула в этом чёрном. */
/* ПОКА ПЕЧАТАЮТ круглой кнопки в полосе нет вовсе: человеку сейчас нужна
   клавиатура, а место кнопка забирает больше всех.

   Класс typing, а НЕ keyboard: второй снимается с задержкой ради растушёвки
   шва (разбор 07.09.2026), и всё, что на него повесят, унаследует чужую
   задержку — кнопка возвращалась через секунду после ухода клавиатуры.

   ИЗВЕСТНАЯ ПЛАТА, ПРИНЯТА СОЗНАТЕЛЬНО (17.09.2026): на один кадр кнопка
   мелькает посреди экрана. Высота слоя приходит от Telegram позже, чем уходит
   клавиатура, и в этот зазор полоса рисуется по кромке ещё короткого слоя.
   Перепробовано: ждать роста слоя — возвращается секунда задержки; не менять
   раскладку вовсе (visibility) — пропадает мигание, но поле ввода встаёт на
   70px выше и над клавиатурой висит пустое место. Выбран кадр мигания.
   Другого сигнала об уходе клавиатуры, кроме высоты слоя, не существует. */
.food-overlay.typing .food-bottom { gap: 0; padding-bottom: calc(var(--safe-bottom) + 32px); }
.food-overlay.typing .food-shutter,
.food-overlay.typing .food-mic-btn { display: none; }
.food-tabs {
  display: flex; gap: 2px; padding: 4px; border-radius: 999px;
  background: rgba(20, 24, 16, 0.45);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
}
.food-tab {
  border: none; cursor: pointer; font: inherit; font-size: 13px; font-weight: 600;
  padding: 8px 15px; border-radius: 999px; background: transparent;
  color: rgba(255, 255, 255, 0.72); white-space: nowrap;
  transition: background 160ms var(--ease-out), color 160ms var(--ease-out);
}
.food-tab.active { background: #fff; color: #1b1e16; }
/* Затвор и микрофон — одна кнопка по виду и по месту: на съёмке в слоте стоит
   первый, на тексте второй. Двух сразу не бывает — showFoodStep прячет ненужную
   через display, а не гасит: иначе в колонке стало бы две кнопки и пилюля
   уехала бы вверх. */
.food-shutter,
.food-mic-btn {
  border: 4px solid rgba(255, 255, 255, 0.45); cursor: pointer;
  width: 66px; height: 66px; border-radius: 50%; background: #fff;
  display: flex; align-items: center; justify-content: center;
  /* Иконка тёмная: кружок белый, и цвет ей даёт currentColor. Тот же тон, что
     у активного пункта переключателя, — они стоят рядом. */
  color: #1b1e16;
  transition: transform 140ms var(--ease-out);
}
/* На других режимах затвор ГАСНЕТ, а не убирается: место под него остаётся
   занятым. display: none подтягивал бы пилюлю вниз, и она прыгала бы с каждым
   переключением — а она тут единственный неподвижный ориентир. */
/* Размер иконки — по ЧЕРНИЛАМ, а не по её коробке. В исходных 24-точечных
   иконках глиф занимает не всю клетку (камера 19x16.7, микрофон 15.9x19.25),
   поэтому одинаковая коробка давала бы разный и мелкий рисунок. При 32px обе
   дают около 25px видимого глифа — 43% от внутреннего круга (58px), обычная
   доля для круглой кнопки такого размера. */
.food-shutter svg,
.food-mic-btn svg { width: 32px; height: 32px; }
.food-shutter.is-off { visibility: hidden; }
.food-shutter.is-gone,
.food-mic-btn.is-gone { display: none; }
.food-shutter:active,
.food-mic-btn:active { transform: scale(0.92); }

/* Панели режимов. Прижаты к низу свободной области — сразу над переключателем,
   чтобы он оставался на своём месте при любом режиме и чтобы поле ввода было
   у самой клавиатуры. Своей заливки нет: контраст даёт фон слоя. */
.food-step-voice,
.food-step-recent,
.food-step-review {
  position: relative; z-index: 2; margin-top: auto; min-height: 0;
  display: flex; flex-direction: column;
  padding: 16px var(--modal-pad-x) 4px;
}
/* Длинные панели дорастают до верхней кромки — крестик им не уступает места
   сам, он лежит отдельным слоем поверх. Отступ сверху и держит их под ним. */
.food-step-recent,
.food-step-review { padding-top: calc(var(--safe-top) + 52px); }
.food-panel-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; }
/* Строки «Недавнего» выше, чем в дневнике: 13px — шаг .settings-row, здешняя
   конвенция для списка, по которому тапают. */
#food-recent-list .meal-row { padding: 13px 0; }
/* Список прокручивается сам. Раньше прокруткой заведовала шторка целиком, но
   шторки больше нет: слой занят камерой и стоит на месте. Право на
   вертикальный жест возвращено явно (см. блок про touch-action ниже), а
   overscroll-behavior: contain не даёт жесту уйти на страницу под слоем. */
.food-recent-list,
.food-review-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }


.food-textarea-wrap { position: relative; }
.food-textarea {
  width: 100%; min-height: 100px; font: inherit; font-size: 13px; font-weight: 500;
  padding: 14px; border-radius: 16px; border: 1px solid var(--glass-border);
  background: var(--input-bg); color: var(--text-primary); resize: none;
}
/* Микрофон переехал из угла поля ввода в нижнюю полосу — на место затвора.
   Смысл тот же, что у затвора на съёмке: главное действие режима всегда под
   большим пальцем, в одной и той же точке экрана. */
.food-mic-btn.recording { background: #e8938c; animation: micPulse 1.1s ease-in-out infinite; }
@keyframes micPulse {
  0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(232, 147, 140, 0.45); }
  50% { transform: scale(1.12); box-shadow: 0 0 0 8px rgba(232, 147, 140, 0); }
}
.food-charcount { font-size: 11px; font-weight: 500; color: var(--text-tertiary); margin-top: 6px; }
.food-analyze-btn {
  width: 100%; margin-top: 12px; border: none; cursor: pointer; font: inherit; font-size: 13px;
  font-weight: 700; padding: 13px 0; border-radius: 14px; background: var(--dot-color); color: var(--on-accent);
  transition: transform 140ms var(--ease-out), opacity 160ms ease;
}
.food-analyze-btn:disabled { opacity: 0.5; }
.food-analyze-btn:active { transform: scale(0.97); }

.food-review-row { padding: 12px 14px; border-radius: 14px; background: var(--chip-bg); margin-bottom: 8px; }
.food-review-row-top { display: flex; align-items: center; gap: 8px; }
.food-review-name {
  flex: 1; min-width: 0; font: inherit; font-size: 13px; font-weight: 600;
  color: var(--text-primary); background: transparent; border: none; padding: 0;
  resize: none; overflow: hidden; white-space: pre-wrap; word-break: break-word; line-height: 1.35;
}
.food-review-name:focus { outline: none; }
.food-review-remove {
  width: 24px; height: 24px; flex-shrink: 0; border: none; border-radius: 50%;
  background: transparent; color: var(--text-tertiary); font-size: 15px; line-height: 1;
  cursor: pointer; transition: opacity 0.2s ease, transform 0.15s ease;
}
.food-review-remove:active { transform: scale(0.88); }
.food-review-row-meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 8px;
  font-size: 13px; font-weight: 500; color: var(--text-secondary);
}
.food-review-grams-control { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
.food-gram-step {
  width: 27px; height: 27px; flex-shrink: 0; border-radius: 50%; border: none; background: transparent;
  color: var(--text-tertiary); font-size: 17px; line-height: 1; cursor: pointer;
  transition: opacity 0.2s ease, transform 0.15s ease;
}
.food-gram-step:active { transform: scale(0.88); }
.food-review-grams-control .text-input {
  width: 50px; flex: none; text-align: center; padding: 5px 2px; border-radius: 8px; font-size: 15px;
  -moz-appearance: textfield;
}
.food-review-grams-control .text-input::-webkit-inner-spin-button,
.food-review-grams-control .text-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.food-review-low-confidence { flex-basis: 100%; font-size: 11px; color: #e0a45a; margin-top: 2px; }
.food-review-totals { display: flex; justify-content: space-between; padding: 12px 4px; font-size: 13px; font-weight: 700; border-top: 1px solid var(--glass-border); margin-top: 4px; }
.food-save-btn {
  width: 100%; margin-top: 12px; border: none; cursor: pointer; font: inherit; font-size: 13px;
  font-weight: 700; padding: 13px 0; border-radius: 14px; background: var(--dot-color); color: var(--on-accent);
  transition: transform 140ms var(--ease-out);
}
.food-save-btn:active { transform: scale(0.97); }
/* Общий спиннер (анализ текста, расшифровка голоса, сохранение) — слой поверх
   всего экрана: шторки, под которой он раньше стоял блоком, больше нет. */
.food-loading {
  position: absolute; inset: 0; z-index: 5; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 12px; text-align: center;
  background: rgba(10, 13, 6, 0.55); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  color: #fff; font-size: 13px; font-weight: 500;
}
.food-loading[hidden] { display: none; }
.food-spinner { width: 28px; height: 28px; border-radius: 50%; border: 3px solid var(--chip-bg); border-top-color: var(--dot-color); animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.food-review-photo { width: 100%; border-radius: 16px; margin-bottom: 12px; object-fit: cover; max-height: 200px; }
.food-review-photo[hidden] { display: none; }

/* ===== Модалка календаря (line 710-742) ===== */
/* fixed, а не absolute: слой лежит внутри .page, а у неё потолок ширины 430px
   (самый широкий iPhone на момент, когда правило писали). На экранах шире —
   16 Pro Max это 440pt — страница уже экрана, и по бокам оставались
   НЕзатемнённые полосы во всю высоту. Видно только при открытой модалке и
   только на широком телефоне: на 375 не воспроизводится. Найдено 27.08.2026
   замером на ширине 440. Расширить слой, оставаясь absolute, нельзя —
   у .page стоит overflow: hidden, он обрежет. Предков с transform/filter
   над ним нет, поэтому fixed отрабатывает как задумано. */
.calendar-overlay {
  position: fixed; inset: 0; z-index: 6; background: rgba(10, 13, 6, 0.55);
  backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center;
  padding: 20px; animation: backdropIn 200ms var(--ease-out);
}
.calendar-overlay[hidden] { display: none; }

/* Прокрутка страницы под открытой модалкой.
   Потянув пальцем за затемнённый фон (или за поля модалки), можно было
   прокрутить и «отбаунсить» весь экран под ней — модалка стоит на месте, а
   содержимое за ней едет. touch-action: none на подложке говорит движку, что
   жест здесь не прокручивает ничего; прокручиваемым областям внутри право на
   вертикальный жест возвращаем явно, а overscroll-behavior: contain не даёт
   докрученному до края списку передать жест дальше, наружу.
   Тот же слой для опросника: он полноэкранный, но и его фон тянулся. */
.calendar-overlay,
.food-overlay,
.onb { touch-action: none; }
.calendar-modal,
.food-review-list,
.food-recent-list,
.weight-history-full,
.onb-body {
  touch-action: pan-y;
  overscroll-behavior: contain;
}
/* Прижатая к верху модалка — для тех, что открываются вместе с клавиатурой.
   Центрированная уезжала бы вслед за тем, как Telegram ужимает раскладку под
   клавиатуру; у прижатой к верху отступ сверху постоянный, и она остаётся на
   месте. Плюс она лежит ВНЕ скролл-контейнера, поэтому прокручивать ничего не
   нужно — а именно прокрутка и её возврат давали рывки у прежнего варианта со
   строкой внутри карточки (разбор в webapp/RENDERING-NOTES.md). */
.calendar-overlay.overlay-top { align-items: flex-start; padding-top: 56px; }

/* Прижатая к НИЗУ — для ввода, который идёт с клавиатуры. Верхний край был
   вынужденной мерой: центрированная модалка уезжала вслед за тем, как Telegram
   ужимает раскладку, и её прибили к верху, чтобы она стояла на месте. Цена —
   поле ввода оказывалось на другом конце экрана от клавиатуры и от пальца
   (замечание пользователя 11.09.2026).
   Теперь высота слоя берётся из переменной Telegram, как у листа еды: слой
   ужимается синхронно с клавиатурой, и модалка едет вместе с ней, оставаясь
   прямо над ней. Разбор приёма — RENDERING-NOTES, разделы 10 и 17. */
.calendar-overlay.overlay-bottom { align-items: flex-end; padding: 0; }
.in-telegram .calendar-overlay.overlay-bottom { height: var(--tg-viewport-height); }
.calendar-overlay.overlay-bottom .calendar-modal {
  width: 100%; max-width: none; border-radius: 24px 24px 0 0;
  /* Отступ снизу НЕ МЕНЬШЕ высоты шва (26px у .keyboard::after), иначе
     растушёвка ложится на кнопки и они выглядят затемнёнными снизу
     (замечено пользователем 11.09.2026 на устройстве: у листа еды этот зазор
     есть по разметке, а здесь его пришлось задать явно). */
  padding-bottom: calc(var(--safe-bottom) + 30px);
}

.water-modal { max-width: 320px; }
.water-modal .water-custom-row { margin-top: 0; }
.water-modal .text-input { font-size: 17px; padding: 12px 4px; }
.water-modal-actions { display: flex; gap: 8px; margin-top: 14px; }
.water-modal-actions .chip-btn { flex: 1; text-align: center; padding: 11px 0; font-size: 13px; }
/* В живом референсе (Home Redesign.dc.html) свойство background:{{ modalBg }}
   у модалок не подставляется их шаблонизатором и выпадает из отрендеренного
   style целиком (проверено через DOM: background отсутствует в атрибуте) —
   на практике модалка календаря и модалка еды там рендерятся без фона,
   прозрачными. Убрано здесь, чтобы совпадало с тем, что реально показывает
   референс, а не с тем, что написано в исходнике прототипа. */
.calendar-modal {
  position: relative;
  width: 100%; max-height: 100%; overflow-y: auto; border-radius: 26px;
  padding: 18px var(--modal-pad-x) 16px; color: var(--text-primary);
  animation: modalPopIn 220ms var(--ease-out);
}
.calendar-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
/* Стрелка «назад» стоит слева от заголовка, крестик «закрыть» — справа.
   Так же устроены экраны профиля в хендоффе. */
.calendar-header.has-back { justify-content: flex-start; }
/* Навигация по месяцам в календаре — то же правило, что и у .day-nav. Отдельный
   класс, а не .calendar-header целиком: тот же класс держит шапку списочных
   модалок, где заголовок слева, а крестик справа, её центрировать нельзя. */
.calendar-header.cal-month-nav { justify-content: center; gap: 4px; }
/* Стрелки месяца — без круглой подложки, только значок: она делала из простого
   переключения два тяжёлых пятна по бокам от названия. Правило узкое, на
   .cal-month-nav: тот же класс .cal-nav носят шесть кнопок-крестиков в
   модалках, и там круг нужен — он единственное, что отличает крестик от текста.
   Зона нажатия остаётся 30x30 от самой кнопки, поэтому ей добавлен тот же
   невидимый слой 44x44, что и стрелкам дня: без круга по кнопке ещё труднее
   попасть, чем с ним. */
.cal-month-nav .cal-nav {
  position: relative;
  background: none;
  color: var(--text-secondary);
}
.cal-month-nav .cal-nav::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.calendar-header.has-back .cal-nav { order: -1; }
.cal-nav {
  /* flex: none обязателен: кнопка — флекс-элемент, и при длинном соседнем
     заголовке (двухстрочное название блюда в оверлее меню) она сжималась
     по ширине ниже 30px, а высота оставалась — круг превращался в эллипс. */
  flex: none;
  border: none; cursor: pointer; width: 30px; height: 30px; border-radius: 50%; background: var(--chip-bg);
  color: var(--text-primary); display: flex; align-items: center; justify-content: center; font-size: 15px;
  transition: transform 120ms var(--ease-out);
}
.cal-nav:active { transform: scale(0.9); }
.cal-nav:disabled { opacity: 0.3; cursor: default; }
/* Сброс фильтров стоит в шапке модалки, слева от крестика: раньше это была
   широкая кнопка в самом низу прокручиваемого тела, и при длинном списке
   тегов она уезжала за нижний край — о ней нельзя было догадаться. В шапке
   она на виду всегда и не спорит с содержимым. Круг и размер — общие с
   крестиком (.cal-nav), поэтому своего у неё только значок. */
#menu-overlay-reset { margin-left: auto; }
#menu-overlay-reset svg { width: 15px; height: 15px; }
/* Обязательно: .cal-nav задаёт display: flex, а авторский display сильнее
   браузерного [hidden] { display: none } — атрибут ставился, но кнопка
   оставалась на экране: и при пустом фильтре, и в чужих модалках. */
#menu-overlay-reset[hidden] { display: none; }
.cal-month-label { font-size: 15px; font-weight: 700; }
.calendar-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; font-size: 11px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: var(--track-caps-md); margin-bottom: 6px; }
.calendar-grid { display: flex; flex-direction: column; }
.calendar-week { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 4px; }
.calendar-cell {
  aspect-ratio: 1; border: none; font: inherit; border-radius: 50%; font-size: 13px; font-weight: 700;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  transition: transform 120ms var(--ease-out), background 160ms ease;
}
.calendar-cell:active { transform: scale(0.9); }
.calendar-cell:disabled { cursor: default; }

/* Состояния ячеек — цвета и приоритет 1:1 с логикой в Home Redesign.dc.html
   (renderVals: bg/textColor per cell). background:transparent + цвет по
   pct-бакету по умолчанию; cal-in-range перекрашивает только фон;
   cal-range-start/end (край диапазона) перекрывает всё — и фон, и цвет —
   поэтому их правило идёт последним в каскаде. */
.calendar-cell.cal-empty { background: transparent; color: transparent; cursor: default; }
.calendar-cell.cal-day { background: transparent; color: var(--text-tertiary); }
.calendar-cell.cal-pct-green { color: var(--dot-color); }
.calendar-cell.cal-pct-yellow { color: var(--macro-fat); }
.calendar-cell.cal-pct-red { color: #e8938c; }
.calendar-cell.cal-disabled { color: var(--text-tertiary); cursor: default; opacity: 0.5; }
.calendar-cell.cal-in-range { background: rgba(200, 242, 74, 0.22); }
.calendar-cell.cal-range-start, .calendar-cell.cal-range-end {
  background: var(--dot-color);
  color: var(--on-accent);
}
.calendar-legend { display: flex; flex-wrap: wrap; gap: 12px; margin: 12px 0 14px; font-size: 10px; font-weight: 500; color: var(--text-secondary); }
.legend-item { display: flex; align-items: center; gap: 5px; }
.legend-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.legend-green { background: var(--dot-color); }
.legend-yellow { background: var(--macro-fat); }
.legend-red { background: #e8938c; }
.calendar-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding-top: 12px; border-top: 1px solid var(--glass-border); }
.calendar-selection { font-size: 13px; font-weight: 600; }
/* Слишком длинный период: подпись становится предупреждением, а «Показать»
   гаснет. Отказ объясняется прямо в том месте, где виден выбор. */
.calendar-selection.over-limit {
  color: var(--miss-color);
  /* Мельче обычной подписи: в колонке рядом с кнопками ~200px, и на 13px
     предупреждение расползалось на три строки, раздувая футер. */
  font-size: 11px; line-height: 1.3;
}
.cal-btn:disabled { opacity: 0.4; cursor: default; }
.calendar-actions { display: flex; gap: 8px; }
.cal-btn { border: none; cursor: pointer; font: inherit; font-size: 13px; padding: 9px 16px; border-radius: 12px; transition: transform 120ms var(--ease-out); }
.cal-btn:active { transform: scale(0.96); }
.cal-btn-ghost { font-weight: 600; background: var(--chip-bg); color: var(--text-primary); }
.cal-btn-primary { font-weight: 700; background: var(--dot-color); color: var(--on-accent); }

/* Вторичные модалки бота — часовой пояс, покупки/избранное меню: своих
   пиксель-спеков в хендоффе нет (там эти экраны декоративны/не в scope),
   переиспользуют .calendar-modal паттерн. */
.list-modal-body { border-radius: 14px; overflow: hidden; }

/* ===== Toast ===== */
.toast {
  position: absolute; top: calc(var(--safe-top) + 14px); left: 50%;
  transform: translateX(-50%) translateY(-12px); padding: 10px 18px; border-radius: 999px;
  background: var(--modal-bg); color: var(--text-primary); font-size: 13px; font-weight: 600;
  opacity: 0; pointer-events: none; transition: opacity 0.25s ease, transform 0.25s ease; z-index: 20;
}
.toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Кнопка «в начало» в модалке. Появляется, когда список промотан заметно вниз;
   держится у нижнего края модалки, а не уезжает с содержимым.
   Фон непрозрачный с блюром: кнопка парит над содержимым, и сквозь неё не должен
   просвечивать текст. Место под неё резервируется паддингом снизу (см.
   .has-to-top), иначе она накрывала кнопки последней строки списка. */
.modal-to-top {
  position: absolute; right: 14px; bottom: 14px; z-index: 2;
  width: 36px; height: 36px; border-radius: 50%; padding: 0; cursor: pointer;
  border: 1px solid var(--glass-border); background: var(--modal-bg);
  backdrop-filter: blur(12px) saturate(160%);
  -webkit-backdrop-filter: blur(12px) saturate(160%);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
  color: var(--text-primary);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transform: translateY(6px);
  transition: opacity 160ms var(--ease-out), transform 160ms var(--ease-out);
}
/* Класс вешается вместе с показом кнопки: добавление паддинга увеличивает
   scrollHeight, но не двигает scrollTop, поэтому содержимое не прыгает. */
.weight-history-full.has-to-top { padding-bottom: 58px; }
.modal-to-top.visible { opacity: 1; pointer-events: auto; transform: none; }
.modal-to-top:active { transform: scale(0.9); }
.modal-to-top svg { width: 17px; height: 17px; }
@media (prefers-reduced-motion: reduce) { .modal-to-top { transition: none; } }

.no-scrollbar::-webkit-scrollbar { width: 0; height: 0; display: none; }
.no-scrollbar { scrollbar-width: none; }

/* ===== Карточка блюда =====
   Один экран на три источника: библиотеку рецептов, меню и избранное. Полный
   экран, а не модалка: блоков шесть, они не помещаются, а кнопка записи нужна
   на любом месте прокрутки. Фон тот же, что у приложения, — иначе слой на весь
   экран выглядит выцветшим рядом с остальным (та же причина, что у .onb). */
.dish-card {
  position: fixed; inset: 0; z-index: 60;
  display: flex; flex-direction: column;
  background-color: var(--bg-base);
  /* Те же правила, что у .page: первый слой (зерно) повторяется в своём
     натуральном размере, растягивается только градиент. С background-size:
     cover растягивались ОБА, и зерно превращалось в крупные пятна. */
  background-image: var(--blur-bg);
  background-attachment: fixed;
  background-size: auto, 100vw var(--bg-h, 100dvh);
  background-repeat: repeat, no-repeat;
  color: var(--text-primary);
  touch-action: none;
}
.dish-card[hidden] { display: none; }
.dish-card-top {
  display: flex; align-items: center; gap: 12px;
  /* Свой верхний отступ: .dish-card лежит fixed inset:0, отступы .page до него
     не доходят. Без этого в полноэкранном режиме шапка уходит под кнопки. */
  padding: calc(var(--safe-top) + 8px) var(--card-pad-x) 10px;
  /* Шапка не уезжает с прокруткой: со стрелкой назад в середине длинного
     рецепта человек иначе теряется. */
  flex: none;
}
.dish-card-title { flex: 1; text-align: center; font-size: 15px; font-weight: 700; }
.dish-card-back {
  flex: none; width: 30px; height: 30px; border: none; background: none;
  color: var(--text-primary); cursor: pointer; display: flex;
  align-items: center; justify-content: center;
  transition: transform 120ms var(--ease-out);
}
.dish-card-back:active { transform: scale(0.9); }
.dish-card-fav.active { background: var(--dot-color); color: var(--on-accent); }
/* Две звезды на кнопку: пока блюдо не в избранном — контурная (Iconly Light),
   когда в избранном — залитая с косой чертой (Iconly Bold «star remove»).
   Заливка сообщает состояние, черта подсказывает, что нажатие уберёт.
   Классы .fav-add / .fav-remove названы по действию нажатия.
   Две готовые иконки вместо перерисовки одной — так же, как у
   .meals-chevron: переключение через display, никаких transform. */
.dish-card-fav .fav-remove, .recipe-fav .fav-remove { display: none; }
.dish-card-fav.active .fav-add, .recipe-fav.active .fav-add { display: none; }
.dish-card-fav.active .fav-remove, .recipe-fav.active .fav-remove { display: block; }
.dish-card-body {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 0 var(--card-pad-x) 16px;
  touch-action: pan-y; overscroll-behavior: contain;
  /* Затухание у краёв включает тот же наблюдатель, что и у прочих списков
     (.fade-top/.fade-bottom навешивает app.js). Своих правил не нужно: маска
     общая, а фон под ней даёт родительский .dish-card. */
}
/* Кнопка прижата к низу и не уезжает: записать блюдо человек хочет и с середины
   рецепта. Отступ снизу — под домашнюю полосу iPhone. */
.dish-card-foot {
  flex: none; padding: 10px var(--card-pad-x) calc(10px + var(--safe-bottom));
}
.dish-card-save { font-size: 15px; padding: 15px 0; }

/* Герой: фото во всю ширину, название и цифры поверх него. */
.dish-hero {
  position: relative; border-radius: 20px; overflow: hidden;
  min-height: 200px; display: flex; align-items: flex-end;
  background: var(--chip-bg);
}
.dish-hero img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Миниатюра вместо ещё не доехавшей полной картинки. Размытие обязательно: это
   квадратный кроп в 160px, растянутый на широкую шапку, и без него он читается
   как испорченная картинка. Небольшое увеличение — чтобы размытые края не
   показывали фон из-под себя. Переход только по прозрачности размытия: подмена
   адреса на уже раскодированную картинку проходит в один кадр. */
.dish-hero img.dish-hero-blur {
  filter: blur(12px); transform: scale(1.08);
}
@media (prefers-reduced-motion: reduce) { .dish-hero img.dish-hero-blur { transform: none; } }
/* Затемнение снизу: белый текст поверх светлой еды иначе не читается. */
.dish-hero::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(8, 11, 5, .82) 0%, rgba(8, 11, 5, .35) 45%, transparent 100%);
}
.dish-hero-text { position: relative; z-index: 1; padding: 16px; width: 100%; }
/* Названия рецептов лежат в базе строчными — поднимаем первую букву, как в
   списках (.recipe-name, .recipe-tile-name). */
.dish-hero-name { font-size: 21px; font-weight: 700; line-height: 1.2; }
.dish-hero-name::first-letter { text-transform: uppercase; }
.dish-hero-meta { display: flex; gap: 16px; margin-top: 10px; font-size: 13px; font-weight: 600; }
.dish-hero-meta span { display: flex; align-items: center; gap: 5px; }
.dish-desc { margin-top: 14px; font-size: 14px; line-height: var(--lh-text); color: var(--text-secondary); }
.dish-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.dish-section-title { font-size: 17px; font-weight: 700; margin: 22px 0 10px; }
/* Строка КБЖУ порции: четыре колонки, как в референсе. */
.dish-macros { display: flex; gap: 18px; }
.dish-macros div { min-width: 0; }
.dish-macros dt { font-size: 12px; color: var(--text-secondary); }
.dish-macros dd { margin: 2px 0 0; font-size: 17px; font-weight: 700; }
.dish-portion { display: flex; gap: 10px; margin-top: 14px; }
.dish-portion .text-input { font-size: 16px; text-align: center; }
.dish-portion-count { flex: none; width: 86px; }
.dish-portion-note { flex: 1; display: flex; align-items: center; padding: 0 14px;
  border-radius: 12px; border: 1px solid var(--glass-border); background: var(--chip-bg);
  font-size: 14px; font-weight: 600; }
/* Ингредиент: название сверху, калории и вес — строкой ниже. */
.dish-part { padding: 10px 0; border-top: 1px solid var(--glass-border); }
/* Первый после заголовка, а не первый ребёнок: в потоке перед ним стоит
   .dish-section-title, и правило на :first-child не срабатывало. */
.dish-section-title + .dish-part { border-top: none; }
.dish-part-name { font-size: 15px; font-weight: 700; }
.dish-part-name::first-letter { text-transform: uppercase; }
.dish-part-meta { font-size: 13px; color: var(--text-secondary); margin-top: 3px; }
/* Кружок с номером — ровно по середине текста шага по высоте (просьба
   15.09.2026). Это отменяет прежнее выравнивание по первой строке от
   09.09.2026: тогда у многострочного шага номер стоял у первой строки, чтобы
   читаться как номер абзаца. Теперь одинаково для шага любой длины.

   Центрирование даёт сам flex (align-items: center), а поправку margin-top у
   текста пришлось убрать: она поднимала буквы на полуинтерлиньяж ради
   выравнивания их верха с верхом кружка. При центрировании она бы сместила
   текстовую коробку вниз, и центр уехал бы ровно на половину этой поправки. */
.dish-step { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
/* Кружок чуть выше строки текста (21px при кегле 14), а не вдвое, как было при
   34px: у шага в одну строку такой кружок висел рядом с пустотой и выглядел
   отдельно от него. */
.dish-step-num {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  border: 1.5px solid var(--dot-color); color: var(--dot-color);
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 700;
}
.dish-step-text { font-size: 14px; line-height: var(--lh-text); }
/* Пищевая ценность: подпункты с отступом, как в референсе. */
.dish-nutri { display: flex; justify-content: space-between; gap: 12px;
  padding: 11px 0; border-top: 1px solid var(--glass-border); font-size: 14px; }
.dish-nutri-note + .dish-nutri { border-top: none; }
.dish-nutri.sub { padding: 7px 0 7px 16px; border-top: none; color: var(--text-secondary); font-size: 13px; }
.dish-nutri b { font-weight: 700; }
.dish-nutri-note { margin-top: 4px; font-size: 12px; color: var(--text-tertiary); }

/* ===== Опросник (onboarding.js) =====
   Слой поверх всего приложения: пока анкета не пройдена, вкладки и нижнее меню
   недоступны — иначе человек попадает на пустую Главную, где нечего смотреть.
   Фон непрозрачный (не стеклянный): под ним лежит интерфейс, и просвечивающие
   цифры чужого экрана мешали бы читать вопрос. */
.onb {
  position: fixed; inset: 0; z-index: 60;
  display: flex; flex-direction: column;
  /* Тот же фон, что у html и карточек: плоская заливка + запечённый градиент
     (--blur-bg). Со сплошным цветом слой выглядел выцветшим рядом с остальным
     приложением — тут виден весь экран разом, и однотонность бросается. */
  background-color: var(--bg-base);
  background-image: var(--blur-bg);
  background-size: auto, 100vw var(--bg-h, 100dvh);
  background-repeat: repeat, no-repeat;
  padding: calc(var(--safe-top) + var(--onb-gap, 14px)) 22px
           calc(var(--safe-bottom) + 18px);
  color: var(--text-primary);
}
.onb[hidden] { display: none; }
/* ВЫСОТА ОТ TELEGRAM, как у .food-overlay. Опросник — сплошные поля ввода, и
   без этого он вёл себя ровно как модалка еды до правки 07.09.2026: Telegram
   обновляет свои переменные через 158 мс, а сам вьюпорт ужимается только через
   575, и слой догонял клавиатуру вторым шагом спустя 420 мс. Переменная
   приходит первой — берём её.
   Вне Telegram правило не действует: SDK ставит переменную и там, но она
   замирает на высоте при загрузке, и слой перестал бы ужиматься под
   клавиатуру. */
.in-telegram .onb { height: var(--tg-viewport-height); }

/* Полноэкранный режим: срезаем СВОЙ запас над содержимым. Зону под вырез и
   кнопки клиента не трогаем — она приходит от него и потому верна на любом
   устройстве. Класс ставит app.js по событию fullscreenChanged. */
.fullscreen { --top-gap: 8px; --onb-gap: 6px; }

/* Полоса прогресса центрируется по экрану, а не по остатку строки: стрелка
   лежит поверх слева, поэтому её появление и исчезновение полосу не двигает. */
.onb-top { position: relative; display: flex; justify-content: center; align-items: center;
  flex: none; height: 30px; }
.onb-back {
  position: absolute; left: 0; top: 0;
  width: 30px; height: 30px; border: none; background: none;
  color: var(--text-primary); font: inherit; font-size: 17px; cursor: pointer;
  opacity: 0.75; transition: opacity 160ms ease, transform 120ms var(--ease-out);
}
.onb-back:active { transform: scale(0.9); }
.onb-back[hidden] { visibility: hidden; display: block; }  /* место держим, кнопка не прыгает */

.onb-progress { width: 62%; height: 3px; border-radius: 3px; background: var(--chip-bg); overflow: hidden; }
.onb-progress-fill {
  height: 100%; width: 0; border-radius: 3px; background: var(--dot-color);
  transition: width 420ms var(--ease-out);
}

/* Тело шага. Центрируем по вертикали: у шагов разная высота (выбор из шести
   вариантов против одного поля ввода), и при верхнем выравнивании вопрос
   прыгал бы по экрану от шага к шагу. */
.onb-body {
  flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; justify-content: center; gap: 18px;
  padding: 20px 0;
}
.onb-step { display: flex; flex-direction: column; gap: 18px; }
.onb-step.center { text-align: center; }
/* Появление шага: сдвиг на 12px и проявление. Направление зависит от того,
   идём мы вперёд или назад — иначе «Назад» ощущается как ещё один шаг вперёд. */
@keyframes onbInForward { from { opacity: 0; transform: translateX(18px); } }
@keyframes onbInBack { from { opacity: 0; transform: translateX(-18px); } }
.onb-step.forward { animation: onbInForward 260ms var(--ease-out) both; }
.onb-step.back { animation: onbInBack 260ms var(--ease-out) both; }

.onb-caption {
  font-size: 11px; font-weight: 600; letter-spacing: var(--track-caps-md);
  text-transform: uppercase; color: var(--text-secondary);
}
.onb-title { font-size: 20px; font-weight: 700; line-height: var(--lh-ui); }
.onb-hint { font-size: 13px; font-weight: 500; line-height: var(--lh-text); color: var(--text-secondary); }

/* Варианты ответа. Крупная зона нажатия: это основной элемент опросника, по
   нему бьют пальцем на ходу. */
.onb-options { display: flex; flex-direction: column; gap: 8px; }
.onb-option {
  width: 100%; border: 1px solid transparent; background: var(--chip-bg);
  color: var(--text-primary); font: inherit; font-size: 15px; font-weight: 600;
  text-align: left; padding: 15px 16px; border-radius: 16px; cursor: pointer;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  transition: background 160ms ease, border-color 160ms ease, transform 120ms var(--ease-out);
}
.onb-option:active { transform: scale(0.98); }
.onb-option.active { background: var(--dot-color); color: var(--on-accent); border-color: var(--dot-color); }
.onb-option-main { display: flex; flex-direction: column; gap: 3px; }
.onb-option-sub { font-size: 12px; font-weight: 500; opacity: 0.7; }
.onb-options.two-col { display: grid; grid-template-columns: 1fr 1fr; grid-auto-rows: 1fr; }
.onb-options.two-col .onb-option { justify-content: center; text-align: center; }
/* Пятый вариант из пяти иначе висит половинкой — тот же приём, что у .chip-list. */
.onb-options.two-col .onb-option:last-child:nth-child(odd) { grid-column: 1 / -1; }

/* Числовой ввод. 17px — единственное место в приложении, где поле крупнее
   обычных 13px: здесь оно главный объект экрана, а не строка в списке.
   16px и выше ещё и не даёт Safari зумить страницу при фокусе. */
/* min-width: 0 на единице тоже: без неё «кг» не сжимается и вылезает за
   правый край, когда поле занимает всю ширину строки. */
.onb-number-row {
  display: flex; align-items: baseline; justify-content: center; gap: 8px;
  width: min(220px, 70%); margin: 4px auto 0;
}
.onb-number-unit { flex: none; }
.onb-number {
  flex: 1; min-width: 0; border: none; border-bottom: 2px solid var(--glass-border);
  background: none; color: var(--text-primary); font: inherit;
  font-size: 28px; font-weight: 700; padding: 6px 0; border-radius: 0;
  text-align: center;
  transition: border-color 200ms ease;
}
.onb-number:focus { outline: none; border-bottom-color: var(--dot-color); }
.onb-number-unit { font-size: 15px; font-weight: 600; color: var(--text-secondary); }
.onb-error { font-size: 12px; font-weight: 600; color: var(--miss-color);
  min-height: 15px; text-align: center; }

/* Итог расчёта. Числа появляются по очереди — короткая пауза между строками
   читается как «посчитали», а не «нарисовали заранее». */
.onb-result { display: flex; flex-direction: column; gap: 10px; text-align: center; }
.onb-result-kcal { font-size: 38px; font-weight: 700; line-height: var(--lh-tight); }
.onb-macros { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.onb-macro { background: var(--chip-bg); border-radius: 14px; padding: 12px; text-align: center; }
.onb-macro-name {
  font-size: 10px; font-weight: 600; letter-spacing: var(--track-caps-sm);
  text-transform: uppercase; color: var(--text-secondary);
}
.onb-macro-value { font-size: 20px; font-weight: 700; margin-top: 4px; }
@keyframes onbRise { from { opacity: 0; transform: translateY(10px); } }
.onb-rise { animation: onbRise 380ms var(--ease-out) both; }



/* Иллюстрации: рисуем их SVG-кодом, а не картинками. Растровая картинка
   тащит собственный фон и разошлась бы с темой — здесь же цвет берётся из
   тех же переменных, что и весь интерфейс, поэтому день и ночь совпадают
   всегда. Решение пользователя, 28.08.2026. */
.onb-art { display: flex; justify-content: center; padding: 4px 0; }
.onb-art svg { width: min(58vw, 220px); height: auto; }
.onb-art .onb-art-track { stroke: var(--glass-border); }
.onb-art .onb-art-arc { stroke: var(--dot-color); }
.onb-art .onb-art-soft { stroke: var(--text-secondary); }
.onb-art .onb-art-fill { fill: var(--dot-color); }
@keyframes onbDraw { from { stroke-dashoffset: var(--dash); } to { stroke-dashoffset: 0; } }
.onb-art .draw { animation: onbDraw 900ms var(--ease-out) both; }

.onb-actions { flex: none; display: flex; align-items: center; gap: 10px; padding-top: 12px; }
.onb-next {
  flex: 1; border: none; background: var(--dot-color); color: var(--on-accent);
  font: inherit; font-size: 15px; font-weight: 700; padding: 15px 0;
  border-radius: 16px; cursor: pointer;
  transition: opacity 160ms ease, transform 120ms var(--ease-out);
}
.onb-next:active { transform: scale(0.98); }
.onb-next:disabled { opacity: 0.45; cursor: default; }
/* Пропуск — под самим вопросом, а не в нижней полосе, где он делил её надвое
   и читался как равноправный с «Далее». Кнопка, а не подчёркнутая ссылка:
   подчёркивание в приложении больше нигде не используется и выглядело
   чужеродно, а мишень у ссылки мельче. Решение пользователя, 28.08.2026. */
.onb-inline-skip {
  align-self: center; border: none; background: var(--chip-bg);
  color: var(--text-primary); font: inherit; font-size: 14px; font-weight: 600;
  padding: 12px 22px; border-radius: 14px; cursor: pointer;
  transition: background 160ms ease, transform 120ms var(--ease-out);
}
.onb-inline-skip:active { transform: scale(0.97); }
.onb-skip { display: none; }

/* Своё ограничение: чип и это поле взаимоисключающие (см. onboarding.js) —
   как на экране питания в профиле. */
.onb-custom,
.onb-text-input {
  width: 100%; border: 1px solid var(--glass-border); background: var(--chip-bg);
  color: var(--text-primary); font: inherit; font-size: 16px; font-weight: 500;
  padding: 12px 14px; border-radius: 14px;
}
.onb-custom:focus,
.onb-text-input:focus { outline: none; border-color: var(--dot-color); }
.onb-custom::placeholder,
.onb-text-input::placeholder { color: var(--text-tertiary); }

/* Уважаем системную настройку: анкету проходят один раз, но человеку с
   вестибулярной чувствительностью двенадцать выездов подряд неприятны. */
@media (prefers-reduced-motion: reduce) {
  .onb-step.forward, .onb-step.back, .onb-rise, .onb-art .draw { animation: none; }
  .onb-progress-fill { transition: none; }
}

/* ---- Библиотека рецептов ------------------------------------------------ */
/* Переключатель режимов — тот же .mode-switch, что во вкладке «Статистика»:
   скользящая подложка, одинаковые зазоры, пересчёт при повороте. Свой вариант
   был лишним.
   flex: none обязателен: вкладка выложена колонкой на flex, и без запрета на
   сжатие переключатель ужимался до 10px при кнопках в 18 — текст обрезался
   overflow: hidden, и на экране оставалась пустая полоска. */
#menu-mode { flex: none; }
/* min-width: 0 самой вкладке: она элемент сетки, а у него по умолчанию
   min-width: auto — вкладка растёт под ширину содержимого. Скролл-контейнер
   внутри вытянут на поля (.scroll-y), и вкладка раздувалась до 384 при экране
   375, съезжая вправо вместе с поиском и переключателем. */
#view-menu { min-width: 0; }
/* Панель рецептов устроена как #menu-content: колонка с min-height: 0, иначе
   #recipes-scroll ничем не ограничен по высоте и вертикальный скролл не
   работает вовсе — список просто уезжает под навигацию.
   :not([hidden]) обязателен: авторский display сильнее браузерного
   [hidden]{display:none}, и скрытая панель осталась бы на экране. */
#recipes-pane:not([hidden]) { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#recipes-scroll { flex: 1; min-height: 0; }

/* stretch, а не center: у поля кегль 16px и своя рамка, у кнопки — иконка и
   кегль 13px, и по своему содержимому она выходит ниже поля. Пусть тянется. */
.recipes-search-row { display: flex; gap: 8px; align-items: stretch; margin-bottom: 12px; }
/* Поле и кнопка — штатные .text-input и .chip-btn, здесь только раскладка.
   16px обязательны: на меньшем кегле iOS зумит страницу при фокусе (тот же
   случай, что у .fav-search). */
.recipes-search { position: relative; flex: 1; display: flex; align-items: center; }
.recipes-search-icon {
  position: absolute; left: 12px; pointer-events: none;
  color: var(--text-secondary); opacity: .7;
}
.recipes-search .text-input { padding-left: 36px; font-size: 16px; }
.recipes-search .text-input::-webkit-search-cancel-button { display: none; }
.recipes-filter-btn {
  position: relative; flex: none; display: flex;
  align-items: center; justify-content: center;
  padding: 0 14px;
  background: var(--dot-color); color: var(--on-accent);
}
/* Кнопка залита акцентом всегда — это заметный вход в фильтр, а не служебная
   иконка; применённые фильтры отмечает точка, а не смена заливки. */
/* Внутри подборки фильтра нет — кнопку убираем, поле поиска (flex: 1) занимает
   освободившееся место само. Правило обязательно: авторский display сильнее
   браузерного [hidden] { display: none }, и кнопка осталась бы на экране. */
.recipes-filter-btn[hidden] { display: none; }
.recipes-filter-dot {
  position: absolute; top: 6px; right: 7px; width: 7px; height: 7px;
  border-radius: 50%; background: var(--on-accent);
  box-shadow: 0 0 0 2px var(--dot-color);
}

#recipes-scroll { padding-bottom: 8px; }
.recipe-card {
  display: flex; gap: 12px; align-items: center;
  padding: 10px 0; border-top: 1px solid var(--glass-border); cursor: pointer;
}
#recipes-list .recipe-card:first-child { border-top: none; }
.recipe-thumb {
  width: 64px; height: 64px; flex: none; border-radius: 12px;
  object-fit: cover; background: rgba(128, 128, 128, .15);
}
.recipe-body { min-width: 0; }
.recipe-name { font-size: 15px; font-weight: 700; }
.recipe-name::first-letter { text-transform: uppercase; }
.recipe-macros { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-top: 3px; }
.recipe-tags {
  margin-top: 5px; display: flex; gap: 5px; overflow: hidden;
  font-size: 11px; color: var(--text-secondary);
}
.recipe-tag {
  padding: 2px 7px; border-radius: 7px; white-space: nowrap;
  background: rgba(128, 128, 128, .14);
}
.recipes-empty { padding: 28px 4px; text-align: center; color: var(--text-secondary); font-size: 14px; }

.filter-slot-row { margin-bottom: 16px; }
.filter-title { font-size: 12px; color: var(--text-secondary); margin: 0 0 8px; }
/* ---- Главный экран рецептов: ряды с горизонтальной лентой -------------- */
.recipe-row { margin-bottom: 22px; }
.recipe-row-head {
  display: flex; align-items: center; gap: 4px; margin-bottom: 10px;
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--text-primary); font: inherit;
}
.recipe-row-title { font-size: 17px; font-weight: 700; }
.recipe-row-title::first-letter { text-transform: uppercase; }
.recipe-row-chevron { color: var(--text-secondary); }
/* Отступ снизу нулевой, а не 10px как у заголовка ряда: под шапкой стоит не
   лента, а первая .recipe-card со своим padding: 10px. Этот паддинг и даёт
   зазор — со своим отступом расстояние до карточки выходило вдвое больше, чем
   до строки поиска сверху. */
/* Шапка подборки — та же история, что и у шапки профиля: она внутри
   скролл-контейнера, и в длинной подборке кнопка возврата уезжала вверх. */
.recipes-back { margin-bottom: 0; }
/* Обязательно: .recipe-row-head задаёт display: flex, а авторский display
   сильнее браузерного [hidden] { display: none } — на витрине под поиском
   оставался пустой шеврон, и он же занимал место при каждом переходе. */
.recipes-back[hidden] { display: none; }
/* min-width: 0 обязателен всей цепочке: у flex-элемента по умолчанию
   min-width: auto, и широкая лента внутри не сжимается, а РАЗДВИГАЕТ родителя —
   экран уезжал вбок целиком. Своих отрицательных полей лента не ставит:
   .scroll-y уже вытянут на поля контейнера, второй такой сдвиг и ломал вёрстку. */
#recipes-rows, .recipe-row { min-width: 0; }
.recipe-strip {
  display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory;
  /* max-width: 100% здесь стоять НЕ должен: он отменяет вылет на поля и
     обрезает ленту по колонке — карточка упиралась в 327 вместо 375, звезда
     срезалась. От растягивания страхует min-width: 0 у вкладки, а не это. */
  min-width: 0;
  /* Лента уходит под края экрана: отменяем отступ .scroll-y отрицательным полем
     и возвращаем его себе внутренним, чтобы первая и последняя карточки не
     прилипали к краю. Раньше это ломало вёрстку, но виновата была не сама
     отмена, а min-width: auto у вкладки — она росла под содержимое. */
  margin-inline: calc(-1 * var(--card-pad-x));
  padding-inline: var(--card-pad-x);
  scroll-padding-inline: var(--card-pad-x);
  scrollbar-width: none;
}
.recipe-strip::-webkit-scrollbar { display: none; }
.recipe-tile {
  /* Карточка начинается ровно от заголовка ряда и занимает экран минус поля, а
     следующая уходит за правый край. Прилипание НАЧАЛОМ: с центрированием первая
     карточка вставала бы посередине, отрезанная слева.
     Ширина 100% — это ширина СОДЕРЖИМОГО ленты, то есть экран минус её отступы.
     Было calc(100vw - 2 * var(--card-pad-x)), и это врало на устройствах с
     «безопасными зонами»: приложение отступает от них (padding-inline с
     env(safe-area-inset-*)), а 100vw про этот отступ не знает — карточка
     выходила шире нужного, и хвосты соседей получались разной длины. */
  flex: 0 0 100%;
  max-width: 460px; scroll-snap-align: start;
  border-radius: 16px; overflow: hidden; cursor: pointer;
  background: var(--chip-bg);
}
.recipe-tile-photowrap { position: relative; }
/* Место под картинку, которую сервер ещё рисует. Без этой пометки блок читался
   как поломка: серый прямоугольник в 16:9 во весь экран и ни намёка, что
   что-то происходит. Медленная тихая волна говорит «идёт работа», не приковывая
   внимание — картинка появляется через десятки секунд, и мигать всё это время
   было бы навязчиво. */
@keyframes photoPending {
  0%   { background-position: 120% 0; }
  100% { background-position: -20% 0; }
}
.recipe-tile-photo.pending, .recipe-thumb.pending {
  background-image: linear-gradient(100deg,
    transparent 20%, rgba(255, 255, 255, 0.07) 50%, transparent 80%);
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: photoPending 2.4s ease-in-out infinite;
  display: flex; align-items: center; justify-content: center;
}
/* Подпись со значком. Приглушена намеренно: это служебное сообщение, а не
   содержимое карточки, и через минуту-другую оно исчезнет само. */
.pending-inner {
  display: flex; align-items: center; gap: 7px;
  color: var(--text-tertiary); font-size: 12px; font-weight: 600;
}
.pending-inner .icon-hourglass { flex: none; }
/* В плоском списке место под фото — квадрат 64px, слова туда не влезут:
   оставляем только значок. */
.recipe-thumb.pending .pending-inner span { display: none; }
@media (prefers-reduced-motion: reduce) {
  .recipe-tile-photo.pending { animation: none; }
}
.recipe-tile-photo {
  /* 16:9, а не 3:2: карточка стала шире (335 против 275), и при 3:2 фотография
     вырастала до 223px — она забирала три четверти карточки и перевешивала
     текст. Обрезка по центру, поэтому тарелка остаётся в кадре. */
  width: 100%; aspect-ratio: 16 / 9; object-fit: cover; display: block;
  background: rgba(128, 128, 128, .15);
}
/* Звезда поверх фото: на карточке нет другого свободного места, а действие
   должно быть под большим пальцем, не открывая рецепт. */
.recipe-fav {
  position: absolute; top: 8px; right: 8px; width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%; cursor: pointer;
  background: rgba(0, 0, 0, .45); color: #fff;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  transition: transform 120ms var(--ease-out);
}
.recipe-fav:active { transform: scale(0.88); }
.recipe-fav.active { color: var(--dot-color); }
.recipe-tile-body { padding: 10px 12px 12px; }
/* Название СТРОГО в одну строку. Карточки в ряду — flex-элементы и тянутся по
   самой высокой: одно длинное название в две строки задирало высоту всему ряду,
   и у коротких внизу оставалось 20px пустоты. Многоточие вместо переноса даёт
   одинаковую высоту всем карточкам — так же сделано у конкурента. */
.recipe-tile-name {
  font-size: 15px; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.recipe-tile-name::first-letter { text-transform: uppercase; }
.recipe-tile-meta,
.recipe-macros { margin-top: 4px; font-size: 13px; font-weight: 500; color: var(--text-secondary); }
.recipe-line { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; }
/* Не прижимаем к краю: при широкой карточке между калориями и БЖУ появлялась
   дыра в полстроки. Отделяем расстоянием — вместе с gap выходит 13px, этого
   хватает, чтобы читалось как отдельная группа, но строка остаётся цельной. */
.recipe-bju { margin-left: 8px; opacity: .75; }
/* vertical-align нужен там, где строка НЕ flex — в карточке блюда меню. */
.recipe-meta-icon { flex: none; opacity: .85; vertical-align: -2px; }

/* ---- Блюдо меню: та же карточка, но в столбик ------------------------- */
/* .recipe-tile рассчитана на горизонтальную ленту (flex-basis по ширине
   экрана). В меню карточки идут столбиком, поэтому базис снимаем. */
.menu-tile { flex: none; width: 100%; max-width: none; margin-bottom: 12px; cursor: pointer; }
/* Отступ у последней карточки НЕ снимаем: под ней волосок итога за день, и без
   отступа полоса слипается с карточкой. Снизу у итога свои 12px — сверху должно
   быть столько же. */

/* Подпись приёма пищи — на фотографии, а не над названием: в карточке место
   под текстом занято названием и КБЖУ, а слот важен как метка, не как заголовок. */
.recipe-tile-slot {
  position: absolute; top: 10px; left: 10px;
  padding: 4px 9px; border-radius: 8px;
  background: rgba(0, 0, 0, .45);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  color: #fff; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-caps-md);
}

/* Две кнопки в столбик у правого края фотографии. */
.recipe-tile-tools {
  position: absolute; top: 8px; right: 8px;
  display: flex; flex-direction: column; gap: 8px;
}
.recipe-tile-tools .recipe-fav { position: static; }

/* Панель избранного: та же колонка, что у рецептов, иначе список не ограничен
   по высоте и уезжает под навигацию. */
#favorites-pane:not([hidden]) { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#favorites-scroll { flex: 1; min-height: 0; }


/* ===== Тарифы (Pro) =====
   Слой один на всё приложение — его открывает и вкладка «Фото», и микрофон, и
   исчерпанный лимит, и инсайты, и рецепты, и меню, и строка в профиле.
   Заливки у модалки нет намеренно, как и у остальных: фон даёт затемнение слоя
   (см. комментарий у .calendar-modal). */
.pro-modal { max-height: 92%; }
/* MAX (03.10.2026, замер в окне): окно мини-аппы там 828pt против 956 в
   полноэкранном Telegram - сверху шапка MAX. 92% давали 762pt: тарифы не
   влезали, «Оформить Pro» уходила под край, а сверху висели пустые 66pt.
   В MAX лист занимает всю высоту; Telegram не меняется. */
.in-max .pro-modal { max-height: 100%; }
.pro-reason {
  font-size: 13px; font-weight: 500; color: var(--text-secondary);
  margin: -6px 0 14px;
}
.pro-reason:empty { display: none; }

.pro-plans { display: flex; flex-direction: column; gap: 8px; }
/* Сетка: слева название и мелкая цена за месяц, справа общая цена. Цена за
   месяц нужна именно здесь — без неё три числа разной длительности не
   сравниваются вовсе. */
.pro-plan {
  position: relative; display: grid; gap: 2px 12px; cursor: pointer;
  grid-template-columns: 1fr auto; align-items: center; text-align: left;
  padding: 12px 14px; border-radius: 16px; font: inherit;
  background: var(--chip-bg); color: var(--text-primary);
  border: 1.5px solid transparent;
  transition: border-color 140ms var(--ease-out), background 140ms var(--ease-out);
}
.pro-plan.active { border-color: var(--dot-color); background: var(--glass-bg); }
.pro-plan-name { font-size: 14px; font-weight: 700; }
.pro-plan-price { font-size: 16px; font-weight: 700; grid-row: 1 / span 2; grid-column: 2; align-self: center; }
.pro-plan-per { font-size: 11px; font-weight: 500; color: var(--text-tertiary); }
/* Выгода — ярлык на кромке, а не строка: он должен читаться до того, как
   человек начнёт сравнивать числа. */
.pro-plan-save {
  position: absolute; top: -7px; left: 12px; padding: 1px 7px; border-radius: 999px;
  font-size: 10px; font-weight: 700; background: var(--dot-color); color: var(--on-accent);
}

.pro-list-title { font-size: 13px; font-weight: 700; margin: 18px 0 10px; }
.pro-features { list-style: none; display: flex; flex-direction: column; gap: 14px; }
/* Две строки на пункт: что получаешь и что это значит. Точка стоит у первой
   строки, а не по центру пункта — иначе у длинного описания она уезжает
   на середину и перестаёт читаться маркером. */
.pro-features li { position: relative; padding-left: 20px; }
.pro-features li::before {
  content: ""; position: absolute; left: 4px; top: 6px; width: 6px; height: 6px;
  border-radius: 50%; background: var(--dot-color);
}
.pro-feature-name {
  display: block; font-size: 13.5px; font-weight: 700; color: var(--text-primary);
}
.pro-feature-note {
  display: block; margin-top: 2px;
  font-size: 12.5px; font-weight: 500; line-height: 1.35; color: var(--text-secondary);
}
.pro-buy { width: 100%; margin-top: 18px; padding: 14px 0; text-align: center; font-size: 14px; }
.pro-note {
  font-size: 11px; font-weight: 500; color: var(--text-tertiary);
  text-align: center; margin-top: 10px;
}

/* Замок на закрытом пункте. Иконка Iconly Bold; цвет берёт у текста, поэтому
   одинаково лежит и на тёмном переключателе, и на светлой строке профиля. */
.lock-icon { width: 13px; height: 13px; flex-shrink: 0; opacity: 0.75; }
.food-tab .lock-icon { margin-left: 5px; vertical-align: -2px; }

/* Замок показываем только бесплатному: класс ставит обновитьЗамки по ответу
   сервера. У платного разметка та же, просто замок не виден. */
.lock-icon { display: none; }
.is-locked .lock-icon,
.is-locked > .lock-icon { display: inline-block; }

/* ===== Закрытые экраны: блюр и подсказка =====
   Показываем, что здесь есть содержимое, но оно закрыто. Пустой экран человек
   читает как «тут ничего нет» и не понимает, за что платить. */
[data-pro-blur] { position: relative; }
.free-tier [data-pro-blur] > *:not(.pro-veil) {
  filter: blur(7px); pointer-events: none; user-select: none;
}
.pro-veil { display: none; }
/* Подсказка поверх: лежит по центру и ловит тап — дети под ней уже не кликаются. */
.free-tier [data-pro-blur] > .pro-veil {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  position: absolute; inset: 0; justify-content: center; cursor: pointer;
  color: var(--text-primary); text-align: center; padding: 20px;
}
.pro-veil-lock { width: 26px; height: 26px; opacity: 0.9; }
.pro-veil-text { font-size: 14px; font-weight: 700; }
.pro-veil-cta {
  font-size: 12px; font-weight: 700; padding: 7px 14px; border-radius: 999px;
  background: var(--dot-color); color: var(--on-accent);
}

/* Закрытое «Своё меню» — та же подсказка по центру, что на инсайтах, и ничего
   больше. Высоту задаём от видимой области (не от vh: внутри мини-аппа он
   больше неё), чтобы замок встал по центру экрана, а не прижался к
   переключателю. */
.menu-pro-stub {
  position: relative;
  min-height: calc(var(--tg-viewport-stable-height, 100dvh) * 0.55);
}

/* Замок на закрытой карточке рецепта. Карточка остаётся видимой целиком:
   человек должен видеть, что в каталоге есть. */
/* Замок лежит НА КАРТИНКЕ, а не на карточке. В списке картинка выровнена по
   центру строки, и замок, привязанный к карточке, вылезал над ней. Поэтому
   миниатюре дана своя обёртка — у плитки такая уже была. */
.recipe-thumbwrap { position: relative; flex: none; display: flex; }
.free-tier .recipe-locked .recipe-thumbwrap::after,
.free-tier .recipe-locked .recipe-tile-photowrap::after {
  /* Слева: справа на картинке живёт звезда избранного, и замок её закрывал. */
  content: ""; position: absolute; top: 6px; left: 6px; width: 22px; height: 22px;
  border-radius: 50%; background: rgba(10, 13, 6, 0.55) center / 12px no-repeat;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cg transform='translate(3.5 2)'%3E%3Cpath d='M8.485 0C11.553 0 14.023 2.415 14.023 5.396v1.533C15.745 7.467 17 9.026 17 10.888v4.937C17 18.131 15.089 20 12.732 20H4.269C1.911 20 0 18.131 0 15.825v-4.937C0 9.026 1.256 7.467 2.977 6.929V5.396C2.987 2.415 5.457 0 8.485 0zm.01 11.384c-.488 0-.884.388-.884.865v2.206c0 .487.396.874.884.874.498 0 .894-.387.894-.874v-2.206c0-.477-.396-.865-.894-.865zm.01-9.645c-2.063 0-3.739 1.63-3.75 3.637v1.338h7.49V5.396c0-2.018-1.677-3.657-3.74-3.657z'/%3E%3C/g%3E%3C/svg%3E");
}

/* Учётная запись — отдельная карточка, а не строка в списке настроек. Строкой
   она читалась как ещё один пункт среди прочих, хотя отвечает на первый вопрос
   к приложению: что мне доступно. Стекло и радиус — общие с остальными
   блоками, своего вида не заводим. */
/* Коробка та же, что у карточек статистики (.kcal-card): скругление 28,
   внутренняя светлая кромка, те же поля. Своего вида не заводим — в приложении
   уже есть блок «важная карточка», и учётная запись это он. */
.account-card {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  width: 100%; text-align: left; font: inherit; cursor: pointer;
  color: var(--text-primary); margin: 0 0 18px;
  /* Кромка НЕ снимается (было border: none — привычка для кнопок): её даёт
     .glass, и без неё карточка отличалась от соседних на весь блок. Сверка
     вычисленных стилей с карточкой статистики это и показала. */
  border: 1px solid var(--glass-border);
  /* Числа — с карточки периода (#period-kcal-card), а не с базовой .kcal-card:
     на экранах она переопределена, и эталон именно она. */
  border-radius: 22px; padding: 18px; 
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
  transition: transform 120ms var(--ease-out);
}
.account-card:active { transform: scale(0.99); }
.account-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* Подпись и её иконка — общий вид с шапками карточек статистики
   (.kcal-card-title): та же ступень, тот же каптал, тот же вес. */
.account-card-kicker { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.account-card-plan { font-size: 17px; font-weight: 700; }
.account-card-note { font-size: 12px; font-weight: 500; color: var(--text-secondary); }
.account-card-note:empty { display: none; }
.account-card-chevron { flex: none; color: var(--text-tertiary); }
/* Правило обязательно: браузерное [hidden]{display:none} объявлено только для
   html-пространства имён и на <svg> не действует. Без него на карточке висели
   оба замка сразу. */
.account-card-lock[hidden] { display: none; }

/* Карточка и переключатель автопродления — одна карточка из двух блоков.
   Когда переключатель виден, у карточки снизу уходят скругление, отступ и
   кромка, у переключателя — сверху: стык читается тонкой чертой, а не щелью.
   :has() по делу — видимость переключателя решает скрипт, стилю незачем знать
   об этом отдельным классом. */
.account-block { margin: 0 0 18px; }
.account-block .account-card { margin-bottom: 0; }
.account-block:has(.account-renew:not([hidden])) .account-card {
  border-bottom-left-radius: 0; border-bottom-right-radius: 0;
  border-bottom-color: transparent;
}
.account-renew {
  border: 1px solid var(--glass-border); border-top: none;
  border-radius: 0 0 22px 22px; padding: 12px 18px 14px;
  box-shadow: inset 0 1px 0 var(--glass-border);
  color: var(--text-primary);
}
.account-renew .toggle-row { font-size: 13.5px; }
/* У блока свой display (от .glass), и он перебивает браузерное [hidden].
   Та же ловушка, что у строк настроек, — без правила блок не прятался. */
.account-renew[hidden] { display: none; }


/* Теги в базе лежат строчными — так они и сравниваются при поиске. Заглавную
   даём только на показе, через ::first-letter: text-transform: capitalize не
   годится, он поднимает КАЖДОЕ слово и делает «Без Глютена».
   Три места, где теги видны: фильтр, карточка в списке и карточка блюда. */
[data-tag]::first-letter,
.recipe-tag::first-letter,
.dish-tags .chip-btn::first-letter { text-transform: uppercase; }
/* ::first-letter применяется только к блочным боксам — у строчного тега на
   карточке его пришлось бы игнорировать. */
.recipe-tag { display: inline-block; }

/* На плитках избранного и меню в левом верхнем углу стоит метка приёма пищи
   («Обед», «Завтрак»), и замок с ней сталкивался. Опускаем его под метку и
   равняем по её левому краю — получается одна колонка, а не куча в углу.
   :has() здесь по делу: метка есть не у всех плиток, а правило одно. */
.free-tier .recipe-locked .recipe-tile-photowrap:has(.recipe-tile-slot)::after {
  top: 40px; left: 10px;
}

/* Подвал «Профиля»: знак, имя, версия сборки.

   Цвет задан отдельной переменной, а не общим --text-tertiary, потому что темы
   здесь ведут себя по-разному. На дневной фон шалфейный (#7b9280), и белый на
   нём упирается в потолок 3,35 — соседние строки настроек сами дают 3,29, а
   подвал на tertiary давал 1,96 и пропадал. Тёмное на том же фоне берёт 4,19,
   то есть читается ЛУЧШЕ белого. Поэтому днём подвал тёмный, ночью светлый.

   Прозрачность подобрана так, чтобы подвал оставался тише строк настроек над
   ним: он служебный, а не орган управления. Замеры после правки — 2,71 днём и
   6,12 ночью против 3,29 и 16,83 у строк.

   Отдельной прозрачности у знака нет намеренно: с ней он в тёмной теме падал
   до 2,84, то есть оказывался бледнее собственного названия. Тише делается
   только строка сборки. */
:root                   { --footer-ink: rgba(255, 255, 255, 0.55); }
:root[data-theme="day"] { --footer-ink: rgba(20, 25, 13, 0.58); }

.app-footer {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 22px 4px 4px;
  color: var(--footer-ink);
}
.app-footer-mark {
  width: 18px;
  height: 18px;
  flex: none;
}
.app-footer-name {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.app-footer-build {
  font-size: 12px;
  opacity: 0.85;
}
.app-footer-build:empty { display: none; }
