/*
    Единый визуальный язык платформы Olista.

    Один файл на три приложения: консоль конструктора (Center.SiteBuilder), сайт организации
    (OlistaTradeEnterprise) и сервер идентификации (Center.Identity). Доставляется как статика библиотеки
    классов Razor — _content/Center.Design/design.css, — то есть лежит на диске в одном экземпляре и
    подключается ссылкой, а не копированием. Почему так, а не три копии, — см. комментарий в Center.Design.csproj.

    Что здесь есть: значения (цвета, радиусы, тени, типографика, отступы, длительности) и оболочка страницы
    (ссылка обхода, шапка, бургер, кнопки). Чего здесь нет: содержимое экранов. Карточки консоли, витрина
    сайта организации и формы входа остаются в своих листах — но берут отсюда всё, что у них должно совпадать.

    Чего здесь нет и не будет: стилей опубликованных сайтов клиентов (blocks*.css). У них своя палитра автора
    и свой макетный закон; общие токены платформы к ним отношения не имеют, и смешать одно с другим значит
    отдать посетителю чужого сайта индиговые кнопки Olista.

    Ни одного внешнего шрифта и ни одного обращения к CDN — как и во всех приложениях платформы: страница
    обязана открыться и выглядеть собой на машине, у которой наружу ничего не пущено.
*/

/* ===================================================================================================
   ЦВЕТ

   Три приложения держали три почти одинаковых фона — #f6f7fa, #f4f5f8, #f6f7fb. Отличить их глазом нельзя,
   а переиспользовать нельзя тоже: каждое следующее место брало ближайший к нему литерал, и множество росло.
   Здесь фон один.

   Пары «краска + подложка» объявлены рядом и обязаны меняться вместе. Подложка обязана быть парной теме:
   светло-зелёная плашка на тёмной консоли не «мягкая», а слепящая — это единственное светлое пятно на экране.

   Контраст всех пар проверен арифметикой ThemeContrast (WCAG 2.1) в тесте DesignSystemTests: 4.5 для текста,
   3.0 для крупного и для границ смысловых цветов. Меняя значение, прогоните тест — глазом эти 0.2 не видно.
   =================================================================================================== */

