/*
    Единый визуальный язык платформы Olista.

    Один файл на все приложения платформы. В этом репозитории из его потребителей остался один — сервер
    идентификации (Center.Identity); консоль конструктора (Center.SiteBuilder) уехала в
    KisaPs/Center.SiteBuilder, сайт организации (OlistaTradeEnterprise) — в KisaPs/Center.Analytics, и
    берут они этот файл пакетом Center.Design. Доставляется он как статика библиотеки классов Razor —
    _content/Center.Design/design.css, — то есть лежит на диске в одном экземпляре и подключается ссылкой,
    а не копированием. Почему так, а не копия на приложение, — см. комментарий в Center.Design.csproj.

    Что здесь есть: значения (цвета, радиусы, тени, типографика, отступы, длительности), оболочка страницы
    (ссылка обхода, шапка, бургер, кнопки) и — с 2026-09-15 — словарь содержимого: карточка, панель, плашка,
    уведомление, пустое состояние (раздел СОДЕРЖИМОЕ ЭКРАНОВ, решение PO — DECISIONS 0.6).

    Прежняя редакция этой строки говорила обратное: «содержимого экранов здесь нет, карточки остаются в
    своих листах — но берут отсюда всё, что должно совпадать». Первая половина обещания держалась на второй,
    а вторая не сработала: замер восьми репозиториев 2026-09-15 нашёл 1696 уникальных имён классов и ни
    одного общего для трёх репозиториев. Значения домены берут отсюда честно, а содержимое каждый пишет
    заново. Граница пересмотрена там, где она не держалась, и только там.

    ИКОНКИ — с 2026-09-17, один спрайт на семью: wwwroot/icons/ds-icons.svg (32 символа — 30 глифов
    Lucide под ISC и знак семьи «Кольца роста» в двух ступенях: полный и малый, для 16 пикселей, где
    три кольца сливаются в спираль). Рисуются ссылкой на символ, а не картинкой:

        <svg class="ds-icon"><use href="/_content/Center.Design/icons/ds-icons.svg#ds-icon-search"/></svg>

    Хосту на Blazor то же самое даёт компонент <DsIcon Name="search" />, и путь к спрайту написан в нём
    один раз. Набор имён объявлен в brand/icons/build.mjs — тем же скриптом спрайт и собирается.
    Правило .ds-icon и две его ступени размера — в разделе ИКОНКИ ниже.

    Чего здесь нет по-прежнему: раскладка экранов и формы конкретных приложений. Словарь даёт кирпич, а не дом.

    Чего здесь нет и не будет: стилей опубликованных сайтов клиентов (blocks*.css). У них своя палитра автора
    и свой макетный закон; общие токены платформы к ним отношения не имеют, и смешать одно с другим значит
    отдать посетителю чужого сайта индиговые кнопки Olista.

    Бренд — Olista (решение PO 2026-09-16, DECISIONS 0.9). Отсюда три вещи, которых раньше не было:
    тёплые нейтральные поверхности, СВОЙ оттенок акцента у каждого приложения по одной формуле OKLCH
    (раздел АКЦЕНТ ПРИЛОЖЕНИЯ) и две самохостимые гарнитуры — Geologica на заголовки и Golos Text на
    текст, обе под SIL OFL 1.1 (раздел ШРИФТЫ).

    Хост ставит на <html> атрибут data-app="<ключ приложения>" — тот же ключ, которым приложение зовётся в
    реестре Center.Design/Platform/PlatformApps.cs (константы PlatformAppKeys). Страница без атрибута —
    Olista. Больше от хоста ничего не требуется: механизм тем (data-theme + системная схема) работает как
    работал, и этот PR его не трогает.

    Ни одного внешнего шрифта и ни одного обращения к CDN — как и во всех приложениях платформы: страница
    обязана открыться и выглядеть собой на машине, у которой наружу ничего не пущено. Гарнитуры лежат
    рядом, в wwwroot/fonts, вместе со своими лицензиями; ни google.com/fonts, ни какого-либо другого
    внешнего адреса в этом файле нет и быть не должно.
*/

/* ===================================================================================================
   ШРИФТЫ

   Две гарнитуры, обе под SIL Open Font License 1.1, обе лежат в репозитории: Geologica на заголовки,
   Golos Text на текст. Тексты лицензий — рядом с файлами (wwwroot/fonts/OFL-Geologica.txt,
   wwwroot/fonts/OFL-GolosText.txt), и это не формальность: OFL требует распространять шрифт вместе с
   лицензией, а пакет Center.Design везёт статику соседям.

   ПОЧЕМУ САМОХОСТИНГ, а не Google Fonts ссылкой. Правило «ни одного обращения к CDN» старше этой палитры
   и записано в шапке файла: страница обязана открыться на машине, у которой наружу ничего не пущено.
   Шрифт через CDN ломает ровно это, причём молча — на машине с интернетом всё выглядит правильно.

   ЧТО ИМЕННО ЛЕЖИТ. Переменные шрифты по одной оси wght, подрезанные до latin + cyrillic + cyrillic-ext:
   geologica-var.woff2 — 40 КБ, golos-text-var.woff2 — 62 КБ, вместе 101 КБ. У Geologica в исходнике
   четыре оси (CRSV, SHRP, slnt, wght); три лишние закреплены в умолчании (fontTools varLib.instancer),
   иначе тот же набор знаков весил бы втрое. Диапазон веса: Geologica 100–900, Golos Text 400–900 — у
   Golos Text нет весов легче обычного, и просить у него 300 бессмысленно, браузер вернёт 400.

   font-display: swap — намеренно. Заголовок, показанный системным шрифтом на 200 мс и заменённый
   Geologica, читается всё это время; block прятал бы его, а fallback на медленном канале оставил бы
   страницу системной навсегда.
   =================================================================================================== */

@font-face {
    font-family: "Geologica";
    src: url("fonts/geologica-var.woff2") format("woff2-variations"),
         url("fonts/geologica-var.woff2") format("woff2");
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Golos Text";
    src: url("fonts/golos-text-var.woff2") format("woff2-variations"),
         url("fonts/golos-text-var.woff2") format("woff2");
    font-weight: 400 900;
    font-style: normal;
    font-display: swap;
}

/* ===================================================================================================
   ЦВЕТ

   Палитра Olista, редакция 2026-09-16 (решение PO, DECISIONS 0.9). До неё платформа несла синевато-серые
   нейтрали (#f5f6fa, #171a22) и один индиговый акцент #4f46e5 на все приложения. Сменилось ровно два
   решения, и оба здесь:

     1. Нейтрали ТЁПЛЫЕ. Фон страницы уходит в желтизну, а не в синеву, и зелёный знак марки на нём не
        выглядит чужим. Имена токенов прежние — ни одно не заведено и ни одно не убрано.
     2. Акцент — СВОЙ У КАЖДОГО ПРИЛОЖЕНИЯ, но по одной формуле. Индиго на всех был ответом на другую
        болезнь: три приложения носили три разных акцента, выбранных по вкусу, и переход между ними
        выглядел переходом в чужую компанию. Общая формула лечит ту же болезнь, не отнимая у приложения
        лица: CRM и конструктор различаются тоном, но одинаковы по светлоте, насыщенности и контрасту,
        поэтому «сюда можно нажать» везде читается одинаково.

   ФОРМУЛА. Все акцентные и смысловые краски вычислены в OKLCH и записаны сюда шестнадцатеричными
   значениями sRGB. В CSS oklch() не пишется намеренно: клетка контраста считает арифметику WCAG по hex,
   а не по тому, во что цвет развернёт браузер. Меняя оттенок — меняйте H и пересчитайте, а не двигайте
   hex на глаз.

       роль                     светлая тема          тёмная тема
       --ds-accent-fill         oklch(0.52 0.13 H)    oklch(0.78 0.12 H)
       --ds-accent (текст)      oklch(0.40 0.12 H)    oklch(0.86 0.09 H)
       --ds-accent-contrast     #ffffff               #121815 (фон тёмной темы)
       --ds-accent-soft         oklch(0.94 0.04 H)    oklch(0.30 0.05 H)

   Хрома — потолок, а не обещание: если (L, C, H) не влезает в sRGB, C снижается шагом 0.001 до охвата.
   Так, бирюзовая аналитика (H 195) получает в светлой теме 0.088 вместо 0.13 — из-за чего она бледнее
   соседей, и это свойство охвата, а не недосмотр.

   ОТТЕНКИ ПРИЛОЖЕНИЙ (H): olista 150, sitebuilder 240, crm 350, center-ai 290, analytics 195,
   connectors 40, lowcode 110, planville 75. Раздел «АКЦЕНТ ПРИЛОЖЕНИЯ» ниже — там же сказано, как хост
   выбирает свой оттенок.

   Смысловые цвета пересчитаны той же формулой на H 150 (success), 75 (warning), 25 (danger), 240 (info):
   прежние значения подбирались под холодный фон и на тёплом спорили с ним. Отступление от формулы одно,
   и оно записано комментарием у самого токена: danger в светлой теме взят с хромой 0.16, потому что на
   0.12 он читается коричневым.

   Пары «краска + подложка» объявлены рядом и обязаны меняться вместе. Подложка обязана быть парной теме:
   светло-зелёная плашка на тёмной консоли не «мягкая», а слепящая — это единственное светлое пятно на экране.

   Контраст всех пар проверен арифметикой WCAG 2.1 в DesignPaletteContractTests: 4.5 для текста, 3.0 для
   крупного. Меняя значение, прогоните тест — глазом эти 0.2 не видно. Исключение названо там же:
   --ds-line — волосяная линия между поверхностями, а не граница смыслового цвета, и 3.0 к ней не
   предъявляется (как не предъявлялось и прежней палитрой: #e2e5ee на #f5f6fa давал 1.15).
   =================================================================================================== */

