/* Публичный лендинг на `/` — templates/core/landing.html.
 *
 * Страница всегда тёмная и намеренно НЕ читает `data-theme`: это витрина
 * продукта, а не экран кабинета. Токены объявлены на `.lp`, чтобы ничего
 * не протекло в остальное приложение.
 *
 * Движение разложено по слоям (см. блок «ГАШЕНИЕ ДВИЖЕНИЯ» внизу):
 *   1 — вестибулярное (потоки, пульсации, вращение, луч) — снимается целиком;
 *   2 — смещения при появлении — заменяются короткой прозрачностью;
 *   3 — цвет и рамка на наведении — остаётся всегда.
 * Снимает движение системная настройка `prefers-reduced-motion`.
 */

/* `clip`, а не `hidden`: `overflow-x: hidden` при видимом Y вычисляется в
   `hidden auto` и делает body скролл-контейнером. Тогда view-таймлайн карты
   цепляется за него, а не за окно, и застывает — объекты не растут. */
body.landing-page {
    background: #121212;
    overflow-x: clip;
}

/* Оболочка кабинета анониму не рендерится, но подстрахуемся от её отступов. */
body.landing-page .main-content { padding: 0; margin: 0; }

.lp {
    --lp-bg: #121212;
    --lp-bg2: #171717;
    --lp-surface: #1C1C1C;
    /* Контраст хайрлайнов подобран по факту: на .09 схематика тонула в фоне. */
    --lp-line: rgba(245, 244, 242, .13);
    --lp-line2: rgba(245, 244, 242, .26);
    --lp-line3: rgba(245, 244, 242, .52);
    --lp-text: #F5F4F2;
    --lp-muted: #A39E99;
    /* Не темнее: подписи на этом токене лежат и на приподнятых поверхностях
       (--lp-surface #1C1C1C), там у #8E8882 — 4.9:1 при пороге AA 4.5:1.
       Прежний #857F79 был подобран по #121212 и на поверхностях проваливался. */
    --lp-dim: #8E8882;
    --lp-accent: #ed8936;
    --lp-warm: #E6E3E0;
    --lp-warm-ink: #1A1A1A;
    --lp-warm-line: rgba(26, 26, 26, .16);
    /* Тёмный акцент для тёплой секции: #ed8936 на #E6E3E0 даёт ~2.3:1,
       мелкие подписи на нём нечитаемы. Этот — 4.6:1, порог AA. */
    --lp-warm-accent: #9E4E0C;

    --lp-wrap: 1200px;
    /* Высота липкой шапки: ряд 84 плюс её хайрлайн. Отсюда её берёт
       `scroll-margin-top` секций — иначе якорь из меню и полоса
       разъезжаются при любой правке высоты. */
    --lp-nav-h: 85px;
    --lp-sans: 'Space Grotesk', 'Helvetica Neue', Arial, sans-serif;
    --lp-mono: 'JetBrains Mono', ui-monospace, monospace;

    /* ── движение ────────────────────────────────────────────────────
       Такт 120мс. Каждая длительность на странице — его кратное, иначе
       десяток независимых циклов расходится и совпадает в случайных
       местах: движение читается как шум, а не как ритм.
       Циклы названы по числу тактов: base = 6s = 50 тактов. */
    --lp-tap: 120ms;      /* цвет, рамка — отклик без хода          */
    --lp-quick: 240ms;    /* наведение                              */
    --lp-enter: 720ms;    /* появление блока                        */
    --lp-draw: 1680ms;    /* прорисовка схемы                       */

    --lp-cyc-base: 7.5s;  /* цепочка смены: 4.2s ход + гашение + пауза */
    --lp-cyc-story: 12s;  /* сцены-истории: диалог, запрос          */
    --lp-cyc-xc: 24s;     /* биржа: конвейер из двух наборов        */

    /* Две кривые на весь лендинг: вход резкий и мягко тормозит, цикл
       симметричен. Раньше их было двенадцать, причём три различались в
       третьем знаке — то есть выбирались не глазом. */
    --lp-ease-out: cubic-bezier(.16, 1, .3, 1);
    --lp-ease-loop: cubic-bezier(.65, 0, .35, 1);

    position: relative;
    background: var(--lp-bg);
    color: var(--lp-text);
    font-family: var(--lp-sans);
    font-size: 17px;
    line-height: 1.6;
    /* Тоже clip: `hidden` — это скролл-контейнер без полосы, и ближайшим
       скроллером для карты оказывался он, а не окно. */
    overflow: clip;
}

/* Не `.lp a`: селектор типа поднимает специфичность до (0,1,1) и бьёт
   одноклассовые правила компонентов — текст на кнопке становился акцентным
   на акцентном фоне. Ссылки красим только там, где это ссылка по смыслу. */
