:root {
  /* color-scheme, а не только наши токены: без него браузер рисует UA-контролы
     светлой схемы в обеих темах — невыбранный чекбокс в тёмной становится
     сплошным белым блоком (то есть выглядит выбранным), а иконки календаря и
     часов у input[type=date|time] — почти чёрными на почти чёрном поле.
     accent-color (ниже) красит только ЗАЛИТОЕ состояние и эту дыру не
     закрывает; заодно чинятся список select, скроллбары и спиннеры чисел. */
  color-scheme: light;
  --page: #f9f9f7;
  --surface: #fcfcfb;
  --text: #0b0b0b;
  --text-2: #52514e;
  /* --muted несёт весь вторичный текст (подписи плиток, шапки таблиц, даты).
     Прежний #898781 давал на --surface 3,5:1 в СВЕТЛОЙ теме — ниже нормы 4,5:1;
     в тёмной тот же цвет даёт 4,9:1 и годится. Поэтому токен разведён по темам,
     а не подобран один на обе. */
  --muted: #6f6d67;
  --grid: #e1e0d9;
  --baseline: #c3c2b7;
  /* граница поля ввода — своя, а не --baseline: у контролов норма контраста 3:1,
     а --baseline (декоративные линии) даёт 1,75:1, и пустое поле читалось как
     призрак. Фон поля — --page, чтобы поле было утоплено относительно панели. */
  --field-border: #a3a199;
  /* поверхность модалки на ступень выше панели: в тёмной теме затемнение поверх
     почти чёрной страницы давало 1,1:1 к фону модалки — диалог не отделялся */
  --surface-raised: #ffffff;
  --backdrop: rgba(0, 0, 0, 0.45);
  --border: rgba(11, 11, 11, 0.10);
  /* Kahurangi (индиго) — брендовый акцент вместо типового «SaaS-синего»;
     единственный из рассмотренных hue, не пересекающийся ни с одним статусным
     цветом ниже (good/warning/serious/critical), что важно в финансовом UI,
     где акцент и статус часто стоят рядом */
  --accent: #4338ca;         /* ink/бордюры/линии — адаптируется по теме для читаемости на своём фоне */
  --accent-solid: #4338ca;   /* сплошная заливка с белым текстом (кнопки, «сегодня» на ленте дней) —
                                 фиксировано, не светлеет в тёмной теме: --accent в тёмной теме специально светлее
                                 для читаемости как текст/линия, и на белом тексте потерял бы контраст (изначальный баг) */
  --series-2: #008300;    /* categorical slot 2 (green) — страны на схеме маршрута */
  --series-3: #e87ba4;    /* categorical slot 3 (magenta) — страны на схеме маршрута */
  /* Подложка плана в bullet-chart — НЕЙТРАЛЬНАЯ намеренно. Раньше она была
     янтарной, то есть того же оттенка, что серия «Предстоит», и на категории с
     обоими сразу (Отели) граница между данными и фоном пропадала. Серый канал
     не спорит ни с одной из трёх серий. */
  --plan-track: rgba(0, 0, 0, 0.07);
  --good: #0ca30c;
  --good-text: #006300;
  --warning: #fab219;
  /* читаемый текст на янтарной подложке: сам --warning на ней нечитаем, а
     раньше бейдж «Предстоит» брал --text — и весил больше «Оплачено»,
     хотя означает «ещё ничего не произошло» */
  --warning-text: #8a5a00;
  --serious: #ec835a;
  --critical: #d03b3b;
  /* доля подмеса у тонированных бейджей; в тёмной теме переопределяется */
  --tint: 14%;
}
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --page: #0d0d0d;
    --surface: #1a1a19;
    --text: #ffffff;
    --text-2: #c3c2b7;
    --muted: #898781;
    --grid: #2c2c2a;
    --baseline: #383835;
    --field-border: #66655e;
    --surface-raised: #232322;
    --backdrop: rgba(0, 0, 0, 0.72);
    --border: rgba(255, 255, 255, 0.10);
    --accent: #8779e6;
    /* тёмный вариант обязателен: #008300 (светлая тема) на 12%-подложке бейджа
       «N ночей» давал 3,2:1 — при 4,9:1 у индиговых соседей одна и та же
       кодировка «страна» читалась с разным весом */
    --series-2: #35c435;
    --series-3: #d55181;
    --warning-text: #fab219;
    /* см. светлую тему: канал, а не данные. На тёмном осветляем, а не
       затемняем — иначе он сливается с поверхностью панели */
    --plan-track: rgba(255, 255, 255, 0.09);
    --good-text: #0ca30c;
    /* Доля подмеса у тонированных бейджей — по теме. color-mix со
       transparent в СВЕТЛОЙ теме уводит подложку от цвета текста, а в тёмной
       К нему: те же 12–15% давали .badge.paid 4,31:1, .badge.reserved 4,10:1
       и .nbadge.nz 4,21:1 при норме 4,5:1 для 12px. Половинный подмес
       возвращает их к 5,5–6:1, сохраняя саму кодировку цветом. */
    --tint: 7%;
  }
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--page);
  color: var(--text);
  font: 15px/1.45 system-ui, -apple-system, "Segoe UI", "Noto Sans", sans-serif;
}
a { color: var(--accent); }
button { font: inherit; }

header.topbar {
  position: sticky; top: 0; z-index: 20;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 10px 16px 0;
}
/* Э19 — полоска «связи нет» в липкой шапке: единственное место, где об этом
   написано. Цвет предупреждения, а не ошибки: приложение работает и показывает
   сохранённое, сломана только связь. Занимает всю ширину шапки и потому
   попадает в --header-h (см. paintConnBar). */
.conn-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 8px -16px 0; padding: 7px 16px;
  font-size: 13px; font-weight: 600;
  color: var(--text);
  background: color-mix(in srgb, var(--warning) 18%, var(--surface));
  border-top: 1px solid var(--warning);
}
.conn-bar[hidden] { display: none; }   /* flex перебил бы hidden */

.brand { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.brand h1 { font-size: 18px; margin: 0; }
.brand .countdown { color: var(--text-2); font-size: 13px; }
.cur-label { color: var(--muted); font-size: 12px; display: flex; align-items: center; gap: 6px; }
.cur-label select { padding: 4px 6px; font-size: 13px; }
.day-group { margin-top: 12px; }

/* чекбокс с подписью в одну строку; в формах селектор усилен, иначе его
   перебивает `.field label` (display:block + серый мелкий текст) */
.check-field, .field label.check-field {
  display: flex; flex-direction: row; align-items: center; gap: 8px;
  cursor: pointer; font-size: 14px; color: var(--text); padding: 6px 0;
}
.check-field input { flex-shrink: 0; }
/* в модальной форме — заметная рамка-плашка, подсвечивается при включении */
.form-grid label.check-field {
  /* --field-border, а не --baseline: у контролов норма контраста 3:1, а
     декоративный --baseline даёт 1,75:1 в светлой и 1,48:1 в тёмной —
     плашка чекбокса в тёмной модалке практически исчезала */
  border: 1px solid var(--field-border); border-radius: 8px; padding: 9px 10px;
}
.form-grid label.check-field:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}