:root {
    color-scheme: light dark;

    /* Поверхности: страница, панель на ней, вложенная в панель подложка, линия между ними. Тёплые. */
    --ds-bg: #f6f4ee;
    --ds-surface: #ffffff;
    --ds-surface-alt: #efede6;
    --ds-line: #dfe3dd;

    --ds-text: #17201b;
    --ds-muted: #5c665f;

    /*
        Акцент по умолчанию — Olista, H 150.

        Здесь он объявлен для страницы, не назвавшей data-app: без атрибута приложение — Olista. Те же
        четыре токена для каждого из восьми приложений объявлены ниже, в разделе «АКЦЕНТ ПРИЛОЖЕНИЯ»,
        включая сам olista — чтобы набор был один и тот же у всех восьми, а не «семь и умолчание».

        Две роли — две переменные, и это решение старше нынешней палитры. --ds-accent читают как текст
        (ссылки, надзаголовки, кольцо фокуса), --ds-accent-fill заливают под --ds-accent-contrast.
        Светлоты у них разные (0.40 против 0.52) именно потому, что одно читают буквами на фоне страницы,
        а другое держит буквы на себе.
    */
    --ds-accent: #005725;
    --ds-accent-fill: #1d7d3e;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #d9f3dd;

    /* Смысловые цвета и их подложки. Та же формула, H: success 150, warning 75, danger 25, info 240. */

    /*
        ОТСТУПЛЕНИЕ ОТ ФОРМУЛЫ, НАЗВАННОЕ ВСЛУХ: у danger в СВЕТЛОЙ теме хрома 0.16, а не общая 0.12.

        oklch(0.40 0.12 25) — это #7c2524, и на тёплом фоне #f6f4ee он читается коричневым, а не
        красным: опасность в интерфейсе узнают по насыщенности, и приглушённый до уровня соседей
        красный перестаёт быть сигналом. Соседям по H столько хромы не нужно — их и так узнают по
        оттенку, — а danger единственный, чья работа в том, чтобы выделяться.

        Контраст от этого не пострадал, а вырос: 9.08 на --ds-bg против 8.89 (норма 4.5), 9.98 на
        --ds-surface, 8.29 на своей подложке. Считано DesignPaletteContractTests.

        ПОДЛОЖКА ОСТАЛАСЬ ПРЕЖНЕЙ, И ЭТО ОХВАТ, А НЕ НЕДОСМОТР. На L 0.94 в sRGB помещается хрома
        0.03 — ни 0.04 формулы, ни тем более 0.16 туда не влезают, и правило «C — потолок, а не
        обещание» снижает её до 0.03 в обоих случаях, давая один и тот же #ffe4e1. Насыщеннее
        розовая подложка станет только став темнее, а темнее она перестанет быть мягкой.

        Тёмная тема не тронута: на тёмном фоне #ffbfb8 коричневым не читается.
    */
    --ds-danger: #8a0314;
    --ds-danger-soft: #ffe4e1;
    --ds-success: #005725;
    --ds-success-soft: #d9f3dd;
    --ds-warning: #624001;
    --ds-warning-soft: #fbe8ce;
    --ds-info: #014d73;
    --ds-info-soft: #d9efff;

    /*
        Цвет марки.

        Знак «Кольца роста» и надзаголовок первого экрана носят зелёный Olista — тот же H 150, что и
        акцент по умолчанию. Прежде здесь стояла бирюза сайта организации, намеренно НЕ равная акценту:
        бренд и интерактив были разными вещами, потому что бренд принадлежал одному приложению, а индиго —
        всем. Теперь марка платформы и акцент её главной — одно решение, и держать их двумя
        несовпадающими зелёными значит рисовать на одной странице два зелёных.

        Приложению со своим H марка остаётся зелёной: --ds-brand НЕ переопределяется в разделе «АКЦЕНТ
        ПРИЛОЖЕНИЯ». Знак Olista в шапке CRM — знак Olista, а не розовое пятно.
    */
    --ds-brand: #005725;
    --ds-brand-soft: #d9f3dd;

    /* =============================================================================================== */

    /*
        ТРИ ступени радиуса, и не больше.

        В ходу было шесть — 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:
        экранов на нём больше всего, и переучивать пришлось бы их.

        Гарнитур две, и роли у них разные. Golos Text — текст: интерфейс, абзацы, подписи, кнопки. Geologica
        — заголовки: у неё узкая, плотная форма, на кегле 30 она держит строку, а на кегле 15 в абзаце
        утомляет. Системный стек остаётся запасником обоих: до того, как приедет woff2, страница уже
        нарисована, и её рисует он.
    */
    --ds-font-display: "Geologica", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --ds-font: "Golos Text", 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;
    /* Седьмая ступень — ПЛОТНАЯ ПАНЕЛЬ ИНСТРУМЕНТА (Center#842, замер редактора Конструктора: 55 из 142
       кеглей мельче xs, от 9 до 12px). Только для подписей в панелях инструмента — палитра, слои,
       инспектор, линейки, — где глаз держит панель, а не читает текст. Бегущему тексту, полям и
       кнопкам она запрещена: там нижняя граница по-прежнему xs. 11, а не 9 и не 10: ниже 11px Golos
       Text теряет счётчики и «е/с» сливаются на не-Retina; всё, что в редакторе было 9–10, поднимается
       к 11 — это и есть цена ступени, названная заказчику. */
    --ds-size-2xs: 11px;

    /* Заголовок страницы и отрицательный трекинг к нему: на крупном кегле системный шрифт без него рыхлый. */
    --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 {
        /*
            Тёмная тема меняет не только светлоту, но и роль контрастного цвета. Белые буквы на
            осветлённом акценте дают 3.6 при норме 4.5 — поэтому под заливкой тёмной темы лежит фон
            страницы (#121815), а не белый: тёмное на светлом, а не светлое на светлом.
        */
        --ds-bg: #121815;
        --ds-surface: #1b2320;
        --ds-surface-alt: #222b27;
        --ds-line: #2a3430;

        --ds-text: #eef1ec;
        --ds-muted: #9aa59d;

        --ds-accent: #a7e2b2;
        --ds-accent-fill: #7ccd8e;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #1a3520;

        --ds-danger: #ffbfb8;
        --ds-danger-soft: #442321;
        --ds-success: #a7e2b2;
        --ds-success-soft: #1a3520;
        --ds-warning: #f4c98e;
        --ds-warning-soft: #3c2a0e;
        --ds-info: #a3d8ff;
        --ds-info-soft: #133144;

        --ds-brand: #a7e2b2;
        --ds-brand-soft: #1a3520;

        /* На тёмном фоне тень чёрным почти не видна — она обязана быть плотнее, а не той же. */
        --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: #f6f4ee;
    --ds-surface: #ffffff;
    --ds-surface-alt: #efede6;
    --ds-line: #dfe3dd;
    --ds-text: #17201b;
    --ds-muted: #5c665f;
    --ds-accent: #005725;
    --ds-accent-fill: #1d7d3e;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #d9f3dd;
    --ds-danger: #8a0314;
    --ds-danger-soft: #ffe4e1;
    --ds-success: #005725;
    --ds-success-soft: #d9f3dd;
    --ds-warning: #624001;
    --ds-warning-soft: #fbe8ce;
    --ds-info: #014d73;
    --ds-info-soft: #d9efff;
    --ds-brand: #005725;
    --ds-brand-soft: #d9f3dd;
    --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: #121815;
    --ds-surface: #1b2320;
    --ds-surface-alt: #222b27;
    --ds-line: #2a3430;
    --ds-text: #eef1ec;
    --ds-muted: #9aa59d;
    --ds-accent: #a7e2b2;
    --ds-accent-fill: #7ccd8e;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #1a3520;
    --ds-danger: #ffbfb8;
    --ds-danger-soft: #442321;
    --ds-success: #a7e2b2;
    --ds-success-soft: #1a3520;
    --ds-warning: #f4c98e;
    --ds-warning-soft: #3c2a0e;
    --ds-info: #a3d8ff;
    --ds-info-soft: #133144;
    --ds-brand: #a7e2b2;
    --ds-brand-soft: #1a3520;
    --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%);
}

/* ===================================================================================================
   АКЦЕНТ ПРИЛОЖЕНИЯ

   Хост ставит на <html> атрибут data-app="<ключ>"; страница без атрибута — Olista. Ключи те же, что у
   слотов реестра Center.Design/Platform/PlatformApps.cs, и берутся оттуда константами PlatformAppKeys
   (имя атрибута — PlatformAppKeys.RootAttribute): второго списка ключей у платформы нет и не будет.

   Три слота — analytics, connectors, lowcode — в реестре приложений пока не заведены: приложений ещё
   нет. Оттенки им объявлены заранее и намеренно. Цвет приложения выбирается один раз на всю его жизнь, и
   выбирать его в тот день, когда заводится хост, значит выбирать из того, что осталось; восемь оттенков,
   разложенных по кругу разом, различимы попарно, а восьмой, добавленный через год, сядет между двумя.

   ПОЧЕМУ СЕЛЕКТОРЫ ТАКИЕ. У :root[data-theme="dark"] специфичность (0,2,0) — ровно как у
   :root[data-app="crm"]. Объяви приложение одной парой правил — и в тёмной теме акцент приложения
   выигрывал бы или проигрывал по тому, какая строка в файле ниже. Поэтому тема и приложение всегда
   названы вместе: (0,3,0) сильнее (0,2,0) при любом порядке строк.

   Системная тёмная взята через :root:not([data-theme="light"])[data-app="…"], а не голым
   [data-app="…"]: человек, выбравший светлую тему на системно-тёмной машине, иначе получил бы светлый
   фон и осветлённый под тёмный фон акцент — то есть бледную ссылку на белом.

   --ds-brand здесь не переопределяется: марка платформы одна на все приложения (см. раздел ЦВЕТ).

   ПОЧЕМУ У КАЖДОГО ПРАВИЛА ЕСТЬ ВТОРОЙ НОСИТЕЛЬ — .ds-switch-tile[data-app="…"]. В панели переключателя
   приложений (решение PO 2026-09-16) каждая плитка носит знак в цвете СВОЕГО приложения, а страница при
   этом остаётся в цвете своего: восемь оттенков на одной странице — это восемь плиток, а не восемь тем.
   Прямого способа прочитать чужой --ds-accent-fill в CSS нет, и первая редакция этой правки завела
   второй список оттенков (--ds-mark-crm и далее) рядом с первым. Второй список — это второе место, где
   правится цвет CRM, и расходятся такие списки молча: панель светилась бы прежним розовым ещё год.

   ТРЕТИЙ НОСИТЕЛЬ — .ds-app[data-app="…"] (0.31.27, Center.Analytics#24): любой элемент вне переключателя,
   которому нужны цвета ДРУГОГО приложения — плитка продукта на титульной хаба, карточка соседа «Работает
   вместе с», герой страницы продукта. Плитка переключателя носит своё имя по-прежнему; для всего нового
   — этот класс, а не .ds-switch-tile поверх чужой раскладки.

   Поэтому оттенок объявляется ОДИН раз, а селекторов у объявления два: корень страницы и плитка. Своё
   объявление на элементе побеждает унаследованное от корня всегда, независимо от специфичности, — то
   есть внутри плитки --ds-accent, --ds-accent-fill и --ds-accent-soft значат «цвета ЭТОГО приложения», и
   разметка плитки написана обычными токенами, ничего не зная про ключи.

   Тема у плитки взята тем же приёмом, что у корня, и по той же причине: :root[data-theme="dark"] …
   .ds-switch-tile и :root:not([data-theme="light"]) … .ds-switch-tile взаимно исключают друг друга по
   :not, поэтому порядок строк на исход не влияет.
   =================================================================================================== */

/* Главная Olista — оттенок марки — H 150 */
:root[data-app="olista"],
:root[data-theme="light"][data-app="olista"],
.ds-app[data-app="olista"],
.ds-switch-tile[data-app="olista"] {
    --ds-accent: #005725;
    --ds-accent-fill: #1d7d3e;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #d9f3dd;
}

/* Конструктор — H 240 */
:root[data-app="sitebuilder"],
:root[data-theme="light"][data-app="sitebuilder"],
.ds-app[data-app="sitebuilder"],
.ds-switch-tile[data-app="sitebuilder"] {
    --ds-accent: #014d73;
    --ds-accent-fill: #0070a6;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #d9efff;
}

/* CRM — H 350 */
:root[data-app="crm"],
:root[data-theme="light"][data-app="crm"],
.ds-app[data-app="crm"],
.ds-switch-tile[data-app="crm"] {
    --ds-accent: #742651;
    --ds-accent-fill: #9e4573;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #ffe2ee;
}

/* Olista AI — H 290 */
:root[data-app="center-ai"],
:root[data-theme="light"][data-app="center-ai"],
.ds-app[data-app="center-ai"],
.ds-switch-tile[data-app="center-ai"] {
    --ds-accent: #493883;
    --ds-accent-fill: #6959ae;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #eae8ff;
}

/* Аналитика (слота в реестре ещё нет) — H 195 */
:root[data-app="analytics"],
:root[data-theme="light"][data-app="analytics"],
.ds-app[data-app="analytics"],
.ds-switch-tile[data-app="analytics"] {
    --ds-accent: #015353;
    --ds-accent-fill: #047879;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #cdf4f3;
}

/* Коннекторы (слота в реестре ещё нет) — H 40 */
:root[data-app="connectors"],
:root[data-theme="light"][data-app="connectors"],
.ds-app[data-app="connectors"],
.ds-switch-tile[data-app="connectors"] {
    --ds-accent: #7a2a06;
    --ds-accent-fill: #a54a28;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #ffe5dc;
}

/* Лаборатория low-code (слота в реестре ещё нет) — H 110 */
:root[data-app="lowcode"],
:root[data-theme="light"][data-app="lowcode"],
.ds-app[data-app="lowcode"],
.ds-switch-tile[data-app="lowcode"] {
    --ds-accent: #4b4b01;
    --ds-accent-fill: #6d6e02;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #eceed0;
}

/* Планвиль — H 75 */
:root[data-app="planville"],
:root[data-theme="light"][data-app="planville"],
.ds-app[data-app="planville"],
.ds-switch-tile[data-app="planville"] {
    --ds-accent: #624001;
    --ds-accent-fill: #8d5e02;
    --ds-accent-contrast: #ffffff;
    --ds-accent-soft: #fbe8ce;
}