.lp a { text-decoration: none; }
.lp-link { color: var(--lp-accent); }
.lp-link:hover { color: #f6ad55; }

/* Все координаты SVG посчитаны для LTR. На ивритской локали dir="rtl"
   наследуется внутрь inline-SVG и text-anchor: start начинает вести глифы
   ВЛЕВО от x — подписи вылезают из карточек и за viewBox. Биди-порядок
   букв внутри каждой строки браузер сохраняет и при direction: ltr. */
.lp svg { direction: ltr; }

/* ── сквозные гайды ──────────────────────────────────────────────────── */
.lp-guides { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
.lp-guides span {
    position: absolute; top: 0; bottom: 0; width: 0;
    border-inline-start: 1px dashed rgba(245, 244, 242, .07);
}
.lp-guides span:nth-child(1) { inset-inline-start: calc(50% - 600px); }
.lp-guides span:nth-child(2) { inset-inline-start: calc(50% - 200px); }
.lp-guides span:nth-child(3) { inset-inline-start: calc(50% + 200px); }
.lp-guides span:nth-child(4) { inset-inline-start: calc(50% + 600px); }

/* ── каркас ──────────────────────────────────────────────────────────── */
/* Экран — нижняя граница блока, а не его размер.

   `min-height`, а не `height`: блоку, которому нужно больше, экран не
   мешает — он вырастает, и текст не приходится резать ради ровной сетки.
   Снизу же блок заперт: короткий не схлопывается в полосу, и страница
   читается как серия экранов, а не как лента.

   Содержимое центрируется по вертикали, поэтому короткий блок стоит
   посреди экрана, а не липнет к верхнему краю.

   `svh`, а не `vh`: на мобильных `vh` меряется по окну без адресной
   строки, и блок каждый раз оказывается выше видимой области. */
.lp-sec {
    /* Липкая шапка: без запаса якорь из меню привёл бы секцию под неё. */
    scroll-margin-top: var(--lp-nav-h);
    position: relative; z-index: 1; padding: 112px 0;
    min-height: 100svh;
    display: flex;
    flex-direction: column;
    justify-content: center;
}
.lp-sec--alt {
    background: var(--lp-bg2);
    border-block: 1px solid var(--lp-line);
}
.lp-sec--warm { background: var(--lp-warm); color: var(--lp-warm-ink); }

.lp-sec--iso { background: var(--lp-bg); border-block: 1px solid var(--lp-line); overflow: clip; }
.lp-sec--iso > .lp-wrap { position: relative; z-index: 1; }
.lp-isobg { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
.lp-planebg { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
.lp-planebg-grid path { fill: none; }
.lp-isobg-row path, .lp-planebg-grid path {
    stroke: rgba(245, 244, 242, .11);
    stroke-width: 1; stroke-dasharray: 3 3;
    vector-effect: non-scaling-stroke;
}
.lp-isobg-body, .lp-isobg-cap { fill: var(--lp-bg); }
[dir="rtl"] .lp-isobg { transform: scaleX(-1); }
.lp-wrap { width: min(var(--lp-wrap), 100% - 48px); margin-inline: auto; }
.lp-stack { display: flex; flex-direction: column; gap: 48px; }
.lp-split { display: flex; gap: 80px; align-items: flex-start; }
.lp-split--center { align-items: center; }
/* Арт слева, текст справа: пять одинаковых сплитов подряд читались как
   один повторённый шаблон. На ≤980 колоночный режим перекрывает reverse. */
.lp-split--flip { flex-direction: row-reverse; }
.lp-col { flex: 1 1 0; display: flex; flex-direction: column; gap: 26px; }
.lp-col--list { gap: 20px; }
.lp-grid { display: grid; gap: 20px; }
.lp-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
.lp-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.lp-grid--5 { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 30px; }

/* ── типографика ─────────────────────────────────────────────────────── */
.lp-tag {
    font-family: var(--lp-mono);
    font-size: 11px; font-weight: 500; letter-spacing: .2em;
    text-transform: uppercase; color: var(--lp-dim);
}
.lp-h1 { margin: 0; font-size: 62px; font-weight: 500; line-height: 1.02; letter-spacing: -.035em; text-wrap: balance; }
.lp-h2 { margin: 0; font-size: 44px; font-weight: 500; line-height: 1.08; letter-spacing: -.03em; text-wrap: balance; }
.lp-h2--lg { font-size: 54px; max-width: 760px; }
.lp-h3 { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -.01em; }
.lp-h3--muted { color: var(--lp-muted); font-weight: 500; }
.lp-lead { margin: 0; font-size: 19px; color: var(--lp-muted); line-height: 1.55; text-wrap: pretty; }
.lp-body { margin: 0; color: var(--lp-muted); text-wrap: pretty; }
.lp-body--sm { font-size: 15px; }
.lp-note {
    margin: 0; font-family: var(--lp-mono);
    font-size: 11px; letter-spacing: .12em; color: var(--lp-dim);
}
.lp-accent { color: var(--lp-accent); }

.lp-lang { unicode-bidi: plaintext; }
.lp-shead { display: flex; flex-direction: column; gap: 18px; max-width: 720px; }

/* .66, не ниже: на #E6E3E0 это 5.0:1 для мелкого моно-текста (AA 4.5:1). */
.lp-sec--warm .lp-tag { color: rgba(26, 26, 26, .66); }
.lp-sec--warm .lp-lead { color: rgba(26, 26, 26, .72); }
.lp-sec--warm .lp-note { color: rgba(26, 26, 26, .66); }
.lp-sec--warm .lp-body { color: rgba(26, 26, 26, .72); }
.lp-sec--warm .lp-card { background: #EFEDEA; border-color: var(--lp-warm-line); }
.lp-sec--warm .lp-card:hover { border-color: rgba(26, 26, 26, .38); background: #F2F0ED; }
.lp-tag--accent { color: var(--lp-accent); }
.lp-sec--warm .lp-tag--accent { color: var(--lp-warm-accent); }
/* Схематика на тёплом фоне: те же роли линий, но чернилами по бумаге. */
.lp-art--xc .lp-stroke { stroke: rgba(26, 26, 26, .42); }
/* .3, не .2: этим тоном покрашены разом каналы, ядро площадки и вторые
   строки карточек. На .2 они дают меньше 3:1 к #E6E3E0 — на телефоне и
   после сжатия сцена рассыпалась на несвязанные объекты. */
.lp-art--xc .lp-stroke--faint { stroke: rgba(26, 26, 26, .3); }
.lp-art--xc .lp-stroke--bright { stroke: rgba(26, 26, 26, .6); }
/* Тёплый акцент, а не общий: #ed8936 на #E6E3E0 — 2.3:1, тонкая линия на
   нём исчезает. Тот же токен, что у акцентных тегов этой секции. */
.lp-art--xc .lp-stroke--accent { stroke: var(--lp-warm-accent); }
.lp-art--xc .lp-fill-accent { fill: var(--lp-warm-accent); }
.lp-art--xc .lp-panel { fill: #F7F5F3; }
.lp-art--xc .lp-panel--deep { fill: #CFC9C3; }
.lp-art--xc .lp-svg-lbl { fill: rgba(26, 26, 26, .66); }
.lp-art--xc .lp-svg-lbl--strong { fill: var(--lp-warm-ink); }
.lp-art--xc .lp-xc-tip { fill: rgba(26, 26, 26, .6); }
/* Спина сцены — единственный канал: сплошная, чтобы выдача читалась
   и тогда, когда движение выключено. */
.lp-art--xc .lp-xc-spine { stroke-width: 1.2; }
/* Спрос — своя плотность, а не свой цвет: третья краска в двухцветной
   сцене стала бы третьим смыслом. Флажок вакансии залит чернилами и
   потому весит больше любого контурного резюме. */
.lp-art--xc .lp-xc-demand-fill { fill: rgba(26, 26, 26, .62); }

/* ── кнопки и навигация ──────────────────────────────────────────────── */
.lp-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 10px;
    min-height: 52px; padding-inline: 28px; border-radius: 999px;
    background: var(--lp-accent); color: #17110A;
    font-size: 16px; font-weight: 600; letter-spacing: -.01em;
    border: none; cursor: pointer;
    transition: filter .2s, transform .2s, color .2s;
}
.lp-btn:hover { filter: brightness(1.08); color: #17110A; transform: translateY(-2px); }
.lp-btn--ghost { background: transparent; color: var(--lp-text); border: 1px solid var(--lp-line2); }
.lp-btn--ghost:hover { border-color: var(--lp-text); color: var(--lp-text); }
.lp-btn--sm { min-height: 42px; padding-inline: 22px; font-size: 15px; }
.lp-actions { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }

/* Шапка липнет к верху и уезжает по ходу чтения — состоянием управляет
   `landing.mjs`, здесь только вид и ход.

   В покое фон прозрачный: сквозные гайды идут через шапку, за это они и
   названы сквозными. Фон приходит с `is-stuck`, когда страница ушла с нуля и
   шапка действительно висит над контентом — иначе над светлой секцией биржи
   (#E6E3E0) её светлый текст пропал бы. Полоса при этом во всю ширину, а
   колонку держит `--inner`: фон, обрезанный по 1200px, пропускал бы
   уезжающий контент в поля.

   Фон ставится независимо от настройки покоя — это читаемость, слой 3.
   Уезжает шапка только когда движение разрешено: это слой 1. */
.lp-nav {
    position: sticky; top: 0; z-index: 10;
    background: transparent;
    border-bottom: 1px solid var(--lp-line);
    /* Четыре такта и симметричная кривая, а не `--lp-move` с резким
       `--lp-ease-out`: полоса уходит и приходит одним и тем же жестом,
       и вход, обрывающийся почти мгновенно, читался как рывок. */
    transition: transform calc(var(--lp-tap) * 4) var(--lp-ease-loop),
                background-color var(--lp-quick) linear;
}
.lp-nav.is-stuck { background: var(--lp-bg); }
.lp-nav-inner {
    display: flex; align-items: center; justify-content: space-between;
    width: min(var(--lp-wrap), 100% - 48px); margin-inline: auto;
    min-height: calc(var(--lp-nav-h) - 1px);
}
.lp-nav.is-hidden { transform: translateY(-100%); }
/* Уехавшая шапка не должна прятать фокус: пришедший табом внутрь возвращает
   её на место, иначе ссылки фокусируются за краем экрана. */
.lp-nav.is-hidden:focus-within { transform: none; }
.lp-nav-links { display: flex; gap: 30px; }
.lp-nav-right { display: flex; align-items: center; gap: 22px; }
.lp-nav-link { color: var(--lp-muted); font-size: 15px; transition: color .2s; }
.lp-nav-link:hover { color: var(--lp-text); }
.lp-logo {
    direction: ltr; white-space: nowrap;
    font-size: 21px; font-weight: 600; letter-spacing: -.02em; color: var(--lp-text);
}
.lp-logo:hover { color: var(--lp-text); }
.lp-logo--sm { font-size: 17px; }

.lp-logo-mark {
    height: 1.24em; width: auto;
    vertical-align: middle;
    margin-inline-end: .14em;
    position: relative; top: -.04em;
}

/* ── переключатель языка ──────────────────────────────────────────────
   Компонент общий — `includes/language_switcher.html`: разметка, форма на
   `set_language` и inline-JS в base.html работают анониму как есть. Здесь
   только шкура: базовые стили рассчитаны на хедер кабинета и на #121212
   читаются как чужой элемент.
   Все отступы логические: на иврите шапка зеркалится вместе со страницей,
   а физические `right`/`left` базы остались бы приколоченными к правому
   краю. Поэтому каждое из них здесь гасится явным `auto`.
   ------------------------------------------------------------------ */
.lp .language-switcher--lp { display: flex; }
.lp .language-switcher--lp .custom-select-wrapper { min-width: 0; }

/* `rtl.css` двигает свитчер в середину шапки кабинета правилом без скоупа
   (`[dir="rtl"] .language-switcher { order: 2 }`). На лендинге оно попадало
   в свою же цель: группа справа сама вставала зеркально, а свитчер внутри
   неё уезжал в конец — язык оказывался слева от кнопки демо. */
[dir="rtl"] .lp .language-switcher--lp { order: 0; }

.lp .language-switcher--lp .custom-select-trigger {
    gap: 8px;
    min-height: 42px;
    padding-block: 0;
    padding-inline: 12px 32px;
    border: 1px solid var(--lp-line);
    border-radius: 10px;
    background: transparent;
    box-shadow: none;
    color: var(--lp-muted);
    font-family: var(--lp-sans);
    font-size: 14px;
    transition: color var(--lp-quick), border-color var(--lp-quick);
}
.lp .language-switcher--lp .custom-select-trigger:hover,
.lp .language-switcher--lp .custom-select.open .custom-select-trigger {
    background: transparent;
    border-color: var(--lp-line3);
    color: var(--lp-text);
}

.lp .language-switcher--lp .globe-icon { width: 16px; height: 16px; flex-shrink: 0; }
.lp .language-switcher--lp .custom-select-text { flex: 0 0 auto; }
.lp .language-switcher--lp .fi { border-radius: 2px; box-shadow: none; }

.lp .language-switcher--lp .custom-select-arrow {
    right: auto;
    inset-inline-end: 12px;
    width: 10px; height: 10px;
    color: var(--lp-dim);
}

.lp .language-switcher--lp .custom-select-options {
    inset-inline: auto 0;
    min-width: 100%;
    width: max-content;
    background: var(--lp-surface);
    border: 1px solid var(--lp-line2);
    border-radius: 10px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, .45);
}
.lp .language-switcher--lp .custom-select-option {
    gap: 8px;
    padding: 10px 14px;
    color: var(--lp-muted);
    font-size: 14px;
    white-space: nowrap;
}
.lp .language-switcher--lp .custom-select-option:hover { background: var(--lp-bg2); color: var(--lp-text); }
.lp .language-switcher--lp .custom-select-option.selected { background: transparent; color: var(--lp-accent); }

/* ── счётчики ────────────────────────────────────────────────────────── */
.lp-stats { display: flex; border-top: 1px solid var(--lp-line); }
.lp-stats--inline { margin-top: 6px; }
.lp-stat { flex: 1 1 0; padding-top: 22px; display: flex; flex-direction: column; gap: 4px; }
.lp-stat-num {
    font-family: var(--lp-mono); font-size: 30px; font-weight: 500;
    letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.lp-stat-lbl { font-size: 13px; color: var(--lp-dim); }

/* ── карточки и списки ───────────────────────────────────────────────── */
.lp-card {
    background: var(--lp-bg2); border: 1px solid var(--lp-line); border-radius: 3px;
    padding: 28px; display: flex; flex-direction: column; gap: 12px;
    transition: border-color .3s, background .3s, transform .3s;
}
.lp-card:hover { border-color: var(--lp-line2); background: var(--lp-surface); transform: translateY(-3px); }
.lp-card--ghost { background: var(--lp-bg); border-style: dashed; justify-content: space-between; }
.lp-card--ghost:hover { background: var(--lp-bg); }
/* Иконки тихие, акцент — откликом на наведение: восемь оранжевых пятен
   в одном экране были самым громким кластером акцента на странице. */
.lp-card-ic { color: var(--lp-muted); height: 24px; transition: color var(--lp-quick); }
.lp-card:hover .lp-card-ic { color: var(--lp-accent); }
.lp-item {
    display: flex; flex-direction: column; gap: 8px;
    padding-bottom: 22px; border-bottom: 1px solid var(--lp-line);
}
.lp-item--last { padding-bottom: 0; border-bottom: none; }

/* Лента резиновая: viewBox 1200 равен wrap, но wrap сужается раньше, чем
   срабатывает мобильная точка, и фиксированный SVG вылезал за контейнер —
   станции уезжали от своих номеров на полколонки. */
.lp-pipe svg { width: 100%; height: auto; }
[dir="rtl"] .lp-pipe svg { transform: scaleX(-1); }
.lp-steps { list-style: none; margin: -26px 0 0; padding: 0; }
.lp-step { display: flex; flex-direction: column; gap: 10px; }

/* ── hero ────────────────────────────────────────────────────────────── */
/* Земля под объектом нарочно шире своей колонки и уходит в боковое поле.
   `clip`, а не `hidden`: hidden сделал бы секцию скролл-контейнером и увёл
   бы якорную навигацию. Подрезка по секции — единственная граница, которая
   у сетки есть; всё остальное гасит маска.

   Пол в экран здесь на 84px ниже, чем у остальных блоков: первый экран
   делит окно с шапкой, и без вычета hero плюс шапка перерастают его уже
   на пустом месте. */
.lp-hero {
    padding-top: 76px; padding-bottom: 0; overflow: clip;
    min-height: calc(100svh - 84px);
}
.lp-hero-art svg { overflow: visible; }
.lp-hero-inner { display: flex; align-items: center; gap: 60px; }
.lp-hero-copy { flex: 0 0 600px; display: flex; flex-direction: column; gap: 30px; }
.lp-hero-lead { max-width: 500px; }
.lp-hero-art { flex: 0 0 540px; }
.lp-art { flex: 0 0 540px; }
.lp-art--phone { flex: 0 0 420px; display: flex; justify-content: center; }
.lp-art--dash { flex: 0 0 540px; display: flex; flex-direction: column; gap: 18px; }
/* Сцена биржи лежит не в колонке сплита, а во всю ширину стека — между
   шапкой и карточками. flex: none — базис 540px здесь стал бы высотой. */
.lp-art--xc { flex: none; display: flex; justify-content: center; }
/* 920, не 1040: на экране 900px высоты секция со сценой в полную ширину
   выталкивала карточки выгод целиком за фолд. */
.lp-art--xc .lp-xc-wide { width: min(920px, 100%); height: auto; }
.lp-art--xc .lp-xc-tall { display: none; width: min(340px, 100%); height: auto; }
.lp-sec--warm .lp-stack { gap: 28px; }
.lp-art--lng { flex: 0 0 540px; }
.lp-cta-inner { display: flex; flex-direction: column; align-items: center; gap: 28px; text-align: center; }
.lp-cta-lead { max-width: 620px; }
.lp-cta { border-top: 1px solid var(--lp-line); }

/* ── схематика ───────────────────────────────────────────────────────── */
.lp-stroke { fill: none; stroke: var(--lp-line2); stroke-width: 1; }
.lp-stroke--faint { stroke: var(--lp-line); }
.lp-stroke--bright { stroke: var(--lp-line3); }
.lp-stroke--accent { stroke: var(--lp-accent); }
.lp-fill-accent { fill: var(--lp-accent); }
/* Класс, а не атрибут fill: presentation attribute проигрывает любому
   правилу таблицы, и `.lp-stroke { fill: none }` его съедает. */
.lp-panel { fill: var(--lp-bg2); }
.lp-panel--deep { fill: var(--lp-bg); }
.lp-channel { stroke-dasharray: 3 5; }
.lp-svg-lbl { font-family: var(--lp-mono); font-size: 11px; letter-spacing: .1em; fill: var(--lp-muted); }
.lp-svg-lbl--strong { fill: var(--lp-text); letter-spacing: .2em; }
/* Подписи узкой раскладки: 320 юнитов на всю сцену, «ВАКАНСИЯ КАБЛАНА»
   на 11px не влезает в свою колонку. 9.5px — предел, ниже подпись
   перестаёт читаться на экране телефона. */
.lp-svg-lbl--sm { font-size: 9.5px; letter-spacing: .06em; }
/* Ключ карточки отчёта — акцентом, значение — основным текстом:
   пара «что это / что там» должна читаться с расстояния как одна строка. */
.lp-svg-lbl--key { fill: var(--lp-accent); letter-spacing: .12em; }

[dir="rtl"] .lp-svg-lbl { letter-spacing: .04em; }
[dir="rtl"] .lp-svg-lbl--strong { letter-spacing: .08em; }
[dir="rtl"] .lp-lng-who { letter-spacing: .06em; }
.lp-svg-lbl--val { fill: var(--lp-text); letter-spacing: .02em; }

/* ── Telegram: мокап телефона ────────────────────────────────────────
   Палитра — ночная тема Telegram (#0E1621 фон, #17212B шапка, #182533
   входящие, #2B5278 исходящие): страница показывает отчёт и бота там,
   где их читают на самом деле, поэтому чат не перекрашен в цвета лендинга. */
/* Пропорции корпуса — 71.6 × 147.6 мм, как у аппарата с Dynamic Island:
   на глаз мокап был приземистым (1.66 против 2.06).
   Радиусы обязаны быть концентричными: внешний = внутренний + рамка
   (10 padding + 1 border), иначе угол экрана идёт непараллельно углу
   корпуса — именно это и читается как кривой рендер. */
.lp-phone {
    position: relative; display: flex;
    width: 320px; aspect-ratio: 71.6 / 147.6; box-sizing: border-box;
    padding: 10px; border-radius: 48px;
    background: #060607; border: 1px solid var(--lp-line2);
    box-shadow: 0 34px 80px rgba(0, 0, 0, .5);
}
/* Торцевые клавиши: слева действие + две громкости, справа питание.
   Доли — от высоты корпуса. Три полосы одной подложкой, потому что
   псевдоэлементов на сторону всего один, а класть их в разметку значит
   дублировать декор в двух блоках. */
.lp-phone::before, .lp-phone::after {
    content: ''; position: absolute; width: 3px; border-radius: 1px;
}
/* Доли высоты корпуса взяты с размерного чертежа iPhone 16 (Apple,
   developer.apple.com/accessories/dimensional-drawings): Action 20.75–25.42%,
   громкость 28.87–36.46% и 38.49–46.08%, питание справа 31.48–43.47%. */
.lp-phone::before {
    left: -3px; top: 20.75%; height: 25.33%;
    background:
        linear-gradient(#33333b, #33333b) 0 0      / 100% 18.44% no-repeat,
        linear-gradient(#33333b, #33333b) 0 45.77% / 100% 29.96% no-repeat,
        linear-gradient(#33333b, #33333b) 0 100%   / 100% 29.96% no-repeat;
}
.lp-phone::after {
    right: -3px; top: 31.48%; height: 11.99%;
    background: #33333b;
}
.lp-phone-screen {
    display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0;
    border-radius: 37px; overflow: hidden; background: #0E1621;
}
.lp-tgm-status {
    position: relative; display: flex; align-items: center;
    justify-content: space-between; padding: 12px 24px 8px;
    background: #17212B; color: #F5F5F5;
}
.lp-tgm-clock { font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.lp-tgm-island {
    position: absolute; left: 50%; top: 9px; transform: translateX(-50%);
    width: 82px; height: 23px; border-radius: 999px; background: #060607;
}
.lp-tgm-head { display: flex; align-items: center; gap: 12px; padding: 8px 14px 12px; background: #17212B; }
/* Шеврон «назад» — направленный глиф, а не картинка: в RTL-раскладке Telegram
   он стоит справа и смотрит вправо. Флекс переносит его сам, разворачивает
   этот трансформ — тем же приёмом, что `.lp-lng-tip` ниже. Треугольник Play
   рядом НЕ зеркалим: направление воспроизведения от письма не зависит. */
[dir="rtl"] .lp-tgm-head > svg:first-child { transform: scaleX(-1); }

/* Строка состояния в RTL зеркалится вместе с системой: часы уезжают вправо,
   сигнал и батарея влево. Флекс переносит группу сам, но глиф остаётся в
   LTR-форме — носик батареи смотрит наружу экрана, шкала сигнала растёт не в
   ту сторону. Их и переворачиваем; цифры часов не трогаем — числа в RTL
   пишутся как есть. Обе иконки лежат в одном svg, хватает одного правила. */
[dir="rtl"] .lp-tgm-status > svg { transform: scaleX(-1); }

/* Спарклайн в плитке — тот же ряд по дням, что и столбцы под ним. Столбцы с
   подписями оси переворачивает сам флекс, а точки polyline заданы в
   координатах и остаются как есть: на иврите выходили две встречные оси
   времени в одном экране. Разворачиваем и его. */
[dir="rtl"] .lp-kpi-spark { transform: scaleX(-1); }
.lp-tgm-ava {
    display: flex; align-items: center; justify-content: center; flex: none;
    width: 36px; height: 36px; border-radius: 50%;
    background: #212E42;
}
.lp-tgm-ava img { display: block; height: 24px; width: auto; }
/* Заголовок по центру, аватар справа — раскладка шапки iOS Telegram. */
.lp-tgm-title { display: flex; flex-direction: column; align-items: center; gap: 1px; flex: 1 1 auto; min-width: 0; }
.lp-tgm-name { font-size: 14.5px; font-weight: 600; color: #F5F5F5; white-space: nowrap; }
.lp-tgm-sub { font-size: 12px; color: #8096AB; }
/* Прижато к низу, как в настоящем чате: новое сообщение приходит к полю ввода. */
/* Чат забирает всю свободную высоту корпуса: у аппарата она задана
   пропорцией, а не длиной переписки. */
/* gap здесь нельзя: он отделяет и нераскрытые пузыри, оставляя внизу
   лесенку пустых отступов. Интервал даёт margin-top пузыря, который
   раскрывается вместе с ним. */
.lp-tgm-chat { display: flex; flex-direction: column; justify-content: flex-end; flex: 1 1 auto; padding: 14px 10px; min-height: 0; }
.lp-tgm-date {
    align-self: center; padding: 3px 10px; border-radius: 999px;
    background: rgba(0, 0, 0, .28); color: #8FA3B5; font-size: 11.5px;
}
/* border-box обязателен: max-height в анимации появления режет content-box,
   и без него нераскрытый пузырь всё равно занимал бы свои 14px padding. */
.lp-tgm-msg {
    box-sizing: border-box; min-height: 0;
    max-width: 86%; padding: 7px 10px; border-radius: 14px;
    font-size: 13.5px; line-height: 1.4; color: #F5F5F5;
}
.lp-tgm-msg--in { align-self: flex-start; background: #182533; border-bottom-left-radius: 5px; }
.lp-tgm-msg--out { align-self: flex-end; background: #2B5278; border-bottom-right-radius: 5px; }
/* Время — в потоке текста, как в Telegram: прижато к нижнему углу пузыря. */
.lp-tgm-time { float: inline-end; margin-block: 8px -2px; margin-inline: 10px -2px; font-size: 10.5px; color: #8096AB; }
.lp-tgm-msg--out .lp-tgm-time { color: #C8D9EC; }
/* Блок, а не inline-flex: время сообщения — плавающий элемент, и рядом с
   инлайновым кадром оно вставало в строку с ним, то есть в правый верхний
   угол пузыря. За блоком float падает на следующую строку — под кадр. */
.lp-tgm-video {
    position: relative; display: grid; place-items: center;
    width: 168px; height: 88px; box-sizing: border-box;
    border-radius: 10px; margin: 2px 0 0;
    background:
        radial-gradient(120px 70px at 75% 20%, rgba(237, 137, 54, .25), transparent 70%),
        linear-gradient(160deg, #26313C, #10171E);
}
/* Длительность — в левом верхнем углу кадра, как в Telegram. */
.lp-tgm-video span {
    position: absolute; top: 6px; inset-inline-start: 7px;
    padding: 1px 7px; border-radius: 9px; background: rgba(0, 0, 0, .45);
    font-size: 10.5px; color: #E7EEF5;
}
.lp-tgm-kbd { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 6px; padding: 8px; background: #17212B; }
.lp-tgm-key {
    text-align: center; padding: 10px 4px; border-radius: 8px;
    background: #232F3D; color: #DFE7EE; font-size: 12.5px;
}
.lp-tgm-input { display: flex; align-items: center; gap: 12px; padding: 12px 16px 6px; background: #17212B; color: #708499; font-size: 14px; }
.lp-tgm-input span { flex: 1 1 auto; }
/* Полоска home-индикатора: экран с island-статусбаром обязан закрываться ею. */
.lp-tgm-home { display: flex; justify-content: center; padding: 4px 0 8px; background: #17212B; }
.lp-tgm-home::after { content: ''; width: 118px; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, .38); }

/* ── аналитика ───────────────────────────────────────────────────────
   Форма взята у дашборда (`dashboard.css`: kpi-tile со спарклайном и
   дельтой, chart-card с сеткой и осями), палитра — лендинга. Высота поля
   графика живёт в --lp-plot-h: по ней же считается шаг сетки, иначе на
   мобильной высоте линии разъезжаются с подписями оси значений. */
.lp-dperiod { display: flex; gap: 6px; }
.lp-dper {
    padding: 5px 12px; border: 1px solid var(--lp-line); border-radius: 3px;
    font-family: var(--lp-mono); font-size: 11px; letter-spacing: .04em; color: var(--lp-dim);
}
.lp-dper--on { border-color: var(--lp-accent); color: var(--lp-accent); }
.lp-kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.lp-kpi {
    display: flex; flex-direction: column; gap: 3px;
    padding: 14px 16px; border: 1px solid var(--lp-line); border-radius: 3px;
    background: var(--lp-surface);
}
.lp-kpi-num {
    font-family: var(--lp-mono); font-size: 26px; font-weight: 500;
    letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.lp-kpi-lbl { font-size: 12px; color: var(--lp-dim); }
.lp-kpi-spark { display: block; width: 100%; height: 26px; margin-top: auto; padding-top: 10px; }
.lp-kpi-spark polyline { fill: none; stroke: var(--lp-accent); stroke-width: 1.5; opacity: .6; vector-effect: non-scaling-stroke; }
/* Прод красит рост в --success-color; на странице один акцент, второй
   цвет ради 11px подписи её не стоит. */
.lp-kpi-delta { font-family: var(--lp-mono); font-size: 11px; color: var(--lp-accent); }
.lp-nlq {
    display: flex; align-items: center; gap: 12px; padding: 16px 20px;
    border: 1px solid var(--lp-line2); border-radius: 3px; background: var(--lp-bg2);
    color: var(--lp-dim);
}
.lp-nlq > svg { flex: 0 0 17px; }
/* min-width: 0 — печать не должна раздвигать колонку: у флекс-ряда
   минимальная ширина по умолчанию равна содержимому, и она росла бы
   вместе с набираемой строкой. */
.lp-nlq-line { display: flex; align-items: center; gap: 12px; flex: 1 1 auto; min-width: 0; overflow: hidden; }
.lp-typed {
    display: inline-block; overflow: hidden; white-space: nowrap; vertical-align: bottom;
    font-family: var(--lp-mono); font-size: 14px; color: var(--lp-text);
}
.lp-caret { width: 8px; height: 17px; background: var(--lp-accent); flex: 0 0 8px; }
.lp-nlq-send {
    display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
    width: 30px; height: 30px; border-radius: 3px;
    background: var(--lp-accent); color: #17110A;
}
.lp-chart {
    display: flex; flex-direction: column; gap: 14px;
    padding: 18px 20px; border: 1px solid var(--lp-line); border-radius: 3px;
    background: var(--lp-surface);
}
.lp-chart-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.lp-chart-title { font-size: 14px; font-weight: 600; }
.lp-chart-sel {
    display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
    padding: 5px 10px; border: 1px solid var(--lp-line); border-radius: 3px;
    font-size: 12px; color: var(--lp-muted);
}
.lp-plot { --lp-plot-h: 168px; display: flex; gap: 10px; }
.lp-yaxis {
    display: flex; flex-direction: column; justify-content: space-between;
    height: var(--lp-plot-h); text-align: end;
    font-family: var(--lp-mono); font-size: 11px; color: var(--lp-dim);
}
.lp-yaxis span { line-height: 0; }
.lp-pcol { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
/* Ось под столбцами: без неё бары «парили». Сетка лежит под ними — как в Chart.js. */
.lp-bars {
    display: flex; align-items: flex-end; gap: 14px; height: var(--lp-plot-h);
    border-bottom: 1px solid var(--lp-line2);
    background-image: repeating-linear-gradient(to bottom, var(--lp-line) 0 1px, transparent 1px calc(var(--lp-plot-h) / 4));
}
.lp-barcol { flex: 1 1 0; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; }
.lp-bar { background: var(--lp-accent); border-radius: 4px 4px 0 0; transform-origin: bottom; }
.lp-xaxis { display: flex; gap: 14px; font-family: var(--lp-mono); font-size: 11px; color: var(--lp-dim); }
.lp-xaxis span { flex: 1 1 0; text-align: center; }
.lp-chart-foot { display: flex; justify-content: flex-end; }
.lp-chart-pin {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 6px 12px; border: 1px solid var(--lp-line); border-radius: 3px;
    font-size: 12px; color: var(--lp-accent);
}

/* ── языки ───────────────────────────────────────────────────────────── */
.lp-langs { display: flex; gap: 12px; flex-wrap: wrap; }
.lp-lang {
    display: flex; align-items: baseline; gap: 10px; min-height: 48px; box-sizing: border-box;
    padding: 12px 20px; border: 1px solid var(--lp-line2); border-radius: 3px;
    font-size: 18px; cursor: default; transition: border-color .2s, background .2s;
}
.lp-lang:hover { border-color: var(--lp-text); background: var(--lp-surface); }
.lp-lang span { font-family: var(--lp-mono); font-size: 11px; letter-spacing: .12em; color: var(--lp-dim); }

/* Сцена перевода: реплика рабочего проходит через узел и приходит к каблану
   на его языке. Три перевода лежат в одной grid-ячейке и сменяются по циклу;
   высота пузыря равна самому высокому из них и не дёргается на смене. */
.lp-lng { --lp-lng-dir: 1; max-width: 540px; margin-inline: auto; }
[dir="rtl"] .lp-lng { --lp-lng-dir: -1; }
.lp-lng-row { display: flex; align-items: stretch; }
.lp-lng-msg {
    box-sizing: border-box; width: min(400px, 78%);
    padding: 15px 18px; border: 1px solid var(--lp-line2); border-radius: 3px;
    background: var(--lp-surface); font-size: 16px; line-height: 1.5;
}
.lp-lng-msg--dst {
    display: grid; grid-template-rows: auto auto;
    border-color: rgba(237, 137, 54, .42); background: rgba(237, 137, 54, .08);
}
.lp-lng-who {
    display: block; margin-bottom: 7px;
    font-family: var(--lp-mono); font-size: 10px; font-weight: 500;
    letter-spacing: .18em; color: var(--lp-dim);
}
.lp-lng-msg--dst .lp-lng-who { grid-row: 1; }
/* Обе реплики блока — чужой язык внутри страницы, и направление каждая
   берёт у себя: ответ перебирает ru/he/zh, исходная всегда узбекская. Без
   этого на иврите узбекская строка вставала справа, а русский ответ слева —
   в блоке, который показывает как раз аккуратный перевод. */
.lp-lng-alt { grid-row: 2; grid-column: 1; unicode-bidi: plaintext; }
/* `display: block` обязателен: у ответа роль блока играет grid-элемент, а
   здесь строка инлайновая — на инлайне `plaintext` заводит изолят, но
   выравнивание строки остаётся за блоком-контейнером. */
.lp-lng-msg--src span[lang] { display: block; unicode-bidi: plaintext; }

.lp-lng-arm { position: relative; flex: 1 1 auto; min-width: 0; }
.lp-lng-arm::before {
    content: ''; position: absolute; inset-inline: 0;
    border-color: var(--lp-line); border-style: dashed; border-width: 0;
}
.lp-lng-arm--out::before {
    top: 50%; bottom: -44px;
    border-block-start-width: 1px; border-inline-end-width: 1px;
}
.lp-lng-arm--in::before {
    top: -44px; bottom: 50%;
    border-block-end-width: 1px; border-inline-start-width: 1px;
}
.lp-lng-mid { position: relative; height: 88px; }
.lp-lng-turn {
    position: absolute; inset-inline: 0; top: 44px; height: 0;
    border-block-start: 1px dashed var(--lp-line);
}
.lp-lng-drop { display: none; }
.lp-lng-node { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); line-height: 0; }
/* `.lp svg { direction: ltr }` держит координаты сцен, но этот наконечник —
   единственный svg на странице, который позиционируется логическим инсетом,
   и правило разворачивало ему смысл `end`: в RTL наконечник вставал у
   дальнего конца нитки, а не у пузыря, к которому ведёт. Текста внутри нет,
   значит направление можно вернуть странице. */
.lp svg.lp-lng-tip { direction: inherit; }
.lp-lng-tip { position: absolute; inset-inline-end: 0; top: 50%; transform: translateY(-50%); }
[dir="rtl"] .lp-lng-tip { transform: translateY(-50%) scaleX(-1); }

/* ── подвал ──────────────────────────────────────────────────────────── */
.lp-footer { position: relative; z-index: 1; border-top: 1px solid var(--lp-line); padding: 40px 0; }
.lp-footer-inner { display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; }

/* ══════════════════════════════════════════════════════════════════════
   ДВИЖЕНИЕ
   ══════════════════════════════════════════════════════════════════════ */

.lp-draw { stroke-dasharray: var(--len); stroke-dashoffset: var(--len); animation: lp-draw var(--lp-draw) var(--lp-ease-out) forwards; }
@keyframes lp-draw { to { stroke-dashoffset: 0; } }

/* ── hero: отчёт по каждому объекту ──────────────────────────────────
   Заголовок обещает отчёт по объекту каждое утро — сцена показывает,
   откуда он берётся: по шести этажам снизу вверх идёт скан смены, и в
   кадре, где он накрывает рабочего, из этой точки вылетает ролик. Три
   точки — три ролика; справа лежит собранный из них отчёт.

   Такт цикла 12s и ход скана linear — только так доля цикла равна доле
   пути, и момент касания можно посчитать, а не подобрать. Скан идёт
   3%→34% от y=496 до y=180 (316 единиц за 3.72s); рабочий на высоте y
   накрывается на 3 + (496-y)/316·31 процентах. Отсюда 15.36 / 23.99 / 32.43.

   Карточки отчёта выпадают ОДИН раз и остаются: это текст, который
   читают, а не движение, на которое смотрят. */
.lp-scan {
    animation: lp-scan var(--lp-cyc-story) linear infinite;
}
/* Прозрачность перемножается с прозрачностью группы: дальние рёбра видны,
   но заведомо слабее ближних, а подсветка не спорит ни с теми, ни с другими. */
.lp-scan-far { stroke-dasharray: 4 4; opacity: .4; }
/* Цвет стопов задаётся здесь, а не в разметке: акцент живёт в одном токене.
   Ореол ярче к центру, но центр закрыт корпусом — наружу выходит только край. */
.lp-scan-glow { fill: url(#lp-scan-glow-fill); }
.lp-glow-in { stop-color: var(--lp-accent); stop-opacity: .3 }
.lp-glow-mid { stop-color: var(--lp-accent); stop-opacity: .13 }
.lp-glow-out { stop-color: var(--lp-accent); stop-opacity: 0 }
@keyframes lp-scan {
    0%, 3%    { transform: translateY(96px);   opacity: 0 }
    6%        { opacity: .9 }
    31%       { opacity: .9 }
    34%       { transform: translateY(-220px); opacity: 0 }
    100%      { transform: translateY(-220px); opacity: 0 }
}

/* Перелёт: 20% цикла на каждый, старт — момент касания сканом. Пакет
   заканчивает в ЦЕНТРЕ куба (500,157.72) и гаснет на последнем отрезке, уже
   под гранью: доля, на которой луч входит в силуэт, посчитана для каждой
   точки отдельно (0.829 / 0.845 / 0.798). Раньше пакет замирал на грани,
   и исчезновение приходилось прикрывать отдельной вспышкой. */
.lp-fly {
    animation-duration: var(--lp-cyc-story);
    animation-timing-function: linear;
    animation-iteration-count: infinite;
}
.lp-fly--a { animation-name: lp-flyA; }
.lp-fly--b { animation-name: lp-flyB; }
.lp-fly--c { animation-name: lp-flyC; }

@keyframes lp-flyA {
    0%, 15.36%  { transform: translate(0, 0);              opacity: 0 }
    17%         { opacity: 1 }
    31.95%      { opacity: 1 }
    35.36%      { transform: translate(195px, -212.28px); opacity: 0 }
    100%        { transform: translate(195px, -212.28px); opacity: 0 }
}
@keyframes lp-flyB {
    0%, 23.99%  { transform: translate(0, 0);              opacity: 0 }
    25.6%         { opacity: 1 }
    40.89%      { opacity: 1 }
    43.99%      { transform: translate(310px, -124.28px); opacity: 0 }
    100%        { transform: translate(310px, -124.28px); opacity: 0 }
}
@keyframes lp-flyC {
    0%, 32.43%  { transform: translate(0, 0);              opacity: 0 }
    34%         { opacity: 1 }
    48.4%      { opacity: 1 }
    52.43%      { transform: translate(238px, -38.28px); opacity: 0 }
    100%        { transform: translate(238px, -38.28px); opacity: 0 }
}

/* Рабочий вспыхивает ровно тогда, когда скан его накрывает: точка не живёт
   своей жизнью, а отвечает на скан — иначе связь «отсюда вылетело» рвётся. */
.lp-wpin {
    transform-box: fill-box; transform-origin: center;
    animation-duration: var(--lp-cyc-story);
    animation-timing-function: var(--lp-ease-out);
    animation-iteration-count: infinite;
}
.lp-wpin--a { animation-name: lp-wpinA; }
.lp-wpin--b { animation-name: lp-wpinB; }
.lp-wpin--c { animation-name: lp-wpinC; }
@keyframes lp-wpinA {
    0%, 13.4% { opacity: .4; transform: scale(1) }
    15.36%    { opacity: 1;  transform: scale(1.7) }
    19%, 100% { opacity: .4; transform: scale(1) }
}
@keyframes lp-wpinB {
    0%, 22%   { opacity: .4; transform: scale(1) }
    23.99%    { opacity: 1;  transform: scale(1.7) }
    27.6%, 100% { opacity: .4; transform: scale(1) }
}
@keyframes lp-wpinC {
    0%, 30.4% { opacity: .4; transform: scale(1) }
    32.43%    { opacity: 1;  transform: scale(1.7) }
    36%, 100% { opacity: .4; transform: scale(1) }
}

.lp-ground {
    opacity: 0;
    animation: lp-ground var(--lp-enter) var(--lp-ease-out) .2s forwards;
}
@keyframes lp-ground { to { opacity: 1 } }

/* Отчёт: один проход, forwards. Без `backwards` стартовое состояние
   осталось бы резтовым, и при выключенном движении карточки пропали бы. */
.lp-rcard {
    opacity: 0;
    animation: lp-rcard var(--lp-enter) var(--lp-ease-out) 1.6s forwards;
}
.lp-rcard--b { animation-delay: 1.78s } .lp-rcard--c { animation-delay: 1.96s }
@keyframes lp-rcard {
    from { opacity: 0; transform: translateX(-10px) }
    to   { opacity: 1; transform: translateX(0) }
}

[dir="rtl"] .lp-rcard text {
    text-anchor: end;
    transform: translateX(224px);
}

/* ── 01 / сейчас: карта объектов ─────────────────────────────────────
   Отметки приходят по очереди — 12%, 45%, 78% общего такта. Вместе с
   каждой вспыхивает её точка и загорается чип своего объекта: связь
   «пришла отметка → обновился этот объект» держится на совпадении
   кадра, а не на подписи. Такт общий с hero, чтобы две изометрические
   сцены на одной странице не жили в разных ритмах. */
/* Объекты собираются по этажам по мере прокрутки — тот же приём, что и в
   hero, только там этажи стоят, а здесь встают. Плита приходит на своё место
   снизу, поэтому стопка растёт, а не проявляется целиком.

   Таймлайн именованный и объявлен на контейнере: `view()` на самих <g>
   внутри SVG опирается на бокс, которого у графических элементов нет.
   Диапазоны стоят ВСТЫК и не перекрываются: в каждый момент прокрутки
   встаёт ровно одна плита — один этаж на один тик колеса. Пересекающиеся
   диапазоны давали кашу, где половина стопки шевелилась одновременно.
   Шаг общий для всех трёх объектов, поэтому площадка растёт по уровням,
   а не по одному зданию за раз. Диапазон задан инлайном — он свой у каждой
   плиты, и держать девятнадцать классов ради двух чисел незачем.

   Осторожно с предками: `overflow: hidden` где угодно выше по дереву
   делает элемент скролл-контейнером без полосы, таймлайн цепляется за
   него и застывает. Поэтому и у `body.landing-page`, и у `.lp` стоит
   `clip`, а не `hidden`. */
.lp-art--map { view-timeline: --lp-map block; }
.lp-floor {
    animation-name: lp-rise;
    animation-timing-function: var(--lp-ease-out);
    animation-fill-mode: both;
    animation-timeline: --lp-map;
    /* Именно `auto`, а не шорткат: `animation: ...` сбрасывает длительность
       в 0s, прогресс-таймлайн получает нулевой отрезок и анимация сразу
       считается завершённой. Там, где scroll-таймлайны не поддержаны, `auto`
       ведёт себя как 0s — и `both` мгновенно ставит конечное состояние,
       то есть собранный объект. Это и есть запасной вариант. */
    animation-duration: auto;
}
/* Только подъём и проявление, без сжатия по вертикали: при scaleY < 1 плита
   сплющивалась, её верхняя грань садилась на нижнюю, и в промежуточных кадрах
   вместо этажа виден был двойной контур. Форма плиты не должна меняться —
   меняется только то, встала она на место или ещё нет. */
@keyframes lp-rise {
    from { opacity: 0; transform: translateY(16px) }
    to   { opacity: 1; transform: translateY(0) }
}

.lp-mpin { opacity: .5; }
.lp-mark, .lp-mpin--a, .lp-mpin--b, .lp-mpin--c {
    transform-box: fill-box; transform-origin: center;
    animation-duration: var(--lp-cyc-story);
    animation-timing-function: var(--lp-ease-out);
    animation-iteration-count: infinite;
}
.lp-mark--a { animation-name: lp-markA } .lp-mpin--a { animation-name: lp-pinA }
.lp-mark--b { animation-name: lp-markB } .lp-mpin--b { animation-name: lp-pinB }
.lp-mark--c { animation-name: lp-markC } .lp-mpin--c { animation-name: lp-pinC }
.lp-chip { animation-duration: var(--lp-cyc-story);
           animation-timing-function: var(--lp-ease-loop);
           animation-iteration-count: infinite; }
.lp-chip--a { animation-name: lp-chipA }
.lp-chip--b { animation-name: lp-chipB }
.lp-chip--c { animation-name: lp-chipC }

@keyframes lp-markA {
    0%, 11.9% { opacity: 0;  transform: scale(.4) }
    12%       { opacity: .9; transform: scale(.4) }
    23%, 100% { opacity: 0;  transform: scale(3.6) }
}
@keyframes lp-markB {
    0%, 44.9% { opacity: 0;  transform: scale(.4) }
    45%       { opacity: .9; transform: scale(.4) }
    56%, 100% { opacity: 0;  transform: scale(3.6) }
}
@keyframes lp-markC {
    0%, 77.9% { opacity: 0;  transform: scale(.4) }
    78%       { opacity: .9; transform: scale(.4) }
    89%, 100% { opacity: 0;  transform: scale(3.6) }
}
@keyframes lp-pinA {
    0%, 11%   { opacity: .5; transform: scale(1) }
    12%       { opacity: 1;  transform: scale(1.8) }
    20%, 100% { opacity: .5; transform: scale(1) }
}
@keyframes lp-pinB {
    0%, 44%   { opacity: .5; transform: scale(1) }
    45%       { opacity: 1;  transform: scale(1.8) }
    53%, 100% { opacity: .5; transform: scale(1) }
}
@keyframes lp-pinC {
    0%, 77%   { opacity: .5; transform: scale(1) }
    78%       { opacity: 1;  transform: scale(1.8) }
    86%, 100% { opacity: .5; transform: scale(1) }
}
@keyframes lp-chipA {
    0%, 11%   { stroke: var(--lp-line) }
    13%, 26%  { stroke: var(--lp-accent) }
    36%, 100% { stroke: var(--lp-line) }
}
@keyframes lp-chipB {
    0%, 44%   { stroke: var(--lp-line) }
    46%, 59%  { stroke: var(--lp-accent) }
    69%, 100% { stroke: var(--lp-line) }
}
@keyframes lp-chipC {
    0%, 77%   { stroke: var(--lp-line) }
    79%, 92%  { stroke: var(--lp-accent) }
    99%, 100% { stroke: var(--lp-line) }
}

/* ── биржа: конвейер из двух наборов, замкнутый без видимого шва ──────
   Такт 24s (свой токен: двум наборам в 12s тесно). Хронометраж:
   0–20%   первая вакансия стоит (прилетела в конце прошлого такта),
           первая зона держит тройку; 6–18% тройка улетает на объект a,
           кольцо a отмечает прибытие (16.5%);
   24%     зона и флажок гаснут — набор закрыт;
   26–36%  на плиту встают трое новых (нижние места);
   40–46%  вторая вакансия прилетает с объекта b;
   48–76%  вторая зона — крупнее и ниже — захватывает оставшегося от
           первого набора вместе с новичками; 54–67% четвёрка улетает
           на объект b, кольцо b (64.5%); зона гаснет;
   78–91%  пополнение: тройка первой зоны и «оставшийся» встают заново;
   93–99%  вакансия следующего набора прилетает с объекта a, первая зона
           проявляется — и такт замыкается на свой нулевой кадр.
   Кровельная группа перенимает бригаду за фасадом (стойки нарисованы
   раньше объектов): появляется в такт прилёту, стоит смену и постепенно
   растворяется — к следующему прилёту крыша пуста; кадра сброса у цикла
   всё равно нет: дорожка кровли b переживает шов.
   Появления разведены своими дорожками, а не задержкой одной: у
   задержанной бесконечной анимации перезапуск цикла приходится на хвост
   соседа. Новички второй волны — «призраки»: базовая прозрачность 0,
   при выключенном движении их нет, и статика равна хвосту такта. */
.lp-xc-ghost { opacity: 0; }
.lp-xc-stand {
    animation-duration: var(--lp-cyc-xc);
    animation-timing-function: var(--lp-ease-loop);
    animation-iteration-count: infinite;
}
.lp-xc-stand--a { animation-name: lp-xc-hire-a; }
.lp-xc-stand--b { animation-name: lp-xc-hire-b; }
.lp-xc-stand--c { animation-name: lp-xc-hire-c; }
.lp-xc-stand--d { animation-name: lp-xc-hire-d; }
.lp-xc-stand--e { animation-name: lp-xc-hire-e; }
.lp-xc-stand--f { animation-name: lp-xc-hire-f; }
.lp-xc-stand--g { animation-name: lp-xc-hire-g; }
.lp-xc-stand--ma { animation-name: lp-xcm-hire-a; }
.lp-xc-stand--mb { animation-name: lp-xcm-hire-b; }
.lp-xc-stand--md { animation-name: lp-xcm-hire-d; }
.lp-xc-stand--me { animation-name: lp-xcm-hire-e; }
.lp-xc-stand--mf { animation-name: lp-xcm-hire-f; }
/* Дельта каждой дорожки — разность координат места на кровле и на плите;
   пока точка невидима, transform возвращается к нулю, и новый человек
   встаёт на освободившееся место с подъёмом 6px. */
@keyframes lp-xc-hire-a {
    0%, 6%    { transform: translate(0, 0); opacity: 1 }
    16%       { transform: translate(525px, -48px); opacity: 1 }
    17%       { transform: translate(525px, -48px); opacity: 0 }
    78%       { transform: translate(0, 6px); opacity: 0 }
    82%, 100% { transform: translate(0, 0); opacity: 1 }
}
@keyframes lp-xc-hire-b {
    0%, 7%    { transform: translate(0, 0); opacity: 1 }
    17%       { transform: translate(476px, -58px); opacity: 1 }
    18%       { transform: translate(476px, -58px); opacity: 0 }
    81%       { transform: translate(0, 6px); opacity: 0 }
    85%, 100% { transform: translate(0, 0); opacity: 1 }
}
@keyframes lp-xc-hire-c {
    0%, 8%    { transform: translate(0, 0); opacity: 1 }
    18%       { transform: translate(469px, -34px); opacity: 1 }
    19%       { transform: translate(469px, -34px); opacity: 0 }
    84%       { transform: translate(0, 6px); opacity: 0 }
    88%, 100% { transform: translate(0, 0); opacity: 1 }
}
@keyframes lp-xc-hire-d {
    0%, 54%   { transform: translate(0, 0); opacity: 1 }
    64%       { transform: translate(599px, -26px); opacity: 1 }
    65%       { transform: translate(599px, -26px); opacity: 0 }
    87%       { transform: translate(0, 6px); opacity: 0 }
    91%, 100% { transform: translate(0, 0); opacity: 1 }
}
@keyframes lp-xc-hire-e {
    0%, 26%   { transform: translate(0, 6px); opacity: 0 }
    30%       { transform: translate(0, 0); opacity: 1 }
    55%       { transform: translate(0, 0); opacity: 1 }
    65%       { transform: translate(585px, -47px); opacity: 1 }
    66%, 100% { transform: translate(585px, -47px); opacity: 0 }
}
@keyframes lp-xc-hire-f {
    0%, 29%   { transform: translate(0, 6px); opacity: 0 }
    33%       { transform: translate(0, 0); opacity: 1 }
    56%       { transform: translate(0, 0); opacity: 1 }
    66%       { transform: translate(555px, -35px); opacity: 1 }
    67%, 100% { transform: translate(555px, -35px); opacity: 0 }
}
@keyframes lp-xc-hire-g {
    0%, 32%   { transform: translate(0, 6px); opacity: 0 }
    36%       { transform: translate(0, 0); opacity: 1 }
    57%       { transform: translate(0, 0); opacity: 1 }
    67%       { transform: translate(536px, -21px); opacity: 1 }
    68%, 100% { transform: translate(536px, -21px); opacity: 0 }
}
@keyframes lp-xcm-hire-a {
    0%, 6%    { transform: translate(0, 0); opacity: 1 }
    16%       { transform: translate(-39px, 228px); opacity: 1 }
    17%       { transform: translate(-39px, 228px); opacity: 0 }
    78%       { transform: translate(0, 6px); opacity: 0 }
    82%, 100% { transform: translate(0, 0); opacity: 1 }
}
@keyframes lp-xcm-hire-b {
    0%, 7%    { transform: translate(0, 0); opacity: 1 }
    17%       { transform: translate(-48px, 208px); opacity: 1 }
    18%       { transform: translate(-48px, 208px); opacity: 0 }
    81%       { transform: translate(0, 6px); opacity: 0 }
    85%, 100% { transform: translate(0, 0); opacity: 1 }
}
@keyframes lp-xcm-hire-d {
    0%, 54%   { transform: translate(0, 0); opacity: 1 }
    64%       { transform: translate(92px, 260px); opacity: 1 }
    65%       { transform: translate(92px, 260px); opacity: 0 }
    87%       { transform: translate(0, 6px); opacity: 0 }
    91%, 100% { transform: translate(0, 0); opacity: 1 }
}
@keyframes lp-xcm-hire-e {
    0%, 26%   { transform: translate(0, 6px); opacity: 0 }
    30%       { transform: translate(0, 0); opacity: 1 }
    55%       { transform: translate(0, 0); opacity: 1 }
    65%       { transform: translate(62px, 235px); opacity: 1 }
    66%, 100% { transform: translate(62px, 235px); opacity: 0 }
}
@keyframes lp-xcm-hire-f {
    0%, 29%   { transform: translate(0, 6px); opacity: 0 }
    33%       { transform: translate(0, 0); opacity: 1 }
    56%       { transform: translate(0, 0); opacity: 1 }
    66%       { transform: translate(37px, 260px); opacity: 1 }
    67%, 100% { transform: translate(37px, 260px); opacity: 0 }
}
/* Флажок гаснет в середине перелёта — резюме закрыто, дальше летит только
   человек; иначе на подлёте верх флажка торчал бы над гранями кровли.
   Дорожек две — по волнам набора; появление обратно стробирует своя
   группа, поэтому новички встают уже с резюме. */
.lp-xc-cv {
    animation-duration: var(--lp-cyc-xc);
    animation-timing-function: var(--lp-ease-loop);
    animation-iteration-count: infinite;
}
.lp-xc-cv--1 { animation-name: lp-xc-cv1; }
.lp-xc-cv--2 { animation-name: lp-xc-cv2; }
@keyframes lp-xc-cv1 {
    0%, 11%   { opacity: 1 }
    14%, 76%  { opacity: 0 }
    81%, 100% { opacity: 1 }
}
@keyframes lp-xc-cv2 {
    0%, 58%   { opacity: 1 }
    62%, 85%  { opacity: 0 }
    90%, 100% { opacity: 1 }
}
/* Перелёты вакансий: первый флажок пересекает шов такта — прилетает в
   93–99% и стоит через нулевой кадр, второй прилетает в середине. */
.lp-xc-fly {
    animation-duration: var(--lp-cyc-xc);
    animation-timing-function: var(--lp-ease-loop);
    animation-iteration-count: infinite;
}
.lp-xc-fly--v1 { animation-name: lp-xcV1; }
.lp-xc-fly--v2 { animation-name: lp-xcV2; }
.lp-xcm-fly--v1 { animation-name: lp-xcmV1; }
.lp-xcm-fly--v2 { animation-name: lp-xcmV2; }
@keyframes lp-xcV1 {
    0%, 93%     { transform: translate(0, 0); opacity: 0 }
    94.2%       { opacity: 1 }
    98.5%       { transform: translate(-375px, 56px); opacity: 1 }
    99.4%, 100% { transform: translate(-375px, 56px); opacity: 0 }
}
@keyframes lp-xcV2 {
    0%, 40%     { transform: translate(0, 0); opacity: 0 }
    41.2%       { opacity: 1 }
    45.5%       { transform: translate(-470px, -9px); opacity: 1 }
    46.5%, 100% { transform: translate(-470px, -9px); opacity: 0 }
}
@keyframes lp-xcmV1 {
    0%, 93%     { transform: translate(0, 0); opacity: 0 }
    94.2%       { opacity: 1 }
    98.5%       { transform: translate(117px, -201px); opacity: 1 }
    99.4%, 100% { transform: translate(117px, -201px); opacity: 0 }
}
@keyframes lp-xcmV2 {
    0%, 40%     { transform: translate(0, 0); opacity: 0 }
    41.2%       { opacity: 1 }
    45.5%       { transform: translate(13px, -266px); opacity: 1 }
    46.5%, 100% { transform: translate(13px, -266px); opacity: 0 }
}
/* Посадочный флажок один на оба набора — место спроса на плите одно.
   Встаёт в момент касания каждого перелёта, гаснет с закрытием набора. */
.lp-xc-vland { animation: lp-xc-vland var(--lp-cyc-xc) var(--lp-ease-loop) infinite; }
@keyframes lp-xc-vland {
    0%, 20%     { opacity: 1 }
    24%, 45%    { opacity: 0 }
    48%, 71%    { opacity: 1 }
    75%, 97%    { opacity: 0 }
    99.5%, 100% { opacity: 1 }
}
/* Зоны таагида появляются под набор и гаснут с выдачей. Первая пересекает
   шов такта вместе с вакансией и потому есть в статике; вторая живёт
   только в середине — её базовая прозрачность 0. */
.lp-xc-zone {
    animation-duration: var(--lp-cyc-xc);
    animation-timing-function: var(--lp-ease-loop);
    animation-iteration-count: infinite;
}
.lp-xc-zone--1 { animation-name: lp-xc-zone1; }
.lp-xc-zone--2 { opacity: 0; animation-name: lp-xc-zone2; }
@keyframes lp-xc-zone1 {
    0%, 20%     { opacity: 1 }
    24%, 95%    { opacity: 0 }
    98.5%, 100% { opacity: 1 }
}
@keyframes lp-xc-zone2 {
    0%, 47%   { opacity: 0 }
    51%, 71%  { opacity: 1 }
    75%, 100% { opacity: 0 }
}
/* Кровельные группы появляются в такт прилёту своей бригады — перелёт
   финиширует за фасадом, и группа его перенимает, — стоят смену и
   постепенно растворяются: люди разошлись внутрь объекта, к следующему
   прилёту крыша пуста. Дорожка b переживает шов такта, поэтому в статике
   объект b под людьми, а объект a уже опустел — базовые прозрачности
   двух дорожек разные. */
.lp-xc-roof {
    opacity: .9;
    animation-duration: var(--lp-cyc-xc);
    animation-timing-function: linear;
    animation-iteration-count: infinite;
}
.lp-xc-roof--a { opacity: 0; animation-name: lp-xc-roof-a; }
.lp-xc-roof--b { animation-name: lp-xc-roof-b; }
@keyframes lp-xc-roof-a {
    0%, 15.5% { opacity: 0 }
    18%       { opacity: .9 }
    58%       { opacity: .9 }
    78%, 100% { opacity: 0 }
}
@keyframes lp-xc-roof-b {
    0%, 6%     { opacity: .9 }
    26%, 63.5% { opacity: 0 }
    66%        { opacity: .9 }
    100%       { opacity: .9 }
}
.lp-xc-ring {
    opacity: 0; transform-box: fill-box; transform-origin: center;
    animation-duration: var(--lp-cyc-xc);
    animation-timing-function: var(--lp-ease-out);
    animation-iteration-count: infinite;
}
.lp-xc-ring--a { animation-name: lp-xc-ring-a; }
.lp-xc-ring--b { animation-name: lp-xc-ring-b; }
@keyframes lp-xc-ring-a {
    0%, 15%    { opacity: 0; transform: scale(.4) }
    16.5%      { opacity: .9; transform: scale(.4) }
    24%, 100%  { opacity: 0; transform: scale(3.4) }
}
@keyframes lp-xc-ring-b {
    0%, 63%    { opacity: 0; transform: scale(.4) }
    64.5%      { opacity: .9; transform: scale(.4) }
    72%, 100%  { opacity: 0; transform: scale(3.4) }
}

/* Раскадровка цикла (7.5s): рабочий идёт по станциям, а не бежит ниткой —
   ход 10% цикла, стоянка 4%, прибытие ровно на такте станции.
     0 →  56%  четыре перехода: станции на 0 / 14 / 28 / 42 / 56%;
    56 →  66%  дошёл до расчёта — кадр держится;
    66 →  78%  общее гашение: станции, номера шагов, рельс и рабочий;
    78 → 100%  пустая лента, 1.65s тишины перед новым заходом.
   Колец прибытия здесь нет намеренно: приход отмечают сразу три вещи — pop
   станции, рост нитки и акцентный номер шага; четвёртая была бы лишней.
   Рабочий и заливка делят ОДИН набор ключей и одну ease-loop — иначе нитка
   уплывает от идущего. Такт держат именные keyframes, а не animation-delay:
   гашение у всех общее, а со сдвигом оно уезжало в следующий цикл — станции
   02..05 светились поверх нового прохода. */
.lp-pipe-fill {
    stroke: var(--lp-accent); stroke-width: 1.5; opacity: .3;
    stroke-dasharray: 984; stroke-dashoffset: 984;
    animation: lp-pipe-fill var(--lp-cyc-base) var(--lp-ease-loop) infinite;
}
/* Сброс длины — под нулевой прозрачностью, иначе рельс дёргался бы назад.
   Кадр motion=off (залитый рельс, тёмные станции) — это момент 66%. */
@keyframes lp-pipe-fill {
    0%, 4%    { stroke-dashoffset: 984; opacity: .3 }
    14%, 18%  { stroke-dashoffset: 738; opacity: .3 }
    28%, 32%  { stroke-dashoffset: 492; opacity: .3 }
    42%, 46%  { stroke-dashoffset: 246; opacity: .3 }
    56%, 66%  { stroke-dashoffset: 0;   opacity: .3 }
    78%       { stroke-dashoffset: 0;   opacity: 0 }
    79%, 100% { stroke-dashoffset: 984; opacity: 0 }
}
.lp-pipe-head {
    fill: var(--lp-accent);
    animation: lp-pipe-head var(--lp-cyc-base) var(--lp-ease-loop) infinite;
}
@keyframes lp-pipe-head {
    0%        { transform: translateX(0);     opacity: 0 }
    2%, 4%    { transform: translateX(0);     opacity: 1 }
    14%, 18%  { transform: translateX(246px); opacity: 1 }
    28%, 32%  { transform: translateX(492px); opacity: 1 }
    42%, 46%  { transform: translateX(738px); opacity: 1 }
    56%, 66%  { transform: translateX(984px); opacity: 1 }
    78%, 100% { transform: translateX(984px); opacity: 0 }
}
.lp-pipe-node {
    fill: var(--lp-bg2); stroke: var(--lp-line2); stroke-width: 1;
    transform-box: fill-box; transform-origin: center;
    animation: lp-pipe-node-1 var(--lp-cyc-base) linear infinite;
}
.lp-pipe-node--2 { animation-name: lp-pipe-node-2 } .lp-pipe-node--3 { animation-name: lp-pipe-node-3 }
.lp-pipe-node--4 { animation-name: lp-pipe-node-4 } .lp-pipe-node--5 { animation-name: lp-pipe-node-5 }
/* Заливка станции всегда --lp-bg2, загорается только контур: пять залитых
   ромбов дали бы кластер акцента (см. предупреждение о восьми пятнах выше),
   а рабочий, стоя на станции, растворялся бы в её заливке.
   Первая станция вспыхивает не в 0%, а на 1.2%: рабочий успевает появиться,
   и цикл начинается движением, а не хлопком по стыку. */
@keyframes lp-pipe-node-1 {
    0%         { stroke: var(--lp-line2);  transform: scale(1) }
    1.2%       { stroke: var(--lp-accent); transform: scale(1.25) }
    4%, 66%    { stroke: var(--lp-accent); transform: scale(1) }
    78%, 100%  { stroke: var(--lp-line2);  transform: scale(1) }
}
@keyframes lp-pipe-node-2 {
    0%, 12%    { stroke: var(--lp-line2);  transform: scale(1) }
    14%        { stroke: var(--lp-accent); transform: scale(1.25) }
    17%, 66%   { stroke: var(--lp-accent); transform: scale(1) }
    78%, 100%  { stroke: var(--lp-line2);  transform: scale(1) }
}
@keyframes lp-pipe-node-3 {
    0%, 26%    { stroke: var(--lp-line2);  transform: scale(1) }
    28%        { stroke: var(--lp-accent); transform: scale(1.25) }
    31%, 66%   { stroke: var(--lp-accent); transform: scale(1) }
    78%, 100%  { stroke: var(--lp-line2);  transform: scale(1) }
}
@keyframes lp-pipe-node-4 {
    0%, 40%    { stroke: var(--lp-line2);  transform: scale(1) }
    42%        { stroke: var(--lp-accent); transform: scale(1.25) }
    45%, 66%   { stroke: var(--lp-accent); transform: scale(1) }
    78%, 100%  { stroke: var(--lp-line2);  transform: scale(1) }
}
@keyframes lp-pipe-node-5 {
    0%, 54%    { stroke: var(--lp-line2);  transform: scale(1) }
    56%        { stroke: var(--lp-accent); transform: scale(1.25) }
    59%, 66%   { stroke: var(--lp-accent); transform: scale(1) }
    78%, 100%  { stroke: var(--lp-line2);  transform: scale(1) }
}
.lp-step-num { animation: lp-step-1 var(--lp-cyc-base) linear infinite; }
.lp-step-num--2 { animation-name: lp-step-2 } .lp-step-num--3 { animation-name: lp-step-3 }
.lp-step-num--4 { animation-name: lp-step-4 } .lp-step-num--5 { animation-name: lp-step-5 }
@keyframes lp-step-1 { 0%    { color: var(--lp-dim) } 1.2%, 66% { color: var(--lp-accent) } 78%, 100% { color: var(--lp-dim) } }
@keyframes lp-step-2 { 0%, 12% { color: var(--lp-dim) } 14%, 66% { color: var(--lp-accent) } 78%, 100% { color: var(--lp-dim) } }
@keyframes lp-step-3 { 0%, 26% { color: var(--lp-dim) } 28%, 66% { color: var(--lp-accent) } 78%, 100% { color: var(--lp-dim) } }
@keyframes lp-step-4 { 0%, 40% { color: var(--lp-dim) } 42%, 66% { color: var(--lp-accent) } 78%, 100% { color: var(--lp-dim) } }
@keyframes lp-step-5 { 0%, 54% { color: var(--lp-dim) } 56%, 66% { color: var(--lp-accent) } 78%, 100% { color: var(--lp-dim) } }


/* ── переписка в телефоне: показ, а не фон ───────────────────────────
   Один проход, реплика раз в 720мс; дальше диалог остаётся на экране —
   его читают, а не смотрят.
   Пузырь до своей реплики не занимает места (max-height: 0) и раскрывается
   на месте, толкая предыдущие вверх, — как в живом чате, где новое всегда
   внизу. Раньше место резервировалось заранее и лента набиралась сверху.
   Порог 130px — высота самого большого пузыря (кружок видео): до него
   рост идёт плавно, а хвост до 999px нужен, чтобы длинный перевод не
   обрезался — в конечном кадре ограничения фактически нет. */
.lp-tgm-msg { margin-top: 7px; overflow: hidden; animation: lp-msg var(--lp-enter) var(--lp-ease-out) both; }
.lp-tgm-d1 { animation-delay: .24s }  .lp-tgm-d2 { animation-delay: .96s }
.lp-tgm-d3 { animation-delay: 1.68s } .lp-tgm-d4 { animation-delay: 2.4s }
/* Паддинги схлопываются вместе с высотой: границей box-sizing высота
   не может стать меньше них, и нераскрытый пузырь оставлял бы 14px следа.
   Возвращаются к 15%, пока рост ещё идёт, — текст не успевает сплющиться. */
@keyframes lp-msg {
    from { max-height: 0;     padding-block: 0;   margin-top: 0;   opacity: 0; transform: translateY(6px) scale(.98); }
    15%  { padding-block: 7px; }
    60%  { max-height: 130px; padding-block: 7px; margin-top: 7px; opacity: 1; transform: none; }
    to   { max-height: 999px; padding-block: 7px; margin-top: 7px; opacity: 1; transform: none; }
}

/* ── сцена перевода: точка увозит реплику в узел, перевод сменяется ──
   Точка идёт 4s, каждый перевод держит треть цикла — смена языка */
.lp-lng-dot { position: absolute; opacity: 0; animation: 4s linear infinite; }
.lp-lng-dot::before {
    content: ''; position: absolute; top: 0; inset-inline-start: 0;
    width: 7px; height: 7px; margin: -3.5px 0 0; margin-inline-start: -3.5px;
    border-radius: 50%; background: var(--lp-accent);
}
.lp-lng-dot--1 { inset-inline: 0; top: 50%; height: 0; animation-name: lp-lng-p1; }
.lp-lng-dot--2 { inset-inline-end: 0; top: 50%; bottom: -44px; width: 0; animation-name: lp-lng-p2; }
.lp-lng-dot--3 { inset-inline: 0; top: 0; height: 0; animation-name: lp-lng-p3; }
.lp-lng-dot--4 { inset-inline-start: 0; top: -44px; bottom: 50%; width: 0; animation-name: lp-lng-p4; }
.lp-lng-dot--5 { inset-inline: 0; top: 50%; height: 0; animation-name: lp-lng-p5; }
@keyframes lp-lng-p1 {
    0%, 25%     { opacity: 0; transform: translateX(0) }
    27%         { opacity: 1 }
    35.7%       { opacity: 1; transform: translateX(calc(var(--lp-lng-dir) * 100%)) }
    35.8%, 100% { opacity: 0; transform: translateX(calc(var(--lp-lng-dir) * 100%)) }
}
@keyframes lp-lng-p2 {
    0%, 35.6%   { opacity: 0; transform: translateY(0) }
    35.7%       { opacity: 1; transform: translateY(0) }
    42%         { opacity: 1; transform: translateY(100%) }
    42.1%, 100% { opacity: 0; transform: translateY(100%) }
}
@keyframes lp-lng-p3 {
    0%, 41.9%   { opacity: 0; transform: translateX(calc(var(--lp-lng-dir) * 100%)) }
    42%         { opacity: 1; transform: translateX(calc(var(--lp-lng-dir) * 100%)) }
    83.1%       { opacity: 1; transform: translateX(0) }
    83.2%, 100% { opacity: 0; transform: translateX(0) }
}
@keyframes lp-lng-p4 {
    0%, 83%     { opacity: 0; transform: translateY(0) }
    83.1%       { opacity: 1; transform: translateY(0) }
    89.4%       { opacity: 1; transform: translateY(100%) }
    89.5%, 100% { opacity: 0; transform: translateY(100%) }
}
@keyframes lp-lng-p5 {
    0%, 89.3% { opacity: 0; transform: translateX(0) }
    89.4%     { opacity: 1; transform: translateX(0) }
    97%       { opacity: 1 }
    100%      { opacity: 0; transform: translateX(calc(var(--lp-lng-dir) * 100%)) }
}

.lp-lng-word { animation: lp-lng-word 4s var(--lp-ease-loop) infinite; }
.lp-lng-mark { opacity: 0; animation: lp-lng-mark 4s var(--lp-ease-loop) infinite; }
.lp-lng-mark-src { font-size: 14px; fill: var(--lp-muted); }
.lp-lng-mark-dst { font-size: 18px; }
.lp-lng-mark-dst--2, .lp-lng-mark-dst--3 { font-size: 15px; }
.lp-lng-mark-dst {
    fill: var(--lp-accent); opacity: 0;
    transform-box: fill-box; transform-origin: center;
    animation: lp-lng-flip var(--lp-cyc-story) var(--lp-ease-loop) infinite;
}
.lp-lng-mark-dst--2 { animation-delay: 4s }
.lp-lng-mark-dst--3 { animation-delay: 8s }
.lp-lng-node svg { overflow: visible; }
.lp-lng-ring {
    opacity: 0; transform-box: fill-box; transform-origin: center;
    animation: lp-lng-ring 4s var(--lp-ease-loop) infinite;
}
@keyframes lp-lng-word {
    0%, 38%   { opacity: 1 }
    43%, 86%  { opacity: 0 }
    91%, 100% { opacity: 1 }
}
@keyframes lp-lng-mark {
    0%, 43%   { opacity: 0 }
    47%, 82%  { opacity: 1 }
    86%, 100% { opacity: 0 }
}
@keyframes lp-lng-flip {
    0%, 20.83%   { opacity: 0; transform: rotateY(-90deg) rotate(-18deg) scale(.72);
                   animation-timing-function: linear }
    20.84%       { opacity: 1 }
    23.76%       { opacity: 1; transform: rotateY(0deg) rotate(0deg) scale(1) }
    51.25%       { opacity: 1; transform: rotateY(0deg) rotate(0deg) scale(1);
                   animation-timing-function: linear }
    54.16%       { opacity: 1; transform: rotateY(90deg) rotate(18deg) scale(.72) }
    54.17%, 100% { opacity: 0; transform: rotateY(90deg) rotate(18deg) scale(.72) }
}
@keyframes lp-lng-ring {
    0%, 59%   { opacity: 0; transform: scale(1) }
    62.5%     { opacity: .5; transform: scale(1) }
    74%, 100% { opacity: 0; transform: scale(1.5) }
}
.lp-lng-alt { opacity: 0; animation: lp-lng-alt var(--lp-cyc-story) var(--lp-ease-loop) infinite; }
.lp-lng-alt--2 { animation-delay: 4s }
.lp-lng-alt--3 { animation-delay: 8s }
@keyframes lp-lng-alt {
    0%        { opacity: 0 }
    3%, 31%   { opacity: 1 }
    34%, 100% { opacity: 0 }
}

/* ── запрос к данным: показ, а не фон ────────────────────────────────
   Печать 2.4s, следом столбцы слева направо. Ответ остаётся на экране:
   прежний цикл обнулял график каждые 11 секунд, и взгляд, вернувшийся
   к секции, заставал пустые оси вместо обещанного ответа.

   Ширину и число шагов ставит landing.mjs, потому что строка переводится:
   в каталогах она от десяти знаков (китайский) до сорока пяти (узбекский).
   Числом в `ch` её не выразить — у иероглифа своя ширина, и посчитанное по
   кириллице значение обрезало бы строку. Мерить умеет только браузер.

   Печать включает класс: без скрипта и при системной настройке покоя
   строка стоит целиком, каретка мигает. Значения в `.lp-typed` — не
   «дефолты на всякий случай», а страховка: `fill-mode: both` держит
   `from`-кадр до старта, и при сорванной переменной строка была бы не
   видна вовсе.

   Ширина здесь меняется намеренно, вопреки правилу «только transform и
   opacity»: каретка обязана ехать за текстом, а обрезкой её не сдвинуть.
   Шагов ровно по числу знаков — это и есть посимвольная печать. */
.lp-typed { --lp-typed-steps: 40; --lp-typed-w: 100%; }
.lp-typed.is-typing { animation: lp-type 2.4s steps(var(--lp-typed-steps), end) both; }
@keyframes lp-type { from { max-width: 0 } to { max-width: var(--lp-typed-w) } }

.lp-caret { animation: lp-caret 1.2s steps(1) infinite; }
@keyframes lp-caret { 50% { opacity: 0 } }

/* Номер столбца живёт в переменной: столбцы растут слева направо,
   обгоняя соседа на один и тот же шаг. */
.lp-barcol { --lp-col: 0 }
.lp-barcol:nth-child(2) { --lp-col: 1 } .lp-barcol:nth-child(3) { --lp-col: 2 }
.lp-barcol:nth-child(4) { --lp-col: 3 } .lp-barcol:nth-child(5) { --lp-col: 4 }
.lp-barcol:nth-child(6) { --lp-col: 5 } .lp-barcol:nth-child(7) { --lp-col: 6 }
.lp-bar {
    animation: lp-bar var(--lp-enter) var(--lp-ease-out) both;
    animation-delay: calc(2.64s + var(--lp-col) * 120ms);
}
@keyframes lp-bar { from { transform: scaleY(0) } to { transform: scaleY(1) } }

/* ── сцена в кадре ───────────────────────────────────────────────────
   Движение живёт, только пока его секция на экране. Без этого два десятка
   бесконечных циклов крутятся по всей странице разом: процессор греется,
   а глазу не за что зацепиться — движется всё сразу, значит ничего.
   `paused` замораживает кадр, а не сбрасывает его: вернувшись к секции,
   посетитель застаёт сцену там, где оставил. */
.lp--js [data-scene]:not(.is-live) * { animation-play-state: paused; }

/* Появление секций ставится ТОЛЬКО когда скрипт поднялся и добавил .lp--js:
   иначе страница видна целиком, а не остаётся пустой. */
.lp--js [data-reveal] { opacity: 0; transform: translateY(20px); }
.lp--js [data-reveal].is-in {
    opacity: 1; transform: none;
    transition: opacity var(--lp-enter) var(--lp-ease-out), transform var(--lp-enter) var(--lp-ease-out);
}
.lp--js [data-reveal][data-d="1"].is-in { transition-delay: 60ms }
.lp--js [data-reveal][data-d="2"].is-in { transition-delay: 120ms }
.lp--js [data-reveal][data-d="3"].is-in { transition-delay: 180ms }
.lp--js [data-reveal][data-d="4"].is-in { transition-delay: 240ms }
.lp--js [data-reveal][data-d="5"].is-in { transition-delay: 300ms }
.lp--js [data-reveal][data-d="6"].is-in { transition-delay: 360ms }
.lp--js [data-reveal][data-d="7"].is-in { transition-delay: 420ms }

/* ══════════════════════════════════════════════════════════════════════
   ГАШЕНИЕ ДВИЖЕНИЯ
   Выключатель один — системная настройка, и работает он без скрипта.
   Слой 1 снимается целиком, слой 2 превращается в короткую прозрачность,
   слой 3 (цвет и фокусные рамки) остаётся. 0.01ms, а не 0s: иначе
   ломается код, ждущий animationend.
   ══════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
    .lp *, .lp *::before, .lp *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        /* Без сброса задержки backwards-fill (у появлений стоит both) держит
           from-кадр — сообщения телефона и столбцы графика были бы невидимы
           до четырёх секунд, а потом появлялись рывками. */
        animation-delay: 0s !important;
    }
    .lp .lp-draw { stroke-dashoffset: 0; }
    .lp .lp-pipe-fill { stroke-dashoffset: 0; }
    .lp .lp-bar { transform: scaleY(1); }
    .lp .lp-tgm-msg { opacity: 1; transform: none; max-height: none; }
    .lp .lp-scan, .lp .lp-fly,
    .lp .lp-mark, .lp .lp-pipe-head,
    .lp .lp-xc-fly, .lp .lp-xc-ring,
    .lp .lp-lng-dot { opacity: 0; }
    .lp .lp-mpin { opacity: .8; }
    .lp .lp-lng-alt { animation: none; opacity: 0; }
    .lp .lp-lng-alt[lang="ru"] { opacity: 1; }
    /* Рост по скроллу — крупный масштаб, слой 1: снимаем целиком.
       Глобальный сброс длительности сюда не достаёт: у scroll-таймлайна
       нет времени, которое можно обнулить. */
    .lp .lp-floor { animation: none; transform: none; opacity: 1; }
    .lp .lp-wpin { opacity: 1; }
    .lp .lp-rcard, .lp .lp-ground { opacity: 1; transform: none; }
    /* Подъём карточки и кнопки под курсором — переход, а не анимация:
       глобальный сброс длительности выше до него не достаёт. */
    .lp .lp-card:hover, .lp .lp-btn:hover { transform: none; }
    .lp--js [data-reveal] { opacity: 1; transform: none; transition-duration: .15s; }
    /* Скрипт при настройке покоя шапку не трогает, но настройку меняют и
       на живой странице — уже уехавшая иначе осталась бы за краем. */
    .lp .lp-nav { transform: none; transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   АДАПТИВ
   ══════════════════════════════════════════════════════════════════════ */

/* ── экран на блок (десктоп) ─────────────────────────────────────────
   Пол в экран задан базово (см. `.lp-sec` выше) и работает на любой
   ширине. Здесь — только десктопные поправки к нему: на широком экране
   у блока другой ритм отступов, а схема в hero тянется по высоте окна. */
@media (min-width: 981px) {
    /* Вертикаль блока держит окно, поэтому собственные отступы можно
       ужать: воздух сверху и снизу теперь добавляет центрирование. */
    .lp-sec { padding-block: 40px; }
    /* По той же причине ужимаем воздух между шапкой блока и содержимым. */
    .lp-stack { gap: 40px; }
    .lp-hero { padding-block: 16px 24px; }
    /* Схема тянется по высоте окна, а не стоит фиксированной: на низком
       экране она уступает место тексту, на высоком остаётся во всю
       величину. Ограничение сверху — её собственный размер. */
    .lp-hero-art { flex: 0 0 auto; }
    .lp-hero-art svg { height: min(523px, 64svh); width: auto; max-width: 100%; }
}

@media (max-width: 1200px) {
    .lp-guides { display: none; }
    .lp-hero-copy { flex: 1 1 0; }
    .lp-hero-art, .lp-art, .lp-art--dash, .lp-art--phone, .lp-art--lng { flex: 1 1 0; min-width: 0; }
    .lp-hero-art svg, .lp-art svg { width: 100%; height: auto; }
    /* Растягивать до ширины колонки надо схемы, но не иконки внутри телефона,
       не пиктограмму строки запроса и не узел сцены перевода: правило выше
       раздувало их на весь блок. */
    .lp-phone svg { width: auto; height: auto; }
    .lp-nlq > svg { width: 17px; height: 17px; }
    .lp-nlq-send svg, .lp-chart svg, .lp-lng svg { width: auto; height: auto; }
    /* В этой полосе колонка макета резиновая, и на 14px хвост вопроса уезжал
       под обрез: печать обязана добежать до конца на любой ширине. Размер
       подобран по узкому краю полосы (981px), там запас всего 7px. */
    .lp-typed { font-size: 11.5px; }
    .lp-nlq { padding: 14px 16px; }
}

@media (max-width: 980px) {
    .lp-isobg { top: auto; height: min(46svh, 400px); }
    .lp-sec { padding: 80px 0; }
    .lp-h1 { font-size: 46px; }
    .lp-h2 { font-size: 34px; }
    .lp-h2--lg { font-size: 40px; }
    /* align-items: center у `--center` в колоночном режиме перестаёт
       центрировать и начинает мешать: колонка не тянется на ширину, а
       сжимается по содержимому, и широкая карточка вылезает за экран. */
    .lp-split, .lp-split--center { flex-direction: column; align-items: stretch; gap: 44px; }
    .lp-split > * { min-width: 0; }
    /* Строка запроса печатается посимвольно и не переносится — на узком
       экране она обязана обрезаться внутри поля, а не распирать колонку. */
    .lp-nlq { overflow: hidden; }
    .lp-typed { flex: 1 1 auto; min-width: 0; }
    /* Тач-таргеты — размером из `--tap-size` (base.css): на узком экране всё
       кликабельное с запасом, и величина у витрины та же, что у кабинета. */
    .lp-btn--sm { min-height: var(--tap-size); }
    .lp-logo { min-height: var(--tap-size); }
    /* «Войти» живёт вне скрываемого списка якорей и остаётся видимым. */
    .lp-nav-link { display: inline-flex; align-items: center; min-height: var(--tap-size); }
    /* base.css прячет свитчер на ≤768px: там его подхватывает бургер-меню
       кабинета. У лендинга бургера нет, и язык — единственная точка входа
       для того, кто зашёл не на своём. */
    .lp .language-switcher--lp { display: flex; }
    .lp .language-switcher--lp .custom-select-trigger { min-height: var(--tap-size); }
    .lp-link { display: inline-flex; align-items: center; min-height: var(--tap-size); }
    .lp-hero-inner { flex-direction: column; align-items: stretch; gap: 40px; }
    .lp-hero-copy { flex: 1 1 auto; }
    .lp-grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .lp-grid--5, .lp-grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .lp-planebg { display: none; }
    .lp-nav-links { display: none; }
    /* Нитка смены горизонтально на узком экране нечитаема — прячем схему,
       шаги остаются списком и несут тот же смысл. Сцена биржи, в отличие
       от нитки, несёт весь блок, поэтому не прячется, а меняет раскладку
       на вертикальную — это отдельная композиция в разметке, а не поворот
       широкой: подписи обязаны остаться горизонтальными. */
    .lp-pipe { display: none; }
    .lp-steps { margin-top: 0; }
    /* Без ленты подсвечивать номера по очереди нечему: мигание в списке
       читалось бы как сбой. Оставляем их акцентными статично. */
    .lp-step-num { animation: none; color: var(--lp-accent); }
    .lp-art--xc .lp-xc-wide { display: none; }
    /* 420 на планшете: 340 — ширина под телефон, в 720-пиксельной колонке
       сцена тонула бы в полях. */
    .lp-art--xc .lp-xc-tall { display: block; width: min(420px, 100%); }
}

@media (max-width: 640px) {
    .lp-wrap { width: calc(100% - 40px); }
    .lp-art--xc .lp-xc-tall { width: min(340px, 100%); }
    .lp-sec { padding: 60px 0; }
    .lp-h1 { font-size: 38px; }
    .lp-h2 { font-size: 27px; }
    .lp-h2--lg { font-size: 30px; }
    .lp-lead { font-size: 16px; }
    .lp-grid--4, .lp-grid--5, .lp-grid--3 { grid-template-columns: minmax(0, 1fr); }
    .lp-stats { flex-wrap: wrap; }
    .lp-stat { flex: 1 1 45%; padding-bottom: 14px; }
    .lp-actions { flex-direction: column; align-items: stretch; }
    .lp-btn { width: 100%; }
    .lp-footer-inner { flex-direction: column; align-items: flex-start; gap: 16px; }
    /* Кратно четырём: шаг сетки считается как четверть высоты поля. */
    .lp-plot { --lp-plot-h: 132px; }
    .lp-bars, .lp-xaxis { gap: 8px; }
    /* Печать живёт там, где строка помещается целиком. В колонку телефона
       она не влезает и обрывалась на полуслове, поэтому здесь вопрос лежит
       в две строки и сразу — каретке нечего вести. */
    .lp-typed, .lp-typed.is-typing { white-space: normal; max-width: none; animation: none; }
    .lp-caret { display: none; }
    .lp-nlq-line { overflow: visible; }
    .lp-phone { width: min(320px, 100%); }
    /* Три элемента в правой части шапки в 320px не помещаются — на телефоне
       у свитчера остаются глобус и флаг, самоназвание уезжает в список.
       Кнопка демо из шапки уходит: даже без подписи свитчера трём элементам
       тесно, надпись на кнопке переносилась на вторую строку. Та же кнопка
       стоит в герое сразу под шапкой и ещё раз в CTA — а «Войти» больше
       взять негде, поэтому уступает кнопка, а не вход. */
    .lp-nav-right { gap: 14px; }
    .lp-nav-right .lp-btn--sm { display: none; }
    .lp .language-switcher--lp .custom-select-text { display: none; }
    .lp .language-switcher--lp .custom-select-trigger { padding-inline: 10px 26px; }
    .lp .language-switcher--lp .custom-select-arrow { inset-inline-end: 8px; }
    .lp-kpis { gap: 8px; }
    .lp-kpi { padding: 10px 12px; }
    .lp-kpi-num { font-size: 20px; }
    .lp-lng-msg { width: 100%; }
    .lp-lng-arm, .lp-lng-turn { display: none; }
    .lp-lng-mid { height: 104px; }
    .lp-lng-drop {
        display: block; position: absolute; inset-block: 0; inset-inline-start: 50%; width: 0;
        border-inline-start: 1px dashed var(--lp-line);
    }
}
.lp-lng-dot--m { inset-inline-start: 0; inset-block: 0; width: 0; animation-name: lp-lng-p4m; }
@keyframes lp-lng-p4m {
    0%, 25% { opacity: 0; transform: translateY(0) }
    27%     { opacity: 1 }
    97%     { opacity: 1 }
    100%    { opacity: 0; transform: translateY(100%) }
}