/* сортируемые заголовки таблиц */
th.sortable { cursor: pointer; user-select: none; }
th.sortable:hover { color: var(--text); }

/* легенда графика */
.legend { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12px; color: var(--text-2); margin-bottom: 6px; }
.legend span { display: inline-flex; align-items: center; white-space: nowrap; }
.legend i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; }

/* карточки дней */
.day-card { margin-top: 10px; }
.day-card.done { opacity: 0.65; }
.day-head { display: flex; align-items: center; gap: 8px; }
.day-head .day-title { flex: 1; }
.day-head .check-field { padding: 0; }
.day-meta { margin: 6px 0 4px 25px; display: grid; gap: 2px; }
.day-places { margin: 6px 0 2px 25px; display: flex; flex-wrap: wrap; gap: 6px; }
.day-card .list-plain { margin-left: 25px; }
.place-chip {
  display: inline-flex; align-items: center; gap: 4px;
  /* Рамка нейтральная: важность кодируется монохромной шкалой (см. .p-must /
     .p-optional ниже), а акцент принадлежит СОСТОЯНИЮ «были здесь» и
     наведению, а не атрибуту. Токен --muted, а не --field-border: чип
     кликабельный, то есть контрол с нормой 3:1, а --field-border даёт на
     поверхности панели 2,5:1. --muted даёт 5:1 в светлой и 4,9:1 в тёмной. */
  background: transparent; border: 1px solid var(--muted); color: var(--text);
  border-radius: 999px; padding: 2px 10px; font-size: 13px;
  cursor: pointer;
}
.place-chip:hover { outline: 1px solid var(--accent); }
.place-chip[draggable="true"] { cursor: grab; }
.place-chip.dragging { opacity: 0.4; cursor: grabbing; }
/* «были здесь» — отметка прямо на чипе; посещённое гасим, но не вычёркиваем:
   зачёркнутый текст на 13px читается хуже, а список нужен рабочим весь день.
   Сам переключатель — рядом с .place-chip button ниже по файлу: там его
   соседи, и там же правило, которое иначе перебило бы зону тапа. */
.place-chip.visited { opacity: 0.55; }

/* Ф4 — 🧭 «открыть на карте»: живёт внутри чипа места и в строке расписания.
   Тап по нему уводит в карты, а тап мимо открывает карточку, поэтому у ссылки
   свой запас по краям: на телефоне в один значок иначе не попасть. */
.map-link {
  text-decoration: none; opacity: 0.7; font-size: 13px; line-height: 1;
  padding: 3px 5px; margin: -3px 0; cursor: pointer; position: relative;
}
.map-link:hover { opacity: 1; }
/* Э7 — 🔗 в строке расхода ведёт к брони, из которой он создан. Запас по краям
   нужен по той же причине, что и у .map-link: промах мимо значка открывает
   карточку расхода, а на телефоне попасть в один эмодзи без запаса нельзя. */
.link-booking { text-decoration: none; padding: 3px 5px; margin: -3px 0; position: relative; }
/* чек-лист «Перед вылетом» (Ф1) */
/* .list-plain li уже даёт flex/baseline/gap — здесь только то, что отличается */
.preflight .pf-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.preflight .pf-head h2 { margin: 0; }
.preflight .pf-icon, .preflight .pf-go { flex: none; }
.preflight .pf-go { color: var(--muted); }
.preflight .rowlink:hover .pf-go { color: var(--accent); }
/* «Брони с доплатой» — напоминание под списком, а не пункт: закрыть его до
   вылета нельзя, платят на месте. Отделено линией, чтобы не читалось шестым
   пунктом чек-листа и не спорило со счётчиком «N пунктов». */
.preflight .pf-note { margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--border); }

/* «Сегодня» без связи: сохранённая копия с явным возрастом данных.
   Правило переехало ПОД .panel: там оно объявлено шорткатом border и с той же
   специфичностью (0,1,0) выигрывало по порядку — прежняя пара
   border-color + background стояла выше и молча перебивалась, из-за чего
   единственное сообщение «ты смотришь копию» рисовалось обычной панелью. */

/* экран «Сегодня» (Э2): шапка дня + действия в один тап */
.today-head { display: grid; gap: 4px; }
.today-title { font-size: 17px; margin-bottom: 2px; }
.today-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.today-actions .btn { flex: 1 1 auto; }
.today-spent { margin-top: 8px; }
/* подзаголовки внутри «Плана дня»: панель одна, разделов в ней два */
.today-sub { font-size: 13px; margin: 0 0 8px; color: var(--muted); font-weight: 600; }
/* чипы мест сегодняшнего дня — в панели «План дня», под расписанием. В карточках
   дней те же чипы сдвинуты под чекбокс «день пройден», здесь чекбокса нет;
   селектор именно .today-plan, а не .panel — иначе перебил бы и ленту дней,
   у которой .day-places тоже прямой ребёнок .panel */
.today-plan > .day-places { margin-left: 0; margin-top: 10px; }

