/*
  Оформление сайта.

  Намеренно НЕ похоже на приложение: приложение тёмное, плотное и
  фиолетовое, сайт — светлый, просторный, почти без цвета. Фирменный
  градиент приложения выведен на одну-единственную роль: им помечен
  раздел про NoteFlow, и больше он не встречается нигде.

  ⚠️ Правило «почти без цвета» относится к СВЕТЛОЙ теме. В тёмной оно
  ослаблено сознательно (решение автора 17.09.2026): там страница почти
  целиком состоит из снимков приложения, и нейтральный серый рядом с ними
  читался посторонним. Поэтому тёмная палитра собрана из настоящих
  токенов приложения, а фиолетовый добавлен ещё в двух местах — линия у
  заголовка части и край снимка. Оба приглушены примесью к серому:
  задача была подкрасить, а не расцветить. Так он работает
  указателем, а не украшением.

  Шрифты системные. Для сайта, который обещает не следить за людьми,
  тянуть шрифты со стороннего CDN — противоречие; вдобавок это лишний
  запрос, который у российских посетителей может и не пройти. Системные
  начертания рисуют кириллицу на любой платформе и появляются мгновенно.
*/

:root {
  /*
    Объявление «я сам умею и светлую, и тёмную». Не украшение и не
    формальность — без него Яндекс.Браузер с включённой «тёмной темой для
    сайтов» перекрашивает страницу СВОИМ алгоритмом поверх нашей тёмной
    темы: цвета уезжают, а снимки экрана — и без того тёмные — выворачивает
    в светлые. В других браузерах всё выглядело правильно, поэтому искали
    поломку в вёрстке, а она была в настройке браузера.

    Движок у Яндекс.Браузера тот же, что у Chrome, и правило там общее:
    страницу, объявившую color-scheme, принудительное затемнение не
    трогает — считается, что автор про тёмную тему подумал сам. Мы
    подумали: палитра ниже, в @media (prefers-color-scheme: dark).

    Дублируется метатегом <meta name="color-scheme"> в <head> каждой
    страницы: метатег читается до того, как дойдут стили, и закрывает
    щель, в которой браузер успел бы применить своё затемнение.
  */
  color-scheme: light dark;

  --bg: #fbfaf8;
  --surface: #ffffff;
  --border: #e6e3dd;
  /* Та же граница, но заметная: ею карточка отвечает на наведение */
  --border-strong: #d3cfc6;
  --text: #17161a;
  --muted: #61606b;
  --faint: #8d8c96;
  --link: #2c2b33;

  /* Единственное место, где живут цвета приложения */
  --app-violet: #8b5cf6;
  --app-blue: #3b82f6;

  --measure: 34rem;
  --wrap: 62rem;
}

/*
  Тёмная тема собрана не «на глаз из серых», а из настоящих токенов
  приложения — `docs/design/DESIGN-TOKENS.md`. Причина простая: в тёмной
  теме страница почти целиком состоит из снимков этого самого приложения,
  и нейтральный серый рядом с ними читался как посторонний. Теперь фон
  сайта и фон на снимке — одного рода, и снимок не выглядит наклейкой.

  Что откуда взято:

    --surface   #181820  surfaceCard приложения, его несущий тон
                         (19–34 % площади каждого экрана)
    --border    #2A2A35  outline, ровно он
    --text      #F5F5F7  textPrimary
    --muted     #9A9AA4  textSecondary

  Светлой темы это не касается: там сайт остаётся почти бесцветным, как и
  задумано. Приложение тёмное — и сходство уместно ровно там, где сайт
  тоже тёмный.
*/
@media (prefers-color-scheme: dark) {
  :root {
    /* Между background (#0B0B0F) и surface (#101014) приложения: чистый
       #0B0B0F на большой площади web-страницы проваливается в яму, а
       карточкам нужно, чтобы фон был заметно темнее их самих */
    --bg: #0f0f14;
    --surface: #181820;
    --border: #2a2a35;
    --border-strong: #3c3b4a;
    --text: #f5f5f7;
    --muted: #9a9aa4;
    /*
      А вот textTertiary приложения (#65656F) сюда не годится. В приложении
      он подписывает мелочь под крупным текстом, на сайте им набран
      надзаголовок раздела — разрядкой, прописными, восьмым кеглем. На
      #181820 он дал бы контраст 2.9:1 при норме 4.5. Взят тон светлее:
      5.2:1, и это заодно чинит старое значение #77757F, у которого было
      3.7:1 — то есть тоже ниже нормы.
    */
    --faint: #8b8b97;
    --link: #ebeaf0;
  }
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

/*
  Здесь напрашивается overflow-x: hidden на html — «чтобы ничего не
  уезжало вбок». Намеренно не поставлено: это не лечение, а повязка на
  глаза. Вылезший за край блок никуда не денется, его просто обрежет, и
  на телефоне человек не увидит половину строки, зато и не поймёт, что
  что-то не так. Плюс на iOS такое правило ломает position: sticky.

  Вместо повязки — измерение: tools/check_widths.py открывает страницу в
  рамке заданной ширины и называет элементы, вылезшие за край. Найденное
  чинится в самом элементе.
*/

/*
  Длинные слова и адреса переносятся, а не вылезают за край. Особенно
  важно для support@dutovsolution.ru и ссылок в документах: на узком
  экране они длиннее строки.
*/
body, p, li, dd, h1, h2, h3, h4 { overflow-wrap: break-word; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, "Noto Sans", "PT Sans", "Helvetica Neue", Arial, sans-serif;
  font-size: 17px;
  line-height: 1.7;
}

.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 24px;
}