@media (prefers-color-scheme: dark) {
    /* Главная Olista — оттенок марки — H 150 */
    :root:not([data-theme="light"])[data-app="olista"],
    :root:not([data-theme="light"]) .ds-app[data-app="olista"],
    :root:not([data-theme="light"]) .ds-switch-tile[data-app="olista"] {
        --ds-accent: #a7e2b2;
        --ds-accent-fill: #7ccd8e;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #1a3520;
    }

    /* Конструктор — H 240 */
    :root:not([data-theme="light"])[data-app="sitebuilder"],
    :root:not([data-theme="light"]) .ds-app[data-app="sitebuilder"],
    :root:not([data-theme="light"]) .ds-switch-tile[data-app="sitebuilder"] {
        --ds-accent: #a3d8ff;
        --ds-accent-fill: #69c1fc;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #133144;
    }

    /* CRM — H 350 */
    :root:not([data-theme="light"])[data-app="crm"],
    :root:not([data-theme="light"]) .ds-app[data-app="crm"],
    :root:not([data-theme="light"]) .ds-switch-tile[data-app="crm"] {
        --ds-accent: #ffb9d9;
        --ds-accent-fill: #f197c2;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #412331;
    }

    /* Olista AI — H 290 */
    :root:not([data-theme="light"])[data-app="center-ai"],
    :root:not([data-theme="light"]) .ds-app[data-app="center-ai"],
    :root:not([data-theme="light"]) .ds-switch-tile[data-app="center-ai"] {
        --ds-accent: #d0c9ff;
        --ds-accent-fill: #b6aaff;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #2d2945;
    }

    /* Аналитика (слота в реестре ещё нет) — H 195 */
    :root:not([data-theme="light"])[data-app="analytics"],
    :root:not([data-theme="light"]) .ds-app[data-app="analytics"],
    :root:not([data-theme="light"]) .ds-switch-tile[data-app="analytics"] {
        --ds-accent: #85e4e3;
        --ds-accent-fill: #3bcfcf;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #023536;
    }

    /* Коннекторы (слота в реестре ещё нет) — H 40 */
    :root:not([data-theme="light"])[data-app="connectors"],
    :root:not([data-theme="light"]) .ds-app[data-app="connectors"],
    :root:not([data-theme="light"]) .ds-switch-tile[data-app="connectors"] {
        --ds-accent: #ffc0ab;
        --ds-accent-fill: #f99c7c;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #43251a;
    }

    /* Лаборатория low-code (слота в реестре ещё нет) — H 110 */
    :root:not([data-theme="light"])[data-app="lowcode"],
    :root:not([data-theme="light"]) .ds-app[data-app="lowcode"],
    :root:not([data-theme="light"]) .ds-switch-tile[data-app="lowcode"] {
        --ds-accent: #d4d791;
        --ds-accent-fill: #bcbe5e;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #2f300f;
    }

    /* Планвиль — H 75 */
    :root:not([data-theme="light"])[data-app="planville"],
    :root:not([data-theme="light"]) .ds-app[data-app="planville"],
    :root:not([data-theme="light"]) .ds-switch-tile[data-app="planville"] {
        --ds-accent: #f4c98e;
        --ds-accent-fill: #e4ac59;
        --ds-accent-contrast: #121815;
        --ds-accent-soft: #3c2a0e;
    }
}

/* Главная Olista — оттенок марки — H 150 */
:root[data-theme="dark"][data-app="olista"],
:root[data-theme="dark"] .ds-app[data-app="olista"],
:root[data-theme="dark"] .ds-switch-tile[data-app="olista"] {
    --ds-accent: #a7e2b2;
    --ds-accent-fill: #7ccd8e;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #1a3520;
}

/* Конструктор — H 240 */
:root[data-theme="dark"][data-app="sitebuilder"],
:root[data-theme="dark"] .ds-app[data-app="sitebuilder"],
:root[data-theme="dark"] .ds-switch-tile[data-app="sitebuilder"] {
    --ds-accent: #a3d8ff;
    --ds-accent-fill: #69c1fc;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #133144;
}

/* CRM — H 350 */
:root[data-theme="dark"][data-app="crm"],
:root[data-theme="dark"] .ds-app[data-app="crm"],
:root[data-theme="dark"] .ds-switch-tile[data-app="crm"] {
    --ds-accent: #ffb9d9;
    --ds-accent-fill: #f197c2;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #412331;
}

/* Olista AI — H 290 */
:root[data-theme="dark"][data-app="center-ai"],
:root[data-theme="dark"] .ds-app[data-app="center-ai"],
:root[data-theme="dark"] .ds-switch-tile[data-app="center-ai"] {
    --ds-accent: #d0c9ff;
    --ds-accent-fill: #b6aaff;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #2d2945;
}

/* Аналитика (слота в реестре ещё нет) — H 195 */
:root[data-theme="dark"][data-app="analytics"],
:root[data-theme="dark"] .ds-app[data-app="analytics"],
:root[data-theme="dark"] .ds-switch-tile[data-app="analytics"] {
    --ds-accent: #85e4e3;
    --ds-accent-fill: #3bcfcf;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #023536;
}

/* Коннекторы (слота в реестре ещё нет) — H 40 */
:root[data-theme="dark"][data-app="connectors"],
:root[data-theme="dark"] .ds-app[data-app="connectors"],
:root[data-theme="dark"] .ds-switch-tile[data-app="connectors"] {
    --ds-accent: #ffc0ab;
    --ds-accent-fill: #f99c7c;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #43251a;
}

/* Лаборатория low-code (слота в реестре ещё нет) — H 110 */
:root[data-theme="dark"][data-app="lowcode"],
:root[data-theme="dark"] .ds-app[data-app="lowcode"],
:root[data-theme="dark"] .ds-switch-tile[data-app="lowcode"] {
    --ds-accent: #d4d791;
    --ds-accent-fill: #bcbe5e;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #2f300f;
}

/* Планвиль — H 75 */
:root[data-theme="dark"][data-app="planville"],
:root[data-theme="dark"] .ds-app[data-app="planville"],
:root[data-theme="dark"] .ds-switch-tile[data-app="planville"] {
    --ds-accent: #f4c98e;
    --ds-accent-fill: #e4ac59;
    --ds-accent-contrast: #121815;
    --ds-accent-soft: #3c2a0e;
}

/* ===================================================================================================
   ОСНОВА

   Три правила, которые в каждом из трёх приложений были написаны по-своему или не написаны вовсе.
   =================================================================================================== */

/*
    Ширина считается по рамке, а не по содержимому.

    Приёмка на телефоне: страница ездила вбок на три десятка пикселей. На узком экране кнопки первого экрана
    встают в колонку и растягиваются на всю ширину, получая 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%; }

/*
    КОРЕНЬ СТРАНИЦЫ — здесь, а не у каждого хоста по-своему.

    Замер 2026-09-17: восемь хостов семьи открывают свой лист правилом body с одним и тем же набором —
    гарнитура, фон, цвет текста, margin: 0 и запрет автоувеличения кегля в мобильном Safari. Значения у
    всех уже токенами (перевод на --ds-* сделан раньше), то есть расходиться им негде, кроме как в
    ПОЛНОТЕ: забыл хост -webkit-text-size-adjust — на телефоне в альбомной ориентации у него поедет
    кегль, и заметит это не он. Набор, написанный восемь раз, отличается от набора, написанного один
    раз, ровно этим.

    Здесь объявлен МИНИМУМ, который обязан быть на всякой странице платформы; раскладка корня остаётся
    хосту. Поэтому нет ни display: flex, ни min-height: 100vh, ни height: 100% — у сервера учётных
    записей колонка на всю высоту, у витрины её нет, и общее правило решало бы за них обоих.

    Хост, у которого те же пять строк написаны своими, может снять их вместе с переходом на это число
    пакета — но снимать не обязан: дубликат с теми же токенами ничего не ломает, он просто лишний.
    Своё значение (другой фон у страницы входа, своя гарнитура раздела) хост по-прежнему пишет у себя:
    его правило стоит в его листе, то есть ниже, и побеждает.
*/
body {
    margin: 0;
    background: var(--ds-bg);
    color: var(--ds-text);
    font-family: var(--ds-font);
    overflow-wrap: break-word;
    -webkit-text-size-adjust: 100%;
}

/*
    Заголовки — гарнитурой заголовков, и это единственное место, где она назначается.

    Вес 500, а не 600 и не 700: у Geologica плотная форма, и на полужирном крупный заголовок начинает
    выглядеть плакатом. Трекинг отрицательный по той же причине, по какой он был отрицательным у
    системного шрифта, — на кегле 30 межбуквенное расстояние, рассчитанное на текст, рыхлит строку;
    токен --ds-tracking-tight для этого и заведён.

    h4–h6 сюда не попадают намеренно: это подзаголовки внутри текста, они живут в абзаце и обязаны быть
    с ним одной гарнитуры. Домен, которому нужен display-шрифт в своём месте, берёт var(--ds-font-display)
    — второй раз назначать его элементам здесь не нужно.

    font-family, а не font: сокращение сбросило бы размер и высоту строки, которые домен уже назначил.
*/
h1, h2, h3, .ds-h1, .ds-h2 {
    font-family: var(--ds-font-display);
    font-weight: 500;
    letter-spacing: var(--ds-tracking-tight);
}

/* Значение, а не текст: слаг, адрес, идентификатор. anywhere ломает в любом месте и, в отличие от
   break-word, учитывается при расчёте минимальной ширины колонки — то есть таблица с адресами перестаёт
   требовать себе экран в полтора раза шире окна. */
code, kbd, samp, .ds-wrap-anywhere { overflow-wrap: anywhere; }

/*
    ССЫЛКА БЕЗ КЛАССА — тоже ссылка платформы.

    Находка #784: «Забыли пароль?» на входе рисовалась синим цветом браузера. Не потому, что кто-то
    ошибся, — потому что этого правила здесь не было: голый <a> в design.css не описан вовсе, и всё,
    что не получило класса, доставалось умолчанию user-agent. Синий #0000EE не входит ни в одну из
    двух наших тем, в тёмной он вдобавок почти не читается на фоне, а посещённая ссылка становилась
    фиолетовой — цвет, которого в палитре нет и который ничего в системе не значит.

    ПОЧЕМУ ЭТО БЕЗОПАСНО ДЛЯ ХОСТОВ, а не «правило на всю страницу соседа». Селектор — один элемент
    (0,0,1), слабее любого класса: всё, что уже носит имя дизайн-системы (.ds-btn, .ds-nav a,
    .ds-switch-tile, .ds-skip) или собственный класс хоста, продолжает краситься своим правилом и
    ничего не замечает. Токен --ds-accent объявлен у КАЖДОГО из восьми приложений — это ось палитры,
    оттенок приложения, и хост без него не собирается вовсе, — поэтому var() здесь не может дать
    пустое значение и остаться без запасного.

    Подчёркивание НЕ снимается: у ссылки внутри абзаца цвет — единственный признак, а по цвету её не
    находит дальтонизм. text-decoration-thickness/underline-offset сдвигают линию от букв, чтобы она
    не резала выносные элементы кириллицы.

    Кольцо фокуса ссылке рисует общее правило :focus-visible ниже — второй раз оно тут не пишется.
    Наведение — тот же цвет плотнее (brightness), а не второй токен: заводить --ds-accent-hover
    значило бы завести его восьми приложениям сразу и проверить каждый на контраст.
*/
a {
    color: var(--ds-accent);
    text-decoration-thickness: 1px;
    text-underline-offset: .18em;
}

a:hover { filter: brightness(1.1); }

/*
    Кольцо ссылке — тем же токеном, что и всем: общее правило :focus-visible ниже даёт его само, и вид
    эта строка не меняет ни на пиксель. Написана она потому, что цвет ссылки объявлен ЗДЕСЬ, а
    видимость фокуса — половина того же обещания: правило, назначающее ссылке краску, обязано рядом же
    показывать, чем эта ссылка отвечает клавиатуре, иначе снявший одно не заметит, что снял и другое.
    (0,1,1) вместо (0,1,0) обыгрывает вдобавок `a:focus { outline: none }` хоста, подключённый РАНЬШЕ
    этого файла. Предел назван: чужое правило той же формы, подключённое позже, побеждает по порядку —
    для этого спора заведён .ds-focusable с !important.
*/
a:focus-visible { outline: var(--ds-focus-ring); outline-offset: 2px; }

/*
    Кольцо фокуса — одно на платформу, и оно было нарисовано у двух приложений из трёх.

    :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)); }

/* Имя — не подпись, а ссылка на главную, то есть цель нажатия наравне с соседями по шапке: замер на
   сенсорном 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);
    /*
        ВЫВЕСКА НАБРАНА ГАРНИТУРОЙ ЗАГОЛОВКОВ, а не текстовой, и это не вкусовщина: слово «olista»
        рядом со знаком — это и есть знак словом. В утверждённых макетах публичных страниц
        (brand/public-pages) и в брендбуке оно строчными, Geologica 500, трекинг −0.02em; здесь же
        стояли Golos Text и вес 650 — то есть вывеска семьи расходилась с собственным макетом в
        единственном месте, где её видят на каждой странице каждого из восьми приложений.

        Вес 500, а не 650, по той же причине, по какой он 500 у h1–h3 (раздел ОСНОВА): у Geologica
        плотная форма, и полужирный превращает надпись в плакат. Трекинг — тот же токен, что у
        заголовков: на плотной гарнитуре межбуквенное расстояние, рассчитанное на текст, рыхлит
        короткое слово.

        Размер НЕ НАЗНАЧАЕТСЯ — он наследуется, как наследовался: вывеска стоит в строке шапки и
        обязана мериться ею, а не собственным числом. Порядок уступок в тесноте от смены гарнитуры не
        зависит вовсе: он выражен потолками по ширинам, а не весами сжатия (разбор ниже).
    */
    font-family: var(--ds-font-display);
    font-weight: 500;
    letter-spacing: var(--ds-tracking-tight);
    white-space: nowrap;
    text-decoration: none;
}