/* стрелки перестановки дня */
.day-move-group { display: inline-flex; gap: 2px; }
.day-move { padding: 2px 7px; line-height: 1; }
/* подсветка дня/пула как цели при перетаскивании метки */
.day-card.drop-hot, #pool-drop.drop-hot {
  outline: 2px dashed var(--accent); outline-offset: 2px;
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}
/* сегодняшний день поездки */
.day-card.today { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

/* лента-навигатор по дням (Э3): липнет под шапкой, --header-h выставляется в render() */
.day-nav-strip {
  position: sticky; top: var(--header-h, 56px); z-index: 15;
  display: flex; gap: 4px; overflow-x: auto;
  padding: 8px calc(2px + var(--fade)) 8px 2px;
  margin: 0 0 12px; background: var(--page);
  -webkit-overflow-scrolling: touch; scrollbar-width: thin;
  --fade: 20px;
}
/* Признак прокрутки у ленты: на телефоне помещается 12 ячеек из 22, полосы
   прокрутки на тач-экране нет, и обрыв на кромке читался как «дней всего
   двенадцать». Не mask-image, а непрозрачный градиент: лента ЛИПКАЯ, и маска
   сделала бы полупрозрачным её собственный фон — под правым краем просвечивал
   бы уезжающий контент. Поэтому непрозрачный градиент поверх, прикреплённый к
   правой кромке самого скроллера (position: sticky внутри него). */
.day-nav-strip::after {
  /* flex-basis, а не width: в строчном флекс-контейнере основной размер задаёт
     именно basis, и при `flex: 0 0 0` градиент был бы нулевой ширины */
  content: ''; position: sticky; right: 0; flex: 0 0 var(--fade);
  align-self: stretch; margin-left: calc(-1 * var(--fade));
  pointer-events: none;
  background: linear-gradient(to right, transparent, var(--page));
}
.day-nav-cell {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 6px;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-variant-numeric: tabular-nums; padding: 0;
  border: 1.5px solid var(--border); background: var(--surface); color: var(--text-2);
  cursor: pointer;
}
/* заполненный день — спокойная плашка, а не акцент: 22 залитых квадрата
   перевешивали и заголовок страницы, и главную кнопку того же цвета.
   Акцент в ленте ровно один — сегодняшний день (правило ниже). */
.day-nav-cell.filled { background: var(--page); border-color: var(--baseline); color: var(--text); font-weight: 600; }
/* padding:0 переобъявлен явно: иначе общий `.empty` (плейсхолдер «Загрузка…»/
   «Пока пусто.», padding:18px 6px) той же специфичности перебивает .day-nav-cell
   по порядку в файле — пустые плитки ленты становились выше заполненных (Д2) */
.day-nav-cell.empty { border-style: dashed; color: var(--muted); padding: 0; }
.day-nav-cell.today { background: var(--accent-solid); border-color: var(--accent-solid); color: #fff; }
/* скролл к дню компенсирует высоту липких шапки+ленты — иначе заголовок дня
   оказывается под ними (Д2); высоты выставляются в JS (render()/renderDays()) */
.day-card { scroll-margin-top: calc(var(--header-h, 56px) + var(--navstrip-h, 54px) + 8px); }

/* пустой день — одна строка вместо полной карточки (Э3) */
.day-expand-toggle {
  border: none; background: none; color: var(--muted); font-size: 13px;
  cursor: pointer; padding: 2px 4px; flex-shrink: 0;
}
.day-expand-toggle:hover { color: var(--accent); }
.place-chip button { background: none; border: none; cursor: pointer; color: var(--muted); font-size: 14px; padding: 0 2px; position: relative; }
.place-chip button:hover { color: var(--critical); }
/* «○/✓ были здесь» — тоже кнопка внутри чипа, поэтому оба правила выше её
   задевают: красный ховер здесь означал бы удаление, а его тут нет. Селекторы
   с .place-chip — чтобы перебить их по специфичности, а не по порядку строк. */
.place-chip .chip-visit.on { color: var(--good-text); font-weight: 700; }
.place-chip .chip-visit:hover { color: var(--accent); }
.place-chip .chip-visit:disabled { opacity: 0.5; cursor: default; }
/* на акцентной заливке «обязательно» и серый, и зелёный нечитаемы */
/* важность места: обязательно — акцентная заливка, желательно — контур
   (базовый стиль чипа), опционально — бледный серый контур (Э6) */
/* Важность места — весом и меткой, НЕ акцентной заливкой. Сплошной индиго
   делал «обязательное» место похожим на главную кнопку: в карточке дня рядом
   стояли залитый чип, контурные чипы и индиговая кнопка «+ Пункт», и чипы
   читались как включённые тумблеры, а не как данные. Хуже того, акцентом же
   кодируется настоящее состояние — «были здесь», — и оно оказывалось тише
   атрибута. Теперь акцент принадлежит только состоянию. */
.place-chip.p-must { border-color: var(--text-2); font-weight: 650; }
.place-chip.p-must::before { content: "★"; color: var(--text-2); font-size: 11px; margin-right: 1px; }
/* «можно» — светлее базового, но не декоративный --baseline (1,75:1): чип
   остаётся кликабельным, и его рамка обязана быть различима */
.place-chip.p-optional { border-color: var(--field-border); color: var(--text-2); }

.progress { height: 6px; border-radius: 999px; background: var(--grid); overflow: hidden; }
.progress i { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
/* Обзор → единая панель: два блока (маршрут + ночёвки) с разделителем */
.overview-panel { display: flex; flex-direction: column; }
.overview-panel .ov-sec + .ov-sec { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }
.overview-panel .ov-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.overview-panel .ov-head h2 { margin: 0; }

/* Обзор → «Ночёвки»: вертикальный рельс городов, где ночуем */
.nrail { position: relative; margin: 10px 0 0 6px; }
.nrail-line { position: absolute; left: 8px; top: 12px; bottom: 12px; width: 2px; background: var(--grid); }
/* колонка города — по содержимому, а не 1fr: при 1fr бейдж «3 ночи» улетал к
   правому краю панели, и глаз шёл ~770px пустоты от даты до него, путая строки */
/* Дорожки задаёт РЕЛЬС, а не каждая строка: при grid на строке третья колонка
   у каждой своей ширины, и левый край бейджей гулял на ~60px. justify-content
   не даёт последней дорожке съесть свободное место и растянуть бейдж. */
/* Д7: правая половина блока пустовала — список ~400px жил в панели 1100px.
   Свободную ширину забирает дорожка-шкала (.ngantt): она и есть третья
   колонка, поэтому justify-content: start больше не нужен — растягивается
   именно шкала, а не бейдж. */
.nrail { display: grid; grid-template-columns: 18px minmax(0, max-content) minmax(80px, 1fr) max-content;
  align-items: center; column-gap: 12px; }
.nrow { display: contents; }
.nrow > * { padding: 4px 0; }
/* Общая шкала поездки: одинаковая для всех строк, поэтому длины стоянок
   сравнимы глазом, а промежуток между полосками — это ночь без жилья. */
.ngantt { position: relative; height: 8px; padding: 0; border-radius: 999px;
  background: var(--grid); }
.ngantt i { position: absolute; top: 0; bottom: 0; border-radius: 999px; }
.ngantt i.nz { background: var(--accent); }
.ngantt i.cn { background: var(--series-2); }
.ndot { width: 12px; height: 12px; border-radius: 50%; display: block; margin-left: 2px; z-index: 1;
  border: 2.5px solid var(--surface); box-shadow: 0 0 0 1px currentColor; }
.ndot.nz { color: var(--accent); background: var(--accent); }
.ndot.cn { color: var(--series-2); background: var(--series-2); }
.nbody { min-width: 0; }
.ndates { color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.nbadge { font-size: 12px; font-weight: 700; padding: 2px 9px; border-radius: 999px; white-space: nowrap; }
.nbadge.nz { color: var(--accent); background: color-mix(in srgb, var(--accent) var(--tint), transparent); }
.nbadge.cn { color: var(--series-2); background: color-mix(in srgb, var(--series-2) var(--tint), transparent); }

/* Точки маршрута БЕЗ ночёвки: пересадки и однодневные вылазки из базы.
   Прежде здесь была горизонтальная лента-«метро» на все города по порядку —
   она требовала прокрутки даже на 1280, а её кружки повторяли кружки рельса
   ночёвок прямо под ней. Города с ночёвкой этот рельс и так перечисляет по
   датам, поэтому от ленты осталось только то, чего в нём нет: строка,
   которая переносится по словам и не прокручивается ни на какой ширине. */
.npassing { margin: 10px 0 0; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; }
.npass { white-space: nowrap; }
.npass + .npass::before { content: "·"; color: var(--baseline); margin-right: 10px; }
.btn-xs { padding: 4px 10px; font-size: 13px; }
/* «Хотим посетить» — сворачиваемая панель с облаком чипов по группам важности */
.pool-panel summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
.pool-panel summary::-webkit-details-marker { display: none; }
.pool-panel summary::before { content: '▸'; color: var(--muted); font-size: 12px; }
.pool-panel details[open] summary::before { content: '▾'; }
.pool-panel summary h2 { margin: 0; }
.pool-panel details[open] summary { margin-bottom: 10px; }
/* «Завтра» на «Сегодня» (Э17) — свёрнутый блок: строка весь день, расписание и
   брони следующего дня по тапу. Маркер тот же, что у «Хотим посетить». */
/* Строка обычным текстовым потоком, а не flex: в <summary> тут текст вперемешку
   с элементами, и каждый кусок текста стал бы отдельным flex-элементом — зазоры
   по 8px на месте одиночных пробелов и перенос строки по ним на 390px. Это же
   правило у «Хотим посетить» работает потому, что там внутри только элементы. */
.next-day summary {
  cursor: pointer; list-style: none;
  font-size: 14px; color: var(--text-2);
}
.next-day summary::-webkit-details-marker { display: none; }
.next-day summary::before { content: '▸ '; color: var(--muted); font-size: 12px; }
.next-day[open] summary::before { content: '▾'; }
.next-day[open] summary { margin-bottom: 10px; }

.pool-group { margin-top: 12px; }
.pool-group-title { font-size: 12px; color: var(--muted); }
.pool-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.pool-empty { margin: 10px 0 0; }
nav.tabs {
  display: flex; gap: 2px; overflow-x: auto;
  margin-top: 8px; padding-bottom: 0;
  scrollbar-width: none;
}
nav.tabs::-webkit-scrollbar { display: none; }
nav.tabs a {
  padding: 8px 12px; white-space: nowrap; text-decoration: none;
  color: var(--text-2); border-bottom: 2px solid transparent;
  font-size: 14px;
}
nav.tabs a.active { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }

main { max-width: 1100px; margin: 0 auto; padding: 16px; }
/* верхняя ступень иерархии: между 20px заголовка и 22px числа в плитке
   разницы не было, и самым крупным текстом страницы оказывалось число */
.page-title { font-size: 26px; margin: 4px 0 14px; font-weight: 700; }

/* --- cards & stat tiles --- */
/* auto-fit, а не auto-fill: при 5 плитках auto-fill держал пустую шестую
   колонку, и ряд обрывался на ~170px раньше панелей под ним */
/* Д6: зазор между блоками всегда давал margin-top у .panel, а у .cards своего
   не было — подняв чек-лист над плитками, мы склеили их вплотную. Отступ
   схлопывается с margin-bottom заголовка на «Бюджете» и «Деньгах», где над
   плитками стоит текст, поэтому лишнего воздуха там не появляется. */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(155px, 1fr)); gap: 10px;
  margin-top: 14px; }
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; padding: 12px 14px;
}
/* подпись занимает две строки всегда: иначе плитка с переносящейся подписью
   («Доплатить по броням») опускает своё число на строку ниже соседних, и ряд
   крупных цифр перестаёт читаться как один ряд */
.card .k {
  font-size: 12px; color: var(--muted); margin-bottom: 4px;
  display: flex; align-items: flex-start; gap: 6px;
  min-height: calc(2 * 1.45em);
}
.info {
  width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0;
  border: 1px solid var(--field-border); background: transparent; color: var(--muted);
  font-size: 11px; line-height: 14px; text-align: center; cursor: pointer; padding: 0;
  display: inline-block; position: relative;
}
.info:hover { color: var(--accent); border-color: var(--accent); }
.card.clickable { cursor: pointer; }
.card.clickable:hover { border-color: var(--accent); }
.info-body p, .info-body ul { font-size: 14px; color: var(--text-2); margin: 8px 0; }
.info-body ul { padding-left: 20px; }
.info-body b { color: var(--text); }
.card .v { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; }
.card .sub { font-size: 12px; color: var(--text-2); margin-top: 2px; font-variant-numeric: tabular-nums; }
.card .sub .nowrap, .card .v { white-space: nowrap; }
.v.good { color: var(--good-text); }
.v.bad { color: var(--critical); }
.v.accent { color: var(--accent); }
/* вход на свой экран с плитки: на «Обзоре» денежная плитка одна, и путь к
   деталям должен быть с неё, а не через меню */
.card-go { white-space: nowrap; text-decoration: none; }
.card-go:hover { text-decoration: underline; }

.panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; padding: 14px; margin-top: 22px;
}
.panel.offline-note { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 1px solid var(--warning);
  background: color-mix(in srgb, var(--warning) 8%, var(--surface)); }