/* --- Шапка --------------------------------------------------------- */

.site-head {
  border-bottom: 1px solid var(--border);
  padding: 22px 0;
}

.site-head .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.brand {
  font-weight: 650;
  letter-spacing: -0.01em;
  text-decoration: none;
  color: var(--text);
  font-size: 1.05rem;
}

.brand span { color: var(--faint); font-weight: 400; }

/*
  Перенос обязателен: ряд из четырёх ссылок занимает около 330 px и без
  переноса не сжимается, то есть на экране 320 px распирал бы шапку, а с
  ней и страницу.

  Оговорка для честности: сначала я решил, что именно это ломает вёрстку
  на 360 px — так выглядел снимок. Оказалось, снимок врал (Chrome не
  делает окно уже ~500 px и просто обрезает картинку до запрошенной
  ширины), а настоящее переполнение было одно и в другом месте, у сетки
  снимков. Перенос всё равно нужен, но проверять надо измерением, а не
  глазами: tools/check_widths.py.
*/
.site-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 22px;
  font-size: 0.94rem;
}

.site-nav a {
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
}

.site-nav a:hover { color: var(--text); border-bottom-color: var(--border); }
.site-nav a[aria-current="page"] { color: var(--text); border-bottom-color: var(--text); }

/* --- Общая типографика --------------------------------------------- */

h1, h2, h3, h4 { letter-spacing: -0.02em; line-height: 1.18; margin: 0; }

h1 { font-size: clamp(2rem, 5vw, 3rem); font-weight: 680; }
h2 { font-size: clamp(1.4rem, 3vw, 1.85rem); font-weight: 640; }
h3 { font-size: 1.1rem; font-weight: 640; letter-spacing: -0.01em; }
/*
  h4 нарочно того же размера, что h3: он появился не для того, чтобы
  выглядеть мельче, а чтобы название экрана внутри группы стояло в
  разметке ниже заголовка группы. Разница между ними — не в кегле, а в
  оформлении самого заголовка группы (см. .screens-group).
*/
h4 { font-size: 1.1rem; font-weight: 640; letter-spacing: -0.01em; }

p { margin: 0 0 1.1em; max-width: var(--measure); }

a { color: var(--link); }

.lede {
  font-size: clamp(1.05rem, 2.2vw, 1.3rem);
  line-height: 1.55;
  color: var(--muted);
  max-width: 30rem;
}

.eyebrow {
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 14px;
}

.note { color: var(--muted); font-size: 0.93rem; }

/* Приписка к названию в заголовке: «NoteFlow — заметки и планирование».
   Тише названия, но того же кегля — это часть заголовка, а не подпись.
   На узком экране уходит на следующую строку целиком, а не по словам. */
.title-note {
  color: var(--muted);
  font-weight: 450;
  white-space: nowrap;
}
/* Подсказка про увеличение стоит сразу под заголовком раздела, а у h2
   собственного отступа снизу нет — иначе строчка липнет к заголовку */
.zoom-hint { margin-top: 14px; }

/* --- Название, которое печатается ------------------------------------ */
/*
  Сама печать — здесь, на CSS. Текст лежит в разметке как обычный текст:
  он виден и без JavaScript, и поисковику, и экранному диктору — анимация
  только показывает уже существующие буквы, а не создаёт их.

  В site.js есть маленький кусок про печать, но он анимацию не рисует, а
  лишь перезапускает: страница, открытая в фоновой вкладке, отыгрывает
  печать до того, как на неё посмотрят. Выключите скрипт — печать
  останется, но в фоновой вкладке пройдёт незамеченной.

  Приём — «печатная машинка»: строка обрезается по ширине, ширина растёт
  от нуля до полной шагами по числу знаков, и на границе стоит курсор.

  Отсюда два требования, оба обязательные:

  1. Шрифт МОНОШИРИННЫЙ. В нём единица `ch` равна ширине любого знака,
     поэтому шаг анимации попадает ровно между буквами. В пропорциональном
     шрифте steps() резал бы строку на равные куски, а буквы в нём разной
     ширины — узкая «l» и широкая «S» рвались бы посередине.

  2. Число в `width` и в `steps()` равно числу знаков названия.
     «Dutov Solution» — 14, пробел тоже считается. Поменяете название —
     поменяйте оба числа, иначе строка либо не допечатается до конца,
     либо допечатает пустоту.

  Курсор мигает шесть раз и замирает видимым — строка выглядит ждущей
  ввода. Бесконечное мигание на странице, которую читают минуту, только
  отвлекало бы, а шести раз хватает, чтобы приём прочитался.
*/