.ds-brand:hover { text-decoration: none; }

.ds-brand-mark, .ds-icon.ds-brand-mark { width: 28px; height: 28px; display: block; flex: none; color: var(--ds-brand); } /* знак семьи, не приложения: KisaPs/Center#1240 DS-01 */

.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; }
/* Выход: значок и подпись; на ≤400 подпись уходит — см. раздел тесноты. */
.ds-signout { gap: 6px; }

/* Подпись, а не действие: имя учётной записи усекается многоточием, уступая место кнопке рядом. */
.ds-who {
    min-width: 0;
    color: var(--ds-muted);
    font-size: 13.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*
    ПОРЯДОК УСТУПОК В ТЕСНОТЕ — один на всю шапку, и записан он здесь целиком, а не собран из четырёх
    правил, каждое из которых называло себя первым.

    Так и было: и у .ds-brand, и у .ds-who, и у .ds-switch-current стоял комментарий «принимает тесноту
    первым», а в физике flex первым уступал ЕДИНСТВЕННЫЙ, кому это было разрешено. Замер витрины на 760
    до этой правки (getBoundingClientRect, вывеска «Дизайн-система Olista»): .ds-brand > span — 31.75 при
    собственных 171, то есть «Д…», тогда как .ds-nav держала свои 325.33, .ds-who — 110.37, а подпись
    переключателя — 31.76 из своих 32. Причина арифметическая, а не оформительская: .ds-switch и
    .ds-actions стояли flex: none, у .ds-nav сжиматься нечему, и весь дефицит строки доставался вывеске,
    потому что она одна объявила min-width: 0.

    Порядок, в котором шапка обязана сдавать место (заказ center-design, находка приёмки 2026-09-17):

        1. подпись переключателя — «где я» повторено крупно заголовком страницы, и это единственная
           надпись шапки, у которой есть дублёр на самом экране;
        2. имя учётной записи — подпись, а не действие;
        3. навигация — на телефоне она и так уходит в бургер целиком;
        4. вывеска семьи — ПОСЛЕДНЕЙ. Она отвечает на вопрос «куда я попал», и обрезанная до одной буквы
           отвечает на него неправдой: «Д…» и «О…» выглядят одинаково у восьми хостов семьи.

    ВЫРАЖЕН ПОРЯДОК ПОТОЛКАМИ ПО ШИРИНАМ, А НЕ ВЕСАМИ СЖАТИЯ — и это измеренный выбор, а не стиль.
    Веса flex-shrink были написаны первыми и сняты по замеру: дефицит строки делится между элементами
    только в пределах их минимального содержимого, а минимальное содержимое коробки с nowrap-надписью
    равно ПОЛНОЙ ширине надписи (overflow: hidden снимает автоминимум с самой надписи, но наружу, в счёт
    родителя, она приходит целой). Замер на 375 с весами 200/60/1: .ds-actions — 174.30 при собственных
    174, то есть группа не отдала ни пикселя, и весь дефицит снова достался вывеске, только теперь ДО
    НУЛЯ. Снять этот счёт можно лишь min-width: 0 у самой группы — и тогда группа сжимается мимо своих
    детей: тот же замер давал .ds-actions 78.95 при содержимом 142, то есть «Выйти» лежало поверх
    бургера. Потолок подписи отдаёт место, не отпуская коробку, и потому здесь он.

    Потолки стоят ЗДЕСЬ и ниже, в одном месте на шапку, а не внутри правил своих имён: три числа,
    разъехавшиеся по файлу, разошлись бы на первой же правке любого из трёх.

    ЧТО ПОЛУЧИЛОСЬ И ЧЕГО ПОРЯДОК НЕ ЧИНИТ — числами, замер той же витрины после правки:

        760: вывеска 82.07 (было 31.75), имя 83.70, разделы 301.68, переключатель 75.76. Хост с коротким
             словом («Olista», 43.5) на этой ширине печатает его ЦЕЛИКОМ — то есть находка приёмки
             «вывеска усечена до „Oli…“ на 758» закрыта, а не смягчена.
        375: вывеска 46.20, имя 50.22 («Мари…»), переключатель 75.76, выход 67.80, бургер 44.00 — строка
             ровно по ширине, документ вбок не едет (scrollWidth 360 при clientWidth 360).
        320: вывеска 0.00 БЕЗ ЗНАКА — состояние, которое замер 2026-09-17 назвал открытым пределом
             («решения о том, чего в шапке не должно быть, нет»). Решение принято 2026-09-24
             (Center.Design#23, второе воспроизведение — живой браузерный смоук
             KisaPs/Center.Identity#8): знак обязателен. Хост без MarkSrc не доезжает уже до рендера —
             PlatformHeader.razor бросает исключение (OnParametersSet). Ветка «хост без знака» ниже по
             файлу и здесь остаётся кодом (снимать её — отдельная, самостоятельная правка со своим
             риском), но через настоящий компонент в неё больше не попасть; сверять этот угол
             самостоятельно, если правите разметку в обход компонента.

             Хост, передавший знак (обязательный MarkSrc), на этой ширине узнаётся по знаку —
             .ds-brand-mark стоит flex: none и не сжимается сам, а с 0.31.36 у ссылки вокруг него есть
             свой пол (min-width: var(--ds-tap) в @media (max-width: 400px)) — цель нажатия «домой» не
             пропадает вместе со словом.
*/
.ds-brand { flex: 0 1 auto; }

/*
    ПОЛ У ПОДПИСИ, А НЕ ОДНО МНОГОТОЧИЕ.

    Прежняя редакция обещала прямо в комментарии ниже по файлу: имя «отдаёт место кнопке само, НЕ ИСЧЕЗАЯ»,
    — и обещание держалось на том, что у .ds-who есть text-overflow. Замер витрины на 375 до этой правки:
    .ds-who — ширина 0.00 при собственных 110.37, то есть исчезло ровно то, что обещано не исчезающим.
    Многоточие рисуется в коробке; коробка нулевой ширины не рисует и его.

    6ch — не круглое число, а пол, ниже которого подпись перестаёт быть подписью: четыре знака имени плюс
    многоточие («Мари…»). ch — ширина нуля текущей гарнитуры, поэтому пол едет вместе с кеглем и не
    требует пересчёта при смене шрифта. Сверху — потолки по ширинам (ниже): пол и потолок отвечают на
    разные вопросы — «меньше этого не читается» и «больше этого подпись отбирает у соседа».

    У ПОДПИСИ ПЕРЕКЛЮЧАТЕЛЯ ПОЛА НЕТ, и это тоже замер, а не пропуск. Самое короткое имя реестра —
    «CRM», 31.76 при ch в 8.94, то есть 3.6ch: любой пол, который защитил бы «Конструктор» от усечения до
    одной буквы, оказался бы ШИРЕ этого имени и раздул бы кнопку CRM на два десятка пикселей у всех
    восьми хостов и на всех ширинах. Пол там у КНОПКИ и равен пальцу (--ds-tap).
*/
.ds-who { min-width: 6ch; }

/*
    ПЕРВАЯ УСТУПКА — подпись переключателя. Потолок в знаках, а не в процентах ширины: усечение обязано
    наступать от длины ИМЕНИ, а не от размера окна, иначе «CRM» на 900 получает потолок в 300 пикселей
    и не усекается никогда, а «Лаборатория low-code» — тот же потолок и не усекается тоже.
*/
@media (max-width: 1024px) {
    .ds-switch-current { max-width: 12ch; }
}

/*
    ВТОРАЯ — имя учётной записи, ТРЕТЬЯ — разделы. Ссылка раздела до этой правки усечения не знала вовсе:
    .ds-nav ужиматься было нечему, и строка разделов держала свои 325.33 на 760, пока вывеску резали до
    31.75. Теперь разделы отдают своё так же, как подписи, — многоточием, а не пропажей пункта.
*/
@media (max-width: 900px) {
    .ds-who { max-width: 10ch; }

    .ds-nav a {
        max-width: 10ch;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
}

/*
    Усечение вывески объявлено ПОСЛЕ подписей — и это не вкус в расстановке строк.

    Вывеска уступает последней (порядок выше), поэтому и правило её усечения читается последним: тот, кто
    придёт править шапку, увидит сначала, кто сдаёт место, и только потом — что бывает, когда сдавать уже
    некому. Сторож ShowcaseContractTests держит этот порядок строк: .ds-brand не может получить усечение
    раньше .ds-who, иначе в файле снова будет написано, что вывеска уступает первой.

    Усекается ИМЕННО подпись, а не ссылка целиком: знак (.ds-brand-mark) стоит flex: none и остаётся на
    месте при любой тесноте. Хост без знака теряет в пределе всё слово, хост со знаком — узнаётся по нему.
*/
.ds-brand > span { min-width: 0; 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;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    color: var(--ds-text);
    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; }

/*
    ТРИ ПОЛОСКИ СНЯТЫ С ОБЩЕЙ ШАПКИ, А ПРАВИЛО ОСТАЛОСЬ — И ЭТО НЕ МУСОР.

    PlatformHeader с 2026-09-17 печатает в бургере знак ds-icon-menu из общего спрайта: та же вещь,
    нарисованная и полосками, и глифом, расходится с набором на первом же изменении толщины линии.
    Но носителей у разметки бургера больше одного, и остальные — рукописные копии в ЧУЖИХ репозиториях
    (Pages/Shared/_Layout.cshtml сервера учётных записей, шапка консоли конструктора). Правило здесь
    живёт их сроком, а не нашим: снять его в тот же день, когда перешла общая шапка, значит выдать двум
    соседям бургер без кнопки — молча, обновлением пакета, и заметят это глазами.

    Снимается оно заявкой: когда каждый носитель перейдёт на <DsIcon Name="menu" /> или на свой значок.
    Проверить это одним grep по дереву нельзя — список межрепозиторный.

    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;
    }
}

/* ===================================================================================================
   ПЕРЕКЛЮЧАТЕЛЬ ПРИЛОЖЕНИЙ

   Одна кнопка с именем текущего приложения и панель за ней — решение PO 2026-09-16 (холст
   «Переключатель приложений»). Ряд иконок рядом с вывеской заказчиком отклонён: он отвечал «куда ещё
   перейти» и молчал о том, где человек стоит, а имени приложения в шапке не было нигде.

   ЧЕМ ОТЛИЧАЕТСЯ ОТ ПЛАНКИ ВЫШЕ ФИЗИКОЙ. Планка — details/summary, то есть раскрытие без единой строки
   скрипта. Здесь раскрытие ведёт design.js, и это отступление названо в самом компоненте: ловушки
   фокуса, стрелок между плитками и возврата фокуса на триггер у details нет, а без них не выполняется
   клавиатурный контракт общей шапки, ради которого правка и делается. Разметка компонента — Blazor,
   то есть поверхность, которая без скриптов не работает вовсе.

   ЦВЕТ ЗНАКА НА ПЛИТКЕ — чужого приложения, а не страницы. Достигается не вторым списком оттенков, а
   вторым селектором у ЕДИНСТВЕННОГО объявления в разделе АКЦЕНТ ПРИЛОЖЕНИЯ (разбор — там же): внутри
   .ds-switch-tile[data-app="crm"] токены --ds-accent* значат цвета CRM, и правила ниже написаны
   обычными токенами, ничего не зная ни про один ключ.

   ШТОРКА НА ТЕЛЕФОНЕ. На ≤720 панель уходит из координат кнопки в координаты окна и садится на нижний
   край: попасть большим пальцем в верх экрана нельзя, а панель здесь — основной способ сменить рабочее
   место. Затемнение — ::before самой обёртки, fixed inset: 0; и оно значит «окно» ровно по той же
   причине и с той же оговоркой, что у бургера: у шапки backdrop-filter, а любое его значение, кроме
   none, делает её содержащим блоком для fixed. Отсюда парное правило :has() ниже. Нажатий затемнение не
   ловит вовсе (в отличие от бургера, который так живёт без скрипта): закрытие мимо здесь и так делает
   design.js, а ловя нажатия, затемнение съедало бы первый щелчок — это уже стоило дефекта планке.
   =================================================================================================== */

/* Коробка переключателя не сжимается: место отдаёт ПОДПИСЬ внутри неё, по потолку из раздела ШАПКА.
   Сжимать саму коробку нельзя — она не flex-контейнер, и её кнопка при этом не ужимается вместе с ней, а
   вылезает наружу: замер на 375 в ходе этой правки давал обёртку в 0.00 с кнопкой в 98 поверх соседа. */
.ds-switch { position: relative; flex: none; }

.ds-switch-trigger {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: var(--ds-tap);
    padding: 0 10px;
    background: none;
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text);
    font: inherit;
    font-size: var(--ds-size-sm);
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
}

.ds-switch-trigger:hover { background: var(--ds-surface); }
.ds-switch-trigger:active { transform: translateY(1px); }
.ds-switch-trigger[aria-expanded="true"] { background: var(--ds-surface); }

/* Имя уступает первым в порядке шапки (разбор и потолки — в разделе ШАПКА): усечением, а не
   исчезновением. Прятать его нельзя, ровно оно и есть ответ на вопрос «где я». */
.ds-switch-current { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ds-icon.ds-switch-trigger-mark { display: none; width: 20px; height: 20px; color: var(--ds-accent-fill); }

/* Стрелка смотрит туда, куда поедет панель. Поворот — единственное движение кнопки, и его гасит общее
   правило prefers-reduced-motion в разделе ДВИЖЕНИЕ вместе со всеми переходами. */
.ds-switch-caret { flex: none; transition: transform var(--ds-fast) var(--ds-ease); }
.ds-switch-trigger[aria-expanded="true"] .ds-switch-caret { transform: rotate(180deg); }

.ds-switch-panel {
    position: absolute;
    inset-inline-start: 0;
    top: calc(100% + var(--ds-space-2));
    z-index: 30;
    width: 380px;
    max-width: calc(100vw - 2 * var(--ds-space-4));
    padding: var(--ds-space-3);
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
    box-shadow: var(--ds-shadow-lg);
}

.ds-switch-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-space-3);
    margin-bottom: var(--ds-space-3);
}

.ds-switch-bar-title { color: var(--ds-muted); font-size: var(--ds-size-xs); }

.ds-switch-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ds-tap);
    height: var(--ds-tap);
    background: none;
    border: 0;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-muted);
    cursor: pointer;
}