/* Заголовок раздела был 14px --text-2 при body 15px --text, то есть весил
   МЕНЬШЕ строк под собой: на «Обзоре» название города в «Ночёвках» читалось
   крупнее заголовка «Ночёвки». Теперь три различимые ступени:
   .page-title 26 → .panel h2 16 → текст 15. */
.panel h2 { font-size: 16px; margin: 0 0 10px; color: var(--text); font-weight: 650; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; align-items: start; }
@media (max-width: 760px) {
  .two-col { grid-template-columns: 1fr; }
  nav.tabs { flex-wrap: wrap; }   /* все вкладки видны на телефоне, без скрытого скролла */
}
/* --- нижний таб-бар (телефон, Э1); display:none по умолчанию, включается
   media-запросом ниже — должен идти РАНЬШЕ него, иначе перебивает по каскаду --- */
.mobile-tabbar {
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  background: var(--surface); border-top: 1px solid var(--border);
  padding: 4px 2px calc(4px + env(safe-area-inset-bottom));
}
.mtab {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 6px 2px; border: none; background: none; cursor: pointer;
  color: var(--muted); text-decoration: none; font-size: 11px; font-family: inherit;
}
.mtab-icon { font-size: 19px; line-height: 1; }
.mtab.active { color: var(--accent); font-weight: 600; }

/* --- mobile card rows (страницы-таблицы на ≤600px заменяются карточками) ---
   Сами переключатели видимости (.mobile-only / .mcard-list / .mcard-summary)
   живут в КОНЦЕ файла: см. комментарий там. */
.mcard { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; font-size: 14px; }
.mcard-clickable { cursor: pointer; }
.mcard-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.mcard-title { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.mcard-edit { color: var(--muted); font-size: 14px; }
.mcard-sub { margin-top: 4px; }
.mcard-plan { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* Счёт: два подписанных поля в столбик, а не ряд — «Остаток … − резерв … USD»
   не помещался в ширину карточки и переносился посреди подписи. Своя сетка, а
   не общий .mcard-plan: его же использует карточка бюджета, где поле одно. */
.mcard-money { display: grid; grid-template-columns: max-content minmax(0, 1fr); align-items: center; gap: 6px 10px; }
.mcard-input { width: 84px; text-align: right; }
/* сумма строки — главное число карточки: не переносится и не уезжает за край,
   ради чего таблицы на телефоне и заменяются карточками */
.mcard-amount { flex: none; font-weight: 650; white-space: nowrap; font-variant-numeric: tabular-nums; }
.mcard-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
/* точка-разделитель между фактами: без неё «11.10.2026 Предстоит Отели 150 USD»
   читается как одна фраза, а дата — единственное, по чему список отсортирован */
.mcard-meta span + span::before { content: '·'; margin-right: 6px; color: var(--muted); }
/* итог списка — своей плашкой над карточками: в <tfoot> на 390px он
   рассыпался в столбик огрызков («оплачено» / «399 686» / «₽») */
.mcard-summary {
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  padding: 9px 12px; margin-top: 12px; font-size: 13px; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.mcard-summary b { color: var(--text); }

/* --- tables --- */
.table-wrap { overflow-x: auto; margin-top: 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); }
table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--grid); white-space: nowrap; }
td.wrap { white-space: normal; min-width: 180px; }
/* Липкая шапка: скроллпортом для неё служит .table-wrap (у него overflow-x,
   значит overflow-y вычисляется в auto), а высота у него не ограничена — он
   не прокручивался, и sticky не срабатывал ни разу. Прижимаем к самому
   документу под шапкой сайта; --header-h выставляет app.js. */