.wordmark {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas,
    "Liberation Mono", "DejaVu Sans Mono", monospace;
  font-size: clamp(1.5rem, 4.5vw, 2.45rem);
  font-weight: 600;
  color: var(--text);

  /*
    Межбуквенный интервал обязан быть нулевым, и это не вкус.
    Ширина строки считается в `ch` — по 1ch на знак. Интервал добавляется
    к каждому знаку сверх этого, поэтому при любом ненулевом значении
    настоящая длина текста расходится с шириной поля: при отрицательном
    курсор отрывается от последней буквы и висит в пустоте, при
    положительном — наезжает на неё и обрезает.
  */
  letter-spacing: 0;

  display: inline-block;
  max-width: 100%;
  width: 14ch;
  overflow: hidden;
  white-space: nowrap;
  vertical-align: bottom;

  border-right: 2px solid var(--app-violet);
  margin: 0 0 18px;

  /*
    Задержка перед началом — чтобы печать не совпала с отрисовкой
    страницы: иначе она успевает пройти, пока браузер ещё дорисовывает
    остальное, и человек видит уже готовую строку. Полсекунды хватает,
    чтобы взгляд успел дойти до места, где будет набираться текст.

    Две с лишним секунды на четырнадцать знаков — примерно скорость
    человека, печатающего вдумчиво. Быстрее — эффект пропадает, медленнее
    — начинает раздражать при повторных заходах.
  */
  animation:
    wordmark-type 2.2s steps(14, end) 0.5s both,
    wordmark-caret 0.65s step-end 0.5s 6 both;
}

@keyframes wordmark-type {
  from { width: 0; }
  to   { width: 14ch; }
}

/*
  Оба крайних кадра заданы явно, хотя 0% и 100% можно было бы опустить.
  Опущенные, они берутся из состояния элемента, и то, каким курсор
  останется после последнего мигания, зависит от того, как браузер
  разбирается со step-end на границе. У меня один и тот же файл в разных
  снимках давал то видимый курсор, то исчезнувший. Здесь гадать нечего:
  100% — видимый, значит после шести миганий он замирает видимым.
*/
@keyframes wordmark-caret {
  0%, 100% { border-right-color: var(--app-violet); }
  50% { border-right-color: transparent; }
}

/*
  Тем, кто просил систему не анимировать ничего, показываем название
  сразу и целиком. Курсор при этом убираем совсем: неподвижная палка
  рядом с текстом читалась бы как случайная чёрточка.
*/
@media (prefers-reduced-motion: reduce) {
  .wordmark {
    width: auto;
    border-right-color: transparent;
    animation: none;
  }
}

section { padding: 76px 0; border-bottom: 1px solid var(--border); }
section:last-of-type { border-bottom: 0; }

.hero { padding: 104px 0 88px; }

/* --- Карточки ------------------------------------------------------- */

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
  gap: 18px;
  margin-top: 34px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 22px;
}

.card h3 { margin-bottom: 8px; }
.card p { margin: 0; color: var(--muted); font-size: 0.95rem; }

/* --- Раздел про NoteFlow -------------------------------------------- */
/*
  Единственное место с фирменным градиентом приложения. Тонкая полоса
  сверху, а не заливка: цвет должен обозначить раздел, а не перекричать
  текст в нём.
*/

.app-block {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 40px 32px 34px;
  overflow: hidden;
  margin-top: 30px;
}

.app-block::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 4px;
  background: linear-gradient(90deg, var(--app-violet), var(--app-blue));
}

/*
  Шапка блока: слева иконка приложения и подпись состояния, справа —
  кнопка магазина. Одна строка, и кнопка прижата к правому краю полем
  `auto`, а не `justify-content`: так левая пара остаётся вместе, когда
  строка переносится.
*/
.app-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 18px;
}

/* Метка раздела — иконка приложения: градиент подложки здесь, рисунок —
   встроенным SVG в index.html. До 19.09.2026 здесь были буквы NF. */
.app-mark {
  display: inline-block;
  width: 46px;
  height: 46px;
  border-radius: 14px;
  overflow: hidden;
  flex: none;
  background: linear-gradient(135deg, var(--app-violet), var(--app-blue));
}

.app-mark svg {
  display: block;
  width: 100%;
  height: 100%;
}

.status {
  display: inline-block;
  font-size: 0.82rem;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 13px;
  margin: 0;
}

.feature-list {
  list-style: none;
  padding: 0;
  margin: 26px 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr));
  gap: 16px 28px;
}

.feature-list li {
  padding-left: 16px;
  border-left: 2px solid var(--border);
  font-size: 0.95rem;
  color: var(--muted);
}

.feature-list b { color: var(--text); font-weight: 620; display: block; }

/*
  Подпись «Опубликовано» — та же пилюля, но зелёная. Цвет задан своими
  числами, а не переменной темы: зелёного в палитре сайта нет вовсе, и
  заводить его ради одной подписи незачем. Оттенки подобраны так, чтобы
  текст читался на обеих темах.
*/
.status-live {
  color: #1f7a45;
  border-color: #9bd4b2;
  background: #eef8f2;
}

@media (prefers-color-scheme: dark) {
  .status-live {
    color: #7fd6a4;
    border-color: #2f5a44;
    background: #16261e;
  }
}