.ds-switch-close:hover { background: var(--ds-surface-alt); color: var(--ds-text); }

.ds-switch-section + .ds-switch-section { margin-top: var(--ds-space-4); }

.ds-switch-title {
    margin: 0 0 var(--ds-space-2);
    font-family: var(--ds-font);
    font-size: var(--ds-size-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ds-muted);
}

.ds-switch-tiles {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--ds-space-2);
}

.ds-switch-tile {
    display: grid;
    gap: 2px;
    padding: var(--ds-space-3);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
    background: var(--ds-surface);
    color: var(--ds-text);
    text-decoration: none;
}

.ds-switch-tile:hover { background: var(--ds-surface-alt); text-decoration: none; }

/* Знак в цвете приложения плитки: --ds-accent-fill здесь объявлен на самой плитке (раздел АКЦЕНТ
   ПРИЛОЖЕНИЯ), а иконка спрайта красится currentColor. */
.ds-switch-mark { display: flex; margin-bottom: var(--ds-space-1); color: var(--ds-accent-fill); }

.ds-switch-name { font-weight: 600; }
.ds-switch-role { color: var(--ds-muted); font-size: var(--ds-size-xs); }

/* Текущее приложение — заливкой И рамкой: одним цветом «вы здесь» и «сюда можно нажать» различают
   плохо, а при дальтонизме не различают вовсе. То же правило, что у активного раздела в .ds-nav. */
.ds-switch-tile[aria-current="page"] {
    background: var(--ds-accent-soft);
    border-color: var(--ds-accent);
}

.ds-switch-more { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }

.ds-switch-more li {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: var(--ds-space-3);
    min-height: 44px;
    padding: 6px 10px;
    border-radius: var(--ds-radius-sm);
}

.ds-switch-more li:hover { background: var(--ds-surface-alt); }
.ds-switch-more .ds-switch-name,
.ds-switch-more .ds-switch-role { grid-column: 1; }

.ds-switch-connect {
    grid-column: 2;
    grid-row: 1 / span 2;
    display: inline-flex;
    align-items: center;
    min-height: var(--ds-tap);
    padding: 0 10px;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-accent);
    font-size: var(--ds-size-sm);
    font-weight: 600;
    white-space: nowrap;
    text-decoration: none;
}

.ds-switch-connect:hover { background: var(--ds-accent-soft); text-decoration: none; }

.ds-switch-foot {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ds-space-4);
    margin-top: var(--ds-space-3);
    padding-top: var(--ds-space-3);
    border-top: 1px solid var(--ds-line);
}

.ds-switch-foot a {
    display: inline-flex;
    align-items: center;
    min-height: var(--ds-tap);
    color: var(--ds-muted);
    font-size: var(--ds-size-sm);
}

.ds-switch-foot a:hover { color: var(--ds-text); }

/*
    Появление панели. Класс ставит design.js и ставит его ТОЛЬКО когда движение разрешено: правило ниже
    гасит анимацию у тех, кто просил меньше движения, и это не дублирование — скрипт в этом случае не
    заводит класс вовсе, а правило страхует случай, когда настройка сменилась при открытой панели.
*/
@keyframes ds-switch-in {
    from { opacity: 0; transform: translateY(-4px); }
}

.ds-switch-motion > .ds-switch-panel { animation: ds-switch-in var(--ds-slow) var(--ds-ease); }

@media (prefers-reduced-motion: reduce) {
    .ds-switch-motion > .ds-switch-panel { animation: none; }
}

/* Пока панель открыта, шапка не может оставаться содержащим блоком для fixed: иначе шторка и затемнение
   значат «высота шапки», а не «окно». Тот же разбор — у бургера. */
.ds-top:has(.ds-switch.is-open) { backdrop-filter: none; }

@media (max-width: 720px) {
    .ds-switch-current { max-width: 34vw; }

    .ds-switch.is-open::before {
        content: "";
        position: fixed;
        inset: 0;
        z-index: 50;
        background: rgb(9 11 18 / 42%);
        pointer-events: none;
    }

    .ds-switch-panel {
        position: fixed;
        inset: auto 0 0 0;
        z-index: 60;
        width: auto;
        max-width: none;
        max-height: 80vh;
        overflow-y: auto;
        padding: var(--ds-space-4);
        padding-bottom: max(var(--ds-space-4), env(safe-area-inset-bottom));
        border-width: 1px 0 0;
        border-radius: var(--ds-radius-lg) var(--ds-radius-lg) 0 0;
    }

    .ds-switch-motion > .ds-switch-panel { animation-name: ds-switch-up; }
}

@keyframes ds-switch-up {
    from { transform: translateY(12px); opacity: 0; }
}

/* ===================================================================================================
   МЕНЮ УЧЁТНОЙ ЗАПИСИ

   Имя вошедшего становится кнопкой, когда хосту есть что положить рядом с ним (Center.Design#10, заказ
   Конструктора: «Устройства» — сессии учётной записи, не сайта). Под кнопкой одно меню: страница
   учётной записи, пункты хоста, последним — выход за линией. Без пунктов меню нет, и эти правила не
   работают ничему: имя печатается подписью .ds-who, как прежде.

   Вид повторяет меню схемы оформления, и это не лень: две панели в одной полосе, открывающиеся
   по-разному, читались бы как две системы. Отличие одно — у кнопки есть слово (имя), поэтому подпись
   не прячется клипом, а усекается многоточием правилом .ds-who, которое здесь просто наследуется:
   порядок уступок в тесноте (раздел выше) остаётся в силе и для кнопки.
   =================================================================================================== */

.ds-account { position: relative; flex: 0 1 auto; min-width: 0; }

.ds-account-trigger {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: 100%;
    height: var(--ds-tap);
    padding: 0 6px 0 8px;
    background: none;
    border: 1px solid transparent;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-muted);
    font: inherit;
    cursor: pointer;
}

.ds-account-trigger:hover { background: var(--ds-surface); color: var(--ds-text); }
.ds-account-trigger:hover .ds-who { color: var(--ds-text); }
.ds-account-trigger:active { transform: translateY(1px); }

.ds-account-trigger[aria-expanded="true"] {
    background: var(--ds-surface);
    border-color: var(--ds-line);
    color: var(--ds-text);
}

/* Шеврон — тот же, что у переключателя приложений: одна вещь, нарисованная одним знаком. */
.ds-account-caret { flex: none; transition: transform var(--ds-t-move); }
.ds-account-trigger[aria-expanded="true"] .ds-account-caret { transform: rotate(180deg); }

.ds-account-menu {
    position: absolute;
    inset-inline-end: 0;
    top: calc(100% + var(--ds-space-2));
    z-index: 30;
    display: grid;
    gap: 2px;
    min-width: 220px;
    padding: var(--ds-space-2);
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
    box-shadow: var(--ds-shadow-lg);
}

/* Закрытое меню закрыто атрибутом hidden — и объявленный display обязан его уважать (урок
   .ds-theme-menu, Center#843: класс весит столько же, сколько [hidden] UA-таблицы, и побеждал). */
.ds-account-menu[hidden] {
    display: none;
}

.ds-account-item {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    min-height: var(--ds-tap);
    padding: 0 10px;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text);
    font-size: var(--ds-size-sm);
    text-decoration: none;
    white-space: nowrap;
}

.ds-account-item:hover { background: var(--ds-surface-alt); text-decoration: none; }

/* Выход — последним и за линией: это единственный пункт, после которого страницы уже нет. Тон
   опасности ему не положен — выход обратим (правило «один акцент», брендбук, раздел 7). */
.ds-account-signout {
    margin-top: var(--ds-space-1);
    padding-top: var(--ds-space-1);
    border-top: 1px solid var(--ds-line);
    border-radius: 0 0 var(--ds-radius-sm) var(--ds-radius-sm);
}

/* На телефоне меню привязано к экрану, а не к кнопке, — тот же довод и то же парное правило, что у
   меню схемы оформления ниже. */
@media (max-width: 720px) {
    .ds-account-menu {
        position: fixed;
        inset-inline-end: var(--ds-space-4);
        inset-inline-start: auto;
        top: calc(var(--ds-header) + var(--ds-space-1));
        z-index: 60;
        min-width: 200px;
        max-width: calc(100vw - 2 * var(--ds-space-4));
    }

    .ds-top:has(.ds-account.is-open) { backdrop-filter: none; }
}

/* ===================================================================================================
   ВЫБОР СХЕМЫ ОФОРМЛЕНИЯ

   Кнопка-значок в шапке и меню из трёх согласий: «Как в системе», «Светлая», «Тёмная» (решение PO
   2026-09-18). Механизм — theme-boot.js; правила ниже отвечают только за вид.

   ПОЧЕМУ ЗНАЧОК КНОПКИ ВЫБИРАЕТ CSS, А НЕ СКРИПТ. Знак обязан показывать то же, что показывает сама
   страница, и признак у обоих один — data-theme на корне. Нарисованы все три, показан один; скрипту не
   приходится знать ни адреса спрайта, ни имён символов, и знак не может разойтись с цветом страницы
   даже на те кадры, пока скрипт грузится. Селекторы взяты той же парой, что у акцента приложения:
   отсутствие атрибута (система) — через :root:not([data-theme]), явный выбор — по значению.

   ОТТЕНОК ПРИЛОЖЕНИЯ ЗДЕСЬ НЕ УЧАСТВУЕТ: переключатель про схему, а не про приложение, и красится
   нейтралями. Отметка выбранного пункта — --ds-accent-soft, то есть тот цвет, который у этой страницы
   уже объявлен, а не свой.
   =================================================================================================== */

.ds-theme { position: relative; flex: none; }

.ds-theme-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ds-tap);
    height: var(--ds-tap);
    padding: 0;
    background: none;
    border: 1px solid transparent;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-muted);
    font: inherit;
    cursor: pointer;
}