th { color: var(--muted); font-weight: 600; font-size: 12px; background: var(--surface); }
.table-wrap { max-height: calc(100dvh - var(--header-h, 96px) - 24px); overflow-y: auto; }
.table-wrap th { position: sticky; top: 0; z-index: 1; }
tr:last-child td { border-bottom: none; }
tbody tr.rowlink { cursor: pointer; }
tbody tr.rowlink:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
td.num, th.num { text-align: right; }
/* моноширинные цифры везде, где числа стоят друг под другом или сравниваются
   глазом. Раньше правило висело только на ячейках таблиц — то есть до самых
   крупных чисел приложения (плитки) и до сумм в списках не доезжало. */
.num, td.num, th.num, .mcard-sub, .badge { font-variant-numeric: tabular-nums; }
tr.total td { font-weight: 650; border-top: 2px solid var(--baseline); }
.muted { color: var(--muted); }
/* «чего-то не хватает, и это мешает» — не серым: серый читается как
   второстепенная подпись, а это единственное место, где сказано, почему
   «Карта» не знает дня перелёта */
.warn-text { color: var(--warning-text); }
.gap-list { margin: 0; padding-left: 18px; }
.gap-list li { margin: 2px 0; }
.nowrap { white-space: nowrap; }
.small { font-size: 12px; }

/* --- badges --- */
.badge { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.badge.paid, .badge.confirmed, .badge.valid { background: color-mix(in srgb, var(--good) var(--tint), transparent); color: var(--good-text); }
/* одна стратегия на все бейджи: цвет статуса текстом на подложке того же тона.
   «Предстоит» раньше был единственным с --text на 20% янтаря — 20 строк такого
   статуса образовывали самую тяжёлую стену на экране, хотя означают «ещё
   ничего не произошло», а закрытые «Оплачено» терялись. */
.badge.planned, .badge.pending, .badge.expiring { background: color-mix(in srgb, var(--warning) 18%, transparent); color: var(--warning-text); }
.badge.reserved { background: color-mix(in srgb, var(--accent) var(--tint), transparent); color: var(--accent); }
.badge.cancelled, .badge.expirednow { background: color-mix(in srgb, var(--critical) var(--tint), transparent); color: var(--critical); }
/* «Истечёт в поездке» — тревога на ступень ниже просроченного: документ ещё
   действителен, но до конца поездки не дотянет */
.badge.expired { background: color-mix(in srgb, var(--serious) var(--tint), transparent); color: var(--serious); }
.badge.muted-badge { background: color-mix(in srgb, var(--baseline) 22%, transparent); color: var(--text-2); font-weight: 500; }
/* роль брони в сегодняшнем дне: заезд / идёт / выезд */
.badge.role { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
.badge.ahead { background: color-mix(in srgb, var(--good) 15%, transparent); color: var(--good-text); }
.badge.behind { background: color-mix(in srgb, var(--critical) 15%, transparent); color: var(--critical); }

/* --- toolbar / filters --- */
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 4px; }
.toolbar .spacer { flex: 1; }
input, select, textarea {
  background: var(--page); color: var(--text);
  border: 1px solid var(--field-border); border-radius: 8px;
  padding: 7px 9px; font: inherit; font-size: 14px;
  max-width: 100%;
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: 0; border-color: transparent; }
/* заблокированные поля (например, расход, управляемый бронью) читаются как
   неактивные. Без opacity: значение в них (сумма брони) нужно читать, а
   прозрачность поверх --muted опускала его до ~2,9:1 — пунктирной рамки и
   приглушённого цвета для «нельзя править» достаточно. */
input:disabled, select:disabled, textarea:disabled {
  background: var(--page); color: var(--muted); cursor: not-allowed;
  border-style: dashed;
}
input[type="checkbox"] { width: 17px; height: 17px; accent-color: var(--accent); }
input[data-toggle] { width: 22px; height: 22px; }
td:has(input[data-toggle]) { cursor: pointer; }
.btn {
  background: var(--accent-solid); color: #fff; border: none; border-radius: 8px;
  padding: 8px 14px; cursor: pointer; font-weight: 600; font-size: 14px;
}
.btn:hover { filter: brightness(1.08); }
/* filter на прозрачном фоне ничего не даёт: у secondary отклика не было вовсе,
   а это все «Отмена», «+ Пункт», «+ Место», «Обновить курсы» */
.btn.secondary:hover { background: var(--page); border-color: var(--accent); }
nav.tabs a:hover { color: var(--text); }
.btn.secondary { background: transparent; color: var(--text); border: 1px solid var(--field-border); font-weight: 500; }
/* контурная, а не залитая: залитая красная в одном ряду с залитой индиговой
   перетягивала взгляд на единственное необратимое действие в форме */
.btn.danger { background: transparent; color: var(--critical); border: 1px solid currentColor; font-weight: 600; }
.btn.danger:hover { background: color-mix(in srgb, var(--critical) 10%, transparent); filter: none; }

/* --- modal --- */
.modal-backdrop {
  position: fixed; inset: 0; background: var(--backdrop); z-index: 50;
  display: flex; align-items: flex-start; justify-content: center; padding: 24px 12px;
  overflow-y: auto;
}
/* прокрутка — внутри диалога, а не у подложки: только так «Сохранить» может
   остаться липким снизу. Без этого на телефоне кнопка стояла в конце длинной
   формы и полностью уходила под клавиатуру. */
.modal {
  background: var(--surface-raised); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.30);
  width: 100%; max-width: 560px; padding: 18px 18px 0;
  max-height: calc(100dvh - 48px); overflow-y: auto;
}
.modal h2 { margin: 0 0 14px; font-size: 17px; }

/* --- «Ещё» на нижнем таб-баре: шторка снизу, а не центрированная модалка --- */
.sheet-backdrop { align-items: flex-end; padding: 0; }
.sheet { max-width: 480px; border-radius: 16px 16px 0 0; padding: 8px 0 calc(8px + env(safe-area-inset-bottom)); max-height: 90dvh; }
.sheet-handle { width: 36px; height: 4px; border-radius: 2px; background: var(--baseline); margin: 4px auto 10px; }
.sheet-item {
  display: flex; align-items: center; gap: 12px; padding: 13px 20px;
  color: var(--text); text-decoration: none; font-size: 15px;
}
.sheet-item:hover, .sheet-item:active { background: var(--page); }
.sheet-item .mtab-icon { font-size: 18px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; }
.form-grid .full { grid-column: 1 / -1; }
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 3px; }
.field input, .field select, .field textarea { width: 100%; }
/* Поля даты и времени на iOS. У них своя минимальная ширина от нативного
   пикера, а grid-элемент по умолчанию имеет min-width: auto — дорожка
   расширялась под неё, и поле выпирало за пределы формы (видно в «Настройках»,
   где форма ограничена 560px). width: 100% этого не лечит: он проигрывает
   собственному минимуму содержимого. Нужны обе строки — min-width: 0 у
   элемента сетки и у самого поля. */