/* --- Кнопка магазина --------------------------------------------------- */
/*
  Знак и подпись в строку, в шапке блока справа от иконки приложения —
  так попросил владелец 26.09.2026. Прежде знак был 64 px и лежал внизу
  блока по центру: он перевешивал иконку самого приложения, хотя главный
  здесь NoteFlow, а магазин — только место, где его взять.

  Знак 40 px против 46 у иконки приложения: почти вровень, но чуть
  мельче — порядок старшинства виден, и строка не перекашивается.

  Поле `auto` слева прижимает кнопку к правому краю; при переносе строки
  оно обнуляется, иначе на узком экране кнопка уехала бы вправо одна.
*/
.store {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-inline-start: auto;
  padding: 7px 14px 7px 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text);
  text-decoration: none;
}

.store-mark {
  display: block;
  width: 40px;
  height: 40px;
  flex: none;
}

.store-text {
  font-size: 0.92rem;
  font-weight: 620;
}

/*
  Отклик на наведение — тот же, что у карточек, и по той же причине
  только при наличии курсора: на телефоне :hover залипает после нажатия.
*/
@media (hover: hover) {
  .store:hover {
    transform: translateY(-3px);
    border-color: var(--border-strong);
    box-shadow: 0 16px 34px -26px rgba(0, 0, 0, 0.55);
  }
}

/* --- Экраны приложения ------------------------------------------------ */
/*
  Один экран — один блок: слева снимок, справа текст про то, что на нём.

  Раньше блоков было три и в каждом лежало по два снимка. Смотрелось
  плотнее, но подпись описывала сразу два экрана и неизбежно получалась
  общей: «заметки и редактор», «напоминания и выгрузка». Шесть отдельных
  блоков дают каждому снимку свои две строки — и текст наконец говорит
  ровно про то, что на картинке.

  Рамка и скругление намекают на телефон, но рисовать вокруг настоящий
  корпус не стал: нарисованная рамка читается как рекламный макет, а тут
  снимок должен читаться как снимок.
*/

/*
  min(330px, 100%) — не украшательство. Голый minmax(330px, 1fr) задаёт
  колонке жёсткий минимум: на экране, где под содержимое остаётся 269 px,
  колонка всё равно требует 330 и выпирает за край вместе со страницей.
  Ровно так на сайте однажды и нашлось единственное переполнение,
  замеренное на 320 px. min() разрешает колонке сжаться до ширины
  контейнера, когда 330 уже не помещаются.
*/
/*
  auto-fill, а не auto-fit, и это важно с тех пор, как экраны разбиты на
  части. auto-fit схлопывает пустые колонки: в части из одной карточки
  она растянулась бы во всю ширину раздела, а её снимок остался бы
  прежних 190 px — карточка на полтысячи пикселей пустого текста рядом с
  узкой картинкой. auto-fill колонку сохраняет, и одинокая карточка
  встаёт ровно там же, где встала бы первая из пяти.
*/
.screens {
  list-style: none;
  padding: 0;
  margin: 18px 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr));
  gap: 20px;
}

