/* Общий хром портала: шапка, нижнее меню, модалки, базовый сброс.
 * Подключается через _header_styles.html; правила — в portal/README.md. */

/* Фокус после тапа нужен скринридеру, рамка — клавиатурной навигации.
 * Кнопки и ссылки сохраняют собственный индикатор фокуса. */
[data-shell-focus="pointer"]:focus { outline: none; }

    /* Базовый сброс едет С ЭТИМ ПАРТИАЛОМ по той же причине, что и токены:
       страницы объявляли его каждая у себя, а `inbox` и `notifications`
       забыли — и весь общий хром на них считался по content-box. Кольцо
       грейда в шапке (27px + 2.5px рамки) вырастало до 32, пилюли до 44
       вместо 42, нижнее меню становилось шире.
       Правило одно на портал, страницы его не дублируют. */
    * { box-sizing: border-box; }

    /* Место под полосу прокрутки резервируем ВСЕГДА. Одни экраны портала
       прокручиваются, другие нет (лента длинная, «Сообщения» короткий), и на
       браузерах с классической полосой ширина вьюпорта менялась при каждом
       переходе — весь макет прыгал на её толщину вбок.
       Резерв, а не принудительная полоса (`overflow-y: scroll`): на системах
       с наложенной полосой (macOS, iOS, вся мобилка) свойство не занимает
       ничего, а принудительная полоса рисовала бы вечную дорожку на десктопе
       ради проблемы, которой там нет. */
    html { scrollbar-gutter: stable; }

    /* Тап срабатывает сразу, а не через ~300 мс. Столько браузер ждёт второго
       касания, чтобы понять, не двойной ли это тап для зума; на элементе с
       `manipulation` двойной тап не зумит, и ждать нечего. Задержка была на
       каждой кнопке портала и читалась как «тормозит», хотя обработчик
       отрабатывал мгновенно. Правило одно на портал — страницы его не
       дублируют; `level.html` объявлял его сам, теперь может не объявлять.
       Прокрутке не мешает: `manipulation` запрещает только двойной тап. */
    button,
    a,
    label,
    summary,
    [role="button"] { touch-action: manipulation; }

    /* Kill iOS Safari rubber-band overscroll at the top of the page —
       otherwise a hard pull reveals the dark body BG above the sticky
       .portal-header. Body BG-color fallback (=gradient top) keeps it
       invisible on browsers that ignore overscroll-behavior.

       ⚠️ Только веб (`body.nav-brand`, класс ставит platform.js синхронно). В
       приложении отскок нужен: он и есть то, чем нативный список говорит «это
       конец». Мы включаем его нативно (`BridgeViewController.enableNativeGestures`),
       а это правило его гасит — CSS сильнее, отскока бы не было. Прятать за
       отскоком нечего: сверху из-под страницы видно `underPageBackgroundColor`,
       он того же цвета, что `html` ниже и непрозрачная шапка.
       `html:has(...)` — потому что на viewport влияет свойство КОРНЯ, а класс
       платформы живёт на body. */
    html:has(body.nav-brand),
    body.nav-brand { overscroll-behavior-y: none; }
    html { background-color: #0f172a; }

    /* Крутящийся индикатор. Живёт ЗДЕСЬ, а не в вендорном файле: разметку с
       ним несёт сам хром (загрузка QR-пропуска), а хром есть на каждом экране
       портала — включая те, что Bootstrap не грузят вовсе. Там кольцо уже
       стояло неподвижно.
       Значения совпадают с вендорными, чтобы на страницах, которые Bootstrap
       ещё грузят, картинка не поменялась: наше правило идёт позже и побеждает.
       Размеры под конкретные места задаются рядом с ними
       (`.qr-loading .spinner-border` ниже, `.loading-state` в book.css). */
    .spinner-border {
        display: inline-block;
        width: 2rem;
        height: 2rem;
        vertical-align: -0.125em;
        border: 0.25em solid currentcolor;
        border-right-color: transparent;
        border-radius: 50%;
        animation: 0.75s linear infinite bp-spinner-border;
    }
    .spinner-border-sm {
        width: 1rem;
        height: 1rem;
        border-width: 0.2em;
    }
    @keyframes bp-spinner-border {
        to { transform: rotate(360deg); }
    }

    /* Scroll lock while a full-screen overlay is up. The body classes were
       already toggled (QR modal / booking overlays) but nothing locked the
       page — iOS momentum kept scrolling content BEHIND the overlay, and
       taps after closing landed on a different spot («дёргается»). */
    body.qr-modal-open,
    body.booking-overlay-open { overflow: hidden; }

    /* Cross-document view transitions: the portal is an MPA, so every
       bottom-nav tab switch is a full navigation. Modern engines (Chrome
       126+/Android WebView, Safari 18.2+/iOS WKWebView) cross-fade the two
       pages instead of flashing; older engines ignore the rule.

       ⚠️ Правило объявлено БЕЗУСЛОВНО, а не под `prefers-reduced-motion`.
       Переход тут делает две разные вещи: анимирует (это движение, и от него
       человек вправе отказаться) и УДЕРЖИВАЕТ старый кадр, пока не отрисован
       новый (это не движение, а отсутствие мигания). Пока правило стояло под
       медиазапросом, телефон с включённым «Уменьшением движения» терял и то и
       другое — и получал ровно ту вспышку, ради которой всё затевалось.
       Ниже движение для такого телефона снимаем отдельно. */
    @view-transition { navigation: auto; }

    @media (prefers-reduced-motion: reduce) {
        /* Ни одной анимации — подмена кадра мгновенная. Удержание старого
           кадра при этом остаётся: переход по-прежнему идёт, просто без
           движения. */
        ::view-transition-group(*),
        ::view-transition-old(*),
        ::view-transition-new(*) { animation: none !important; }
    }

    /* Постоянный хром переезжает СВОЕЙ группой, а не в снимке страницы.
       Без имени переход берёт ОДИН снимок всего документа: старый гаснет до
       нуля, новый проявляется с нуля — и шапка с нижним меню гаснут вместе с
       контентом, хотя на обоих экранах они одинаковые. Со стороны это читается
       как «шапка и подвал переоткрываются на каждый клик». Именованный элемент
       движок сопоставляет между документами и анимирует отдельно; у нас его
       пиксели почти совпадают, поэтому он просто стоит на месте.
       Имя обязано быть УНИКАЛЬНЫМ в документе — два элемента с одним именем
       отменяют переход целиком и молча (гейт — tests/unit/portal/
       test_portal_chrome_view_transitions.py). Имя нижнего меню объявлено
       рядом с самим меню, в bottom-nav.css. */
    .portal-header { view-transition-name: portal-header; line-height: normal; }

    /* Контенту хватает короткого перехода: 250 мс по умолчанию на смене
       вкладки ощущаются как задержка отклика, а не как плавность. */
    @media (prefers-reduced-motion: no-preference) {
        ::view-transition-old(root),
        ::view-transition-new(root) { animation-duration: 180ms; }
    }

    /* === Header shell ===
       Padding honours safe-area-inset so the login CTA / chips don't slide
       under the Android status-bar / camera punch-hole in Capacitor's
       edge-to-edge WebView (and iOS notch in landscape).
       Falls back to Capacitor's --safe-area-inset-* CSS vars for Android
       WebView <140, where native env() is broken (see @capacitor/core
       system-bars.md). Requires viewport-fit=cover on the page. */
    .portal-header {
        background: rgba(15, 23, 42, 0.78);
        backdrop-filter: blur(16px) saturate(180%);
        -webkit-backdrop-filter: blur(16px) saturate(180%);
        border-bottom: 1px solid rgba(255, 255, 255, 0.06);
    }
    /* Engines without (or with broken) backdrop-filter would show a
       washed-out translucent header over content — fall back to opaque. */
    @supports not (backdrop-filter: blur(1px)) {
        .portal-header { background: #0f172a; }
    }
    /* Capacitor app (body.nav-ios / .nav-md set by _platform.html): opaque
       header, NO backdrop-filter. position:sticky + backdrop-filter is a
       known WebKit compositing bug — the header intermittently vanishes
       after backgrounding. An opaque header makes the bug physically
       impossible; the web keeps the blur. ❌ Don't re-add blur here without
       checking the telemetry for a vanishing header stays at zero. */
    body.nav-ios .portal-header,
    body.nav-md .portal-header {
        background: #0f172a;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
    .portal-header {
        padding-top: max(10px, var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
        padding-right: max(14px, var(--safe-area-inset-right, env(safe-area-inset-right, 0px)));
        padding-bottom: 10px;
        padding-left: max(14px, var(--safe-area-inset-left, env(safe-area-inset-left, 0px)));
        position: sticky;
        top: 0;
        z-index: 100;
    }

    .portal-header-inner {
        max-width: 420px;
        margin: 0 auto;
        display: flex;
        flex-direction: column;
        gap: 8px;
    }

    .header-top-row {
        display: flex;
        align-items: center;
        gap: 10px;
    }

    /* === Logo block === */
    .brand-block[hidden] { display: none; }
    .brand-block {
        display: inline-flex;
        align-items: center;
        text-decoration: none;
        min-width: 0;
        margin-right: auto;
    }
    .brand-logo {
        height: clamp(22px, 6.5vw, 28px);
        width: auto;
        display: block;
    }

    /* === Выбор клуба на месте логотипа ===
       Занимает ту же роль, что `.brand-block`: тянет свободную ширину и
       отжимает действия вправо. Имя клуба пишется ПОЛНОСТЬЮ — «Москва -
       Савеловская», а не «Савеловская»: сокращение экономило место, но
       отвечало на вопрос «где я» наполовину. Длинные имена («Тверь -
       Центральный рынок» — 25 знаков) режутся многоточием; шеврон не
       сжимается, иначе кнопка перестаёт читаться как выбор. */
    /* `display` в правиле ниже сильнее браузерного `[hidden] { display: none }`,
       поэтому скрытие приходится объявлять самим — иначе кнопка видна пустой
       на всех страницах, где клуба нет. То же и у логотипа ниже. */
    .header-club[hidden] { display: none; }
    .header-club {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        min-width: 0;
        /* 40 px — рост соседей по ряду (уровень, пропуск, аватар). Ряд подняли
           с 34: там, где имя клуба влезало в одну строку, кнопка оказывалась
           ниже нормы для пальца, а промах по главному переключателю экрана
           стоит дороже шести пикселей высоты шапки. */
        min-height: 40px;
        margin-right: auto;
        padding: 6px 10px 6px 12px;
        /* Клуб заметнее прежнего логотипа, а не тише его: он занял место
           главного элемента шапки, и от него зависит весь экран. Тусклая
           серая пилюля читалась как служебная подпись — люди бы его просто не
           нашли после переноса. Акцентная рамка и подложка делают его первым,
           за что цепляется взгляд. */
        border: 1px solid rgba(146, 196, 58, 0.45);
        border-radius: 999px;
        background: rgba(146, 196, 58, 0.12);
        color: #fff;
        font-family: inherit;
        font-size: var(--bp-fs-body);
        font-weight: 700;
        line-height: 1.2;
        cursor: pointer;
    }
    /* Имя клуба показываем ЦЕЛИКОМ: обрезанное «Москва - Савелов…» не отвечает
       на вопрос, в каком ты клубе, а именно за этим на него и смотрят. Не
       влезло в строку — переносим на вторую и даём кнопке подрасти; на
       широком экране перенос не срабатывает и высота прежняя. Потолок в две
       строки нужен на 320 px с именами вроде «Петропавловск-Камчатский»:
       третья строка выдавила бы шапку в половину экрана. */
    .header-club-name {
        min-width: 0;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
        white-space: normal;
        /* Именно `break-word`, а не `anywhere`: второй уменьшает СОБСТВЕННУЮ
           предпочтительную ширину элемента до самого длинного слова, и flex
           отдавал кнопке эту ширину — имя ломалось на две строки даже там,
           где спокойно влезало в одну. */
        overflow-wrap: break-word;
        line-height: 1.12;
        text-align: left;
    }
    .header-club-caret {
        flex: 0 0 auto;
        color: var(--bp-green);
        font-size: var(--bp-fs-caption);
    }
    @media (hover: hover) and (pointer: fine) {
        .header-club:hover { background: rgba(255, 255, 255, 0.1); }
    }
    /* На узких экранах имя клуба длиннее свободного места, поэтому кнопка
       ужимается сама: полное имя важнее воздуха вокруг него. */
    @media (max-width: 400px) {
        .header-club {
            gap: 2px;
            padding: 6px 6px 6px 9px;
            /* 0.84 вместо 0.88: соседний чип уровня держит за собой 78 px под
               «10.0 A+», и на этих девяти пикселях «Москва - Савеловская»
               переставала помещаться в строку. Читаемости не стоило — имя
               набрано жирным на контрастной подложке. */
            font-size: var(--bp-fs-secondary);
        }
        /* Соседей по строке тоже поджимаем: каждые 4 px зазора — это буква
           имени клуба, которая иначе уедет на вторую строку. */
        .header-top-row { gap: 4px; }
    }

    /* 320 px — самый узкий экран, который мы держим. Там на имя клуба
       остаётся ~100 px, и двух строк не хватает: «Тверь - Оснабрюкская»
       обрывалась на «Оснабрюкс…», то есть ровно на той части, которая и
       отличает клуб от соседнего. Мельче шрифт и третья строка дают полное
       имя; кнопка при этом остаётся ниже ряда плашек. */
    @media (max-width: 360px) {
        .header-club { font-size: var(--bp-fs-secondary); }
        .header-club-name { -webkit-line-clamp: 3; }
    }

    /* Метка окружения: оранжевая точка на аватаре вместо плашки «DEV».
       Слово занимало ~50 px в строке, где их не хватает имени клуба; точка
       стоит поверх и не отнимает ширины вовсе. Рамка цветом шапки отделяет
       её от фото — на светлых аватарах точка иначе сливается. */
    .header-avatar.has-env-dot,
    .login-btn.has-env-dot { position: relative; }
    /* Точку рисуем ВНУТРЬ: у аватара с фото стоит `overflow: hidden` (им
       фотография обрезается в круг), и вынесенная за край точка исчезала
       вместе с ним. Смещение внутрь работает при любом состоянии аватара. */
    .header-avatar.has-env-dot::after,
    .login-btn.has-env-dot::after {
        content: "";
        position: absolute;
        top: 1px;
        right: 1px;
        width: 10px;
        height: 10px;
        border-radius: 50%;
        background: var(--bp-orange, #f97316);
        box-shadow: 0 0 0 2px #0f172a;
        pointer-events: none;
        z-index: 1;
    }

    /* === Compact QR icon (row 1) ===
       The big «QR-пропуск» CTA moved to the bottom-nav center button; this
       small icon stays as the habitual header entry point. Lime-tinted so it
       reads as the same affordance without competing with the nav button. */
    .header-qr-mini {
        width: 40px;
        height: 40px;
        flex-shrink: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border-radius: 11px;
        background: rgba(146, 196, 58, 0.14);
        border: 1.5px solid rgba(146, 196, 58, 0.5);
        color: var(--bp-green);
        font-size: var(--bp-fs-h2);
        cursor: pointer;
        transition: transform .12s ease, background .15s ease;
    }
    /* :hover — только там, где есть мышь; пара с pointer: fine нужна, чтобы
       правило не сработало на тач-экране */
    @media (hover: hover) and (pointer: fine) {
        .header-qr-mini:hover { background: rgba(146, 196, 58, 0.24); transform: translateY(-1px); }
    }
    .header-qr-mini:active { transform: scale(0.94); }

    /* === Уровень игры (row 1, ведёт на разбор /level) ===
       Тот же рост и радиус, что у QR-кнопки рядом: это соседи в одной
       строке, и разная высота читалась бы как сбой вёрстки. */
    .header-level {
        height: 40px;
        /* Ширина ФИКСИРОВАНА и одна на оба состояния — и на «10.0 A+», и на
           призыв «Узнать уровень» (он переносится на две строки). Чип
           дозаполняется запросом, и пока ширину диктовало содержимое, он рос
           вбок за счёт соседа: имя клуба переносилось на лишнюю строку и
           толкало вниз весь экран. Замер: 78 px при 390, 66 px при 320. */
        width: 78px;
        flex-shrink: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 5px;
        padding: 0 10px;
        border-radius: 11px;
        background: rgba(146, 196, 58, 0.14);
        border: 1.5px solid rgba(146, 196, 58, 0.5);
        color: var(--bp-green);
        text-decoration: none;
        transition: transform .12s ease, background .15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .header-level:hover { background: rgba(146, 196, 58, 0.24); transform: translateY(-1px); }
    }
    .header-level:active { transform: scale(0.94); }
    .header-level-num { font-size: var(--bp-fs-body); font-weight: 800; font-variant-numeric: tabular-nums; }
    .header-level-grade {
        font-size: var(--bp-fs-micro); font-weight: 700; letter-spacing: .02em;
        opacity: .85;
    }
    /* Оценка по прежней самооценке — приглушаем, чтобы не путали с заданной */
    .header-level.is-estimate { border-style: dashed; opacity: .8; }
    /* На 320 px четыре контрола в строке сразу не помещаются, и уступает чип
       уровня: имя клуба отвечает на вопрос «где я играю». Блок стоит ПОСЛЕ
       базового правила: специфичность одна, выше по файлу он бы проиграл. */
    @media (max-width: 360px) {
        .header-level {
            width: 66px;
            padding: 0 6px;
        }
        .header-level-num { font-size: var(--bp-fs-secondary); }
    }

    /* Значение не приехало: место держим, показывать нечего. Именно
       `visibility` — `hidden`/`display` отдали бы место соседям. */
    .header-level.is-reserved { visibility: hidden; }
    /* Уровня нет — зовём определить; текст не режем, он переносится */
    .header-level-empty .header-level-num {
        font-size: var(--bp-fs-caption); font-weight: 700; white-space: normal;
        line-height: 1.1; text-align: center;
    }
    .header-level-empty { padding: 0 6px; }

    /* === Avatar (row 1, opens profile modal) === */
    .header-avatar {
        /* Опора для фотографии и точки окружения — обе лежат абсолютно. */
        position: relative;
        width: 40px;
        height: 40px;
        flex-shrink: 0;
        border-radius: 50%;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        background: linear-gradient(135deg, #2e7fd6, #1b4e8c);
        border: 1.5px solid rgba(255, 255, 255, 0.18);
        color: #fff;
        font-family: var(--ff-display);
        font-weight: 800;
        font-size: var(--bp-fs-secondary);
        letter-spacing: 0.02em;
        cursor: pointer;
        transition: transform .12s ease, box-shadow .15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .header-avatar:hover { transform: translateY(-1px); box-shadow: 0 6px 14px -8px rgba(46, 127, 214, 0.7); }
    }
    /* When a photo is set the gradient/letters give way to the image. */
    .header-avatar.has-img { padding: 0; overflow: hidden; }
    /* Фото лежит ПОВЕРХ инициалов, а не вместо них. Пока картинка едет (а с
       мобильной сети это заметно), кружок раньше стоял пустым и буквы
       проступали рывком — теперь они видны сразу, а фото просто закрывает их
       собой. Если файла нет, обработчик в `_header.html` снимет `has-img` и
       вернёт фоновый градиент; буквы уже на месте. */
    .header-avatar-img {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        border-radius: 50%;
        object-fit: cover;
    }

    /* === Pillars (row 2): Школа · серия · баллы ===
       Equal tiles; value 16px/800 + tiny mono label, per the design handoff. */
    /* ОДИН ряд с прокруткой вбок вместо сетки 2×2.
       Четыре плашки в строку не помещаются, но вторая строка стоила экрану
       записи ещё ~50 px и делала выбор клуба в шапке мельче плашек под ним —
       главное на экране выглядело второстепенным. Прокрутка честнее: важное
       (счёт, школа) видно сразу, остальное достаётся жестом.
       Затухание у правого края показывает, что ряд продолжается. */
    .header-pillars {
        display: flex;
        gap: 8px;
        overflow-x: auto;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
        scroll-snap-type: x proximity;
        -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
        mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
    }
    .header-pillars::-webkit-scrollbar { display: none; }
    .pillar {
        flex: 0 0 auto;
        scroll-snap-align: start;
        min-width: 0;
        display: flex;
        align-items: center;
        gap: 8px;
        height: 42px;
        padding: 0 10px;
        border-radius: 13px;
        background: rgba(255, 255, 255, 0.05);
        border: 1px solid rgba(255, 255, 255, 0.1);
        color: #e2e8f0;
        text-decoration: none;
        cursor: pointer;
        text-align: left;
        transition: transform .12s ease, background .15s ease, border-color .15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .pillar:hover {
            background: rgba(255, 255, 255, 0.08);
            border-color: rgba(255, 255, 255, 0.18);
            transform: translateY(-1px);
        }
    }
    .pillar[hidden] { display: none !important; }
    .pillar-icon { font-size: var(--bp-fs-h2); line-height: 1; flex-shrink: 0; }
    .pillar-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
    .pillar-value {
        font-family: var(--ff-body);
        font-weight: 700;
        font-size: var(--bp-fs-secondary);
        line-height: 1.05;
        color: #fff;
        white-space: nowrap;
        font-variant-numeric: tabular-nums;
    }
    .pillar-value .unit { font-size: var(--bp-fs-micro); color: #94a3b8; font-weight: 500; }
    .pillar-label {
        font-family: var(--ff-mono);
        font-size: 8.5px;
        letter-spacing: 0.1em;
        text-transform: uppercase;
        color: #5c6884;
        white-space: nowrap;
    }

    /* ── Плитки: иконка сверху, подпись снизу, как иконки приложений ──
       Ряд прямоугольных плашек «иконка слева, две строки справа» вмещал
       на 390 px три с половиной штуки, и то, что правее, человек не видел,
       пока не листнёт. Квадратная плитка уже вдвое, поэтому в тот же ряд
       целиком встают пять, а шестая выглядывает краем — видно, что ряд
       листается.

       Нижняя строка подписи («счёт», «оплатить», «мои») в плитку не
       помещается и спрятана: смысл несёт пара «пиктограмма + слово», как
       на домашнем экране айфона. В `aria-label` плашки полное название
       осталось. Высота плитки задаёт высоту ряда шапки — меняешь её,
       помни, что ниже на столько же съезжает весь экран. */
    /* Плиток обычно пять, и в ряд они влезают целиком с пустым хвостом
       справа — ряд выглядел недособранным и не совпадал по краям с баннером
       ниже. Поэтому плитки растут поровну и занимают всю ширину; сжиматься
       ниже 60 px им нельзя — лишние уходят в прокрутку.
       Затухание правого края — подсказка «листай». Когда ряд помещается
       целиком, оно гасило бы последнюю плитку зря, поэтому включается
       только классом `is-overflowing`, который ставит `header-club.js`. */
    .header-pillars.is-tiles {
        gap: var(--bp-space-2);
        -webkit-mask-image: none;
        mask-image: none;
    }
    .header-pillars.is-tiles.is-overflowing {
        -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
        mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
    }
    .header-pillars.is-tiles .pillar {
        flex: 1 0 60px;
        flex-direction: column;
        justify-content: center;
        gap: var(--bp-space-1);
        min-width: 60px;
        height: 60px;
        padding: 0 2px;
        border-radius: 16px;
        text-align: center;
    }
    .header-pillars.is-tiles .pillar-icon { font-size: var(--bp-fs-title); }
    .header-pillars.is-tiles .pillar-ring { width: 26px; height: 26px; font-size: var(--bp-fs-secondary); }
    .header-pillars.is-tiles .pillar-text { align-items: center; max-width: 100%; }
    .header-pillars.is-tiles .pillar-value {
        /* Кегль подобран под самое длинное слово ряда — «Парковка»: при 11px
           оно не влезало и рвалось посреди («Парковк|а»). Слово целиком
           важнее лишнего пикселя кегля: подписи здесь — одно слово, как у
           иконок на домашнем экране. */
        font-size: var(--bp-fs-micro);
        letter-spacing: -0.01em;
        white-space: nowrap;
        line-height: 1.1;
    }
    .header-pillars.is-tiles .pillar-value .unit { font-size: var(--bp-fs-micro); }
    .header-pillars.is-tiles .pillar-label { display: none; }

    /* Организаторский pillar «Мои турниры» — активен на /organizer. */
    .pillar-organizer .pillar-icon { color: var(--bp-green); }
    .pillar-organizer.on {
        background: rgba(146, 196, 58, 0.16);
        border-color: rgba(146, 196, 58, 0.4);
    }

    /* Школа pillar — grade ring. Shows a mortarboard until the async grade
       loader (_portal_school_grade.html) fills #pillar-grade and flips
       .has-grade on the tile. */
    .pillar-ring {
        width: 27px;
        height: 27px;
        flex-shrink: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        background: #0e1730;
        border: 2.5px solid transparent;
        background-image: linear-gradient(#0e1730, #0e1730), linear-gradient(135deg, #2e7fd6, var(--bp-green));
        background-origin: border-box;
        background-clip: padding-box, border-box;
        color: var(--bp-green);
        font-size: var(--bp-fs-secondary);
    }
    /* Кольцо заполняется по ДОЛЕ пройденного, а внутри — процент.
       Буква грейда отсюда убрана: рядом в шапке живёт грейд УРОВНЯ ИГРЫ по
       той же азбуке D…A, и две одинаковые буквы про разное читались как одно.
       Долю видно с одного взгляда, вычитывать нечего. */
    .pillar-ring-grade {
        display: none;
        font-family: var(--ff-display);
        font-weight: 800;
        font-size: var(--bp-fs-micro);
        color: #fff;
    }
    #pillar-school.has-grade .pillar-ring i { display: none; }
    #pillar-school.has-grade .pillar-ring-grade { display: inline; }
    /* Дуга прогресса поверх кольца: --school-progress ставит скрипт */
    #pillar-school.has-grade .pillar-ring {
        background-image:
            linear-gradient(#0e1730, #0e1730),
            conic-gradient(var(--bp-green) calc(var(--school-progress, 0) * 1%),
                           rgba(255, 255, 255, 0.16) 0);
    }

    /* Streak pillar — three states, same semantics as the old chip:
       idle (hidden), active (1-2 weeks), hot (3+, set by renderStreakChip). */
    /* Серия: подсветка вместо эмодзи и без анимации. Мигающий огонёк тянул
       внимание сильнее счёта, хотя счёт важнее; длинная серия теперь заметна
       цветом числа и рамкой, а не движением. */
    .pillar-streak.active { border-color: rgba(146, 196, 58, 0.45); }
    .pillar-streak.active .num { color: #cdec99; }
    .pillar-streak.active .pillar-icon { color: var(--bp-green); }
    .pillar-streak.hot { border-color: rgba(251, 146, 60, 0.55); }
    .pillar-streak.hot .num,
    .pillar-streak.hot .pillar-icon { color: var(--bp-orange, #f97316); }

    /* Account pillar — gold accent: за ней средства клиента, баллы и
       абонементы. Правила для звезды и числа убраны вместе с ними: плашка
       ведёт в развилку и своего значения не показывает. */
    .pillar-points { border-color: rgba(250, 204, 21, 0.32); }

    /* Coach pillar — workspace toggle (blue accent, the old chip's colour
       language so coaches recognise it instantly). */
    .pillar-coach {
        border-color: rgba(96, 165, 250, 0.45);
        background: rgba(59, 130, 246, 0.08);
    }
    .pillar-coach .pillar-icon { color: #9cc2f2; font-size: var(--bp-fs-body); }
    .pillar-coach .pillar-value { color: #dbeafe; }
    @media (hover: hover) and (pointer: fine) {
        .pillar-coach:hover { background: rgba(59, 130, 246, 0.16); }
    }

    /* === Logged-out state === */
    .login-btn {
        display: inline-flex;
        align-items: center;
        gap: 7px;
        height: 32px;
        padding: 0 14px;
        border-radius: 8px;
        background: var(--bp-green);
        color: var(--bp-ink);
        border: 1.5px solid rgba(15, 23, 42, 0.55);
        box-shadow: 0 6px 16px -10px rgba(146, 196, 58, 0.5);
        cursor: pointer;
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.12em;
        text-transform: uppercase;
        transition: transform .12s ease, box-shadow .15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .login-btn:hover { transform: translateY(-1px); box-shadow: 0 10px 22px -10px rgba(146, 196, 58, 0.65); }
    }

    /* === Narrow viewports — tighten chrome, drop pillar labels (design
       handoff reflow rules: labels go first, logo shrinks via clamp()). === */
    @media (max-width: 399px) {
        .pillar { height: 38px; gap: 6px; padding: 0 9px; }
        .pillar-label { display: none; }
    }
    @media (max-width: 380px) {
        .portal-header {
            padding-top: max(11px, var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
            padding-right: max(10px, var(--safe-area-inset-right, env(safe-area-inset-right, 0px)));
            padding-bottom: 12px;
            padding-left: max(10px, var(--safe-area-inset-left, env(safe-area-inset-left, 0px)));
        }
        .portal-header-inner { gap: 8px; }
        .header-pillars { gap: 6px; }
    }
    @media (max-width: 330px) {
        .pillar-value { font-size: var(--bp-fs-secondary); }
        .pillar { gap: 5px; }
    }

    /* === QR Modal — Club Pass ===
       A physical entry pass on paper, sitting in a dim turnstile hall.
       Overlay is a vertical wash from brand ink-blue to slate, with a
       faint grain so the paper card has texture to land on. */
    .qr-modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background:
            radial-gradient(1100px 600px at 50% -10%, rgba(4, 65, 132, 0.55), transparent 60%),
            linear-gradient(180deg, #0b1626 0%, #060a13 100%);
        z-index: 1000;
        display: none;
        flex-direction: column;
        overflow-y: auto;
    }
    .qr-modal-overlay::before {
        content: "";
        position: absolute;
        inset: 0;
        pointer-events: none;
        opacity: 0.18;
        mix-blend-mode: overlay;
        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.55 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
    }
    .qr-modal-overlay.open {
        display: flex;
    }

    .qr-modal {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        position: relative;
        /* Honour the safe-area inset: this overlay is edge-to-edge under the
           status bar (viewport-fit=cover), so the card AND the close button
           must shift down by the notch/Dynamic-Island height — otherwise the
           × lands inside the status bar and is untappable (iOS). Capacitor's
           --safe-area-inset-* var covers Android; env() covers iOS. */
        /* Верх — геометрия кнопки закрытия (её отступ + высота), а не ступень
           ритма: карточка обязана начинаться под ×. Низ — ступень модалки. */
        padding: calc(56px + var(--safe-area-inset-top, env(safe-area-inset-top, 0px))) 16px
                 calc(var(--bp-space-6)
                      + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
        max-width: 460px;
        margin: 0 auto;
        width: 100%;
        box-sizing: border-box;
    }

    .qr-modal-close {
        position: absolute;
        /* Below the status bar (see .qr-modal note) — keeps the same 14px gap
           above the card the design intends, but past the safe-area inset. */
        top: calc(14px + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
        right: max(14px, var(--safe-area-inset-right, env(safe-area-inset-right, 0px)));
        background: rgba(255,255,255,0.08);
        border: 1px solid rgba(255,255,255,0.12);
        border-radius: 50%;
        width: 40px;
        height: 40px;
        color: #e2e8f0;
        font-size: var(--bp-fs-h3);
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 10;
        transition: background 0.15s ease, color 0.15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .qr-modal-close:hover { background: rgba(255,255,255,0.16); color: #fff; }
    }

    /* === The pass itself ===
       Paper-toned card with a perforated bottom edge (radial cutouts).
       Inside the card, --bp-green resets to canonical so dark-mode body
       override doesn't leak onto paper.  */
    .qr-pass {
        --bp-green: #5c8a1a;
        --bp-green-dark: #436513;
        /* Opt-out of Android Samsung Internet / Mi Browser auto-dark
           selective inversion. Without this the paper-coloured pass card
           (light background + dark text) gets inverted inside our dark
           modal, the QR code goes dark-on-dark and stops scanning. */
        color-scheme: light;
        position: relative;
        width: 100%;
        background: var(--bp-paper);
        color: var(--bp-ink);
        border-radius: 14px;
        box-shadow:
            0 30px 60px -30px rgba(0, 0, 0, 0.7),
            0 12px 24px -12px rgba(0, 0, 0, 0.5),
            inset 0 0 0 1px rgba(23, 24, 26, 0.08);
        overflow: hidden;
        isolation: isolate;
    }
    .qr-pass::before {
        content: "";
        position: absolute;
        inset: 0;
        pointer-events: none;
        opacity: 0.5;
        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='p'><feTurbulence type='fractalNoise' baseFrequency='1.6' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.09  0 0 0 0 0.094  0 0 0 0 0.102  0 0 0 0.05 0'/></filter><rect width='100%25' height='100%25' filter='url(%23p)'/></svg>");
        mix-blend-mode: multiply;
    }

    /* Offline banner inside the pass — only shown when body.is-offline.
       Sits above the pass header so it's the first thing the user reads
       at the gate when their phone has no signal. */
    .qr-offline-banner {
        display: none;
        align-items: flex-start;
        gap: 10px;
        padding: 12px 16px;
        background: #b8351a;
        color: #fff8ee;
        font-size: var(--bp-fs-secondary);
        line-height: 1.35;
        font-weight: 500;
        letter-spacing: 0.01em;
        position: relative;
        z-index: 1;
    }
    .qr-offline-banner i {
        font-size: var(--bp-fs-h2);
        line-height: 1.2;
        flex-shrink: 0;
    }
    body.is-offline .qr-offline-banner { display: flex; }
    body.is-offline .qr-pass-foot { opacity: 0.55; }

    .qr-pass-head {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        gap: 12px;
        padding: 18px 20px 12px;
        position: relative;
    }

    .qr-pass-brand {
        display: inline-flex;
        align-items: center;
        gap: 10px;
    }
    .qr-pass-mark {
        width: 36px;
        height: 36px;
        display: block;
        flex-shrink: 0;
    }
    .qr-pass-wordmark {
        display: flex;
        flex-direction: column;
        line-height: 0.86;
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: var(--bp-fs-title);
        letter-spacing: -0.02em;
        text-transform: uppercase;
        color: var(--bp-ink);
    }

    /* Stamp — corner ink mark over the paper.
       tier-1 (1 week): small, green, just a marker.
       tier-2 (2 weeks): red, big, prominent number — the milestone.
       tier-3 (3+ weeks): same red, plus pulse. */
    .qr-pass-stamp {
        text-transform: uppercase;
        text-align: center;
        line-height: 1;
        border: 2.5px solid currentColor;
        border-radius: 5px;
        transform: rotate(-7deg);
        transform-origin: center;
        opacity: 0.94;
        user-select: none;
        display: inline-flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 2px;
        box-shadow: inset 0 0 0 1px currentColor;
        background: transparent;
        flex-shrink: 0;
    }
    .qr-pass-stamp[hidden] { display: none; }

    .qr-pass-stamp .stamp-top,
    .qr-pass-stamp .stamp-bottom {
        font-family: var(--ff-mono);
        font-weight: 700;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.2em;
    }
    .qr-pass-stamp .stamp-num {
        font-family: var(--ff-display);
        font-weight: 900;
        letter-spacing: -0.02em;
    }

    .qr-pass-stamp.tier-1 {
        color: var(--bp-green-dark);
        border-width: 2px;
        padding: 5px 9px 4px;
        min-width: 0;
        gap: 1px;
    }
    .qr-pass-stamp.tier-1 .stamp-top { display: none; }
    .qr-pass-stamp.tier-1 .stamp-num { font-size: var(--bp-fs-body); }
    .qr-pass-stamp.tier-1 .stamp-bottom { font-size: 8px; letter-spacing: 0.15em; }

    .qr-pass-stamp.tier-2,
    .qr-pass-stamp.tier-3 {
        color: var(--bp-stamp-red);
        padding: 7px 14px 6px;
        min-width: 92px;
        gap: 3px;
    }
    .qr-pass-stamp.tier-2 .stamp-num,
    .qr-pass-stamp.tier-3 .stamp-num {
        font-size: var(--bp-fs-display);
        margin: 1px 0;
    }

    .qr-pass-stamp.tier-3 {
        animation: stamp-pulse 1.6s ease-in-out infinite;
    }
    @keyframes stamp-pulse {
        0%, 100% { transform: rotate(-7deg) scale(1); opacity: 0.94; }
        50%      { transform: rotate(-7deg) scale(1.05); opacity: 1; }
    }

    .qr-pass-meta {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px;
        padding: 8px 20px;
        border-top: 1px solid var(--bp-line);
        border-bottom: 1px solid var(--bp-line);
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: var(--bp-ink-soft);
    }
    .qr-pass-meta .sep {
        color: var(--bp-muted);
        opacity: 0.7;
    }
    .qr-pass-meta .id {
        margin-left: auto;
        color: var(--bp-blue);
        font-variant-numeric: tabular-nums;
    }

    .qr-pass-holder {
        padding: 14px 20px 6px;
        display: flex;
        flex-direction: column;
        gap: 4px;
    }
    .qr-pass-holder-label {
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: var(--bp-muted);
    }
    .qr-pass-holder-name {
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: var(--bp-fs-h2);
        letter-spacing: -0.01em;
        text-transform: uppercase;
        color: var(--bp-ink);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .qr-code-container {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 12px 20px 18px;
        background: transparent;
    }
    .qr-code-container img,
    .qr-code-container .qr-frame {
        width: 100%;
        max-width: 240px;
        aspect-ratio: 1;
        background: var(--bp-paper);
        border-radius: 6px;
        padding: 0;
        box-sizing: border-box;
        mix-blend-mode: multiply;
    }

    /* === Pass foot — perforated separator + paper-2 tear-off === */
    .qr-pass-foot {
        position: relative;
        background: var(--bp-paper-2);
        border-top: 1px dashed var(--bp-line);
    }
    .qr-pass-foot::before,
    .qr-pass-foot::after {
        content: "";
        position: absolute;
        top: -10px;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        background: #060a13;
    }
    .qr-pass-foot::before { left: -10px; }
    .qr-pass-foot::after  { right: -10px; }

    .qr-balance-strip {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 14px 20px;
        border: 0;
        border-bottom: 1px solid var(--bp-line);
        cursor: pointer;
        background: transparent;
        width: 100%;
        transition: background 0.15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .qr-balance-strip:hover { background: rgba(23, 24, 26, 0.04); }
    }
    .qr-balance-strip .lbl {
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: var(--bp-ink-soft);
    }
    .qr-balance-strip .val {
        display: inline-flex;
        align-items: baseline;
        gap: 6px;
        color: var(--bp-ink);
    }
    .qr-balance-strip .val .num {
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: var(--bp-fs-title);
        letter-spacing: -0.01em;
        font-variant-numeric: tabular-nums;
    }
    .qr-balance-strip .val .star { color: var(--bp-green-dark); font-size: var(--bp-fs-secondary); }
    .qr-balance-strip .val .arr { color: var(--bp-muted); font-size: var(--bp-fs-secondary); margin-left: 2px; }

    .qr-loyalty-body { padding: 14px 20px 18px; }
    .qr-loyalty-row {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        margin-bottom: 12px;
        gap: 12px;
    }
    .qr-loyalty-row .lhs {
        font-family: var(--ff-body);
        font-weight: 400;
        font-size: var(--bp-fs-secondary);
        color: var(--bp-ink-soft);
        line-height: 1.4;
    }
    .qr-loyalty-row .lhs strong { color: var(--bp-ink); font-weight: 600; }
    .qr-loyalty-row .rhs {
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: var(--bp-fs-h2);
        color: var(--bp-green-dark);
        font-variant-numeric: tabular-nums;
        white-space: nowrap;
        display: inline-flex;
        align-items: baseline;
        gap: 4px;
        text-transform: uppercase;
    }
    .qr-loyalty-row .rhs .pts {
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        color: var(--bp-green-dark);
        opacity: 0.75;
        letter-spacing: 0.16em;
        text-transform: uppercase;
    }

    .qr-week-track {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 6px;
    }
    .qr-week-cell {
        padding: 10px 8px;
        border-radius: 8px;
        text-align: center;
        background: transparent;
        border: 1px solid var(--bp-line);
    }
    .qr-week-cell .label {
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.13em;
        text-transform: uppercase;
        color: var(--bp-muted);
        margin-bottom: 4px;
    }
    .qr-week-cell .pts {
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: var(--bp-fs-body);
        color: var(--bp-ink-soft);
        font-variant-numeric: tabular-nums;
    }
    .qr-week-cell.done {
        background: rgba(121, 172, 43, 0.18);
        border-color: var(--bp-green-dark);
    }
    .qr-week-cell.done .pts { color: var(--bp-green-dark); }
    .qr-week-cell.done .label { color: var(--bp-green-dark); }
    .qr-week-cell.next {
        background: rgba(4, 65, 132, 0.08);
        border-color: var(--bp-blue);
    }
    .qr-week-cell.next .pts { color: var(--bp-blue); }
    .qr-week-cell.next .label { color: var(--bp-blue); }

    .qr-help-link {
        margin-top: 18px;
        text-align: center;
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: rgba(255, 255, 255, 0.45);
        background: transparent;
        border: 0;
        padding: 8px 12px;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        gap: 8px;
        justify-content: center;
        width: 100%;
        transition: color 0.15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .qr-help-link:hover { color: rgba(255, 255, 255, 0.8); }
    }
    .qr-help-link i { font-size: var(--bp-fs-caption); }

    /* Ряд с бейджем «Добавить в Apple Wallet». Своих рамок и фона не даём:
       вид бейджа задан Apple, и обрамление ломает узнаваемость. */
    .qr-wallet-row {
        margin-top: 18px;
        display: flex;
        justify-content: center;
    }

    /* `.visually-hidden-label` живёт в apple-wallet-badge.css — рядом с
       бейджем, которому он и нужен: страницу добавления карты открывает
       системный браузер, и эту тему она не загружает. */

    .qr-not-ready {
        color: var(--bp-ink-soft);
        font-size: var(--bp-fs-body);
        text-align: center;
        padding: var(--bp-space-8) 20px;
    }

    .qr-loading {
        display: flex;
        align-items: center;
        justify-content: center;
        color: var(--bp-green-dark);
        font-size: 2.5rem;
    }

    .qr-loading .spinner-border {
        width: 3rem;
        height: 3rem;
        border-width: 3px;
    }

    /* === Bonuses receipt — "long check" in QR-pass aesthetic ===
       Same paper card, perforated edges, brand-tokenised. Drives the modal
       opened from the header points chip. */
    .tx-receipt-overlay {
        display: none;
        position: fixed;
        inset: 0;
        z-index: 9999;
        background:
            radial-gradient(900px 500px at 50% -10%, rgba(4, 65, 132, 0.45), transparent 60%),
            linear-gradient(180deg, rgba(11, 22, 38, 0.92), rgba(6, 10, 19, 0.96));
        align-items: center;
        justify-content: center;
        /* Safe-area padding so a tall receipt (overflow scroll) never slides
           its close button under the status bar / home indicator. */
        padding: max(24px, var(--safe-area-inset-top, env(safe-area-inset-top, 0px))) 16px
                 max(24px, var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
        overflow-y: auto;
    }
    .tx-receipt-overlay.open { display: flex; }

    .tx-receipt {
        --bp-green: #5c8a1a;
        --bp-green-dark: #436513;
        position: relative;
        width: 100%;
        max-width: 380px;
        max-height: 85vh;
        display: flex;
        flex-direction: column;
        background: var(--bp-paper);
        color: var(--bp-ink);
        border-radius: 14px;
        box-shadow:
            0 30px 60px -30px rgba(0, 0, 0, 0.7),
            0 12px 24px -12px rgba(0, 0, 0, 0.5),
            inset 0 0 0 1px rgba(23, 24, 26, 0.08);
        overflow: hidden;
        isolation: isolate;
    }
    .tx-receipt::before {
        content: "";
        position: absolute;
        inset: 0;
        pointer-events: none;
        opacity: 0.5;
        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='p'><feTurbulence type='fractalNoise' baseFrequency='1.6' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.09  0 0 0 0 0.094  0 0 0 0 0.102  0 0 0 0.05 0'/></filter><rect width='100%25' height='100%25' filter='url(%23p)'/></svg>");
        mix-blend-mode: multiply;
    }

    .tx-receipt-close {
        position: absolute;
        top: 10px;
        right: 10px;
        width: 32px;
        height: 32px;
        border-radius: 50%;
        background: rgba(23, 24, 26, 0.06);
        border: 1px solid rgba(23, 24, 26, 0.1);
        color: var(--bp-ink-soft);
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: var(--bp-fs-secondary);
        z-index: 2;
        transition: background .15s ease, color .15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .tx-receipt-close:hover { background: rgba(23, 24, 26, 0.12); color: var(--bp-ink); }
    }

    .tx-receipt-head {
        padding: 22px 20px 16px;
        text-align: center;
        position: relative;
    }
    .tx-receipt-wordmark {
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: var(--bp-fs-secondary);
        letter-spacing: 0.06em;
        text-transform: uppercase;
        color: var(--bp-ink);
        margin-bottom: 6px;
    }
    .tx-receipt-balance {
        display: inline-flex;
        align-items: baseline;
        gap: 8px;
        margin-bottom: 4px;
    }
    .tx-receipt-balance .num {
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: 40px;
        letter-spacing: -0.02em;
        color: var(--bp-ink);
        font-variant-numeric: tabular-nums;
        line-height: 1;
    }
    .tx-receipt-balance .star { color: var(--bp-green-dark); font-size: var(--bp-fs-title); }
    .tx-receipt-caption {
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.2em;
        text-transform: uppercase;
        color: var(--bp-muted);
    }

    /* Tear-off perforated separator — same notch trick as the QR pass foot */
    .tx-receipt-perforation {
        position: relative;
        height: 0;
        border-top: 1px dashed var(--bp-line);
        background: var(--bp-paper-2);
    }
    .tx-receipt-perforation::before,
    .tx-receipt-perforation::after {
        content: "";
        position: absolute;
        top: -10px;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        background: #060a13;
    }
    .tx-receipt-perforation::before { left: -10px; }
    .tx-receipt-perforation::after  { right: -10px; }

    /* «Следующие сгорания» — nearest burn dates above the history.
       :empty collapses the container when nothing is expiring soon. */
    .tx-upcoming:empty { display: none; }
    .tx-upcoming-inner {
        background: var(--bp-paper-2);
        padding: 12px 20px 4px;
    }
    .tx-upcoming-title {
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.2em;
        text-transform: uppercase;
        color: var(--bp-muted);
        margin-bottom: 6px;
    }
    .tx-upcoming-row {
        display: flex;
        justify-content: space-between;
        gap: 12px;
        padding: var(--bp-space-2) 0;
        border-bottom: 1px dashed var(--bp-line);
    }
    .tx-upcoming-row:last-child { border-bottom: 0; }
    .tx-upcoming-row .d {
        font-family: var(--ff-body);
        font-size: var(--bp-fs-secondary);
        color: var(--bp-ink);
    }
    .tx-upcoming-row .a {
        font-family: var(--ff-mono);
        font-weight: 700;
        font-size: var(--bp-fs-secondary);
        font-variant-numeric: tabular-nums;
        color: var(--bp-stamp-red);
        white-space: nowrap;
    }

    .tx-receipt-body {
        background: var(--bp-paper-2);
        flex: 1;
        overflow-y: auto;
        padding: 14px 20px 8px;
        min-height: 80px;
    }

    .tx-receipt-empty,
    .tx-receipt-error {
        text-align: center;
        padding: var(--bp-space-8) 8px;
        font-family: var(--ff-body);
        font-size: var(--bp-fs-secondary);
        color: var(--bp-ink-soft);
    }

    /* «Мой счёт»: карточка баллов и карточки абонементов. Строятся на той же
       чековой сетке, что и ряды начислений, — общий визуальный язык у соседних
       кнопок шапки.

       ⚠️ Шрифт задаём КАЖДОМУ элементу явно, по ролям чека: `--ff-display` —
       числам-героям, `--ff-body` — названиям, `--ff-mono` — служебным
       подписям и суммам. Наследование здесь обманывает: у чека семейство
       стоит на самих строках, и блок без своего правила получает шрифт
       контейнера — из-за чего баллы и абонементы на одном экране были
       набраны разными гарнитурами. */
    /* Подпись раздела ВНУТРИ чека: сверху — ступень блока, снизу — ступень
       строки, чтобы подпись липла к своему списку. Ступень заголовка секции
       СТРАНИЦЫ (24) разорвала бы чек пополам. */
    .acc-section {
        margin: var(--bp-space-3) 0 var(--bp-space-2);
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.2em;
        text-transform: uppercase;
        color: var(--bp-muted);
    }

    /* Ожидание внутри чека. Полосы повторяют строки, которые придут на их
       место, и держат их высоту: модалка открывается поверх экрана, ответ
       идёт запросом, и однострочная «Загрузка…» сменялась блоком в три
       строки — карточка прыгала под пальцем. */
    .acc-skeleton {
        display: flex;
        flex-direction: column;
        gap: 8px;
        /* Держит высоту строк, на место которых встанет: их ступень. */
        padding: var(--bp-space-3) 0;
    }
    .acc-skeleton__row {
        height: 14px;
        border-radius: 7px;
        background: linear-gradient(
            90deg,
            var(--bp-paper) 25%,
            var(--bp-line) 37%,
            var(--bp-paper) 63%
        );
        background-size: 400% 100%;
        animation: acc-skeleton-shimmer 1.4s ease infinite;
    }
    .acc-skeleton__row--short { width: 60%; }
    .acc-skeleton__row--btn { height: 36px; border-radius: 10px; }
    @keyframes acc-skeleton-shimmer {
        0% { background-position: 100% 50%; }
        100% { background-position: 0 50%; }
    }
    @media (prefers-reduced-motion: reduce) {
        .acc-skeleton__row { animation: none; }
    }

    .acc-section--buy { margin-bottom: 8px; }

    /* Переключатель клуба — полноразмерное поле, а не подпись у заголовка:
       от него зависят ВСЕ цены ниже, а прижатый к краю мелкий select читался
       как служебная пометка, и клуб покупки замечали уже после оплаты. */
    .acc-club-field {
        position: relative;
        display: block;
        margin-bottom: var(--bp-space-3);
        padding: 6px 10px 7px;
        border: 1px solid var(--bp-green, #5c8a1a);
        border-radius: 10px;
        background: var(--bp-paper);
    }
    .acc-club-field__label {
        display: block;
        font-family: var(--ff-mono);
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: var(--bp-green-dark, #436513);
    }
    .acc-club-field__chevron {
        position: absolute;
        right: 12px;
        bottom: 10px;
        font-size: var(--bp-fs-caption);
        color: var(--bp-green-dark, #436513);
        pointer-events: none;
    }
    .acc-club {
        width: 100%;
        /* Высоту задаём явно: у `appearance: none` селект схлопывается до
           строки текста, и палец попадал по нему только с третьего раза. */
        min-height: 28px;
        padding: 2px 22px 0 0;
        border: 0;
        border-radius: 0;
        background: transparent;
        color: var(--bp-ink);
        font-family: var(--ff-body);
        font-weight: 600;
        font-size: var(--bp-fs-body);
        text-transform: none;
        letter-spacing: 0;
        /* Родная стрелка увела бы поле в системный вид формы; свою рисуем
           сами, чтобы поле читалось как элемент чека. */
        appearance: none;
        -webkit-appearance: none;
    }
    .acc-club:focus-visible {
        outline: 2px solid var(--bp-green, #5c8a1a);
        outline-offset: 2px;
    }

    .acc-notice:not(:empty) {
        margin-bottom: 8px;
        padding: 8px 10px;
        border-radius: 8px;
        background: var(--bp-paper-2);
        font-family: var(--ff-body);
        font-size: var(--bp-fs-caption);
        color: var(--bp-ink-soft);
    }

    .acc-offer {
        padding: var(--bp-space-3) 0;
        border-bottom: 1px dashed var(--bp-line);
    }
    .acc-offer:last-child { border-bottom: 0; }
    .acc-offer__head {
        display: flex;
        align-items: center;
        gap: 8px;
    }
    .acc-offer__title {
        min-width: 0;
        overflow-wrap: anywhere;
        font-family: var(--ff-body);
        font-weight: 600;
        font-size: var(--bp-fs-secondary);
    }
    .acc-offer__badge {
        padding: 1px 6px;
        border-radius: 999px;
        background: rgba(92, 138, 26, 0.14);
        color: var(--bp-green-dark, #436513);
        font-family: var(--ff-mono);
        font-size: var(--bp-fs-micro);
        font-weight: 700;
    }
    .acc-offer__terms {
        margin: 2px 0 6px;
        font-family: var(--ff-body);
        font-size: var(--bp-fs-caption);
        color: var(--bp-ink-soft);
    }
    .acc-offer__buy {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 1px;
        width: 100%;
        padding: 8px 12px;
        border: 0;
        border-radius: 10px;
        background: var(--bp-green, #5c8a1a);
        color: #fff;
        font-family: var(--ff-body);
        font-weight: 600;
        font-size: var(--bp-fs-secondary);
        font-variant-numeric: tabular-nums;
        cursor: pointer;
    }
    .acc-offer__buy-price { font-size: var(--bp-fs-secondary); }
    .acc-offer__buy-where {
        font-weight: 400;
        /* Без `opacity`: она гасила подпись до 3.4:1 на зелёном, а это тот
           самый клуб, в который человек сейчас заплатит. Тише строки цены
           подпись делает размер, а не прозрачность. */
        font-size: var(--bp-fs-micro);
        line-height: 1.2;
        overflow-wrap: anywhere;
    }
    .acc-offer__buy:disabled { opacity: 0.6; cursor: default; }

    .acc-ab { border-bottom: 1px dashed var(--bp-line); }
    .acc-ab:last-child { border-bottom: 0; }
    /* Завершённые бледнее живых: они в списке ради вопроса «куда делись
       деньги», а не для выбора, чем платить. */
    .acc-ab--done { opacity: 0.6; }

    .acc-ab__head {
        display: grid;
        grid-template-columns: 1fr auto auto;
        grid-template-areas:
            "title value go"
            "term  term  go";
        gap: 2px 10px;
        width: 100%;
        padding: var(--bp-space-3) 0;
        border: 0;
        background: none;
        text-align: left;
        color: inherit;
        font: inherit;
        cursor: pointer;
    }
    /* `min-width` — чтобы длинное название сжималось, а не распирало колонку:
       без него ячейка `1fr` растёт до своего содержимого и выталкивает остаток
       со стрелкой за край узкого экрана. */
    .acc-ab__title {
        grid-area: title;
        min-width: 0;
        overflow-wrap: anywhere;
        font-family: var(--ff-body);
        font-weight: 600;
        font-size: var(--bp-fs-secondary);
    }
    /* Остаток абонемента — такое же «число-герой» карточки, как баланс баллов
       выше, и набирается той же гарнитурой. Моноширинный оставлен строкам
       движений: там цифры стоят столбцом и должны выравниваться по разрядам,
       а рядом с балансом баллов он читался как другой сорт числа. */
    .acc-ab__value {
        grid-area: value;
        font-family: var(--ff-display);
        font-weight: 900;
        font-size: var(--bp-fs-body);
        letter-spacing: -0.01em;
        white-space: nowrap;
    }
    .acc-ab__term {
        grid-area: term;
        font-family: var(--ff-body);
        font-size: var(--bp-fs-caption);
        color: var(--bp-ink-soft);
    }
    .acc-ab__go { grid-area: go; align-self: center; color: var(--bp-ink-soft); }

    /* Строка движения в чеке абонемента: за что, когда и на сколько. */
    .acc-mv {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: 0 10px;
        /* Движение — строка ВНУТРИ чека абонемента, ступенью ниже его
           собственных строк: их тут бывает три десятка. */
        padding: var(--bp-space-2) 0;
        border-bottom: 1px dashed var(--bp-line);
        font-size: var(--bp-fs-secondary);
    }
    .acc-mv:last-child { border-bottom: 0; }
    .acc-mv__title {
        min-width: 0;
        font-family: var(--ff-body);
    }
    .acc-mv__at {
        font-family: var(--ff-mono);
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.04em;
        color: var(--bp-ink-soft);
    }
    .acc-mv__sum {
        grid-row: 1 / span 2;
        grid-column: 2;
        align-self: center;
        font-family: var(--ff-mono);
        font-weight: 700;
        font-variant-numeric: tabular-nums;
        white-space: nowrap;
    }

    .tx-receipt-row {
        display: flex;
        justify-content: space-between;
        gap: 12px;
        padding: var(--bp-space-3) 0;
        border-bottom: 1px dashed var(--bp-line);
    }
    .tx-receipt-row:last-child { border-bottom: 0; }

    .tx-receipt-row .lhs {
        flex: 1;
        min-width: 0;
        display: flex;
        flex-direction: column;
        gap: 2px;
    }
    .tx-receipt-row .lhs .date {
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.14em;
        text-transform: uppercase;
        color: var(--bp-muted);
    }
    .tx-receipt-row .lhs .desc {
        font-family: var(--ff-body);
        font-size: var(--bp-fs-secondary);
        color: var(--bp-ink);
        line-height: 1.35;
        word-break: break-word;
    }
    .tx-receipt-row .lhs .expires {
        font-family: var(--ff-mono);
        font-size: var(--bp-fs-micro);
        color: var(--bp-muted);
        line-height: 1.4;
        margin-top: 2px;
    }

    .tx-receipt-row .rhs {
        text-align: right;
        flex-shrink: 0;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 2px;
    }
    .tx-receipt-row .rhs .pts {
        font-family: var(--ff-mono);
        font-weight: 700;
        font-size: var(--bp-fs-secondary);
        font-variant-numeric: tabular-nums;
        white-space: nowrap;
    }
    .tx-receipt-row.earn .pts { color: var(--bp-green-dark); }
    .tx-receipt-row.spend .pts { color: var(--bp-stamp-red); }

    .tx-receipt-foot {
        background: var(--bp-paper-2);
        padding: 12px 20px 18px;
        text-align: center;
        border-top: 1px dashed var(--bp-line);
    }
    .tx-receipt-help {
        background: transparent;
        border: 0;
        cursor: pointer;
        font-family: var(--ff-mono);
        font-weight: 600;
        font-size: var(--bp-fs-micro);
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: var(--bp-blue);
        padding: 6px 10px;
        display: inline-flex;
        align-items: center;
        gap: 6px;
        transition: opacity .15s ease;
    }
    @media (hover: hover) and (pointer: fine) {
        .tx-receipt-help:hover { opacity: 0.75; }
    }
    .tx-receipt-help i { font-size: var(--bp-fs-caption); }