.form-grid .field, .field { min-width: 0; }
.field input[type="date"], .field input[type="time"] {
  -webkit-appearance: none; appearance: none;
  min-width: 0; max-width: 100%;
}
/* чекбокс не растягиваем: правило выше (width:100%) при равной специфичности
   перебивало input[type=checkbox]{width:17px} и выталкивало подпись из модалки */
.field input[type="checkbox"] { width: 17px; }
/* заметку читают целиком: без min-height абзац прокручивался в окне на две
   строки, хотя места под него в диалоге сколько угодно */
.field textarea { min-height: 96px; resize: vertical; }
/* липкая подложка действий — см. .modal выше (скролл внутри диалога) */
.modal .actions {
  display: flex; gap: 8px; position: sticky; bottom: 0; z-index: 1;
  background: var(--surface-raised); border-top: 1px solid var(--border);
  margin: 16px -18px 0; padding: 12px 18px calc(12px + env(safe-area-inset-bottom));
}
.modal .actions .spacer { flex: 1; }
/* редактор пар город↔страна в форме дня */
.stops-editor { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; justify-items: start; }
.stop-row { display: flex; gap: 6px; width: 100%; align-items: center; }
.field .stop-row input { flex: 1; width: auto; min-width: 0; }
/* плечи перелёта (#16): коды IATA короткие, даты широкие — поэтому не flex,
   а сетка, иначе поле «Откуда» на 390px растягивается наравне с датой */
.legs-editor { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; justify-items: start; }
.leg-row {
  display: grid; width: 100%; gap: 6px; align-items: center;
  grid-template-columns: 4.5em 4.5em minmax(0, 1fr) minmax(0, 1fr) 5em auto;
}
.field .leg-row input { width: 100%; min-width: 0; text-transform: none; }
.field .leg-row input[data-leg-from], .field .leg-row input[data-leg-to] { text-transform: uppercase; }
@media (max-width: 600px) {
  /* на телефоне шесть колонок не помещаются: коды и рейс в первый ряд, даты во второй */
  .leg-row { grid-template-columns: 4.5em 4.5em minmax(0, 1fr) auto; }
  .field .leg-row input[data-leg-date], .field .leg-row input[data-leg-arrive] { grid-column: span 2; }
}

/* --- charts --- */
.chart { position: relative; }
.chart svg { display: block; width: 100%; }
.chart .tooltip {
  position: absolute; pointer-events: none; z-index: 10;
  background: var(--text); color: var(--page);
  border-radius: 6px; padding: 5px 9px; font-size: 12px;
  transform: translate(-50%, calc(-100% - 10px));
  white-space: nowrap; display: none;
}
.chart .tooltip b { font-variant-numeric: tabular-nums; }

/* --- misc --- */
.empty { color: var(--muted); padding: 18px 6px; text-align: center; }
/* «проверено, всё закрыто» — не то же, что «данных нет»: раньше оба состояния
   выглядели одинаковым серым текстом, и хорошая новость читалась как пустота */
.empty.ok { color: var(--good-text); }
.list-plain { list-style: none; margin: 0; padding: 0; }
.list-plain li { padding: 7px 0; border-bottom: 1px solid var(--grid); display: flex; gap: 10px; align-items: baseline; }
.list-plain li:last-child { border-bottom: none; }
/* фиксированная дорожка, а не минимум: при min-width строка с диапазоном
   времени шире остальных, и левый край названий скакал на 21px */
.list-plain .date { color: var(--muted); font-size: 12px; flex: 0 0 78px; font-variant-numeric: tabular-nums; }
.list-plain.with-time .date { flex-basis: 152px; }
.list-plain .li-body { flex: 1; min-width: 0; }
.list-plain li.rowlink { cursor: pointer; }
.list-plain li.rowlink:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.rates-note { font-size: 12px; color: var(--muted); margin-top: 10px; }
.settings-form { max-width: 560px; }
/* pointer-events: тост висит на z-index 100 над всем, включая липкую строку
   действий модалки. Длинная форма (бронь-перелёт) достаёт до низа экрана, и
   после ошибки валидации «Сохранить» и «Удалить» оказывались ПОД тостом:
   нажатие уходило в него и молча ничего не делало — ровно в тот момент, когда
   человек и правит ошибку. Сам тост кликов не ждёт, ждёт только «Отменить»,
   поэтому события пропускаются насквозь, а кнопке возвращаются точечно. */
#toast {
  position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%);
  background: var(--text); color: var(--page); padding: 9px 16px; border-radius: 8px;
  font-size: 14px; z-index: 100; display: none; pointer-events: none;
}
.toast-action { pointer-events: auto; }
/* Э12 — «Отменить» прямо в тосте. Кнопка наследует цвет тоста (он меняется
   между обычным и ошибочным), поэтому красится через currentColor. */
.toast-action {
  margin-left: 14px; padding: 3px 10px; cursor: pointer;
  background: none; border: 1px solid currentColor; border-radius: 6px;
  color: inherit; font: inherit; font-weight: 600;
}
.toast-action:hover { background: rgba(127, 127, 127, 0.25); }

/* --- карта --- */
.map-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 14px; }
.map-controls select { flex-shrink: 0; }
#map-route-info { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.leaflet-map-el {
  height: 62vh; min-height: 340px; border-radius: 10px; overflow: hidden;
  border: 1px solid var(--border); margin-bottom: 14px;
  /* своя стековая среда: без этого внутренние z-index Leaflet (панели/попапы
     до 700) сравниваются напрямую с модалкой (z-index:50) в общем контексте
     страницы и перекрывают её — см. .modal-backdrop */
  position: relative; z-index: 0;
}
.leaflet-map-el.drop-hot { outline: 2px dashed var(--accent); outline-offset: -2px; }
/* Точка центрируется в своей рамке ЯВНО. Рамка иконки 26×26 — это зона тапа
   (сама точка 14px), и без центрирования точка прижималась к верхнему левому
   углу: метка рисовалась на 6px выше и левее своих координат, а перетаскивание
   писало в базу положение центра рамки, а не той точки, по которой целились. */
.place-pin { display: flex; align-items: center; justify-content: center; }
.place-pin span {
  display: block; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid #fff; box-shadow: 0 0 3px rgba(0, 0, 0, .5);
}
/* Концы дня и города (#16) — значком, а не кружком: это точки другой природы,
   чем места дня, и кружок читался бы как ещё одно место. Подложка нужна, чтобы
   эмодзи не терялось на пёстрой карте (и в тёмной теме, где тайлы инвертированы). */