/*
  Заголовок части: «Заметки», «Планирование», «Перенос». Одиннадцать
  карточек подряд читались как опись — части разбивают их по тому, чем
  человек занят в этот момент.

  Отличается от названий экранов не размером, а набором: разрядка и
  капитель роднят его с надстрочниками разделов (.eyebrow) и сразу
  показывают, что это подпись к группе, а не ещё одна карточка. Тонкая
  линия слева отделяет его от предыдущей группы, не занимая строки.
*/
.screens-group {
  margin: 46px 0 0;
  padding-left: 14px;
  border-left: 3px solid var(--border-strong);
  font-size: 0.82rem;
  font-weight: 620;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Первая группа идёт сразу за подсказкой про увеличение — ей такой
   отбивки не нужно, иначе раздел начинается с пустоты */
.zoom-hint + .screens-group { margin-top: 30px; }

/*
  В тёмной теме линия у заголовка части берёт фирменный фиолетовый.
  Три коротких штриха на всю страницу — ровно столько цвета, сколько
  нужно, чтобы раздел со снимками читался как часть приложения, а не
  как чужая галерея. В светлой теме линия остаётся серой: там сайт
  держится правила «почти без цвета».

  Приглушён до 55 %: чистый #8B5CF6 на тёмном фоне светится и тянет
  взгляд сильнее, чем заголовок, к которому он приставлен.
*/
@media (prefers-color-scheme: dark) {
  .screens-group { border-left-color: color-mix(in srgb, var(--app-violet) 55%, var(--border)); }

  /*
    Снимки тёмные, карточка под ними тоже. Серая рамка на этом стыке
    пропадает, и снимок сливается с карточкой в одно пятно. Фиолетовая
    примесь даёт краю различимость, не превращая рамку в подсветку.
  */
  .screen-shot { border-color: color-mix(in srgb, var(--app-violet) 22%, var(--border)); }
}

/*
  Выделение текста — единственный фирменный цвет, который есть в обеих
  темах. Мелочь, но она попадается на глаза каждому, кто копирует адрес
  почты или кусок документа, и серое системное выделение на этом месте
  выглядит чужим.
*/
::selection {
  background: color-mix(in srgb, var(--app-violet) 28%, transparent);
  /* Цвет самого текста не трогаем: на светлой теме он тёмный, на тёмной
     светлый, и оба остаются читаемыми на такой полупрозрачной подложке */
}

.screen-card {
  margin: 0;
  display: flex;
  /*
    Текст выравнивается по середине снимка, а не по его верху. Снимок
    высокий (вдвое выше своей ширины), подпись — четыре строки; прижатая
    к верху, она оставляла под собой полблока пустоты, и блок выглядел
    незаконченным.
  */
  align-items: center;
  gap: 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 20px;
}

/*
  Ширина снимка задана долей и потолком сразу. Доля нужна, чтобы блок
  делился предсказуемо на любой ширине; потолок — чтобы на широком экране
  снимок не разъезжался: он высотой вдвое больше своей ширины, и каждые
  лишние 50 px ширины добавляют блоку сотню в высоту.
*/
.screen-shot {
  flex: 0 0 40%;
  max-width: 190px;
  height: auto;
  border: 1px solid var(--border);
  border-radius: 10px;
  /* Снимки тёмные: на светлой карточке край без подложки выглядел бы
     обрубленным, а тень мягко отделяет их от фона */
  box-shadow: 0 8px 20px -14px rgba(0, 0, 0, 0.5);
  background: #0b0b0f;
}

.screen-text h4 { margin-bottom: 8px; }

.screen-text p {
  margin: 0;
  max-width: none;
  font-size: 0.92rem;
  color: var(--muted);
  line-height: 1.6;
}

/* --- Движение --------------------------------------------------------- */
/*
  Всё движение на сайте собрано здесь, и у него одна общая оговорка:
  оно живёт под классом `anim` на <html>. Класс ставит site.js.

  Отсюда следствие, ради которого так и сделано: без скрипта страница
  видна целиком и сразу. Прятать блоки «до появления» умеет только тот,
  кто умеет их показать; если бы начальное состояние стояло без `anim`,
  отключённый JavaScript оставил бы посетителя перед пустой страницей.

  Если система просила поменьше движения, рядом с `anim` стоит `still`.
  Тогда блоки проявляются, но не выезжают, и всё, что именно движется, —
  сдвиг, перелив полосы, рост снимка под курсором, — гасится правилами
  `html.still` в конце раздела. До 19.09.2026 при этой настройке класс
  `anim` не ставился вовсе и страница была неподвижной целиком; почему
  отказались — в site.js.

  Печать названия — исключение: она на чистом CSS, объявлена без `anim`,
  и её выключает свой @media prefers-reduced-motion выше. Печать — это
  тоже движение, курсор мигает, поэтому там она не возвращена.
*/

/*
  Что появляется при прокрутке, задаёт разметка — атрибут `data-reveal` на
  блоке. Списка селекторов здесь нет намеренно: он был бы записан ещё и в
  site.js и разошёлся бы с этим при первой правке страницы.
*/
html.anim [data-reveal] {
  opacity: 0;
  /*
    Сдвиг задан свойством `translate`, а не `transform`, и это не
    придирка. Наведение на карточку приподнимает её тем самым transform;
    будь появление тоже на нём, одно затирало бы другое, и карточка,
    появившаяся при прокрутке, переставала бы отзываться на курсор.
    Отдельные свойства складываются друг с другом сами.
  */
  translate: 0 14px;
}

html.anim [data-reveal].shown {
  opacity: 1;
  translate: none;
}

/*
  Переходы объявлены одним правилом на всё: и появление, и отклик на
  наведение. Разнеси их по двум — правило с `data-reveal` перебило бы
  второе целиком (у него выше вес), и карточка после появления потеряла
  бы плавность наведения. Ошибка тихая: выглядит как «иногда дёргается».
*/
html.anim [data-reveal],
.card, .screen-card, .screen-shot, .store, .site-nav a, .site-foot a {
  transition:
    opacity 0.55s ease,
    translate 0.55s cubic-bezier(0.22, 0.68, 0.36, 1),
    transform 0.28s ease,
    border-color 0.28s ease,
    box-shadow 0.28s ease,
    color 0.2s ease;
}

/*
  Карточки в ряду появляются не разом, а по очереди — взгляд успевает
  пройти слева направо. Только на широком экране: на телефоне карточки
  стоят столбиком и въезжают в вид по одной, там очередь превратилась бы
  в задержку на ровном месте.
*/
@media (min-width: 621px) {
  html.anim .cards > [data-reveal]:nth-child(2) { transition-delay: 0.09s; }
  html.anim .cards > [data-reveal]:nth-child(3) { transition-delay: 0.18s; }
  html.anim .cards > [data-reveal]:nth-child(4) { transition-delay: 0.27s; }

  /*
    Экраны стоят в две колонки, поэтому очередь у них своя: задержка не
    по порядку в списке, а по чётности. Правый блок в паре появляется
    следом за левым — и так в каждом ряду. Считай мы по порядку, задержка
    росла бы до конца списка, и шестой блок ждал бы полсекунды.
  */
  html.anim .screens > [data-reveal]:nth-child(even) { transition-delay: 0.09s; }
}

/* Подзаголовок идёт следом за заголовком, а не вместе с ним */
html.anim .hero .lede { transition-delay: 0.12s; }

/*
  Снимок внутри карточки появляется вторым шагом — сама карточка уже
  встала, и на неё ложится картинка. Движение читается как «карточка
  собирается», а не «всё выехало разом»; на одиннадцати карточках подряд
  разница заметная.

  Снимок не помечен data-reveal намеренно: наблюдатель в site.js следит
  за карточкой, а картинка идёт следом за ней по классу `shown`. Будь у
  неё своя метка, она появлялась бы по собственному пересечению с краем
  экрана — то есть иногда раньше своей же карточки.
*/
html.anim .screen-card .screen-shot {
  opacity: 0;
  translate: 0 10px;
}

html.anim .screen-card.shown .screen-shot {
  opacity: 1;
  translate: none;
  transition-delay: 0.14s;
}

/*
  Отклик на наведение — только там, где есть чем наводить. На телефоне
  `:hover` залипает после нажатия: карточка остаётся приподнятой, пока не
  тронешь другую, и выглядит это как застрявшая анимация.
*/
@media (hover: hover) {
  .card:hover,
  .screen-card:hover {
    transform: translateY(-3px);
    border-color: var(--border-strong);
    box-shadow: 0 16px 34px -26px rgba(0, 0, 0, 0.55);
  }

  /* Увеличение снимка делает скрипт, поэтому и подсказка про него — под
     классом anim: без скрипта нажимать не на что */
  html.anim .screen-shot:hover { transform: scale(1.03); }

  /*
    Задержка в 0.14s поставлена появлению снимка и после появления
    остаётся на элементе. Без этой строки она досталась бы и увеличению
    под курсором: наводишь — и картинка отзывается через полтора десятых
    секунды, будто страница задумалась.
  */
  html.anim .screen-card:hover .screen-shot { transition-delay: 0s; }
}

html.anim .screen-shot { cursor: zoom-in; }

/*
  Полоса над разделом NoteFlow медленно переливается. Двенадцать секунд
  на проход — движение на грани заметности: взгляд его ловит, чтение оно
  не перебивает. Секунды три превратили бы полосу в бегущую строку.
*/
html.anim .app-block::before {
  background: linear-gradient(90deg,
    var(--app-violet), var(--app-blue), var(--app-violet));
  background-size: 200% 100%;
  animation: app-sheen 12s linear infinite;
}

@keyframes app-sheen {
  from { background-position: 0 0; }
  to   { background-position: -200% 0; }
}

/*
  «Поменьше движения». Остаётся только проявление — прозрачность меняется
  на месте, ничто не едет и не растёт. Сдвиг снимается и у начального
  состояния, и у перехода: убери его только у одного — и блок всё равно
  проедет свои 14 px, просто без плавности, рывком.
*/
html.anim.still [data-reveal],
html.anim.still .screen-card .screen-shot {
  translate: none;
}

html.anim.still .app-block::before { animation: none; }

@media (hover: hover) {
  html.anim.still .card:hover,
  html.anim.still .screen-card:hover,
  html.anim.still .store:hover { transform: none; }
  html.anim.still .screen-shot:hover { transform: none; }
}

/* --- Снимок во весь экран -------------------------------------------- */
/*
  Окно открывает site.js на родном <dialog>: затемнение фона, закрытие по
  Esc и возврат фокуса на снимок достаются от браузера даром. Здесь —
  только оформление.

  Фон и текст заданы числами, а не переменными темы: под окном лежит
  затемнение, и на нём светлая подпись обязана остаться светлой, какая бы
  тема ни стояла у посетителя.
*/

.zoom {
  border: 0;
  padding: 0;
  background: transparent;
  max-width: min(92vw, 420px);
  max-height: 92vh;
  overflow: visible;
}

.zoom::backdrop { background: rgba(10, 10, 14, 0.86); }

.zoom[open] { animation: zoom-open 0.22s cubic-bezier(0.2, 0.7, 0.3, 1) both; }

@keyframes zoom-open {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: none; }
}