.ds-theme-trigger:hover { background: var(--ds-surface); color: var(--ds-text); }
.ds-theme-trigger:active { transform: translateY(1px); }

.ds-theme-trigger[aria-expanded="true"] {
    background: var(--ds-surface);
    border-color: var(--ds-line);
    color: var(--ds-text);
}

/* Показывается ровно один знак из трёх — по состоянию корня. Снятый data-theme значит «как в системе»,
   и именно поэтому первый селектор написан через :not(), а не как data-theme="system": такого значения
   на корне не бывает вовсе (docs/brand/adoption.md, раздел data-theme). */
.ds-theme-mark { display: none; }

:root:not([data-theme]) .ds-theme-mark[data-ds-theme-mark="system"],
:root[data-theme="light"] .ds-theme-mark[data-ds-theme-mark="light"],
:root[data-theme="dark"] .ds-theme-mark[data-ds-theme-mark="dark"] { display: inline-flex; }

/*
    Подпись кнопки убрана с экрана, но не из документа: у кнопки-значка нет слова рядом, из которого
    программа чтения взяла бы имя. Приём — «клип в один пиксель», а не display: none и не visibility:
    оба убирают узел и из чтения тоже, и кнопка снова становится безымянной. Ширина в 1px, а не 0:
    нулевую коробку часть читалок пропускает.
*/
.ds-theme-name {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.ds-theme-menu {
    position: absolute;
    inset-inline-end: 0;
    top: calc(100% + var(--ds-space-2));
    z-index: 30;
    display: grid;
    gap: 2px;
    min-width: 220px;
    padding: var(--ds-space-2);
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
    box-shadow: var(--ds-shadow-lg);
}

/* Закрытое меню закрыто атрибутом hidden — и это правило обязано его уважать. UA-таблица прячет
   [hidden] правилом без !important и с весом одного атрибута; наше «.ds-theme-menu { display: grid }»
   весит столько же, стоит позже и ПОБЕЖДАЛО: закрытое меню оставалось видимым, а на узком экране —
   ещё и фиксированным поверх страницы, перехватывая клики под шапкой (Center#843, первый живой
   адоптер вне Center — Planville#116: 23 из 24 падений браузерного гейта). Селектор с атрибутом
   весит больше класса при любом порядке загрузки. .ds-switch-panel этой болезни не имеет: display
   там не объявлен. */
.ds-theme-menu[hidden] {
    display: none;
}

/* Меню стоит у правого края полосы, и открывается оно ВЛЕВО от кнопки: inset-inline-end держит его в
   окне на любой ширине, тогда как выравнивание по началу уводило бы панель за край экрана — молча, и
   только на узком. */

.ds-theme-option {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--ds-space-2);
    min-height: var(--ds-tap);
    padding: 0 10px;
    background: none;
    border: 0;
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text);
    font: inherit;
    font-size: var(--ds-size-sm);
    text-align: start;
    white-space: nowrap;
    cursor: pointer;
}

.ds-theme-option:hover { background: var(--ds-surface-alt); }

.ds-theme-option-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/*
    Выбранный пункт — подложкой И галочкой: одной краской «выбрано» и «под курсором» различают плохо, а
    при дальтонизме не различают вовсе. Тот же довод, что у текущего приложения в панели переключателя.
    Галочка не рисуется у невыбранных, а не красится прозрачным: прозрачный знак остаётся в потоке и
    оставляет столбец, из-за чего строки выглядят разъехавшимися.
*/
.ds-theme-check { display: none; color: var(--ds-accent); }

.ds-theme-option[aria-checked="true"] { background: var(--ds-accent-soft); font-weight: 600; }
.ds-theme-option[aria-checked="true"] .ds-theme-check { display: inline-flex; }

/*
    НА ТЕЛЕФОНЕ МЕНЮ ПРИВЯЗАНО К ЭКРАНУ, А НЕ К КНОПКЕ, и это не украшение раскладки.

    Кнопка схемы стоит в середине полосы действий, а не у её края: справа от неё имя учётной записи,
    выход и бургер. Меню, выровненное по правому краю КНОПКИ, растёт от неё влево — и на 375 уезжает за
    левый край экрана. Замер витрины до этой строки: left −31 при ширине окна 375, то есть первый
    столбец пунктов (знаки состояний) обрезан начисто, и обрезан молча — страница при этом не
    прокручивается вбок, потому что overflow за начальным краем окна не даёт полосы прокрутки.

    Поэтому на узком меню фиксируется у правого края ОКНА. Шапка на телефоне липкая и стоит вверху
    всегда, так что вертикаль берётся её ростом. Парное правило ниже снимает у шапки размытие: элемент
    с backdrop-filter становится содержащим блоком для position: fixed, и без него «фиксированный»
    отсчитывался бы от коробки шапки — тот же приём и та же причина, что у панели переключателя
    приложений.
*/
@media (max-width: 720px) {
    .ds-theme-menu {
        position: fixed;
        inset-inline-end: var(--ds-space-4);
        inset-inline-start: auto;
        top: calc(var(--ds-header) + var(--ds-space-1));
        z-index: 60;
        min-width: 200px;
        max-width: calc(100vw - 2 * var(--ds-space-4));
    }

    .ds-top:has(.ds-theme.is-open) { backdrop-filter: none; }
}

/* ===================================================================================================
   КНОПКИ

   Одна шкала: 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:not(:disabled):not([aria-disabled="true"]):active,
.ds-burger summary:active { transform: translateY(1px); }

/*
    НЕДОСТУПНАЯ КНОПКА — приглушением, а не краской.

    Находка приёмки витрины 2026-09-18: правила на это состояние в файле НЕ БЫЛО вовсе, и «Недоступная»
    в разделе 4 рисовалась ровно как «Основная» — тем же заливным акцентом, с той же тенью, светлеющая
    на наведении. Отказ тихий в самом дорогом месте: человек видит кнопку, которую можно нажать, жмёт
    её и не получает ничего. Браузер на `disabled` не рисует НИЧЕГО сам ровно потому, что заливку и
    цвет назначили мы.

    ПОЧЕМУ opacity, А НЕ СВОЙ СЕРЫЙ. Модификаторов у кнопки четыре (заливная, прозрачная, опасная,
    крупная/мелкая), и приглушение краской означало бы четыре новых значения — то есть четыре новых
    цвета в палитре, каждый из которых пришлось бы считать на контраст в обеих темах. Прозрачность
    гасит ЛЮБУЮ заливку одинаково и одинаково же работает в тёмной теме, где под кнопкой другой фон.
    Новых hex этот блок не вводит вовсе — это его свойство, а не совпадение, и держит его клетка
    Center.Design.Tests.

    0.55, а не 0.4: недоступное обязано оставаться ЧИТАЕМЫМ — «нельзя сейчас» отличается от
    «сломалось» тем, что подпись видно. Тот же довод стоит у выключенного поля ввода в разделе СЛОВАРЬ.

    ДВА СЕЛЕКТОРА, ПОТОМУ ЧТО НЕДОСТУПНОСТЬ ОБЪЯВЛЯЕТСЯ ДВУМЯ СПОСОБАМИ. `:disabled` есть у <button>,
    но у <a class="ds-btn"> его нет вовсе: ссылку выключают `aria-disabled="true"`, и без второго
    селектора ссылка-кнопка осталась бы неотличимой от живой. Снятие самого перехода (нажатие, hover)
    сделано отрицанием в селекторах выше и ниже, а не перекрытием здесь: перекрывать пришлось бы
    border-color и color у каждого модификатора отдельно.

    Чего это правило НЕ делает: не мешает нажать. `disabled` у кнопки отменяет нажатие сам, а у ссылки
    с `aria-disabled` это остаётся на разметке хоста — краска не заменяет отсутствие href.
*/
.ds-btn:disabled,
.ds-btn[aria-disabled="true"] {
    opacity: .55;
    box-shadow: none;
    cursor: not-allowed;
}

/*
    ЗАНЯТАЯ КНОПКА — СОСТОЯНИЕ, А НЕ ТЕГ (Center.Design#7, заказ Конструктора SB#186: у BootstrapBlazor
    это IsAsync — спиннер на время запроса и запрет повторного нажатия; переводя кнопки на ds-btn,
    подменой тега это не выразить).

    Объявляется атрибутом aria-busy="true", и это не выбор синтаксиса: атрибут — то, что читалка и так
    произносит («занято»), а класс пришлось бы дублировать атрибутом ради неё. Хост ставит один атрибут
    и снимает один атрибут; никакого .ds-btn-loading нет и не будет.

    ТЕКСТ ОСТАЁТСЯ В РАЗМЕТКЕ и остаётся в потоке: краска прозрачная, а не display: none и не
    visibility. Первое держит ширину — кнопка не прыгает, когда слово сменяется кругом и обратно;
    второе держит имя — aria-busy без объявленного содержимого читается пустым. Значок внутри кнопки
    красится currentColor, поэтому гаснет вместе со словом, и никакого правила на него не нужно.

    Круг — псевдоэлемент, а не вложенный узел: разметка кнопки принадлежит хосту (у BootstrapBlazor —
    библиотеке), и требовать от него дочерний <span> значило бы требовать правки в каждом месте.
    Цвет круга — по модификатору: на заливной кнопке контраст акцента, на опасной — её краска, у
    остальных — текст. Новых цветов нет.

    Повторное нажатие отрезано pointer-events, а не disabled: disabled сбрасывает фокус с кнопки, и
    после ответа сервера человек с клавиатуры оказывается в начале страницы. Клавиатурный Enter
    остаётся на разметке хоста — он и так ждёт ответа, а не жмёт второй раз.

    ДВИЖЕНИЕ КРУГА — ЧЕСТНЫЙ ИНДИКАТОР (--ds-t-spin, раздел ДВИЖЕНИЕ), и оно переживает просьбу
    «поменьше движения»: застывший круг — это картинка «сломалось», а не «идёт». Исключение названо
    поимённо в блоке prefers-reduced-motion вместе с .ds-motion-essential.
*/
.ds-btn[aria-busy="true"] {
    position: relative;
    color: transparent;
    pointer-events: none;
    cursor: progress;
}

.ds-btn[aria-busy="true"]::after {
    content: "";
    position: absolute;
    inset: 0;
    width: 18px;
    height: 18px;
    margin: auto;
    border: 2px solid var(--ds-text);
    border-inline-end-color: transparent;
    border-radius: 50%;
    animation: ds-spin var(--ds-t-spin) linear infinite;
}

.ds-btn-primary[aria-busy="true"]::after { border-color: var(--ds-accent-contrast); border-inline-end-color: transparent; }
.ds-btn-danger[aria-busy="true"]::after { border-color: var(--ds-danger); border-inline-end-color: transparent; }
.ds-btn-sm[aria-busy="true"]::after { width: 14px; height: 14px; }

/* Тень заливной кнопке — в цвете её самой: серая тень под цветной кнопкой читается как грязь под ней, а не
   как высота. Цвет берётся из заливки, поэтому в тёмной теме, где заливка своя, тень едет за ней сама. */
.ds-btn-primary {
    background: var(--ds-accent-fill);
    color: var(--ds-accent-contrast);
    box-shadow: var(--ds-shadow-accent);
}

/* Наведение — только у доступной: осветлённая выключенная кнопка снова выглядит живой. */
.ds-btn-primary:not(:disabled):not([aria-disabled="true"]):hover { filter: brightness(1.1); }

.ds-btn-ghost { background: var(--ds-surface); border-color: var(--ds-line); color: var(--ds-text); }
.ds-btn-ghost:not(:disabled):not([aria-disabled="true"]):hover { border-color: var(--ds-accent); color: var(--ds-accent); }

/*
    Опасное действие и мелкая кнопка — два модификатора, приехавшие из Center.Identity 2026-09-15.

    Они не придуманы про запас: там уже были .btn-danger (11 мест) и .btn-small (49), написанные своими
    правилами поверх копии .ds-btn. Перевести разметку на общий класс, не имея этих двух, значило бы
    оставить домену повод держать копию — а повод и есть механизм, которым копии заводятся.

    Рост мелкая кнопка НЕ снимает. Уменьшить его — снова опустить цель нажатия ниже пальца, ровно тот
    дефект, ради которого весь перевод и затеян; мельче она отступом и кеглем, а не высотой.
*/
.ds-btn-sm { padding: 0 12px; font-size: 13px; }
.ds-btn-danger { background: var(--ds-danger-soft); border-color: var(--ds-danger); color: var(--ds-danger); }
.ds-btn-danger:not(:disabled):not([aria-disabled="true"]):hover { filter: brightness(0.97); }