:root {
    color-scheme: light dark;

    /* Поверхности: страница, панель на ней, вложенная в панель подложка, линия между ними. */
    --ds-bg: #f5f6fa;
    --ds-surface: #ffffff;
    --ds-surface-alt: #eff1f6;
    --ds-line: #e2e5ee;

    --ds-text: #171a22;
    --ds-muted: #5f6678;

    /*
        Один акцент на всю платформу.

        До этого у сайта организации был бирюзовый #0b6b62, у двух остальных индиговый #4f46e5, и переход
        «нажал „Войти" в конструкторе» выглядел переходом на сайт другой компании. Индиго выбран потому,
        что его носят две поверхности из трёх и все кнопки действия.

        Две роли — две переменные. --ds-accent читают как текст (ссылки, надзаголовки, кольцо фокуса),
        --ds-accent-fill заливают под --ds-accent-contrast. В светлой теме это один и тот же цвет, в тёмной
        нет: осветлённый под чтение индиго под белыми буквами даёт 3.6 при норме 4.5.
    */
    --ds-accent: #4f46e5;
    --ds-accent-fill: #4f46e5;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #eceafe;

    /* Смысловые цвета и их подложки. */
    --ds-danger: #b91c1c;
    --ds-danger-soft: #fdecea;
    --ds-success: #146c33;
    --ds-success-soft: #e7f5ec;
    --ds-warning: #b45309;
    --ds-warning-soft: #fff7ed;
    --ds-info: #1f5fa9;
    --ds-info-soft: #e8f1fb;

    /*
        Цвет марки — не акцент.

        Бирюзовый остаётся сайту организации знаком: логотип и надзаголовок первого экрана. В интерактиве его
        нет ни в одной из трёх поверхностей — кнопка, ссылка и кольцо фокуса везде индиговые, иначе «сюда
        можно нажать» опять начнёт означать в каждом приложении своё.
    */
    --ds-brand: #0b6b62;
    --ds-brand-soft: #e6f2f0;

    /* =============================================================================================== */

    /*
        ТРИ ступени радиуса, и не больше.

        В ходу было шесть — 0, 8, 10, 12, 14, 16, — и выбор между 10 и 12 каждый раз делался заново. Ступень
        назначена по размеру вещи: мелкому нужен мелкий радиус, иначе он выглядит таблеткой.
    */
    --ds-radius-sm: 8px;   /* поля ввода, чипы, пункты меню */
    --ds-radius-md: 12px;  /* кнопки, мелкие карточки, всплывающие панели */
    --ds-radius-lg: 16px;  /* крупные карточки и полосы */
    --ds-radius-pill: 999px;

    /*
        ТРИ ступени тени.

        Тени были у всех — и в этом была беда, а не в их отсутствии: на 981daed по листам платформы жило
        четырнадцать разных значений (0 22px 50px 8%, 0 22px 50px 12%, 0 16px 34px 22%, 0 12px 32px .18,
        0 8px 24px .06, 0 6px 20px 18%, 0 10px 28px .18, 0 18px 48px .18…), и каждое место называло высоту
        заново. Токен был ровно один, у сервера идентификации. При этом ровно то, чему высота нужнее всего —
        карточки обеих публичных страниц, — не имело её вовсе и отделялось от фона одной рамкой в пиксель.

        Ступень выбирается по тому, ЧТО она отделяет, а не по тому, «насколько сильно». Имена короткие и
        потому обманчивые — читать надо это описание, а не их:

        sm — хайрлайн: подпирает сплошную линию, но не заменяет её. 6% на одном пикселе — это одна ступень
             яркости под краем, и как единственное отделение карточки от фона он невидим (замер на 375 в
             светлой теме: под краем 231 при фоне 232, дальше 232 ровно). Его место — там, где граница уже
             есть: у прокрученной шапки вместе с border-bottom-color.
        md — предмет на плоскости: карточка, панель, выпадающий список. Ступень по умолчанию для всего, что
             «лежит на странице», — вопреки тому, как звучит имя sm.
        lg — накладной слой: модальное окно, витрина первого экрана, меню бургера.
    */
    --ds-shadow-sm: 0 1px 2px rgb(16 18 31 / 6%);
    --ds-shadow-md: 0 1px 2px rgb(16 18 31 / 6%), 0 8px 24px rgb(16 18 31 / 8%);
    --ds-shadow-lg: 0 2px 6px rgb(16 18 31 / 8%), 0 22px 50px rgb(16 18 31 / 12%);

    /* Подсветка наведения, окрашенная акцентом: едет за сменой акцента сама. */
    --ds-shadow-accent: 0 8px 24px color-mix(in srgb, var(--ds-accent-fill) 22%, transparent);

    /* =============================================================================================== */

    /*
        ТИПОГРАФИКА

        Кегль был 15 в конструкторе, 15 на сайте организации и 16 на сервере идентификации — то есть текст
        менял размер ровно в тот момент, когда человек уходил вводить пароль. Взят конструкторский 15/1.5:
        экранов на нём больше всего, и переучивать пришлось бы их.
    */
    --ds-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --ds-font-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Menlo, Consolas, monospace;

    --ds-size: 15px;
    --ds-leading: 1.5;
    --ds-size-lg: 17px;
    --ds-size-sm: 14px;
    --ds-size-xs: 12.5px;

    /* Заголовок страницы и отрицательный трекинг к нему: на крупном кегле системный шрифт без него рыхлый. */
    --ds-h1: 30px;
    --ds-h2: 20px;
    --ds-tracking-tight: -0.02em;
    --ds-leading-tight: 1.2;

    /* =============================================================================================== */

    /* ОТСТУПЫ: шаг 4, шесть ступеней. Всё, что между ними, — это либо ошибка, либо новая ступень. */
    --ds-space-1: 4px;
    --ds-space-2: 8px;
    --ds-space-3: 12px;
    --ds-space-4: 16px;
    --ds-space-5: 22px;
    --ds-space-6: 32px;

    /*
        ВЫСОТЫ УПРАВЛЯЮЩИХ ЭЛЕМЕНТОВ

        46 у крупной кнопки и 40 у обычной — шкала конструктора. Нижняя граница цели нажатия по WCAG 2.2 —
        24 пикселя, и не из вежливости: попадание пальцем и попадание мышью при треморе считаются одинаково.
        Поэтому 32 — минимум для строчной ссылки в шапке, а не «сколько получится по строке текста».

        32 — это про мышь. У пальца своя цифра, и она приезжает ниже, в @media (pointer: coarse).
    */
    --ds-control: 40px;
    --ds-control-lg: 46px;
    --ds-tap: 32px;

    /*
        КОЛЬЦО ФОКУСА — одним значением, чтобы его можно было не только нарисовать, но и вернуть.

        Кольцо уже стоит глобальным правилом :focus-visible ниже, и на публичных страницах этого хватает.
        Не хватает там, где поверх наших стилей лежит чужая библиотека: BootstrapBlazor снимает обводку у
        своих кнопок (.btn:focus-visible { outline: 0 }), а его бандл подключён ПОСЛЕ design.css — то есть
        спор одинаковых специфичностей он выигрывает всегда. Отсюда токен и утилита .ds-focusable: место,
        где кольцо возвращают руками, не переписывая цвет заново.
    */
    --ds-focus-ring: 2px solid var(--ds-accent);

    /* Шапка одной высоты на всех четырёх поверхностях. До этого было 60 у конструктора и 108 у входа. */
    --ds-header: 56px;

    /* Ширина колонки страницы: по ней выровнено содержимое шапки, подвала и разделов. */
    --ds-page: 1140px;

    /* =============================================================================================== */

    /*
        ДЛИТЕЛЬНОСТИ — СТАРШЕЕ ПОКОЛЕНИЕ

        Два значения: «отклик» — для того, что меняется под курсором прямо сейчас, и «движение» — для того,
        что едет или проявляется. На них стоят переходы шапки и кнопки и тест, который их сторожит, поэтому
        они остаются как есть. Новые правила берут ступени ниже: там сказано не «быстро/медленно», а по
        какому признаку выбирать, — и этот признак проверяем, а «быстро» не проверяемо ничем.
    */
    --ds-fast: 120ms;
    --ds-slow: 180ms;
    --ds-ease: ease-in-out;

    /*
        ДВИЖЕНИЕ: ПРАВИЛО ВЫБОРА

        Длительность выбирается не по вкусу и не по важности, а по тому, СКОЛЬКО ПИКСЕЛЕЙ ПРОХОДИТ ВЕЩЬ.
        Правило целиком:

            Посмотри, что меняется. Только краска — 90. Вещь сдвинулась на несколько пикселей и осталась
            на месте — 150. Появился слой поверх страницы — 240 на приход, 160 на уход. Слой пришёл с края
            экрана — 320 на приход, 210 на уход. Приход и отклик на палец — ease-out, уход — ease-in,
            туда-и-обратно по одной дороге — ease-in-out. Промежуточных значений не бывает: если кажется,
            что нужно 200, — ты неверно определил, ЧТО движется. Определи заново.

        Почему уход всегда примерно 0.66 прихода: уходящее уже прочитано, и медленный уход — это ожидание.
        Почему имена длинные и с описанием: у нас уже был --ds-shadow-sm, чьё имя обещало «тень поменьше», а
        замер показал, что тени нет вовсе. Читать надо описание, а не имя.

        Что анимировать МОЖНО: transform (и translate/scale/rotate), opacity, filter, цвета, тень.
        Что анимировать НЕЛЬЗЯ: height, width, top/left, margin, padding — они пересчитывают макет каждый
        кадр, и на телефоне это видно рывками. Ни пружин, ни overshoot, ни бесконечного вращения (кроме
        честного индикатора работы), ни движения за курсором, ни бегущей строки, ни печатающегося текста.
    */
    --ds-t-tint: 90ms;        /* меняется только краска: цвет, фон, рамка, тень, яркость. Путь = 0 */
    --ds-t-move: 150ms;       /* вещь сдвинулась и осталась на месте: просадка 1px, подъём 2-3px, шеврон. До 8px */
    --ds-t-enter: 240ms;      /* пришёл небольшой слой поверх: меню, поповер, тост, подсказка. 8-24px + прозрачность */
    --ds-t-enter-out: 160ms;  /* его уход */
    --ds-t-layer: 320ms;      /* слой перекрыл страницу или пришёл с края: диалог, лист снизу, ящик. От 24px */
    --ds-t-layer-out: 210ms;  /* его уход */
    --ds-t-spin: 800ms;       /* полный оборот честного индикатора незавершённой работы */

    /* Лестница соседей: пятая карточка ряда появляется без задержки, иначе низ полосы ждёт полсекунды. */
    --ds-t-step: 60ms;

    --ds-ease-out: cubic-bezier(.2, 0, 0, 1);  /* всё, что ПРИХОДИТ, и всё, что отвечает пальцу */
    --ds-ease-in: cubic-bezier(.4, 0, 1, 1);   /* всё, что УХОДИТ */
}

@keyframes ds-spin { to { transform: rotate(360deg); } }

/*
    Палец — не курсор, и это единственное место, где об этом сказано числом.

    32 пикселя хватает мыши и не хватает подушечке: ряд кнопок консоли ростом в 21 пиксель на телефоне
    промахивается через раз, и промахивается он по соседней кнопке, а не мимо. Поднимается ровно на
    сенсорных устройствах: на десктопе 44 в строке подписей раздули бы шапку и списки без всякой пользы.

    pointer: coarse — про основное указывающее устройство, а не про «есть ли тач вообще»: у ноутбука с
    сенсорным экраном основное устройство мышь, и ему остаётся 32.
*/
@media (pointer: coarse) {
    :root { --ds-tap: 44px; }
}