/* «Поменьше движения»: окно проявляется, но не вырастает */
html.still .zoom[open] { animation-name: zoom-fade; }

@keyframes zoom-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.zoom form {
  display: flex;
  justify-content: flex-end;
  margin: 0 0 10px;
}

.zoom-close {
  width: 38px;
  height: 38px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}

.zoom-close:hover { background: rgba(255, 255, 255, 0.26); }

.zoom img {
  display: block;
  width: 100%;
  height: auto;
  /* Снимок высокий и узкий: без потолка по высоте он на широком экране
     вылезал бы за нижний край вместе с подписью */
  max-height: 76vh;
  object-fit: contain;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: #0b0b0f;
}

.zoom-caption {
  margin: 12px 0 0;
  max-width: none;
  text-align: center;
  font-size: 0.9rem;
  line-height: 1.5;
  color: #d6d5df;
}

/* --- Контакты и подвал ---------------------------------------------- */

.contact-line { font-size: 1.15rem; margin-bottom: 8px; }

.details {
  margin: 26px 0 0;
  font-size: 0.92rem;
  color: var(--muted);
  line-height: 1.9;
}

.details dt {
  color: var(--faint);
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin-top: 14px;
}

.details dd { margin: 0; }

.site-foot {
  border-top: 1px solid var(--border);
  padding: 34px 0 60px;
  font-size: 0.88rem;
  color: var(--faint);
}

.site-foot .wrap {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
}