/* ===================================================================================================
   ТЕЛЕФОН

   Якорная строка уходит — в шапке она отбирала место у единственной кнопки, ради которой шапка и нужна, —
   но разделы не пропадают: их забирает бургер.
   =================================================================================================== */

@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; }

    /*
        Имя учётной записи на телефоне НЕ ПРЯЧЕТСЯ. Здесь стояло `display: none`, и оно пережило появление
        своего первого потребителя: правило писалось, когда `.ds-who` никто не печатал, а с переходом шапки
        на `UserName` (#709) оно стало отвечать за жалобу PO «не вижу своего логина» — и отвечать на неё
        «нет». На телефоне от блока учётной записи оставалась одна кнопка «Выйти». Найдено заказом
        KisaPs/Center#738 от SiteBuilder, тот же дефект был у Center.AI.

        Вместо скрытия — предел ширины: класс усекает многоточием, значит имя отдаёт место кнопке само.

        «Не исчезая» это обещание держало только на словах: усечение рисуется в коробке, а коробке ничто
        не мешало стать нулевой. Замер витрины на 375 до правки 2026-09-17: .ds-who — 0.00 при собственных
        110.37, то есть имени не было вовсе. Обещание теперь держит пол min-width: 6ch в разделе ШАПКА, а
        строка ниже осталась тем, чем и была, — потолком.
    */
    .ds-who { max-width: 12ch; }

    /*
        Предел ширины — потолок, а не бронь. Блок действий в прежней редакции стоял `flex: none`, и имя с
        max-width держало бы свои проценты даже тогда, когда строке не хватает места: на 320 с длинным
        именем шапка выезжала за экран вместо усечения. Найдено ревью на #741 — подстроки в клетке
        переполнения не видят, его видит только геометрия.

        Теперь блок сжимается, кнопки в нём — нет: усечение съедает ровно столько, сколько не хватает,
        и первым уступает имя, как и задумано классом. Ниже пола в 6ch имя при этом не уходит — пол
        объявлен выше, в порядке уступок, и на телефоне действует так же, как на десктопе.
    */
    .ds-actions { flex: 0 1 auto; }
    .ds-actions .ds-btn { flex: 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; }

    /*
        На 360 рядом с именем стоят ещё переключатель, «Выйти» и бургер, и место здесь делится счётом, а
        не на глаз. Замер витрины на 375: под вывеску и имя вместе остаётся 80 пикселей — всё прочее
        занято целями нажатия, которые не сжимаются по правилу «действие тесноту не принимает». Потолок
        имени опущен до пола (6ch, «Мари…»), а кнопке выхода на этой ширине урезано боковое поле: 18
        пикселей с каждой стороны — это шкала десктопа, а не палец, и рост цели от них не зависит.
        Вместе это возвращает вывеске около 45 — то есть «Olista» целиком у того, кто зовётся коротко.
    */
    .ds-who { max-width: 6ch; }
    .ds-actions .ds-btn { padding: 0 10px; }

    /*
        ТЕСНОТА 360–400 С ПЕРЕКЛЮЧАТЕЛЕМ СХЕМЫ (Center#845, замер хаба Аналитики на 375, clientWidth 360).
        До 0.31.24 строка «знак + вывеска, переключатель приложений, имя, выход, бургер» сходилась на 360
        впритык — вывеска уже стояла в нуле (названный предел выше). Кнопка схемы прибавила 32 + зазор, и
        бургер — единственный вход в разделы — уехал за край: .ds-burger 366–410 при окне 360, документ
        410. Уступать было некому: переключатель приложений (flex: none, 133 под «Приложения»), кнопки и
        бургер тесноту не принимают по правилу, имя уже на полу 6ch.

        Что уступает и в каком порядке — решение дизайна, не подгонка:
        1. Имя текущего приложения в переключателе — до 6ch («Прило…»), как имя учётной записи: обе подписи
           одной природы, и потолок у них один. Кнопка остаётся целью нажатия полной высоты.
        2. Слово вывески рядом со знаком уходит: хост, передавший знак, на этой ширине узнаётся по знаку —
           это же сказано у .ds-brand для 320, теперь правило, а не предел. Хост без знака слово оставляет.
        3. Зазоры строки 12 → 8 и боковое поле 16 → 12: на 360 это 20 пикселей, которых как раз не хватало.
        Замер после правки (хаб Аналитики локально, 375/360, анонимный, ещё с двумя кнопками): переключатель
        20–118, схема 126–158, кнопки 166–233 и 241–307, бургер 315–359 — строка по ширине, документ 360,
        elementFromPoint над бургером возвращает бургер.
    */
    .ds-top { gap: var(--ds-space-2); padding-inline: var(--ds-space-3); }
    /* DS-02 (KisaPs/Center#1240): вместо обрывка «Прило…» — знак приложения; имя остаётся для читалки. */
    .ds-icon.ds-switch-trigger-mark { display: inline-block; }
    .ds-switch-current { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
    .ds-brand:has(> .ds-brand-mark) > span { display: none; }

    /*
        ВОШЕДШИЙ НА 360 СО ЗНАКОМ (замер Планвиля 2026-09-20, /projects под tester): знак 28 + переключатель 98
        + схема 32 + имя 50 (пол 6ch) + «Выйти» 68 + бургер 44 + зазоры и поля 56 = 376 — на 16 больше, чем
        есть, и знак, единственный сжимаемый, ужимался до 4px. Уступает «Выйти»: на этой ширине это значок
        log-out целью в палец (--ds-tap), подпись прячется, имя ссылки остаётся читалке через aria-label.
        Знак — не сжимается никогда: без слова он один называет семью.
    */
    .ds-actions .ds-signout { min-width: var(--ds-tap); padding: 0; justify-content: center; }
    .ds-actions .ds-signout-text { display: none; }

    /*
        ЗНАК БЕЗ СЛОВА — ЦЕЛЬ НАЖАТИЯ, А НЕ ТОЛЬКО КАРТИНКА (Center.Design#23, второе воспроизведение —
        браузерный смоук KisaPs/Center.Identity#8, живой вход/регистрация, 375, обе темы: `a.ds-brand
        28×44`). Правило выше даёт знаку `flex: none` — коробка ссылки перестаёт сжиматься, но перестаёт
        и расти: без слова-ребёнка в потоке (span скрыт `display:none`, а не сжат) ширина ссылки равна
        ширине ОДНОГО знака, 28px, — а .ds-brand-mark сам не обещает быть целью нажатия, он декоративная
        картинка 28×28 (документ на этот счёт молчал, потому что до слияния меню учётной записи здесь
        всегда был текст, и ссылка была бы шире 44 без единой правки).

        Тот же приём, что двумя строками выше для «Выйти»: минимальная ширина = --ds-tap, знак остаётся
        28×28 внутри более широкой ссылки, а не растягивается сам. `justify-content: center` — знак стоит
        по центру новой коробки, а не прижимается к левому краю (align-items уже центрирует по высоте,
        justify-content — по ширине, у inline-flex это разные оси). Высота у .ds-brand уже была
        `min-height: var(--ds-tap)` (правило базовое, не в этом media) — падал только один из двух
        размеров, отчёт называл ровно «28×44».
    */
    .ds-brand:has(> .ds-brand-mark) { flex: none; min-width: var(--ds-tap); justify-content: center; }
}

/* ===================================================================================================
   ДИАЛОГ

   Один способ на все приложения, и его даёт браузер. Родной <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 пикселей вверх и проявление. Не пять на выбор — пять означают, что
   на одной странице встретятся три.

   Содержимое видно по умолчанию и не прячется НИКОГДА (Center.Design#51): приём — анимация при входе в
   экран, а не спрятанное состояние, которое скрипт потом снимает. Спрятанное состояние превращало
   «красивое появление» в пустые полосы у превью, печати и того, у кого скрипт не доехал.
   =================================================================================================== */

/*
    НАЧАЛЬНОЕ СОСТОЯНИЕ — ВИДИМОЕ ВСЕГДА (заказ Core KisaPs/Center.Design#51, находка CO-19 аудита хаба).
    Прежде класс готовности прятал все [data-ds-reveal] до прокрутки: полностраничные превью, печать и
    снимки ботов показывали пустые полосы там, где стояли «Один вход» и «Как это работает». Теперь
    спрятанного состояния нет вовсе: приём — анимация, которую скрипт проигрывает элементу, когда тот
    ВХОДИТ в экран снизу. Что было в экране при загрузке — не анимируется (мигание при первой отрисовке),
    что ниже — видно в превью и печати и проявляется, когда человек до него доскроллил.
*/
@keyframes ds-reveal {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: none; }
}

.ds-reveal-ready [data-ds-reveal].ds-revealing {
    animation: ds-reveal var(--ds-t-enter) var(--ds-ease-out) both;
}

/* Лестница соседей: значение атрибута — номер ступени. Четыре ступени и всё, дальше без задержки: пятая
   карточка ряда ждала бы треть секунды после того, как человек её уже увидел. */
.ds-reveal-ready [data-ds-reveal="2"].ds-revealing { animation-delay: var(--ds-t-step); }
.ds-reveal-ready [data-ds-reveal="3"].ds-revealing { animation-delay: calc(2 * var(--ds-t-step)); }
.ds-reveal-ready [data-ds-reveal="4"].ds-revealing { animation-delay: calc(3 * var(--ds-t-step)); }

@media print {
    .ds-reveal-ready [data-ds-reveal].ds-revealing { animation: none; }
}

/* ===================================================================================================
   ПЛАШКА СБОРКИ

   Разметку печатает общий компонент (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; }
}

/* ===================================================================================================
   СОДЕРЖИМОЕ ЭКРАНОВ

   Граница этого файла пересмотрена 2026-09-15, решение PO — DECISIONS 0.6. Прежняя редакция заголовка
   обещала: содержимого экранов здесь нет, карточки остаются в листах приложений, «но берут отсюда всё,
   что у них должно совпадать». По ЗНАЧЕНИЯМ обещание сдержано — замер восьми репозиториев 2026-09-15:
   Center.Crm объявляет пять токенов и все пять ведут в --ds-*, Center.AI — тридцать два из тридцати двух.
   По СОДЕРЖИМОМУ не сдержано: 1696 уникальных имён классов на семью и НИ ОДНОГО, которое встречалось бы
   в трёх репозиториях. Карточка написана независимо в пяти, пустое состояние в пяти, плашка в четырёх.

   Цена видна на плашке. Один рецепт — пилюля со смягчённой заливкой — записан тремя способами:
   .admin-badge (Center.Identity) берёт радиус токеном, .ma-preview-badge (Center.Analytics) и
   .sb-canvas-badge (Center.SiteBuilder) пишут 999px литералом. Пока имя у каждого своё, расхождение
   не с чем сравнивать: сверять нечего, и оно растёт молча — ровно как росло множество фонов до раздела
   ЦВЕТ выше.

   Здесь шесть имён, и ни одним больше. Каждое взято из замера — столько-то доменов уже написали это
   по-своему, — а не из вкуса. Седьмое заводится тем же порядком: замером, а не догадкой. Причина
   строгости арифметическая: каждое изменение этого файла стоит публикации на приватный фид и подъёма
   версии в семи репозиториях.

   Шестое — ПОЛЕ ВВОДА (.ds-field), заведено 2026-09-17 тем же порядком и по следу витрины, которая
   печатала его своими правилами и сама же писала рядом, что в словаре его нет. Разбор — у правила.

   ЧАСТИ имени (.ds-empty-title, .ds-label, .ds-input, .ds-hint, .ds-error) — не отдельные записи
   словаря, а детали своего имени; считать их новыми именами не нужно.

   Чего здесь по-прежнему нет и не будет: стилей опубликованных сайтов клиентов (blocks*.css) — у них
   своя палитра автора, см. заголовок файла; и раскладки страниц. Словарь даёт кирпич, а не дом.

   ТОН — общий механизм, а не свойство каждого имени. .ds-tone-* задаёт пару «краска + подложка», и её
   читают и плашка, и уведомление. Иначе у плашки завёлся бы свой набор модификаторов, у уведомления
   свой, и через два домена они разошлись бы — та же болезнь этажом ниже. Пара объявляется целиком:
   краска без подложки даёт нечитаемую заливку, и это тот же закон, что у раздела ЦВЕТ.

   Умолчание тона живёт в var()-запаснике каждого имени, а не отдельным правилом: у .ds-badge и
   .ds-tone-success одинаковая специфичность, и правило-умолчание победило бы или проиграло в
   зависимости от порядка строк в файле. Запасник от порядка не зависит.
   =================================================================================================== */