/*
    Тёмная тема объявлена здесь один раз на три приложения. До этого каждое несло свой набор тёмных
    значений, и правка светлого цвета в одном месте оставляла тёмный от прежнего.
*/
@media (prefers-color-scheme: dark) {
    :root {
        --ds-bg: #12151b;
        --ds-surface: #191d25;
        --ds-surface-alt: #21262f;
        --ds-line: #2b313d;

        --ds-text: #e9ebf2;
        --ds-muted: #9aa1b4;

        /*
            Акцент светлеет вместе с темой: тёмно-индиговый #4f46e5 на почти чёрном фоне даёт контраст 2.74
            при норме 4.5 — то есть «Забыли пароль?» и «Создать учётную запись» нечитаемы у всех, у кого
            система в тёмной теме. Заливка при этом остаётся глубокой: белые буквы на осветлённом индиго
            дают 3.6, а это ровно те кнопки, ради которых страница написана.
        */
        --ds-accent: #a5b4fc;
        --ds-accent-fill: #5b57d6;
        --ds-accent-contrast: #ffffff;
        --ds-accent-soft: #262a44;

        --ds-danger: #f2b8b5;
        --ds-danger-soft: #3a1d1b;
        --ds-success: #a6d9bb;
        --ds-success-soft: #16301f;
        --ds-warning: #fbbf24;
        --ds-warning-soft: #2a2211;
        --ds-info: #a8c8ee;
        --ds-info-soft: #16263a;

        /* Светлая бирюза читается на тёмном; глубокая на нём пропадает вовсе. */
        --ds-brand: #5ed3c4;
        --ds-brand-soft: #14262a;

        /* На тёмном фоне тень чёрным почти не видна — она обязана быть плотнее, а не той же. */
        --ds-shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
        --ds-shadow-md: 0 1px 2px rgb(0 0 0 / 40%), 0 8px 24px rgb(0 0 0 / 45%);
        --ds-shadow-lg: 0 2px 6px rgb(0 0 0 / 45%), 0 22px 50px rgb(0 0 0 / 55%);
    }
}

/*
    Явный выбор человека всегда сильнее системного, и в обе стороны. Переключатель темы на сервере
    идентификации ставит data-theme на корень; без светлой ветки он менял бы фон и оставлял ссылки от
    прежней темы.
*/
:root[data-theme="light"] {
    --ds-bg: #f5f6fa;
    --ds-surface: #ffffff;
    --ds-surface-alt: #eff1f6;
    --ds-line: #e2e5ee;
    --ds-text: #171a22;
    --ds-muted: #5f6678;
    --ds-accent: #4f46e5;
    --ds-accent-fill: #4f46e5;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #eceafe;
    --ds-danger: #b91c1c;
    --ds-danger-soft: #fdecea;
    --ds-success: #146c33;
    --ds-success-soft: #e7f5ec;
    --ds-warning: #b45309;
    --ds-warning-soft: #fff7ed;
    --ds-info: #1f5fa9;
    --ds-info-soft: #e8f1fb;
    --ds-brand: #0b6b62;
    --ds-brand-soft: #e6f2f0;
    --ds-shadow-sm: 0 1px 2px rgb(16 18 31 / 6%);
    --ds-shadow-md: 0 1px 2px rgb(16 18 31 / 6%), 0 8px 24px rgb(16 18 31 / 8%);
    --ds-shadow-lg: 0 2px 6px rgb(16 18 31 / 8%), 0 22px 50px rgb(16 18 31 / 12%);
}

:root[data-theme="dark"] {
    --ds-bg: #12151b;
    --ds-surface: #191d25;
    --ds-surface-alt: #21262f;
    --ds-line: #2b313d;
    --ds-text: #e9ebf2;
    --ds-muted: #9aa1b4;
    --ds-accent: #a5b4fc;
    --ds-accent-fill: #5b57d6;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #262a44;
    --ds-danger: #f2b8b5;
    --ds-danger-soft: #3a1d1b;
    --ds-success: #a6d9bb;
    --ds-success-soft: #16301f;
    --ds-warning: #fbbf24;
    --ds-warning-soft: #2a2211;
    --ds-info: #a8c8ee;
    --ds-info-soft: #16263a;
    --ds-brand: #5ed3c4;
    --ds-brand-soft: #14262a;
    --ds-shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
    --ds-shadow-md: 0 1px 2px rgb(0 0 0 / 40%), 0 8px 24px rgb(0 0 0 / 45%);
    --ds-shadow-lg: 0 2px 6px rgb(0 0 0 / 45%), 0 22px 50px rgb(0 0 0 / 55%);
}

/* ===================================================================================================
   ОСНОВА

   Три правила, которые в каждом из трёх приложений были написаны по-своему или не написаны вовсе.
   =================================================================================================== */

/*
    Ширина считается по рамке, а не по содержимому.

    Приёмка на телефоне: страница ездила вбок на три десятка пикселей. На узком экране кнопки первого экрана
    встают в колонку и растягиваются на всю ширину, получая 343; при content-box — умолчании CSS, которое
    никто не отменял, — к ним прибавляются внутренние поля с обеих сторон и рамка, и получается 389 при окне
    в 375. На десктопе кнопки лежат в строку и ширины не получают, поэтому там ничего не видно — и поэтому же
    дефект дожил до продакшена. Консоль конструктора его не показывала: reboot из бандла BootstrapBlazor
    объявляет то же самое, а публичная главная бандла не грузит.

    Правило глобальное и стоит до всего остального: чинить такое поштучно значит чинить его заново на каждой
    новой кнопке.
*/
*, *::before, *::after { box-sizing: border-box; }

/*
    Две строки против двух самых частых способов уехать вбок на телефоне.

    Широкая картинка: <img> без ограничения занимает свою настоящую ширину, и снимок с телефона в 3000
    пикселей растягивает документ втрое. height: auto здесь обязателен, а не «для порядка»: браузер берёт
    ширину и высоту из атрибутов тега, и если ужать одну ширину — картинка сплющится. У svg своей высоты
    может не быть вовсе (иконка размером с строку), поэтому ему только максимум ширины.

    Длинное слово: адрес, слаг или почта в 60 символов не переносится по пробелам, потому что пробелов в нём
    нет, — и раздвигает колонку до своей длины. overflow-wrap наследуется, поэтому объявлен один раз на
    документ; break-word ломает слово только тогда, когда оно и так не помещается.
*/
img, video { max-width: 100%; height: auto; }
svg { max-width: 100%; }

body { overflow-wrap: break-word; }

/* Значение, а не текст: слаг, адрес, идентификатор. anywhere ломает в любом месте и, в отличие от
   break-word, учитывается при расчёте минимальной ширины колонки — то есть таблица с адресами перестаёт
   требовать себе экран в полтора раза шире окна. */
code, kbd, samp, .ds-wrap-anywhere { overflow-wrap: anywhere; }

/*
    Кольцо фокуса — одно на платформу, и оно было нарисовано у двух приложений из трёх.

    :focus-visible, а не :focus: иначе кольцо появляется от щелчка мышью, его начинают убирать — и вместе с
    мышью оно пропадает у клавиатуры.
*/
:focus-visible {
    outline: var(--ds-focus-ring);
    outline-offset: 2px;
    border-radius: var(--ds-radius-sm);
}

/*
    Утилита для тех мест, где глобальное правило проиграло спор.

    !important здесь не от лени. Спор ровно один и он предрешён: бандл BootstrapBlazor подключается после
    design.css и снимает обводку у своих кнопок правилом той же специфичности — при равенстве побеждает
    тот, кто ниже. Кольцо фокуса — единственное, по чему клавиатура понимает, где находится; библиотека,
    убравшая его ради вида, не должна выигрывать этот спор ни при каком порядке подключения.
*/
.ds-focusable:focus-visible {
    outline: var(--ds-focus-ring) !important;
    outline-offset: 2px !important;
}