.anchor-pin { display: flex; align-items: center; justify-content: center; }
.anchor-pin span {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 50%; font-size: 14px; line-height: 1;
  background: var(--surface); border: 2px solid var(--text);
  box-shadow: 0 0 3px rgba(0, 0, 0, .5);
}

.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--surface); color: var(--text); }
.leaflet-container a.leaflet-popup-close-button { color: var(--muted); }
@media (prefers-color-scheme: dark) {
  /* тайлы OSM затемняем инверсией — без платного tile-провайдера под тёмную тему */
  .leaflet-map-el .leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(0.95) contrast(0.9); }
  .leaflet-bar a { background: var(--surface); color: var(--text); border-color: var(--border); }
  .leaflet-bar a:hover { background: var(--page); }
}

/* --- координаты места: поиск-и-подтверждение (форма «Место», roadmap #15) --- */
.coords-field { display: flex; flex-direction: column; gap: 8px; }
.coords-search-row { display: flex; gap: 8px; }
.coords-search-row input { flex: 1; }
.coords-map-el { height: 220px; min-height: 220px; margin-bottom: 0; }
.coords-candidates { display: flex; flex-direction: column; gap: 4px; }
.coords-candidate {
  text-align: left; background: var(--page); border: 1px solid var(--border); border-radius: 6px;
  padding: 6px 10px; font-size: 13px; cursor: pointer; color: var(--text);
}
.coords-candidate:hover { border-color: var(--accent); }
.coords-manual { display: flex; gap: 8px; }
/* подписи, а не placeholder: как только координата заполнена, placeholder
   исчезает, и это были единственные поля приложения без подписи */
.coords-num { flex: 1; display: block; }
.coords-num span { display: block; font-size: 12px; color: var(--muted); margin-bottom: 3px; }
.coords-manual input { width: 100%; }

/* ==========================================================================
   АДАПТИВНОСТЬ — последний блок файла, и это часть его смысла.

   Медиазапрос не повышает специфичность: правило внутри него перебивается
   любым базовым правилом того же веса, стоящим НИЖЕ по файлу. На этом молча
   отвалились три правила сразу — одноколоночные формы, плотность таблиц и
   скрытие мобильного дубля селектора валюты. По той же причине здесь стоят и
   базовые «спрятать» для .mobile-only/.mcard-*: `.mobile-only{display:none}`
   выше по файлу проигрывал `.toolbar{display:flex}` ниже, и блок, написанный
   только для телефона, показывался на десктопе.

   Новые правила видимости и любые мобильные переопределения — только сюда.
   Регрессию стережёт tests/visual/layout.mjs: он проверяет, что на десктопе не
   виден ни один .mobile-only, а на телефоне — ни один .desktop-only.
   ========================================================================== */
.mcard-list, .mcard-summary, .mobile-only { display: none; }