.site-foot a { color: var(--muted); text-decoration: none; }
.site-foot a:hover { color: var(--text); }

.foot-links { display: flex; gap: 20px; flex-wrap: wrap; }

/* --- Юридические документы ------------------------------------------ */
/*
  Текст документов приходит из docs/legal — тех же файлов, что показывает
  само приложение. Здесь задано только оформление: в приложении документ
  открывается своей вёрсткой, на сайте — этой.
*/

.doc {
  max-width: 44rem;
  padding: 56px 0 90px;
}

.doc h1 { font-size: clamp(1.7rem, 4vw, 2.3rem); margin-bottom: 10px; }
.doc h2 { font-size: 1.22rem; margin: 44px 0 12px; }
.doc h3 { font-size: 1.02rem; margin: 26px 0 8px; }

.doc p, .doc li { max-width: none; color: var(--text); }

/*
  Строка «Редакция от …» у двух согласий. В самих текстах даты нет, её
  ставит tools/docx_to_html.py — это наша пометка, а не слова юриста.
  Поэтому она набрана как служебная строка, а не как первый абзац
  документа: так видно, что это выходные данные, а не часть текста.
*/
.doc p.edition {
  color: var(--muted);
  font-size: 0.9rem;
  margin-bottom: 26px;
}
.doc ul, .doc ol { padding-left: 22px; margin: 0 0 1.1em; }
.doc li { margin-bottom: 6px; }

.doc .dates {
  color: var(--faint);
  font-size: 0.9rem;
  margin-bottom: 34px;
}

.doc code {
  font-family: ui-monospace, "SF Mono", Consolas, "Liberation Mono", monospace;
  font-size: 0.9em;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 5px;
}

/* Широкие таблицы прокручиваются сами, а не растягивают страницу вбок */
.doc .table-wrap {
  overflow-x: auto;
  margin: 18px 0 26px;
}

.doc .table-wrap table { margin: 0; }

.doc footer {
  margin-top: 46px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
  color: var(--faint);
  font-size: 0.9rem;
}

.doc table {
  width: 100%;
  border-collapse: collapse;
  margin: 18px 0 26px;
  font-size: 0.94rem;
}

.doc th, .doc td {
  border: 1px solid var(--border);
  padding: 10px 12px;
  text-align: left;
  vertical-align: top;
}

.doc th { background: var(--surface); font-weight: 620; }

.doc .callout, .doc blockquote {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--app-violet);
  border-radius: 0 10px 10px 0;
  padding: 14px 18px;
  margin: 20px 0;
}

.doc blockquote { font-style: normal; }

.doc hr { border: 0; border-top: 1px solid var(--border); margin: 34px 0; }

/* --- Мелочи ---------------------------------------------------------- */

.skip {
  position: absolute;
  left: -9999px;
}

.skip:focus {
  left: 24px;
  top: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 14px;
  z-index: 10;
}

/* --- Под разные экраны ------------------------------------------------ */
/*
  Границы выбраны не по названиям устройств, а по тому, где вёрстка
  начинает мешать чтению. «Планшет» бывает и 768, и 1180 — привязываться
  к нему бессмысленно.
*/

/* Планшет в книжной ориентации и узкое окно на ноутбуке */
@media (max-width: 860px) {
  .wrap { padding: 0 20px; }
  section { padding: 64px 0; }
  .hero { padding: 76px 0 64px; }
}

/* Телефон */
@media (max-width: 620px) {
  .wrap { padding: 0 18px; }

  section { padding: 52px 0; }
  .hero { padding: 44px 0 48px; }

  .site-head { padding: 16px 0; }
  .site-head .wrap { gap: 12px; }
  .site-nav { gap: 8px 18px; font-size: 0.9rem; }

  .app-block { padding: 30px 20px 26px; }

  /* Кнопка магазина переносится на свою строку — и встаёт под иконкой,
     а не у правого края: иначе она висела бы в пустоте одна */
  .store { margin-inline-start: 0; }
  .screen-card { padding: 16px; gap: 14px; }
  .screens { gap: 14px; }

  /* Столбец в столбце незачем: карточки и так во всю ширину */
  .cards { gap: 14px; }
  .feature-list { gap: 14px 0; }

  .site-foot { padding: 28px 0 44px; }
  .site-foot .wrap { gap: 12px; }
  .foot-links { gap: 8px 16px; }

  .doc { padding: 40px 0 70px; }
  .doc h2 { margin-top: 34px; }
}

/*
  Совсем узкие экраны — от 320 px. Снимок и текст рядом здесь делят между
  собой 270 px: снимок сжимается до сотни, текст остаётся колонкой в
  двадцать знаков, и читать становится нечего. Ставим их друг под друга —
  блок выше, зато и на снимке что-то видно, и строка нормальной длины.
*/
@media (max-width: 460px) {
  .screen-card { flex-direction: column; align-items: center; }

  .screen-shot {
    flex: none;
    width: 62%;
    max-width: 190px;
  }

  .screen-text { align-self: stretch; }
}

/* Широкие экраны: не даём строке разъезжаться до нечитаемой длины */
@media (min-width: 1400px) {
  :root { --wrap: 68rem; }
}