/*
    Единственное место, где кольцо снимается без замены, — и снимается намеренно.

    tabindex="-1" не достижим табуляцией: попасть на такой элемент можно только скриптом, и здесь это делает
    ровно один скрипт — FocusOnNavigate маршрутизатора Blazor, который после каждого перехода ставит фокус на
    <h1>, чтобы читалка объявила, куда человек попал. Показывать «вы здесь» рамкой там некому: заголовок
    объявлен словами, а нажать по нему нельзя. Без этой строки главная открывается с рамкой вокруг заголовка
    первого экрана при каждой загрузке — на десктопе с мышью её замечают последней.
*/
[tabindex="-1"]:focus,
[tabindex="-1"]:focus-visible { outline: none; }

/* Видна только тогда, когда до неё дошли табуляцией. Первая в порядке табуляции на каждой странице:
   шапка и подвал одинаковы везде, и без неё до содержимого каждый раз идти через них. */
.ds-skip {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 40;
    padding: 10px 14px;
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text);
    text-decoration: none;
}

.ds-skip:focus { left: 12px; top: 12px; }

/* ===================================================================================================
   ШАПКА

   Одна на четыре поверхности: сайт организации, продающая главная конструктора, /pricing и все страницы
   сервера идентификации. Замеры приёмки до этой правки: 60 пикселей у конструктора против 108 у входа,
   бренд слева у одного и по центру у другого, «Войти» кнопкой в 40 пикселей у одного и голой ссылкой в 25
   у другого. Человек, нажавший «Войти», попадал на сайт другой компании.

   Липкая, во всю ширину, содержимое выровнено по колонке страницы. Отсюда боковое поле через max(): сама
   полоса тянется от края до края — иначе на широком экране под ней проезжал бы неразмытый текст, — а
   логотип и кнопки остаются на своих местах.
   =================================================================================================== */

.ds-top {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: var(--ds-space-5);
    min-height: var(--ds-header);
    padding: 0 max(var(--ds-space-5), calc((100% - var(--ds-page, 1140px)) / 2 + var(--ds-space-5)));
    background: color-mix(in srgb, var(--ds-bg) 88%, transparent);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid transparent;
    transition: border-color var(--ds-slow) var(--ds-ease), box-shadow var(--ds-slow) var(--ds-ease);
}

/*
    Прокрученная страница отделяется от шапки линией и тенью, непрокрученная — ничем: рамка над первым
    экраном разрезает его пополам без всякой пользы, а под уехавшим текстом она единственное, что говорит
    «полоса лежит поверх».
*/
.ds-top.is-scrolled {
    border-bottom-color: var(--ds-line);
    box-shadow: var(--ds-shadow-sm);
}

/* Якорные цели держат запас сверху под липкую шапку: без него прокрутка ставит начало раздела ровно под
   её нижний край, и заголовок оказывается накрыт. */
:root { scroll-padding-top: calc(var(--ds-header) + var(--ds-space-4)); }

/*
    Тесноту в шапке первым принимает имя, а не действие.

    «Olista Trade Enterprise» на 375 не помещалось рядом с кнопкой и бургером, и flex ломал по словам не
    имя, а кнопку: «Войти» вставало в две строки, а шапка вырастала. Имя здесь вывеска, а действие —
    единственное, ради чего шапка на телефоне и нужна; поэтому сжимается имя (min-width: 0 разрешает
    flex-элементу стать уже содержимого) и усекается многоточием, а кнопка не сжимается вовсе.
*/
/* Имя — не подпись, а ссылка на главную, то есть цель нажатия наравне с соседями по шапке: замер на
   сенсорном 375 давал 77×28 при пальце в 44. Высоту задаёт токен, а не число, — на мыши остаётся 32. */
.ds-brand {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    min-width: 0;
    min-height: var(--ds-tap);
    color: var(--ds-text);
    font-weight: 650;
    white-space: nowrap;
    text-decoration: none;
}

.ds-brand > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.ds-brand:hover { text-decoration: none; }

.ds-brand-mark { width: 28px; height: 28px; border-radius: var(--ds-radius-sm); display: block; flex: none; }

.ds-nav { display: flex; align-items: center; gap: var(--ds-space-1); }

.ds-nav a {
    display: inline-flex;
    align-items: center;
    min-height: var(--ds-tap);
    padding: 0 10px;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-muted);
    font-size: var(--ds-size-sm);
    text-decoration: none;
}

.ds-nav a:hover { background: var(--ds-surface); color: var(--ds-text); text-decoration: none; }

/* Текущий раздел — не только цветом: «вы здесь» и «сюда можно нажать» одним цветом различают плохо, а при
   дальтонизме не различают вовсе. */
.ds-nav a.active,
.ds-nav a[aria-current="page"] {
    color: var(--ds-text);
    font-weight: 600;
    box-shadow: inset 0 -2px 0 var(--ds-accent);
}

/* Свободное место отдано одному отступу — перед действиями, и он держится, даже когда якорные ссылки на
   телефоне убраны: два «auto» разошлись бы поровну и оставили бы вход посреди шапки. */
.ds-actions { display: flex; align-items: center; gap: var(--ds-space-2); margin-inline-start: auto; flex: none; }

/* Подпись, а не действие: имя учётной записи первым принимает тесноту и усекается многоточием. */
.ds-who {
    min-width: 0;
    color: var(--ds-muted);
    font-size: 13.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Форма выхода в шапке (сервер идентификации): POST с токеном, а не ссылка, — но выглядеть обязана как
   соседние пункты. */
.ds-inline-form { display: inline-flex; margin: 0; }

/* ===================================================================================================
   БУРГЕР

   details/summary, а не скрипт: страницы обязаны открываться и работать до и без всякого JS. Скрипт
   (design.js) только заканчивает жест — закрытие после выбора пункта и по Esc.

   Затемнение — ::before самого summary, и браузер считает тап по нему тапом по summary: так меню закрывается
   тапом мимо без единой строки скрипта. Это путь для страницы без скриптов; со взведённым design.js закрытие
   делает он, а затемнение перестаёт ловить нажатия — разбор ниже, у правила .ds-shell-ready.

   Прежняя редакция ссылалась здесь на «бургер сайта организации» как на образец, у которого физика взята.
   Такого бургера в дереве больше нет: сайт организации перешёл на PlatformHeader. Ссылка на доказательство,
   которого не существует, — ровно то, чем образец опаснее правила.
   =================================================================================================== */

.ds-burger { display: none; position: relative; flex: none; }

/* 44×44: бургер — цель для пальца, а не для курсора, и это единственная кнопка навигации на телефоне. */
.ds-burger summary {
    list-style: none;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    width: 44px;
    height: 44px;
    padding: 0 11px;
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-sm);
    cursor: pointer;
}

.ds-burger summary::-webkit-details-marker { display: none; }