@media (max-width: 600px) {
  .desktop-only { display: none; }
  .mcard-list { display: grid; gap: 8px; margin-top: 12px; }
  .mcard-summary { display: block; }
  .mobile-only { display: flex; align-items: center; gap: 6px; margin-top: 10px; }
  .hm { display: none; }           /* второстепенные колонки таблиц — только на широких экранах */
  td.wrap { min-width: 130px; }
  th, td { padding: 7px 8px; }
  /* итоговая строка таблицы: подпись + сумма в один ряд не влезают — переносим на отдельные строки */
  tr.total td { display: block; width: 100%; white-space: normal; }
  tr.total td:empty { display: none; }
  tr.total td.num { text-align: right; }
  /* шапка — только логотип/счётчик/валюта в одну строку; 9 вкладок съедали
     первый экран (Э1) — вместо них фиксированный нижний таб-бар */
  nav.tabs { display: none; }
  /* Вкладки в шапке скрыты (их роль играет нижний таб-бар), а padding шапки
     задан как «10px 16px 0» — снизу ноль. На десктопе зазор до линии давал
     ряд вкладок; убрав его, мы посадили селектор валюты прямо на border-bottom,
     и поле выглядело приклеенным к линии. Своего отступа у шапки не было. */
  header.topbar { padding-bottom: 8px; }
  .mobile-tabbar { display: flex; }
  /* высота бара 57px + домашняя полоска iPhone: прежние фиксированные 76px
     срезали низ каждой страницы, и доскроллить до него было нельзя */
  main { padding-bottom: calc(76px + env(safe-area-inset-bottom)); }
  /* тост с «Отменить» стоял поверх ячеек таб-бара: промах мимо кнопки уводил
     на другую вкладку, и восьмисекундное окно отмены сгорало */
  #toast { bottom: calc(73px + env(safe-area-inset-bottom)); }

  /* формы в одну колонку. Раньше это правило стояло ВЫШЕ базового .form-grid
     и не работало — модалки оставались двухколоночными по ~170px. */
  .form-grid { grid-template-columns: 1fr; }

  /* Safari на iPhone зумит страницу при фокусе в поле с font-size < 16px и
     обратно не отматывает. Под это попадали все поля модалок, поиск и вход. */
  input, select, textarea { font-size: 16px; }
  .cur-label select, .mcard-input { font-size: 16px; }

  /* цели тапа. Норма — 44px; там, где элемент обязан остаться визуально
     мелким («?», крестик чипа, значки 🧭/🔗), зону расширяет невидимый
     псевдоэлемент — он не меняет вёрстку. */
  .btn, .btn-xs { min-height: 44px; }
  .day-move { min-height: 40px; min-width: 40px; }
  .day-nav-cell { width: 32px; height: 32px; font-size: 12px; }
  .place-chip { min-height: 36px; padding: 4px 12px; }
  .sheet-item { min-height: 48px; }
  .info::after, .map-link::after, .link-booking::after {
    content: ''; position: absolute; inset: -13px -10px;
  }
  /* «×» расширяется только НАРУЖУ чипа: симметричный запас налезал на соседний
     🧭 на 16px и, будучи ниже по DOM, перехватывал его тап — промах мимо
     «открыть на карте» откреплял место от дня */
  .place-chip button { min-width: 24px; min-height: 36px; padding: 0 4px; }
  .place-chip button::after { content: ''; position: absolute; inset: -4px -10px -4px 0; }
  /* «○/✓ были здесь» стоит СЛЕВА в чипе, поэтому запас нужен зеркальный: с
     правилом выше он налезал бы на название на 10px и перехватывал тап по
     нему — вместо карточки места ставилась отметка. Ровно тот же промах, что
     когда-то был у «×» и 🧭, только с другой стороны чипа. */
  .place-chip .chip-visit::after { inset: -4px 0 -4px -10px; }

  /* у <input> псевдоэлементов нет (замещаемый элемент) — зону тапа даёт
     обёртка-<label>, она и так кликабельна */
  .check-field, .field label.check-field { min-height: 44px; }

  /* На 390px дорожка даты съедала 43% строки, и название события ломалось на
     3–4 строки. Дата уходит на свою строку — кроме «Плана дня», где в ней
     только время и она узкая. */
  .panel .list-plain li { flex-wrap: wrap; }
  .panel .list-plain .date { flex-basis: 100%; }
  .today-plan .list-plain li { flex-wrap: nowrap; }
  .today-plan .list-plain.with-time .date { flex-basis: 74px; }

  /* Шапка в одну строку. Она липкая и висит над каждым экраном, а на «Днях» к
     ней добавляются лента дней и таб-бар — вместе это забирало ~175px из 844.
     Убранного «Валюта:» не хватило: селектор в 16px (см. правило выше, оно
     против зума iOS) всё равно переносил метку на вторую строку. */
  .brand { flex-wrap: nowrap; gap: 8px; }
  /* Сжимаются подписи, а не селектор. Без min-width:0 флекс-элемент не может
     стать уже своего содержимого: в поездке счётчик длиннее («идёт 4-й день
     поездки 🌿» против «до вылета 38 дней»), шапка переставала помещаться, и
     вбок ехала КАЖДАЯ страница — 420px содержимого при вьюпорте 390. */
  .brand h1, .brand .countdown { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Сжимается счётчик, а не название: shrink 1 против 100. Иначе первым
     обрезалось «Новая Зеландия» — то, по чему приложение узнают. */
  /* Сжимается счётчик, а не название: shrink 1 против 100 (порядок в
     сокращении — grow, shrink, basis). Иначе первым обрезалось «Новая
     Зеландия» — то, по чему приложение узнают. */
  .brand h1 { font-size: 14px; flex: 0 1 auto; }
  .brand .countdown { font-size: 12px; flex: 0 100 auto; }
  .cur-label { flex: none; }
  .cur-label select { padding: 3px 4px; }

  /* Поля — такой же орган управления, как кнопки: на «Бюджете» и «Деньгах»
     число правят прямо в строке. При 16px (правило выше, против зума iOS) и
     padding 7px они давали 35px против нормы 44. */
  /* Только поля ввода данных. Не .cur-label select (он в липкой шапке, и
     44px возвращали бы ей вторую строку) и не чекбоксы (у них своя геометрия:
     padding растянул бы сам квадратик). */
  .field input, .field select, .field textarea,
  .toolbar input, .toolbar select, .mcard-input, td input, th input {
    min-height: 44px; padding: 10px 9px;
  }
  .field input[type="checkbox"], td input[type="checkbox"] { min-height: 0; padding: 0; }
  /* правило писалось под сетку .mcard-money («Деньги»), но попадало и в
     .mcard-plan («Бюджет»), где строка свёрстана flex-wrap: поле распирало
     карточку, и «₽» уезжал на отдельную строку во всех десяти категориях */
  .mcard-money .mcard-input { width: 100%; }
  .mcard-plan .mcard-input { flex: 1; min-width: 0; }

  /* Тост: без явных left/right он ужимался до остатка от `left: 50%`, то есть
     максимум до половины экрана, и сообщение об ошибке с кнопкой «Отменить»
     рассыпалось в узкий столбик. */
  #toast {
    left: 16px; right: 16px; transform: none;
    width: max-content; max-width: calc(100vw - 32px); margin: 0 auto;
  }

  /* Ночёвки: шкала стоянок — для широкой панели. На 390px её минимум в 80px
     выдавливает строку за экран, а сравнивать длины стоянок глазом на телефоне
     незачем — числа стоят рядом в бейдже. Два правила обязаны идти ПАРОЙ:
     display: none убирает шкалу из сетки целиком (у .nrow стоит display:
     contents, так что ячейки считаются по детям строки), и в строке остаётся
     три элемента. Колонок должно стать столько же — иначе бейджи поедут в
     чужие ячейки и блок рассыпется по диагонали. */
  .nrail { grid-template-columns: 18px minmax(0, 1fr) max-content; }
  .ngantt { display: none; }

  /* Голова карточки дня: [галочка][заголовок] первой строкой, действия —
     второй.
     Исходно все четыре контрола (по 44px после правки целей тапа в раунде 3)
     делили строку с заголовком, и тому оставалось ~130px: «День 20 · Вс, 11
     октября · Гонконг, Пекин» ломался на четыре строки. Первая попытка —
     `flex: 1 0 100%` у заголовка — выталкивала его на отдельную строку
     ЦЕЛИКОМ, и галочка оставалась висеть одна над ним, без подписи и без
     видимой связи с чем-либо. Теперь галочка делит строку с заголовком, а
     перенос перед действиями задаёт распорка ::after, а не сам заголовок. */
  .day-head { flex-wrap: wrap; }
  .day-head .day-title { flex: 1 1 auto; min-width: 0; }
  /* Зазор между строками задаёт высота самой распорки, а не row-gap: распорка
     занимает отдельную флекс-строку, и row-gap насчитался бы дважды. */
  .day-head::after { content: ""; flex-basis: 100%; height: 6px; order: 2; }
  .day-head .day-move-group { order: 3; }
  .day-head > .btn { order: 4; }
  /* действия прижаты вправо, чтобы левый край строки заголовка и строки
     действий не спорили за внимание */
  .day-head .day-move-group { margin-left: auto; }

  /* Правило писалось для расписания (там время держит свою колонку), но
     действовало на весь список «Плана дня»: у брони nowrap отдавал ~140px
     сумме, и название аренды ломалось на три строки в 175px. */
  .today-plan .list-plain li { flex-wrap: wrap; }
  .today-plan .list-plain.with-time li { flex-wrap: nowrap; }

  /* flex: 1 1 auto растягивал перенесённую на вторую строку «✎ День» во всю
     ширину — самая редкая кнопка экрана становилась самой крупной */
  .today-actions .btn { flex: 0 1 auto; }

  /* кликабельные строки списков остались целями ~36px (padding 7px + 15/1.45),
     хотя всё остальное раунд 3 поднял до 44 */
  .list-plain li.rowlink { min-height: 44px; padding: 10px 0; }

  /* прокрутка модалки и горизонтальных лент не должна утекать на страницу
     под подложкой и на жест «назад» в Android */
  .modal, .modal-backdrop, .table-wrap, .day-nav-strip {
    overscroll-behavior: contain;
  }

  /* липкая шапка таблиц — десктопное поведение: на телефоне таблицы заменены
     карточками, а ограничение высоты здесь только отрезало бы список */
  .table-wrap { max-height: none; }

  /* Карточка, у которой ведущий контрол — галочка («Вещи»): её жмут десятки
     раз подряд стоя над чемоданом, поэтому цель у неё во всю высоту карточки,
     а не 17px по размеру самого квадратика. */
  .mcard-lead .mcard-top { align-items: center; gap: 4px; }
  /* Зона попадания — <label> в 44px, а не сам квадратик: промах в несколько
     пикселей выше галочки открывал форму правки вместо переключения (клики
     принимает только поле, а margin высоту цели не создаёт). */
  .mcard-lead-hit {
    display: flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px; margin: -10px 0; cursor: pointer;
    flex-shrink: 0;
  }
  .mcard-lead-hit input[type="checkbox"] {
    width: 22px; height: 22px; min-height: 22px; margin: 0; padding: 0;
  }
}