/* --- Согласие на счётчик ----------------------------------------------- */
/*
  Полоса внизу, а не окно посередине. Модальное окно загораживает страницу
  до ответа, то есть требует решения от того, кто ещё ничего не прочитал;
  выбор в такой обстановке делают не думая, лишь бы убрать. Полоса не
  мешает читать и не мешает уйти по ссылке в политику — а именно за этим
  человека и отправляют, прежде чем он ответит.

  На страницах документов баннера нет вовсе: /consent.js туда не
  подключён. Причина в самом файле и в пункте 3 списка проверки в README.
*/
.consent {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  background: var(--surface);
  border-top: 1px solid var(--border-strong);
  /* Тень наверх: полоса лежит НА странице, а не в конце текста */
  box-shadow: 0 -8px 24px rgba(0, 0, 0, .07);
}

.consent-in {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: .95rem 1.25rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .75rem 1.5rem;
}

.consent-text {
  margin: 0;
  flex: 1 1 22rem;
  font-size: .9rem;
  line-height: 1.5;
  color: var(--muted);
}

.consent-text a { color: var(--link); }

.consent-buttons {
  display: flex;
  gap: .6rem;
  flex: 0 0 auto;
}

.consent-buttons button {
  font: inherit;
  font-size: .9rem;
  padding: .5rem 1.15rem;
  border-radius: 6px;
  cursor: pointer;
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text);
}

/*
  «Принять» заметнее «Отклонить», но обе — настоящие кнопки одного
  размера. Отказ, спрятанный под серой строчкой мелким шрифтом, — это
  согласие, полученное измором, и стоит оно ровно столько же, сколько
  всякое согласие, которого человек не давал.
*/
.consent-buttons .consent-yes {
  border-color: var(--text);
  background: var(--text);
  color: var(--bg);
}

.consent-buttons button:hover { border-color: var(--text); }

.consent-buttons button:focus-visible {
  outline: 2px solid var(--app-violet);
  outline-offset: 2px;
}

/* Подвал не должен уходить под полосу — иначе его не дочитать */
body.consent-open { padding-bottom: 7.5rem; }

@media (max-width: 560px) {
  .consent-buttons { width: 100%; }
  .consent-buttons button { flex: 1 1 auto; }
  body.consent-open { padding-bottom: 11rem; }
}

/* Ссылка «Счётчик посещаемости» в подвале — ею выбор меняют */
.foot-links button[data-consent] {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
}

/* --- Печать ----------------------------------------------------------- */
/*
  Из этих правил получается PDF политики сайта — страница печатается в
  файл, и всё, что на бумаге лишнее, убирается здесь. Ими же печатается
  любая страница сайта, если кому-то понадобится бумажная копия.

  Документы приложения (/privacy, /terms) печатать отсюда не нужно: у них
  свой печатный вид внутри docs/legal, и PDF для магазинов собирается из
  исходного файла, а не с сайта. Правила ниже им не мешают — просто
  применяются к другой копии того же текста.

  ⚠️ Блок обязан стоять ПОСЛЕ @media (prefers-color-scheme: dark). Вес у
  правил одинаковый, спор решает порядок в файле: встань он выше, тёмная
  тема системы перекрасила бы бумагу в чёрный, а тонер — расходник.
*/
@media print {
  :root {
    --bg: #ffffff;
    --surface: #f4f4f7;
    --border: #cfcfd8;
    --text: #000000;
    --muted: #3d3d47;
    --faint: #55545e;
    --link: #000000;
  }

  @page { margin: 18mm 16mm; }

  body {
    background: #fff;
    color: #000;
    font-size: 10.5pt;
    /* Без этого браузер выбрасывает фоны «ради экономии», и врезки с
       таблицами теряют подложку, по которой их и отличают от текста */
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* Шапка, подвал и всё интерактивное на бумаге бессмысленны */
  .site-head,
  .site-foot,
  .skip,
  .zoom,
  .zoom-hint,
  .consent { display: none !important; }

  /* Отступ под полосу согласия на бумаге не нужен */
  body.consent-open { padding-bottom: 0; }

  .wrap { max-width: none; padding: 0; }
  .doc { max-width: none; padding: 0; }

  h1 { font-size: 19pt; }
  h2 { font-size: 12.5pt; margin-top: 26px; }

  /* Заголовок не должен оставаться один внизу страницы */
  h1, h2, h3 { break-after: avoid; page-break-after: avoid; }

  .callout, blockquote, .table-wrap, table, tr, li, .doc footer {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  .table-wrap { overflow: visible; }

  a { color: #000; text-decoration: underline; }

  /*
    На бумаге ссылка не нажимается — печатаем адрес рядом. Почтовые
    ссылки не трогаем: у них адрес и так написан текстом, вышло бы
    «support@… (mailto:support@…)».
  */
  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 0.82em;
    color: #3d3d47;
    word-break: break-all;
  }

  /*
    Свои ссылки записаны от корня — «/privacy». На бумаге это не адрес, а
    загадка: с чего начинать, читатель знать не обязан. Дописываем домен,
    иначе единственная ссылка из политики сайта на политику приложения
    оказывается непроходимой.
  */
  a[href^="/"]::after {
    content: " (dutovsolution.ru" attr(href) ")";
    font-size: 0.82em;
    color: #3d3d47;
    word-break: break-all;
  }
}