/*
    Затемнение фона — и настоящая причина, по которой документ на телефоне был шире окна.

    Здесь стояло inset: -100vmax, и стояло не по недосмотру: у шапки backdrop-filter, а любое его значение,
    кроме none, делает шапку содержащим блоком для position: fixed. То есть «на весь экран» превращалось в
    «на высоту шапки», и обойти это пытались размером.

    Обход стоил дороже, чем чинил. Элемент, чей содержащий блок не окно, а обычный элемент, участвует в
    прокручиваемой области страницы наравне с абсолютно позиционированными — то есть 100vmax вправо от
    шапки браузер честно считает содержимым документа. Замер на телефоне 375×812: vmax = 812, документ
    получается 375 + 812 = 1187 при окне 375. Ровно это и видела приёмка: страница ездит вбок, и ездит она
    только с открытым бургером.

    Правильный размер — inset: 0, окно; но он значит «окно» только тогда, когда шапка перестала быть
    содержащим блоком. Поэтому вторая строка снимает у неё размытие ровно на время раскрытого меню. Терять
    там нечего: под меню весь экран и так затемнён на 42%, а размытого фона за ним никто не увидит.

    Деградация без :has() (браузеры до Safari 15.4 / Firefox 121): размытие остаётся, и затемнение снова
    ложится по шапке вместо окна. На закрытие это больше не влияет — его делает design.js, которому ни
    затемнение, ни размытие не нужны; страдает только вид, потому что тёмным оказывается не весь экран, а
    полоса шапки. Раньше здесь стояло «но не по тапу в середину экрана»: это было правдой ровно до того,
    как механика уехала в скрипт. Без скриптов же тап мимо по-прежнему закрывает меню самим затемнением —
    в таких браузерах оно опять по шапке, и тогда не закрывает; это и есть цена отсутствия :has().

    z-index: -1 держит затемнение под содержимым шапки, но над её подложкой.
*/
.ds-top:has(.ds-burger[open]) { backdrop-filter: none; }

.ds-burger[open] summary::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    background: rgb(9 11 18 / 42%);
}

/*
    Со взведённым скриптом затемнение — вид, а не механика.

    Закрытие нажатием мимо делает design.js делегированно на документе, и делает надёжнее: затемнение
    работает только внутри .ds-top и только пока над ним нет слоя выше двадцатого, а в консоли конструктора
    планка стоит в шапке без контекста наложения — там оно уходит под фон страницы и не ловит ничего.

    Ловя нажатия, оно вдобавок съедает первый щелчок мимо: человек метит в кнопку под открытым меню,
    попадает в затемнение и нажимает второй раз. Класс ставит design.js первой строкой, поэтому страница без
    скриптов остаётся при старом поведении — тап мимо закрывает меню сам.
*/
.ds-shell-ready .ds-burger[open] summary::before,
.ds-shell-ready .ds-apps[open] summary::before { pointer-events: none; }

/* position: relative — полоски над затемнением: они внутри summary и без своего слоя ушли бы под его
   псевдоэлемент вместе со страницей. */
.ds-burger-bar {
    display: block;
    position: relative;
    height: 2px;
    border-radius: 1px;
    background: var(--ds-text);
}

.ds-burger[open] summary { background: var(--ds-surface); }

/* Меню — панель под липкой шапкой, а не выпадашка в потоке: раскрытие в потоке сдвигало бы кнопку входа
   вниз на высоту меню. */
.ds-burger-menu {
    position: absolute;
    inset-inline-end: 0;
    top: calc(100% + var(--ds-space-2));
    display: grid;
    min-width: 220px;
    padding: 6px;
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
    box-shadow: var(--ds-shadow-lg);
}

.ds-burger-menu a {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0 12px;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text);
    text-decoration: none;
}

.ds-burger-menu a:hover { background: var(--ds-surface-alt); text-decoration: none; }

.ds-burger-menu a.active,
.ds-burger-menu a[aria-current="page"] {
    background: var(--ds-accent-soft);
    color: var(--ds-text);
    font-weight: 600;
    box-shadow: inset 3px 0 0 var(--ds-accent);
}

/* ===================================================================================================
   ПЛАНКА ПРИЛОЖЕНИЙ

   Переход в соседнее приложение платформы — одинаковый на всех рабочих местах. Механика та же, что у
   бургера: details/summary, открытие без единой строки скрипта, затемнение — ::before самого summary.

   Оговорка, стоившая дефекта: тап мимо закрывает меню затемнением ТОЛЬКО под этой шапкой. У планки два
   носителя, и второй — консоль конструктора — стоит в шапке без контекста наложения, где затемнение уходит
   под фон страницы и не ловит ничего. Поэтому закрытие нажатием мимо теперь делает design.js, одинаково для
   обоих носителей; затемнение осталось видом.

   Отличие от бургера одно, и оно смысловое: планка видна на любой ширине. Разделы на телефоне уходят в
   бургер, потому что они и так рядом — страница про них; а между приложениями с телефона ходят так же,
   как с десктопа, и спрятать этот переход в элемент, которого на широком экране нет, значит спрятать его
   насовсем. На узком остаётся значок без подписи — место в шапке отдано входу.

   Оба правила затемнения повторены сознательно (см. длинный разбор у бургера): inset: 0 значит «окно»
   только тогда, когда шапка перестала быть содержащим блоком, — иначе документ едет вбок.
   =================================================================================================== */

.ds-apps { position: relative; flex: none; }

.ds-apps summary {
    list-style: none;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: var(--ds-tap);
    padding: 0 10px;
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-sm);
    color: var(--ds-muted);
    font-size: var(--ds-size-sm);
    white-space: nowrap;
    cursor: pointer;
}

.ds-apps summary::-webkit-details-marker { display: none; }

.ds-apps summary:hover { background: var(--ds-surface); color: var(--ds-text); }
.ds-apps[open] summary { background: var(--ds-surface); color: var(--ds-text); }
.ds-apps summary:active { transform: translateY(1px); }

.ds-top:has(.ds-apps[open]) { backdrop-filter: none; }

.ds-apps[open] summary::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    background: rgb(9 11 18 / 42%);
}

/* Значок — оформление: имя приложения читается из пунктов меню, а сетка из квадратов ничего не добавляет
   к нему голосом. */
.ds-apps-mark { position: relative; line-height: 1; }

/*
    Закрытая планка ничего не показывает — и ничего не занимает.

    Свёрнутый <details> в Chromium прячет содержимое через content-visibility, а не display: none, и
    внепоточная панель при этом сохраняет геометрию: замер на 375 давал ссылки шириной 34 при высоте под
    тысячу — текст, сложенный в столбик внутри схлопнутой панели. Для человека невидимо, для всего, что
    меряет страницу коробками, — вполне существует.

    Читалки и клавиатура сюда не относятся, хотя раньше здесь были названы: замер (PR #258) показал, что
    свёрнутое содержимое Chromium сам убирает из порядка табуляции и из текстового дерева — со строкой
    ниже и без неё одинаково. Строка адресована замеру, и только ему; прибор с тех пор спрашивает
    видимость у браузера, так что новую такую ограду не заводят — см. docs/engineering-guidelines.md.
*/
.ds-apps:not([open]) .ds-apps-menu { display: none; }

.ds-apps-menu {
    position: absolute;
    inset-inline-start: 0;
    top: calc(100% + var(--ds-space-2));
    display: grid;
    gap: 2px;
    min-width: 264px;
    padding: 6px;
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
    box-shadow: var(--ds-shadow-lg);
}