/* ds-vocabulary:begin — границы словаря названы явно: сторож режет файл по ним, а не по заголовку
   раздела. Упоминание имени раздела встречается и в шапке файла, и срез по нему брал бы прозу вместо
   правил — зелёный результат на пустом множестве. */

.ds-tone-accent  { --ds-tone: var(--ds-accent);  --ds-tone-soft: var(--ds-accent-soft); }
.ds-tone-success { --ds-tone: var(--ds-success); --ds-tone-soft: var(--ds-success-soft); }
.ds-tone-warning { --ds-tone: var(--ds-warning); --ds-tone-soft: var(--ds-warning-soft); }
.ds-tone-danger  { --ds-tone: var(--ds-danger);  --ds-tone-soft: var(--ds-danger-soft); }
.ds-tone-info    { --ds-tone: var(--ds-info);    --ds-tone-soft: var(--ds-info-soft); }

/* Карточка — единица содержимого на подложке страницы: своя поверхность, своя граница, своя тень.
   Крупный радиус, потому что карточка крупная: шкала радиусов в разделе ЦВЕТ объявляет --ds-radius-lg
   именно для «крупных карточек и полос». */
.ds-card {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-2);
    padding: var(--ds-space-4);
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-lg);
    box-shadow: var(--ds-shadow-sm);
}

/* Панель — та же коробка, но ВНУТРИ содержимого, а не вместо него: подложка альтернативная, тени нет.
   Тень внутри карточки читается как вторая карточка и ломает порядок слоёв. */
.ds-panel {
    padding: var(--ds-space-3);
    background: var(--ds-surface-alt);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-md);
}

/* Плашка — короткое состояние рядом с именем: «черновик», «опубликовано», «12 ошибок».
   Не кнопка и не ссылка: нажимать не на что, поэтому ни высоты пальца, ни фокуса ей не нужно. */
.ds-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-1);
    padding: 2px 10px;
    border-radius: var(--ds-radius-pill);
    background: var(--ds-tone-soft, var(--ds-surface-alt));
    color: var(--ds-tone, var(--ds-muted));
    font-size: var(--ds-size-xs);
    font-weight: 600;
    line-height: var(--ds-leading-tight);
    white-space: nowrap;
}

/* Плашки идут подряд — в ячейке таблицы, в строке списка, — и без зазора слипаются в одно пятно.
   Зазор даётся СМЕЖНОСТЬЮ, а не внешним отступом самой плашки: отступ у одиночной плашки сдвигал бы
   текст рядом с ней, и каждый домен заводил бы себе поправку на этот сдвиг. */
.ds-badge + .ds-badge { margin-inline-start: var(--ds-space-1); }

/* Уведомление — сообщение о состоянии экрана целиком: «черновик не сохранён», «фид недоступен».
   Полоса тона по начальному краю, а не иконка: иконка требует шрифта или картинки, а тон различим и
   без цвета — по положению полосы, — тогда как одной краской дальтонизм не различает. */
.ds-notice {
    display: flex;
    gap: var(--ds-space-2);
    padding: var(--ds-space-3);
    background: var(--ds-tone-soft, var(--ds-surface-alt));
    border: 1px solid var(--ds-tone, var(--ds-line));
    border-inline-start-width: 3px;
    border-radius: var(--ds-radius-md);
    color: var(--ds-text);
}

/* Пустое состояние — экран, на котором ещё ничего нет. Пишется заново в каждом домене чаще прочего,
   и каждый раз чуть иначе: то по левому краю, то по центру, то приглушённым всё, то ничего. */
.ds-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--ds-space-2);
    padding: var(--ds-space-6) var(--ds-space-4);
    color: var(--ds-muted);
    text-align: center;
}

.ds-empty-title {
    margin: 0;
    color: var(--ds-text);
    font-size: var(--ds-size-lg);
    font-weight: 600;
}

/*
    ПОЛЕ ВВОДА — шестое имя словаря, и заведено оно тем же порядком, что первые пять: замером, а не
    вкусом. Витрина до этой правки печатала поле СВОИМИ правилами (.sc-field, .sc-input) и рядом
    честную оговорку: «поля ввода в словаре нет, имён в нём пять». Оговорка и была находкой: страница,
    на которую приходят сверяться, показывала рецепт, которого система не обещает, — и показывала его
    убедительно, потому что написан он теми же токенами.

    Почему поле, а не «ещё одно полезное имя». Форма — второй по частоте экран платформы после списка,
    и у каждого домена она уже написана: замер 2026-09-17 по листам семьи — .form-control и .admin-input
    у сервера учётных записей, своя рамка у конструктора, своя у CRM. Это ровно та же картина, что была
    у плашки, где 999px разошлись с --ds-radius-pill: рецепт один, имён три, сверять нечего.

    ЧТО ВХОДИТ В ИМЯ. Обёртка (.ds-field), подпись (.ds-label), сам контрол (.ds-input), подсказка
    (.ds-hint) и ошибка (.ds-error). Пять частей — одно понятие, как .ds-empty-title при .ds-empty:
    считать их пятью новыми записями словаря не нужно, поле без подписи и без места под ошибку — это
    не поле, а прямоугольник.

    СОСТОЯНИЯ БЕРУТСЯ У СИСТЕМЫ, А НЕ ПИШУТСЯ ЗАНОВО. Кольцо фокуса — общее :focus-visible раздела
    ОСНОВА, здесь оно только не заменяется на рамку своего цвета. Отказ объявляется АТРИБУТОМ
    aria-invalid, а не классом-модификатором: класс говорит только глазу, атрибут — и глазу, и читалке,
    и им же пользуется всякая проверка формы. Недоступность — :disabled, тоже состояние элемента, а не
    класс: класс на выключенном поле расходится с самим выключением на первой же динамической форме.
*/
.ds-field {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-1);
}

.ds-label { color: var(--ds-text); font-size: var(--ds-size-sm); font-weight: 600; }

/*
    Рост поля — та же шкала, что у кнопки (--ds-control), и по той же причине не мельче пальца: поле
    стоит в одной строке с кнопкой, и два разных роста рядом видно сразу. У textarea высоту задаёт
    число строк, поэтому ей min-height не навязывается — только внутреннее поле сверху и снизу.
*/
.ds-input {
    min-height: max(var(--ds-control), var(--ds-tap));
    padding: 0 var(--ds-space-3);
    background: var(--ds-surface);
    border: 1px solid var(--ds-line);
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text);
    font: inherit;
}

textarea.ds-input {
    min-height: 0;
    padding: var(--ds-space-2) var(--ds-space-3);
    line-height: var(--ds-leading);
    resize: vertical;
}

/* Подсказка-заполнитель — приглушённая, но не до нечитаемости: --ds-muted проверен на контраст в
   разделе ЦВЕТ, а собственный оттенок серого здесь завёл бы шестой фон. */
.ds-input::placeholder { color: var(--ds-muted); opacity: 1; }

/* Кольцо не заменяется рамкой: рамка внутри границы поля даёт прыжок на пиксель, а общее кольцо
   отрисовано снаружи и одинаково у поля, кнопки и ссылки. */
.ds-input:focus-visible { outline: var(--ds-focus-ring); outline-offset: 2px; }

.ds-input[aria-invalid="true"] { border-color: var(--ds-danger); background: var(--ds-danger-soft); }

/* Выключенное поле остаётся читаемым: серый текст на сером фоне — это «сломалось», а не «нельзя». */
.ds-input:disabled {
    background: var(--ds-surface-alt);
    color: var(--ds-muted);
    cursor: not-allowed;
}

.ds-hint { color: var(--ds-muted); font-size: var(--ds-size-xs); }

/* Ошибка — краской отказа и словами. Знак перед текстом ставит разметка (иконка словаря), потому что
   одной краской отказ от подсказки не отличает дальтонизм. */
.ds-error {
    display: flex;
    align-items: center;
    gap: var(--ds-space-1);
    color: var(--ds-danger);
    font-size: var(--ds-size-xs);
}

/* ds-vocabulary:end */


/* ===================================================================================================
   ИКОНКИ

   Один спрайт на семью — wwwroot/icons/ds-icons.svg, — и одно правило на его вставку:

       <svg class="ds-icon"><use href="/_content/Center.Design/icons/ds-icons.svg#ds-icon-search"/></svg>

   ПОЧЕМУ ССЫЛКА НА СИМВОЛ, А НЕ КАРТИНКА ФОНОМ. Иконка обязана краситься тем же цветом, что и буква
   рядом: текстом --ds-text в строке, акцентом приложения в ссылке, --ds-danger в предупреждении, белым
   на залитой кнопке — и менять его вместе с темой. currentColor это делает сам, background-image не
   умеет вовсе, а <img> красит ровно так, как нарисовали в файле. Отсюда и stroke="currentColor" на
   каждом символе спрайта.

   ПОЧЕМУ РАЗМЕР В em, А НЕ В ПИКСЕЛЯХ. Иконка внутри строки — такая же буква: не вырастет вместе с
   текстом кнопки — разъедется с ним на первом же шаге типографики. Пиксельные ступени заведены для
   мест, где текста рядом нет: кнопка-значок (--sm) и шапка (--lg). Их ровно две, и третьей быть не
   должно — с радиусами и тенями этот разговор в файле уже был.

   vertical-align: -.125em — та самая доля, на которую буква сидит ниже своей строки. Без неё иконка
   встаёт на базовую линию и выглядит подпрыгнувшей; flex-shrink: 0 держит её от сплющивания внутри
   flex-строки, куда её чаще всего и ставят.
   =================================================================================================== */

.ds-icon {
    width: 1em;
    height: 1em;
    display: inline-block;
    vertical-align: -.125em;
    flex-shrink: 0;
}

.ds-icon--sm { width: 16px; height: 16px; }
.ds-icon--lg { width: 24px; height: 24px; }


/*
    Просьба системы «поменьше движения» — не пожелание оформления. Её включают при вестибулярных
    расстройствах, и для человека, который её включил, поехавший или мигающий элемент — это тошнота, а не
    неудачная анимация.

    Правило блокирующее и с !important: перечислять здесь всё, что движется, значит однажды забыть новое.
    Кроме переходов этого файла оно накрывает и то, что рисует не он, — анимации диалога переподключения,
    которые приезжают шаблоном Blazor вовсе без защиты. Длительность не 0, а исчезающе малая: событие
    окончания анимации при нуле в части браузеров не приходит, и код, который его ждёт, замирает навсегда.

    ИСКЛЮЧЕНИЙ два, и оба названы: класс .ds-motion-essential и круг занятой кнопки
    (.ds-btn[aria-busy="true"]::after, раздел КНОПКИ) — тот самый честный индикатор работы.

    Есть движение, которое не украшает, а сообщает: крутящийся индикатор говорит «идёт работа, не жмите
    второй раз». Погашенный до нуля, он превращается в застывшую загогулину — то есть в картинку «сломалось».
    Для такого места правильная замена — не «то же самое, но медленнее», а другая форма: ступенчатая полоса,
    смена подписи, слово «сохраняем». Класс выдаётся вручную и ровно тому элементу, который движется:
    свойства перехода не наследуются, и повесить его на родителя значит не выдать вовсе.

    Мест таких две-три на всю платформу. Если их станет десять — исключением злоупотребили.
*/
@media (prefers-reduced-motion: reduce) {
    *:not(.ds-motion-essential),
    *:not(.ds-motion-essential)::before,
    *:not(.ds-motion-essential):not([aria-busy="true"])::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].ds-revealing { animation: none; }
}

/*
    Подвал платформы (PlatformFooter; аудит KisaPs/Center#1240, DS-07) — один на все хосты. Строка
    владельца слева, ссылки справа; на узкой ширине ссылки переносятся под неё. Цель нажатия — --ds-tap,
    как у всех ссылок-кнопок оболочки.
*/
.ds-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-space-2) var(--ds-space-4);
    padding: var(--ds-space-4);
    border-top: 1px solid var(--ds-line);
    font-family: var(--ds-font);
    font-size: var(--ds-size-sm);
    color: var(--ds-muted);
}

.ds-footer-links { display: flex; flex-wrap: wrap; gap: 0 var(--ds-space-4); }

.ds-footer-links a {
    display: inline-flex;
    align-items: center;
    min-height: var(--ds-tap);
    color: inherit;
}

.ds-footer-links a:hover { color: var(--ds-text); }