.ds-apps-menu a {
    display: grid;
    align-content: center;
    gap: 2px;
    min-height: 44px;
    padding: 6px 12px;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text);
    text-decoration: none;
}

.ds-apps-menu a:hover { background: var(--ds-surface-alt); text-decoration: none; }

.ds-apps-desc { color: var(--ds-muted); font-size: var(--ds-size-xs); }

@media (max-width: 720px) {
    /*
        Подпись уходит, планка остаётся: на 375 рядом с логотипом, входом и бургером слову места нет.

        Ширина при этом держится пальцем, а не содержимым: со снятой подписью кнопка ужалась до 32 при
        высоте 44 — половина цели, и смок поймал это ровно так же, как ловит любую мелкую цель. Рост берётся
        из --ds-tap, и он же поднимается на сенсорном указателе, поэтому число здесь не выписано.
    */
    .ds-apps-label { display: none; }

    .ds-apps summary {
        justify-content: center;
        min-width: var(--ds-tap);
        padding: 0 9px;
    }

    /*
        Панель уходит из координат планки в координаты окна — иначе она вылезает за край документа.

        Замер на 375: планка стоит за вывеской, её левый край на 121-м пикселе, а панель шириной 264
        заканчивается на 385-м — документ становится шире окна на десяток пикселей, и страница едет вбок
        ровно в тот момент, когда планка открыта. Ровно эта болезнь уже была у затемнения бургера, и
        лечится она так же: координатами окна вместо размера.

        top: auto оставляет вертикаль на месте — у fixed с auto-смещением это позиция, которую элемент
        занял бы в потоке, то есть сразу под кнопкой. Хардкод высоты шапки был бы неверен: у консоли
        конструктора шапка своя, с переносом строки, и её высота меняется.
    */
    .ds-apps-menu {
        position: fixed;
        inset-inline: var(--ds-space-4);
        top: auto;
        margin-top: var(--ds-space-2);
        min-width: 0;
    }
}

/* ===================================================================================================
   КНОПКИ

   Одна шкала: 40 обычная, 46 крупная, радиус — ступень «кнопки». До этого сервер идентификации рисовал
   кнопку радиусом 8 и высотой по строке текста, конструктор — 38/44, сайт организации — 40/46.
   =================================================================================================== */

.ds-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-1);
    /*
        Шкала кнопки — 40, но не мельче пальца.

        Два числа отвечают на два разных вопроса, и брать надо большее: --ds-control говорит, какого роста
        кнопка в этой шкале, --ds-tap — ниже какого роста по ней промахиваются. На мыши побеждает шкала
        (40 > 32), на сенсорном — палец (44 > 40), и ни одно из чисел не пришлось писать заново. Замер на
        сенсорном 375 до этой строки: a.ds-btn 79×40 при требуемых 44.

        Крупной кнопке (.ds-btn-lg) того же не нужно: 46 больше пальца при любом указателе.
    */
    min-height: max(var(--ds-control), var(--ds-tap));
    padding: 0 18px;
    border-radius: var(--ds-radius-md);
    border: 1px solid transparent;
    font: inherit;
    font-size: 14.5px;
    font-weight: 600;
    text-align: center;
    text-decoration: none;
    /* Подпись кнопки не переносится по словам: «Войти» в две строки — это не кнопка, а происшествие. */
    white-space: nowrap;
    cursor: pointer;
    transition: filter var(--ds-fast) var(--ds-ease), border-color var(--ds-fast) var(--ds-ease),
                color var(--ds-fast) var(--ds-ease), transform var(--ds-fast) var(--ds-ease);
}

.ds-btn:hover { text-decoration: none; }
.ds-btn-lg { min-height: var(--ds-control-lg); padding: 0 24px; font-size: 15.5px; }

/*
    Просадка на нажатие.

    Приёмка на телефоне: у всех кнопок обеих публичных страниц состояние менялось мгновенным скачком, и
    палец не получал подтверждения, что попал. Наведения на телефоне нет вовсе — :hover там не наступает, —
    поэтому единственный отклик, который вообще может увидеть палец, это отклик на нажатие. Нажатие без
    отклика человек повторяет, и повторяет он его по кнопке, которая уже сработала.

    Один пиксель и 120 миллисекунд: заметно пальцу, незаметно глазу. Гасится общим блоком
    prefers-reduced-motion ниже — там движение снимается со всей платформы разом.
*/
.ds-btn:active,
.ds-burger summary:active { transform: translateY(1px); }

/* Тень заливной кнопке — в цвете её самой: серая тень под цветной кнопкой читается как грязь под ней, а не
   как высота. Цвет берётся из заливки, поэтому в тёмной теме, где заливка своя, тень едет за ней сама. */
.ds-btn-primary {
    background: var(--ds-accent-fill);
    color: var(--ds-accent-contrast);
    box-shadow: var(--ds-shadow-accent);
}

.ds-btn-primary:hover { filter: brightness(1.1); }

.ds-btn-ghost { background: var(--ds-surface); border-color: var(--ds-line); color: var(--ds-text); }
.ds-btn-ghost:hover { border-color: var(--ds-accent); color: var(--ds-accent); }

/* ===================================================================================================
   ТЕЛЕФОН

   Якорная строка уходит — в шапке она отбирала место у единственной кнопки, ради которой шапка и нужна, —
   но разделы не пропадают: их забирает бургер.
   =================================================================================================== */

@media (max-width: 720px) {
    .ds-top { gap: var(--ds-space-3); padding-inline: var(--ds-space-4); }
    .ds-nav { display: none; }
    .ds-burger { display: block; }
    .ds-who { display: none; }

    /*
        Ссылка в правой части, если это не кнопка, — цель нажатия того же роста, что и остальные.

        Здесь стояло `min-height: 44px` на этом же месте и ещё на .ds-nav a — то есть число пальца, второй
        раз выписанное вручную, и выписанное не под тем условием: рост цели решает указатель, а не ширина
        окна. Узкое окно на десктопе получало 44 без всякой нужды, а телефон в альбомной ориентации — 32,
        то есть ровно то, ради чего правило и писали, там не работало. Теперь высоту несёт --ds-tap, а он
        поднимается в @media (pointer: coarse) — и .ds-nav a берёт его из своего же правила выше.
    */
    .ds-actions a:not(.ds-btn) { min-height: var(--ds-tap); }
}

@media (max-width: 400px) {
    /* Второе действие в шапке рядом с логотипом и бургером на 360 не помещается. Уходит призыв, а не вход:
       призыв стоит первым же экраном под шапкой, а «Войти» больше нигде на странице нет. */
    .ds-actions .ds-cta { display: none; }
}

/* ===================================================================================================
   ДИАЛОГ

   Один способ на все приложения, и его даёт браузер. Родной <dialog> + showModal() приносит бесплатно всё,
   что мы писали руками и писали хуже: ловушку фокуса, Esc, возврат фокуса на то, откуда открыли, слой
   поверх всего без счётной войны z-index и ::backdrop. У модального окна библиотеки ловушка фокуса
   выключена самим пакетом — с нашей стороны это не чинится вовсе.

   На телефоне окно превращается в лист снизу. Это не мода: в окне по центру ряд кнопок оказывается там,
   куда его вытолкнул текст, и «Создать» уезжала на 178 пикселей ниже сгиба без единого намёка на прокрутку.
   У листа снизу кнопки всегда внизу экрана, то есть всегда под большим пальцем и всегда видны.

   Появление и уход — чистый CSS: @starting-style задаёт, откуда приходить, transition-behavior:
   allow-discrete держит элемент в слое, пока уход не доигран. Обоим — Baseline 2024 (последним подтянулся
   Firefox 129, август 2024). Деградация: окно появляется и исчезает скачком, и больше ничего.
   =================================================================================================== */

.ds-dialog {
    /* padding: 0 — не косметика. Закрытие по клику мимо ловится тем, что цель клика — сам <dialog>;
       собственные поля диалога дают полосу внутри окна, клик по которой браузер тоже считает кликом по
       диалогу, и окно закрывалось бы от промаха рядом с кнопкой. Поля — у шапки, тела и ряда кнопок. */
    padding: 0;
    width: min(560px, calc(100% - 2 * var(--ds-space-4)));
    max-width: none;
    max-height: min(85dvh, 720px);
    flex-direction: column;
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-lg);
    background: var(--ds-surface);
    color: var(--ds-text);
    box-shadow: var(--ds-shadow-lg);

    opacity: 0;
    translate: 0 8px;

    /* Уход — длительность и кривая ухода; приход перебивает их правилом [open] ниже. display и overlay в
       списке обязаны быть: без них элемент выпадает из слоя в первом же кадре, и уход не виден. */
    transition-property: opacity, translate, display, overlay;
    transition-duration: var(--ds-t-enter-out);
    transition-timing-function: var(--ds-ease-in);
    transition-behavior: allow-discrete;
}

/* display объявлен только у открытого: правило display: flex у самого .ds-dialog перебило бы
   dialog:not([open]) { display: none } из таблицы браузера, и закрытое окно висело бы на странице. */
.ds-dialog[open] {
    display: flex;
    opacity: 1;
    translate: 0 0;
    transition-duration: var(--ds-t-enter);
    transition-timing-function: var(--ds-ease-out);
}

.ds-dialog::backdrop {
    background: rgb(9 11 18 / 42%);
    opacity: 0;
    transition-property: opacity, display, overlay;
    transition-duration: var(--ds-t-enter-out);
    transition-timing-function: var(--ds-ease-in);
    transition-behavior: allow-discrete;
}

.ds-dialog[open]::backdrop {
    opacity: 1;
    transition-duration: var(--ds-t-enter);
    transition-timing-function: var(--ds-ease-out);
}

/* Откуда приходить. Без @starting-style переход от display: none к display: flex начинается уже в конечном
   состоянии, то есть не начинается вовсе. */
@starting-style {
    .ds-dialog[open] { opacity: 0; translate: 0 8px; }
    .ds-dialog[open]::backdrop { opacity: 0; }
}

/* Шапка и ряд кнопок — не липкие, а вынесенные из прокрутки: колонка, в которой прокручивается только
   тело. Липкость потребовала бы, чтобы прокручивался весь диалог, и на инерционной прокрутке телефона
   ряд кнопок дрожал бы. */
.ds-dialog-head {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    padding: var(--ds-space-4) var(--ds-space-5);
    border-bottom: 1px solid var(--ds-line);
    font-size: var(--ds-size-lg);
    font-weight: 650;
}

.ds-dialog-body { padding: var(--ds-space-5); overflow-y: auto; }

.ds-dialog-foot {
    display: flex;
    justify-content: flex-end;
    gap: var(--ds-space-2);
    padding: var(--ds-space-4) var(--ds-space-5);
    border-top: 1px solid var(--ds-line);
}

@media (max-width: 720px) {
    .ds-dialog {
        width: 100%;
        max-height: 90dvh;
        /* margin: auto прижимает окно к центру; auto сверху и 0 снизу — ко дну экрана. */
        margin: auto auto 0;
        border-width: 1px 0 0;
        border-radius: var(--ds-radius-lg) var(--ds-radius-lg) 0 0;
        translate: 0 100%;
        transition-duration: var(--ds-t-layer-out);
    }

    .ds-dialog[open] { translate: 0 0; transition-duration: var(--ds-t-layer); }

    @starting-style {
        .ds-dialog[open] { translate: 0 100%; }
    }

    /* Ряд кнопок листа — в зоне большого пальца и во всю ширину: на 375 две кнопки в строку по краю
       правого поля означают, что до левой дотягиваются второй рукой. */
    .ds-dialog-foot { padding-bottom: max(var(--ds-space-4), env(safe-area-inset-bottom)); }
}

/* ===================================================================================================
   НЕМОДАЛЬНОЕ: атрибут popover

   Меню, подсказка, раскрытая карточка. Esc, закрытие по клику мимо, слой поверх всего и возврат фокуса
   браузер делает сам, без единой строки скрипта, — потому это и годится там, где скрипта может не быть.

   Чего он НЕ делает сам: не ставит панель рядом с кнопкой. Привязка к кнопке (anchor-name/position-anchor)
   поддержана далеко не везде, а считать координаты скриптом здесь нельзя — строгая политика заголовков
   сервера идентификации запрещает инлайновые стили. Поэтому позиционирование остаётся за вызывающей
   стороной, а меню, которое обязано висеть под своей кнопкой, лучше делать так же, как бургер: <details>
   с абсолютной панелью. Он уже работает без скрипта и без ограничений по браузерам.
   =================================================================================================== */

.ds-pop {
    margin: 0;
    padding: 6px;
    width: max-content;
    max-width: min(320px, calc(100vw - 2 * var(--ds-space-4)));
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
    background: var(--ds-surface);
    color: var(--ds-text);
    box-shadow: var(--ds-shadow-lg);

    opacity: 0;
    translate: 0 -4px;
    transition-property: opacity, translate, display, overlay;
    transition-duration: var(--ds-t-enter-out);
    transition-timing-function: var(--ds-ease-in);
    transition-behavior: allow-discrete;
}

.ds-pop:popover-open {
    opacity: 1;
    translate: 0 0;
    transition-duration: var(--ds-t-enter);
    transition-timing-function: var(--ds-ease-out);
}

@starting-style {
    .ds-pop:popover-open { opacity: 0; translate: 0 -4px; }
}

/*
    Единственная опасная деградация во всём файле, и потому она закрыта явно.

    Браузер, не знающий popover, атрибут игнорирует — вместе с правилом своей таблицы, которое прячет
    закрытую панель. То есть меню, которое должно было появиться по нажатию, просто лежит на странице
    развёрнутым. Пусть лучше его не будет: пункты меню всегда доступны и по-другому, а панель посреди
    страницы выглядит поломкой вёрстки.
*/
@supports not selector(:popover-open) {
    [popover] { display: none; }
}

/* ===================================================================================================
   ПОЯВЛЕНИЕ ПРИ ПРОКРУТКЕ

   Один приём на всю платформу: сдвиг 16 пикселей вверх и проявление. Не пять на выбор — пять означают, что
   на одной странице встретятся три.

   Порядок предохранителя обратный привычному: содержимое видно по умолчанию, и прячется только после того,
   как скрипт подтвердил классом на корне, что умеет его показать. Обратная сборка — самый обычный способ
   превратить «красивое появление» в пустую страницу у того, у кого скрипт не доехал.
   =================================================================================================== */

.ds-reveal-ready [data-ds-reveal] {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity var(--ds-t-enter) var(--ds-ease-out),
                transform var(--ds-t-enter) var(--ds-ease-out);
}

.ds-reveal-ready [data-ds-reveal].ds-revealed { opacity: 1; transform: none; }

/* Лестница соседей: значение атрибута — номер ступени. Четыре ступени и всё, дальше без задержки: пятая
   карточка ряда ждала бы треть секунды после того, как человек её уже увидел. */
.ds-reveal-ready [data-ds-reveal="2"] { transition-delay: var(--ds-t-step); }
.ds-reveal-ready [data-ds-reveal="3"] { transition-delay: calc(2 * var(--ds-t-step)); }
.ds-reveal-ready [data-ds-reveal="4"] { transition-delay: calc(3 * var(--ds-t-step)); }

/* ===================================================================================================
   ПЛАШКА СБОРКИ

   Разметку печатает общий компонент (Chrome/BuildStamp.razor), а на сервере идентификации — те же теги
   руками: там нет Blazor. Связывает их этот файл: имена ds-stamp-* и есть тот контракт, по которому две
   реализации остаются одной плашкой.
   =================================================================================================== */

.ds-stamp { position: relative; font-size: var(--ds-size-xs); color: var(--ds-muted); }

/* Своя аффорданс вместо стандартного треугольника раскрытия: он читается как содержимое страницы. */
.ds-stamp-summary {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    min-height: 24px;
    padding: 2px 9px;
    border-radius: var(--ds-radius-sm);
    list-style: none;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition: background var(--ds-t-tint) var(--ds-ease), color var(--ds-t-tint) var(--ds-ease);
}

.ds-stamp-summary::-webkit-details-marker { display: none; }

.ds-stamp-summary:hover,
.ds-stamp[open] .ds-stamp-summary { background: var(--ds-surface-alt); color: var(--ds-text); }

/* Серая, пока разметка статична; загорается в тот момент, когда островок стал интерактивным. Это
   единственное место семейства, где гидратацию видно глазом. */
.ds-stamp-dot {
    width: 7px;
    height: 7px;
    border-radius: var(--ds-radius-pill);
    background: var(--ds-muted);
    opacity: .45;
}

.ds-stamp-live { background: var(--ds-success); opacity: 1; }

/* Карточка раскрывается вверх и не двигает страницу вокруг себя. */
.ds-stamp-details {
    position: absolute;
    inset-inline-end: 0;
    bottom: calc(100% + 6px);
    z-index: 30;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 250px;
    margin: 0;
    padding: 10px 12px;
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
    box-shadow: var(--ds-shadow-lg);
}

.ds-stamp-details div { display: flex; justify-content: space-between; gap: 18px; }
.ds-stamp-details dt { font-weight: 400; color: var(--ds-muted); }
.ds-stamp-details dd { margin: 0; color: var(--ds-text); font-variant-numeric: tabular-nums; }

/* Над редактором: свой угол, полупрозрачна, пока на неё не показали, — поверх панелей, но под диалогами. */
.ds-stamp-floating {
    position: fixed;
    inset-inline-end: 12px;
    bottom: 10px;
    z-index: 25;
    opacity: .65;
}

.ds-stamp-floating:hover,
.ds-stamp-floating[open] { opacity: 1; }

.ds-stamp-floating .ds-stamp-summary { background: var(--ds-surface); border: 1px solid var(--ds-line); }

/* В продакшене сдержанная — приглушена, без индикатора, только версия, — но никогда не отсутствующая:
   «какая сборка ответила» — первый вопрос любого разбора. */
.ds-stamp-quiet { opacity: .6; }
.ds-stamp-quiet .ds-stamp-dot { display: none; }

/*
    На узком экране подвал складывается в колонку, и плашка стоит у левого края: карточка, привязанная к
    правому, уезжала левым краем за борт окна — замер на 375 давал −53, то есть пятую часть подписей за
    экраном. Горизонтальной прокрутки выезд влево не создаёт, поэтому дефект и не всплывал: он не шевелит
    страницу, он просто отрезает.

    Вторая половина того же — и она уже шевелит. Развернув карточку от левого края, min-width: 250px стал
    требованием ширины, которой у карточки нет: в подвале консоли плашка стоит справа (ряд выровнен по
    концу и в колонку не складывается), её левый край на 375 приходится на 162, и 162 + 250 = 412 при окне
    375. То есть страница едет вбок на всех десяти экранах консоли, и едет только с раскрытой плашкой.

    Требование снимается, а не уменьшается числом: 250 — это «сколько нужно, чтобы пары подпись—значение
    стояли в строку», и подобрать вместо него другое число нельзя — сколько места осталось, зависит от
    того, где стоит сама плашка, а этого таблица стилей не знает. Ноль отдаёт ширину сжатию по содержимому,
    и оно ограничено содержащим блоком, то есть самой плашкой: карточка получается ровно такой ширины,
    какая под неё есть, где бы плашка ни стояла. Пары при этом переносятся — читать их это не мешает.
*/
@media (max-width: 720px) {
    .ds-stamp-details { inset-inline-end: auto; inset-inline-start: 0; min-width: 0; }
}

/*
    Просьба системы «поменьше движения» — не пожелание оформления. Её включают при вестибулярных
    расстройствах, и для человека, который её включил, поехавший или мигающий элемент — это тошнота, а не
    неудачная анимация.

    Правило блокирующее и с !important: перечислять здесь всё, что движется, значит однажды забыть новое.
    Кроме переходов этого файла оно накрывает и то, что рисует не он, — анимации диалога переподключения,
    которые приезжают шаблоном Blazor вовсе без защиты. Длительность не 0, а исчезающе малая: событие
    окончания анимации при нуле в части браузеров не приходит, и код, который его ждёт, замирает навсегда.

    ИСКЛЮЧЕНИЕ ровно одно, и оно называется .ds-motion-essential.

    Есть движение, которое не украшает, а сообщает: крутящийся индикатор говорит «идёт работа, не жмите
    второй раз». Погашенный до нуля, он превращается в застывшую загогулину — то есть в картинку «сломалось».
    Для такого места правильная замена — не «то же самое, но медленнее», а другая форма: ступенчатая полоса,
    смена подписи, слово «сохраняем». Класс выдаётся вручную и ровно тому элементу, который движется:
    свойства перехода не наследуются, и повесить его на родителя значит не выдать вовсе.

    Мест таких две-три на всю платформу. Если их станет десять — исключением злоупотребили.
*/
@media (prefers-reduced-motion: reduce) {
    *:not(.ds-motion-essential),
    *:not(.ds-motion-essential)::before,
    *:not(.ds-motion-essential)::after {
        animation-duration: .01ms !important;
        animation-delay: -1ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        transition-delay: 0s !important;
        scroll-behavior: auto !important;
    }

    /* Появление при прокрутке скрипт в этом режиме не заводит вовсе (design.js спрашивает matchMedia сам),
       но если класс готовности всё же попал на страницу — содержимое обязано остаться видимым. */
    .ds-reveal-ready [data-ds-reveal] { opacity: 1; transform: none; }
}
