    /* Оверлей Whisperer — ОДИН набор стилей на все страницы, где он стоит.
       ПОЧЕМУ ОТДЕЛЬНЫМ ФАЙЛОМ. Раньше эти четыреста строк жили внутри
       `_template.html`, и оверлей существовал ровно на одной странице — на
       лендинге. Посадочные показывали вместо него записанную анимацию, и
       подпись под ней уверяла, что страница не слушает вовсе: две разные
       демонстрации одного продукта, расходящиеся при каждой правке. Копия
       решила бы задачу на день
       и разошлась бы на второй, поэтому стили вынесены сюда и инлайнятся и
       лендингом (`generate_landing.py`), и посадочными (`_landing_solutions.py`,
       `_landing_graph.py`).

       ЧЕГО ЗДЕСЬ НЕТ. Правил `.hero .demo` — это РАЗМЕЩЕНИЕ панели в первом
       экране лендинга, а не сама панель; на посадочной она стоит иначе. Всё,
       что начинается с `.hero`, остаётся в шаблоне лендинга.

       ПОРЯДОК ВАЖЕН: базовые правила, затем адаптив. Файл инлайнится целиком и
       ровно в этом порядке — media-блок, уехавший выше базового правила, просто
       перестал бы работать. */

    /* ── Атрибут `hidden` работает ВЕЗДЕ, где стоит панель ───────────────────
       Половина её узлов прячется атрибутом (`ov-try`, лента распознавания,
       плашка снимка, панель перевода), а у каждого из них задана раскладка —
       и `display:flex` сильнее браузерного правила для `hidden`. На лендинге
       это чинила строка `[hidden] { display:none !important; }` в его
       СОБСТВЕННОМ шаблоне; при переносе панели на посадочные она осталась
       там, а оболочка посадочных такой строки не имеет.

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

       Поэтому правило живёт ТЕПЕРЬ В КОМПОНЕНТЕ и ограничено самой панелью:
       компонент обязан вести себя одинаково всюду, куда его инлайнят, и не
       вправе менять поведение страницы вокруг себя. */
    .demo [hidden] { display:none !important; }

    /* ── Наследство страницы внутрь панели не пускаем ─────────────────────────
       На посадочной панель стоит ВНУТРИ `.sol`, а у той свои правила для голых
       элементов: `.sol p` красит абзацы в `--muted`, `.sol b` — в `--ink`,
       заголовки получают свой кегль и семейство. Прямое правило всегда сильнее
       НАСЛЕДОВАНИЯ, поэтому текст ответа, у которого цвет приходил от `#demoA`,
       становился тёмно-серым на тёмной панели — «тусклые шрифты».

       Сбрасываем ровно те свойства, которые страница задаёт (цвет, кегль,
       межстрочный, насыщенность, семейство, разрядка, отступ), и ровно для тех
       элементов, для которых она их задаёт. Блок стоит ПЕРВЫМ в наборе панели:
       всё, что панель объявляет ниже, сильнее по порядку либо по специфичности,
       и сброс ему не мешает. Ограничен `.demo` — страницу вокруг не трогаем. */
    .demo h1, .demo h2, .demo h3, .demo p, .demo b, .demo a, .demo li {
      font-family:inherit; font-size:inherit; line-height:inherit; font-weight:inherit;
      color:inherit; letter-spacing:inherit; }
    .demo p + p { margin-top:0; }

    /* Демо-окно */
    /* Демо в первом экране — точный портрет оверлея из macos-client.
       Числа и цвета взяты из Sources/UI/OverlayTheme.swift и OverlayView.swift,
       а не подобраны на глаз: панель #0E1014 при 72% с размытием, кант white 12%,
       радиус 26 и БЕЗ тени (в клиенте её убрали намеренно), акцент #E7AE46. */
    /* min-width:0 — окно демо это элемент грида, а у элементов грида ширина по
       умолчанию не опускается ниже содержимого (`min-width:auto`). Широкая
       диаграмма в ответе раздувала окно за пределы колонки: на 390 пикселях оно
       росло с 358 до 460 и вылезало за макет. Ноль возвращает окну ширину
       колонки, а диаграмма прокручивается внутри своего блока. */
    .demo { --ov-acc:#E7AE46; min-width:0; position:relative; border:1px solid rgba(255,255,255,.12); border-radius:26px;
      background:rgba(14,16,20,.86); color:#fff; padding:16px 22px 14px; overflow:hidden;
      backdrop-filter:blur(20px) saturate(180%); -webkit-backdrop-filter:blur(20px) saturate(180%); }
    .demo * { font-family:inherit; }
    .ov-top { display:flex; align-items:center; gap:14px; }
    .ov-btn { width:38px; height:38px; border-radius:50%; background:rgba(255,255,255,.04);
      border:1px solid rgba(255,255,255,.08); display:inline-flex; align-items:center; justify-content:center;
      flex:none; color:#fff; cursor:pointer; padding:0; transition:background 140ms var(--ease); }
    .ov-btn:hover { background:rgba(255,255,255,.10); }
    .ov-btn.on { background:rgba(255,255,255,.10); border-color:rgba(231,174,70,.5); color:var(--ov-acc); }
    .ov-pause { background:rgba(255,255,255,.08); border-color:rgba(255,255,255,.16); color:var(--ov-acc); gap:3px; }
    .ov-pause i { width:3.5px; height:13px; border-radius:1px; background:currentColor; display:block; }
    /* Пауза → одна треугольная «play», как в клиенте. */
    .demo.paused .ov-pause i:first-child { width:0; height:0; border-radius:0; background:transparent;
      border-block:7px solid transparent; border-inline-start:11px solid currentColor; }
    .demo.paused .ov-pause i:last-child { display:none; }
    /* Эквалайзер: 8 полос по 3px с шагом 3 в поле 56x20 — ровно как WaveformIndicator. */
    .ov-eq { width:56px; height:20px; display:inline-flex; align-items:center; gap:3px; flex:none; }
    /* `flex:none` обязателен: без него на узком окне полоски сжимались до нулевой
       ширины и эквалайзер вырождался в пустое место (замер в Chrome при 800px). */
    .ov-eq i { width:3px; flex:none; border-radius:2px; background:var(--ov-acc); height:18%;
      animation:oveq 1.1s ease-in-out infinite; }
    .ov-eq i:nth-child(1){animation-delay:0s} .ov-eq i:nth-child(2){animation-delay:.09s}
    .ov-eq i:nth-child(3){animation-delay:.18s} .ov-eq i:nth-child(4){animation-delay:.27s}
    .ov-eq i:nth-child(5){animation-delay:.36s} .ov-eq i:nth-child(6){animation-delay:.45s}
    .ov-eq i:nth-child(7){animation-delay:.54s} .ov-eq i:nth-child(8){animation-delay:.63s}
    @keyframes oveq { 0%,100% { height:18%; } 50% { height:88%; } }
    /* На паузе клиент заменяет эквалайзер волосяной чертой 1x26. */
    .demo.paused .ov-eq { width:1px; height:26px; background:rgba(255,255,255,.12); }
    .demo.paused .ov-eq i { display:none; }
    .ov-field { flex:1; min-width:0; display:flex; align-items:center; gap:10px;
      background:rgba(0,0,0,.20); border:1px solid rgba(255,255,255,.07); border-radius:16px;
      padding:7px 8px 7px 18px; }
    .ov-field input { flex:1; min-width:0; background:transparent; border:none; outline:none;
      color:#fff; font-size:15px; }
    .ov-field input::placeholder { color:rgba(255,255,255,.55); }
    .ov-shot { width:30px; height:30px; border-radius:9px; background:transparent; border:none; color:#fff;
      display:inline-flex; align-items:center; justify-content:center; cursor:pointer; position:relative; flex:none; }
    .ov-shot:hover { background:rgba(255,255,255,.10); }
    .ov-shot b { position:absolute; top:-3px; inset-inline-end:-3px; min-width:18px; height:18px; border-radius:9px;
      background:var(--ov-acc); color:#12141A; border:1.5px solid rgba(255,255,255,.85); font-size:11px;
      font-weight:700; display:none; align-items:center; justify-content:center; }
    .ov-shot.has b { display:inline-flex; }
    /* Уведомление о снимке — портрет ScreenshotToast: камера 11, подпись caption,
       акцентный цвет, чёрная плашка 55% с радиусом 8. Крестик наш: в клиенте
       пачку сбрасывает отправка, а здесь кадр один и его надо уметь снять. */
    .ov-shot-toast { display:flex; align-items:center; justify-content:center; gap:8px;
      margin:10px auto 0; width:fit-content; max-width:100%; padding:6px 12px; border-radius:8px;
      background:rgba(0,0,0,.55); color:var(--ov-acc); font-size:12px; }
    .ov-shot-toast svg { width:11px; height:11px; flex:none; }
    .ov-shot-toast span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .ov-shot-toast button { flex:none; width:18px; height:18px; border-radius:5px; border:none;
      padding:0; background:transparent; color:var(--ov-acc); cursor:pointer; font-size:11px;
      line-height:1; font-family:inherit; }
    .ov-shot-toast button:hover { background:rgba(231,174,70,.18); }
    /* При живом микрофоне высоту полосок считает JS по формуле WaveformIndicator
       (бегущая волна, промасштабированная РЕАЛЬНЫМ уровнем входа), поэтому здесь
       только снимаем расписанную анимацию — иначе она спорила бы с расчётом. */
    .demo.listening .ov-eq i { animation:none; transition:height 60ms linear; }
    .ov-pill { display:inline-flex; gap:5px; padding:6px 7px; border-radius:11px; background:rgba(255,255,255,.05);
      border:1px solid rgba(255,255,255,.12); cursor:pointer; flex:none; }
    .ov-pill b { width:22px; height:22px; border-radius:6px; background:rgba(255,255,255,.08);
      display:inline-flex; align-items:center; justify-content:center; font-size:13px; color:rgba(255,255,255,.64); }
    .ov-pill:hover b { color:#fff; }
    /* Блокнот — портрет NotesField: пока на экране ответ модели, он свёрнут в
       заголовочную строку и не отнимает высоту у текста, который читают;
       раскрывается нажатием на строку. В демо ответ на экране ВСЕГДА, поэтому
       свёрнутое состояние здесь и есть состояние по умолчанию. */
    .ov-notes { margin-top:12px; }
    .ov-notes-h { display:flex; align-items:center; gap:6px; width:100%; margin:0 0 5px; padding:0;
      background:transparent; border:none; cursor:pointer; font-family:inherit; text-align:start; }
    .ov-label { font-size:10px; font-weight:700; letter-spacing:.5px; color:rgba(255,255,255,.64); }
    /* Шеврон один: вправо у свёрнутого, вниз у раскрытого — поворотом, а не второй
       иконкой (в клиенте это chevron.right / chevron.down). */
    .ov-notes-h .chev { width:8px; height:8px; flex:none; color:rgba(255,255,255,.64);
      transition:transform 140ms var(--ease); }
    .ov-notes.open .ov-notes-h .chev { transform:rotate(90deg); }
    /* В RTL свёрнутый шеврон смотрит влево — к началу строки. А раскрытый смотрит
       вниз, и «вниз» одинаково в обе стороны письма: зеркалить нечего, поэтому
       здесь чистый поворот без scaleX. С зеркалом он давал бы поворот в обратную
       сторону и шеврон указывал бы ВВЕРХ — проверено на арабской версии. */
    [dir="rtl"] .ov-notes-h .chev { transform:scaleX(-1); }
    [dir="rtl"] .ov-notes.open .ov-notes-h .chev { transform:rotate(90deg); }
    .ov-notes-h:hover .ov-label, .ov-notes-h:hover .chev { color:#fff; }
    /* Свёрнутый блокнот с текстом — не «пустой»: точка показывает, что запись есть. */
    .ov-notes-h .dot { width:4px; height:4px; border-radius:50%; background:var(--ov-acc); display:none; }
    .ov-notes.has:not(.open) .ov-notes-h .dot { display:block; }
    .ov-notes .box { display:none; width:100%; height:68px; border-radius:12px;
      background:rgba(255,255,255,.045); border:1px solid rgba(255,255,255,.07); padding:8px;
      color:#fff; font-family:inherit; font-size:13px; line-height:1.45; resize:none; outline:none; }
    .ov-notes.open .box { display:block; }
    .ov-notes .box:focus { border-color:rgba(255,255,255,.16); }
    /* Потолок высоты обязателен с приходом НАСТОЯЩИХ ответов: записанный сценарий
       был отмерен по месту, а модель пишет сколько сочтёт нужным, и без него панель
       растягивала бы первый экран на несколько тысяч пикселей. */
    /* Подсказка после витрины: акцентная плашка над ответом. */
    .ov-try { margin-top:12px; padding:10px 14px; border-radius:12px;
      background:rgba(231,174,70,.12); border:1px solid rgba(231,174,70,.38);
      display:flex; flex-direction:column; gap:2px; }
    .ov-try b { font-size:13.5px; font-weight:700; color:var(--ov-acc); }
    .ov-try span { font-size:11.5px; color:rgba(255,255,255,.6); }
    /* Лента распознанной речи — LiveTranscriptStrip: волна 11, текст 12 при 70%,
       две строки с обрезкой С НАЧАЛА, радиус 10, заливка белым 3%. */
    .ov-transcript { display:flex; align-items:flex-start; gap:8px; margin-top:10px;
      padding:8px 12px; border-radius:10px; background:rgba(255,255,255,.03); }
    .ov-transcript svg { width:11px; height:11px; flex:none; margin-top:3px;
      color:rgba(255,255,255,.5); }
    .ov-transcript p { font-size:12px; line-height:1.45; color:rgba(255,255,255,.7);
      display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
    .ov-ans { margin-top:12px; border-radius:14px; background:rgba(255,255,255,.04);
      border:1px solid rgba(255,255,255,.08); padding:12px 14px; min-height:100px;
      max-height:min(46vh, 340px); overflow-y:auto; overscroll-behavior:contain; }
    .ov-ans::-webkit-scrollbar { width:8px; }
    .ov-ans::-webkit-scrollbar-thumb { background:rgba(255,255,255,.14); border-radius:4px; }
    .ov-ans .who { display:inline-block; font-size:10px; font-weight:700; letter-spacing:.5px;
      color:#12141A; background:var(--ov-acc); border-radius:5px; padding:2px 6px; margin-bottom:8px; }
    /* Состояние рядом с меткой: «слушаю», «расшифровываю», «думаю». Без него
       пауза между нажатием и первым словом выглядит зависанием. */
    .ov-ans .ov-state { display:inline-flex; align-items:center; gap:6px; margin-inline-start:8px;
      font-size:11px; font-weight:600; color:rgba(255,255,255,.6); vertical-align:middle; }
    .ov-ans .ov-state::before { content:""; width:6px; height:6px; border-radius:50%;
      background:var(--ov-acc); animation:blink 1.1s ease-in-out infinite; }
    /* Кегль задан ТОЛЬКО на контейнере: ползунок «Размер шрифта» пишет его в
       инлайн-стиль #demoA, а собственное правило у <p> перебивало наследование —
       ползунок двигался, текст не менялся. Внутренние блоки наследуют. */
    #demoA { font-size:15px; line-height:1.55; color:#fff; }
    #demoA p, #demoA li { font-size:inherit; line-height:inherit; }
    /* Разметка ответа. Модель отвечает абзацами, списками и блоками кода —
       показать это одним куском текста значит показать не ответ, а его исходник. */
    #demoA > * + * { margin-top:.62em; }
    #demoA ul, #demoA ol { padding-inline-start:1.15em; }
    #demoA li + li { margin-top:.28em; }
    #demoA .ov-h { font-weight:700; }
    /* Пока диаграмма не нарисовалась (или не нарисуется вовсе), внутри лежит
       исходник в <pre> — на него и рассчитаны отступы. */
    #demoA .ov-dia { margin:0; }
    /* Диаграмма показывается в НАТУРАЛЬНУЮ величину и прокручивается вбок, а не
       ужимается под ширину панели. Вписывание убивало широкие схемы: замер на
       цепочке `graph LR` из шести узлов дал 559×29 пикселей на десктопе и 302×15
       на телефоне — подписи в такой высоте нечитаемы. Клиент решает это так же,
       панорамированием (DiagramScrollView), а не масштабом.

       Размер ставит скрипт, прямо на элементе. Правила ширины здесь СОЗНАТЕЛЬНО
       нет: `!important` в таблице стилей сильнее инлайнового значения, и первая
       попытка так и провалилась — таблица отменяла посчитанный размер, схема
       снова растягивалась по контейнеру. */
    #demoA .ov-dia-svg { overflow-x:auto; overscroll-behavior-x:contain; }
    #demoA .ov-dia-svg::-webkit-scrollbar { height:8px; }
    #demoA .ov-dia-svg::-webkit-scrollbar-thumb { background:rgba(255,255,255,.14); border-radius:4px; }
    #demoA .ov-dia-svg svg { display:block; margin-inline:auto; }
    #demoA code { font-family:"JetBrains Mono",monospace; font-size:.88em;
      background:rgba(255,255,255,.08); border-radius:5px; padding:1px 5px; }
    #demoA pre { font-family:"JetBrains Mono",monospace; font-size:12px; line-height:1.5;
      background:rgba(0,0,0,.30); border:1px solid rgba(255,255,255,.08); border-radius:10px;
      padding:10px 12px; overflow-x:auto; white-space:pre; direction:ltr; text-align:start; }
    #demoA pre code { background:transparent; padding:0; font-size:inherit; }
    #demoA .ov-fail { color:#F2554C; }
    /* Исчерпанный лимит — не поломка, а повод зарегистрироваться: показываем
       обычным текстом с кнопкой, а не красным отказом. */
    #demoA .ov-limit { color:rgba(255,255,255,.86); }
    #demoA .ov-limit-cta { display:inline-flex; align-items:center; gap:6px; margin-top:10px;
      padding:9px 16px; border-radius:999px; background:var(--ov-acc); color:#12141A;
      font-size:13px; font-weight:700; text-decoration:none; }
    #demoA .ov-limit-cta:hover { background:#F0BC5A; }
    .demo.typing #demoA > *:last-child::after { content:"▌"; color:var(--ov-acc); margin-inline-start:1px;
      animation:blink 1s step-end infinite; }
    /* Нижняя лента: ⌘← · счётчик страниц ответа · ⌘→ — как BottomNavRow. */
    .ov-bottom { display:flex; align-items:center; justify-content:space-between; padding:2px 8px 0; margin-top:10px; }
    .ov-nav { background:transparent; border:none; cursor:pointer; color:#fff; font-size:13px;
      display:inline-flex; align-items:center; gap:4px; padding:4px; }
    .ov-nav[disabled] { opacity:.35; cursor:default; }
    .ov-count { font-size:13px; color:rgba(255,255,255,.7); background:rgba(255,255,255,.05);
      border-radius:999px; padding:4px 14px; }
    .ov-grip { position:absolute; inset-inline-end:6px; bottom:6px; width:13px; height:13px; opacity:.6;
      color:var(--ov-acc); pointer-events:none; }
    /* Свёрнутая пилюля: 34x76, #121418 — CollapsedOverlayView. */
    /* overflow:visible нужен панели по наведению: она абсолютная и по построению
       выходит за габариты пилюли, а у .demo стоит overflow:hidden. */
    .demo.mini { padding:0; width:34px; height:76px; touch-action:none; border-radius:17px; background:#121418;
      display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; cursor:pointer;
      overflow:visible; backdrop-filter:none; -webkit-backdrop-filter:none; }
    .demo.mini > *:not(.ov-mini):not(.ov-peek) { display:none; }
    .demo.mini.dragging { cursor:grabbing; }
    /* Перетаскивание уводит пилюлю из потока — иначе первый экран схлопнулся бы
       на её место. Координаты ставит JS.
       margin:0 здесь обязателен. У позиционированного элемента left/top задают
       положение ВНЕШНЕГО (margin) края, а getBoundingClientRect() отдаёт
       border-box — при ненулевом боковом отступе это разные точки. У .hero .demo
       отступ доходит до 140px (min(140px,10vw)), поэтому пилюля в момент захвата
       прыгала вправо ровно на него и дальше ехала со смещением: замер показал
       +128px при окне 1280. Обнуление сводит обе системы координат к border-box —
       в них же считается и ограничитель по краям окна ниже. */
    .demo.mini.floating { position:fixed; z-index:80; margin:0; }
    .ov-mini { display:none; flex-direction:column; align-items:center; gap:8px; }
    .demo.mini .ov-mini { display:flex; }
    /* «W» — настоящая кнопка, как в клиенте: именно она разворачивает оверлей
       (в CollapsedOverlayView это Button → settings.collapsed = false). Буква
       СЕРАЯ при золотом логотипе намеренно: под ней золотой эквалайзер, и золото
       на золоте замыливает акцент с живого индикатора записи. */
    .ov-mini-w { position:relative; width:24px; height:24px; padding:0; border:none; background:transparent;
      cursor:pointer; font-family:inherit; font-size:18px; font-weight:800; line-height:1;
      color:rgba(255,255,255,.64); transition:color 140ms var(--ease); }
    .ov-mini-w:hover { color:#fff; }
    .ov-mini .ov-eq { width:21px; height:20px; }
    .ov-mini .ov-eq i:nth-child(n+5) { display:none; }

    /* ── Панель по наведению на свёрнутую пилюлю ── */
    /* Портрет CollapsedOverlayView: по наведению рядом с пилюлей раскрывается
       лента реплик + поле вопроса. Числа из клиента, а не на глаз: панель 400,
       зазор 10, поля 12, радиус 20, лента 300, пузырь 13.5/9+12/радиус 14.
       Анимации нет намеренно — в клиенте её убрали, чтобы не дёргалась пилюля. */
    /* Смещения считаются от PADDING-box пилюли, а её border-box на 1px рамки шире
       с каждой стороны: «100% + 10px» давало зазор 9px, а top:0 опускал панель на
       пиксель ниже пилюли. Рамку добавляем явно — в клиенте пилюля и панель стоят
       в одном HStack с выравниванием по верху, то есть ровно вровень. */
    .ov-peek { position:absolute; top:-1px; inset-inline-end:calc(100% + 11px);
      width:400px; max-width:calc(100vw - 60px); display:none; flex-direction:column; gap:10px;
      padding:12px; border-radius:20px; background:rgba(14,16,20,.86);
      border:1px solid rgba(255,255,255,.12); box-shadow:0 18px 40px rgba(0,0,0,.45);
      backdrop-filter:blur(20px) saturate(180%); -webkit-backdrop-filter:blur(20px) saturate(180%);
      text-align:start; cursor:default; transform:translateX(var(--peek-nudge, 0px)); }
    .demo.mini.peeking .ov-peek { display:flex; }
    /* Сторона раскрытия: по умолчанию inline-start (в LTR слева, в RTL справа —
       ровно правило клиента, и логические свойства дают его без JS). У края окна
       JS вешает .peek-flip, и панель уходит на противоположную сторону. */
    .demo.mini.peek-flip .ov-peek { inset-inline-end:auto; inset-inline-start:calc(100% + 11px); }
    /* Так же по вертикали: у нижнего края равняем панель по низу пилюли. */
    .demo.mini.peek-up .ov-peek { top:auto; bottom:-1px; }
    /* Телефон: панель шире, чем место с ЛЮБОЙ стороны (330px против 390px экрана
       вместе с пилюлей), и переворот тут бессилен — сбоку она встала бы поверх
       пилюли. Поэтому ставим её под пилюлю (у нижнего края — над), а поперёк
       довдигает в окно тот же --peek-nudge. Отступ 11px = зазор 10 + рамка. */
    .demo.mini.peek-stack .ov-peek { inset-inline-end:auto; inset-inline-start:0; top:calc(100% + 11px); }
    .demo.mini.peek-stack.peek-up .ov-peek { top:auto; bottom:calc(100% + 11px); }
    .ov-peek-feed { max-height:300px; overflow-y:auto; overscroll-behavior:contain;
      display:flex; flex-direction:column; gap:8px; }
    .ov-peek-feed::-webkit-scrollbar { width:8px; }
    .ov-peek-feed::-webkit-scrollbar-thumb { background:rgba(255,255,255,.14); border-radius:4px; }
    /* Реплики различаются стороной, а не подписью — как в клиенте: своя справа,
       чужая слева, «пружина» минимум 40px не даёт пузырю занять всю ширину. */
    .pk { display:flex; }
    .pk.me { justify-content:flex-end; }
    .pk p { max-width:calc(100% - 40px); font-size:13.5px; line-height:1.5; color:#fff;
      padding:9px 12px; border-radius:14px; background:rgba(255,255,255,.045);
      border:1px solid rgba(255,255,255,.07); }
    /* Ответ AI выделен акцентной рамкой: его читают, а не просматривают. */
    .pk.ans p { background:rgba(231,174,70,.10); border-color:rgba(231,174,70,.35); }
    .ov-peek-ask { display:flex; align-items:center; gap:8px; }
    .ov-peek-field { flex:1; min-width:0; font-size:13.5px; color:#fff;
      padding:9px 12px; border-radius:14px; background:rgba(255,255,255,.045);
      border:1px solid rgba(255,255,255,.07); outline:none; font-family:inherit; }
    .ov-peek-field::placeholder { color:rgba(255,255,255,.42); }
    .ov-peek-field:focus { border-color:rgba(255,255,255,.16); }
    .ov-peek-btn { flex:none; display:inline-flex; align-items:center; gap:6px; padding:10px 12px;
      border-radius:999px; background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.12);
      font-size:12.5px; font-weight:600; color:#fff; white-space:nowrap; cursor:pointer;
      font-family:inherit; }
    .ov-peek-btn:hover { background:rgba(255,255,255,.24); }
    /* 13px, а не 11 как в клиенте: там SF Symbol «sparkles», он при том же кегле
       заметно плотнее контурной иконки — на 11px она читалась крапинкой. */
    .ov-peek-btn svg { width:13px; height:13px; flex:none; }
    /* Сенсорный экран: наведения нет, панель открывается касанием пилюли. Заодно
       расширяем цель у «W» — 24px под палец мало, а габариты пилюли трогать
       нельзя, поэтому растёт только зона попадания. */
    @media (hover: none) {
      .ov-mini-w::after { content:""; position:absolute; inset:-7px -5px; }
    }
    /* Настройки — панель как SettingsMenuView: ширина 348, радиус 22, тёмная
       заливка БЕЗ канта по контуру (в клиенте его убрали намеренно). Показываем
       состояние тарифа Max: все переключатели включены. */
    /* Высота ФИКСИРОВАННАЯ — 600, ровно `.frame(maxHeight: 600)` клиента. Была
       привязана к высоте окна демо, и панель то и дело меняла размер вслед за
       длиной ответа: в приложении она так себя не ведёт. На коротком экране
       ужимается по высоте окна — иначе уехала бы за его пределы. */
    /* ПЛАВАЮЩЕЕ окно, а не absolute внутри оверлея. Первый экран (`.hero`)
       обрезает переполнение — и обязан: демо-панель намеренно выдвинута за
       правый край, без обрезки появилась бы горизонтальная прокрутка. Но панель
       настроек выше окна демо (600 против ~260), и `overflow:hidden` предка
       срезал ей нижнюю половину — на замере ниже середины уже виднелась
       страница. Слоем это не лечится: обрезка предка сильнее любого z-index.
       Позицию считает JS от места оверлея на экране (`placeSettings`). */
    .ov-set { position:fixed; width:348px; max-width:calc(100vw - 24px);
      height:600px; max-height:calc(100vh - 140px); overflow-y:auto; overscroll-behavior:contain;
      border-radius:22px; background:#121418; padding:0 0 12px; display:none;
      /* Слой: выше шапки (30), выпадающего языка (40), полосы прогресса (60) и
         плавающей пилюли (80), но НИЖЕ cookie-баннера (200) — перекрывать
         запрос согласия нельзя, это его единственная точка показа. */
      z-index:90;
      box-shadow:0 18px 40px rgba(0,0,0,.45); }
    .ov-set::-webkit-scrollbar { width:8px; }
    .ov-set::-webkit-scrollbar-thumb { background:rgba(255,255,255,.14); border-radius:4px; }
    .demo.settings .ov-set { display:block; }
    /* Пока настройки открыты, окно демо снимает размытие подложки. Это не про
       вид, а про раскладку: `backdrop-filter` делает элемент точкой отсчёта для
       ВСЕХ потомков с `position:fixed`. С ним панель отсчитывалась от окна демо
       и обрезалась им — замер показывал предка-обрезателя `demo` и панель,
       уехавшую за нижний край экрана. Без него `fixed` считается от экрана, как
       и задумано. Тот же приём уже применён к свёрнутой пилюле выше.

       Потеря незаметна: подложка окна и так почти непрозрачна (86 %), а при
       открытых настройках окно почти целиком закрыто самой панелью. */
    .demo.settings { backdrop-filter:none; -webkit-backdrop-filter:none; }
    .ov-set-h { position:sticky; top:0; background:#121418; z-index:1;
      display:flex; align-items:center; gap:10px; padding:13px 16px; }
    .ov-set-h .g { width:28px; height:28px; border-radius:50%; background:rgba(255,255,255,.06); flex:none;
      display:inline-flex; align-items:center; justify-content:center; color:var(--ov-acc); }
    .ov-set-h .g svg { width:13px; height:13px; }
    .ov-set-h b { flex:1; font-size:17px; font-weight:700; color:#fff; }
    .ov-set-h .x { width:28px; height:28px; border-radius:50%; background:rgba(255,255,255,.06);
      display:inline-flex; align-items:center; justify-content:center; font-size:12px; font-weight:700;
      color:rgba(255,255,255,.7); flex:none; border:none; padding:0; cursor:pointer;
      font-family:inherit; transition:background 140ms var(--ease), color 140ms var(--ease); }
    .ov-set-h .x:hover { background:rgba(255,255,255,.16); color:#fff; }
    .ov-set-h .x:focus-visible { outline:2px solid var(--ov-acc); outline-offset:2px; }
    /* Заголовок раздела: 10.5/bold, tracking 0.7, белый 38% и отступ 8px до
       карточки — ровно `section(_:content:)` из SettingsMenuView. */
    .ov-set-s { font-size:10.5px; font-weight:700; letter-spacing:.7px; text-transform:uppercase;
      color:rgba(255,255,255,.38); padding:14px 16px 8px; }
    /* Разделы — карточками, как в клиенте: строки внутри разделены волосяной
       линией, а не висят на общем фоне. */
    /* Карточка-группа: радиус 16, заливка белым 4.5% и кант белым 7% —
       SettingsCardModifier. Канта здесь раньше не было вовсе, и группы
       сливались с фоном панели. */
    .ov-card { margin:0 12px; border-radius:16px; background:rgba(255,255,255,.045);
      border:1px solid rgba(255,255,255,.07); }
    /* Разделитель — белый 6% с отступом 14 (rowDivider). */
    .ov-hr { height:1px; background:rgba(255,255,255,.06); margin:0 14px; }
    .ov-set-r { display:flex; align-items:center; gap:12px; padding:11px 14px; font-size:13.5px; color:#fff; }
    .ov-set-r span { flex:1; min-width:0; font-weight:600; }
    .ov-set-r span i { display:block; font-style:normal; font-size:11.5px; font-weight:400;
      color:rgba(255,255,255,.45); margin-top:2px; }
    .ov-set-r em { font-style:normal; font-size:12.5px; color:rgba(255,255,255,.64); }
    .ov-set-r:has(em.pick) { padding-block:10px; }
    /* Выпадающий список — пилюля с иконкой и шевроном, как в pickerRow. */
    /* CustomSelector: капсула с кантом белым 16%, заливка белым 7%, подпись
       13/medium, отступы 8 на 13, предел ширины 190. Канта не было, и пилюля
       сливалась с карточкой. */
    .ov-set-r em.pick { display:inline-flex; align-items:center; gap:6px; padding:8px 13px;
      border-radius:999px; background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.16);
      color:#fff; font-size:13px; font-weight:500; max-width:190px;
      white-space:nowrap; overflow:hidden; }
    .ov-set-r em.pick svg { width:12px; height:12px; flex:none; color:rgba(255,255,255,.64); }
    .ov-set-r em.pick svg.ch { width:10px; height:10px; opacity:.7; }
    .ov-set-r em.pick .up { text-transform:uppercase; }
    /* Закрыть приложение — closeAppButton: радиус 13, заливка danger 10%,
       кант danger 45%, подпись 14/semibold, отступы 12 по вертикали и 16 по краям.
       Заливки здесь не было вовсе — кнопка читалась пустой рамкой. */
    .ov-quit { margin:14px 16px 0; padding:12px; border-radius:13px;
      border:1px solid rgba(242,85,76,.45); background:rgba(242,85,76,.10);
      color:#F2554C; font-size:14px; font-weight:600; display:flex; align-items:center;
      justify-content:center; gap:9px; }
    .ov-quit svg { width:16px; height:16px; }
    /* Ползунок настоящий: input[type=range] тянется мышью и с клавиатуры, значение
       рядом обновляется, а панель реагирует сразу — как в приложении. */
    .ov-set-sl { flex-wrap:wrap; }
    .ov-set-sl em { color:var(--ov-acc); font-weight:600; font-size:13px; }
    .ov-set-sl input[type=range] { flex:0 0 100%; margin-top:8px; height:16px; -webkit-appearance:none;
      appearance:none; background:transparent; cursor:pointer; }
    .ov-set-sl input[type=range]::-webkit-slider-runnable-track { height:5px; border-radius:3px;
      background:linear-gradient(var(--ov-acc),var(--ov-acc)) 0/var(--fill,50%) 100% no-repeat, rgba(255,255,255,.12); }
    .ov-set-sl input[type=range]::-moz-range-track { height:5px; border-radius:3px; background:rgba(255,255,255,.12); }
    .ov-set-sl input[type=range]::-moz-range-progress { height:5px; border-radius:3px; background:var(--ov-acc); }
    .ov-set-sl input[type=range]::-webkit-slider-thumb { -webkit-appearance:none; appearance:none;
      width:16px; height:16px; border-radius:50%; background:#fff; margin-top:-5.5px;
      box-shadow:0 1px 3px rgba(0,0,0,.4); }
    .ov-set-sl input[type=range]::-moz-range-thumb { width:16px; height:16px; border:none; border-radius:50%;
      background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.4); }
    /* Тумблер — портрет PillToggleStyle (OverlayTheme.swift): капсула 42x25,
       белый бегунок 20 с мягкой тенью, поля 2.5. Включённый залит АВРОРОЙ —
       градиентом accent→accentDeep по диагонали, а не плоским акцентом: именно
       он отличает «своё» состояние от нарисованного на глаз. */
    .ov-set-r .ov-sw { width:42px; height:25px; border-radius:12.5px; background:rgba(255,255,255,.13);
      text-decoration:none; flex:none; position:relative;
      transition:background 180ms var(--ease); }
    .ov-set-r .ov-sw::after { content:""; position:absolute; top:2.5px; inset-inline-start:2.5px;
      width:20px; height:20px; border-radius:50%; background:#fff;
      box-shadow:0 .5px 1.5px rgba(0,0,0,.28);
      transition:inset-inline-start 260ms cubic-bezier(.34,1.3,.64,1); }
    .ov-set-r .ov-sw.on { background:linear-gradient(135deg, #E7AE46, #D9923A); }
    .ov-set-r .ov-sw.on::after { inset-inline-start:19.5px; }
    /* Тумблер стал настоящей кнопкой — снимаем оформление кнопки, оставляя
       только сам ползунок, и возвращаем видимый фокус для клавиатуры. */
    button.ov-sw { -webkit-appearance:none; appearance:none; border:none; padding:0; margin:0;
      font:inherit; cursor:pointer; }
    button.ov-sw:focus-visible { outline:2px solid var(--ov-acc); outline-offset:2px; }
    /* Строка возможности, которой в демонстрации нет (см. features/demo/domain/toggles.py):
       приглушена, ни нажатием, ни клавиатурой не включается, подсказка объясняет почему. */
    .ov-set-r.locked, .ov-quit.locked { opacity:.45; cursor:not-allowed; }
    /* Гасим нажатие на самом ЭЛЕМЕНТЕ управления, а не на строке: строка обязана
       остаться кликабельной, иначе её обработчик не остановит всплытие и нажатие
       уйдёт панели. У «Закрыть приложение» элемент управления и есть строка. */
    .ov-set-r.locked .ov-sw, .ov-set-r.locked .pick { pointer-events:none; }
    @media (prefers-reduced-motion: reduce) { .ov-eq i { animation:none; height:52%; } }

    /* ── Адаптив оверлея ─────────────────────────────────────────────────────
       Живёт здесь, а не в адаптиве страницы: правило про `.ov-field` бесполезно
       там, где оверлея нет, и обязано ехать вместе с ним. */
    @media (max-width: 980px) {
      .demo { padding:12px 14px 12px; border-radius:20px; }
      .ov-top { gap:8px; }
      .ov-btn { width:34px; height:34px; }
      .ov-eq { width:34px; }
      .ov-eq i:nth-child(n+6) { display:none; }
      .ov-field { padding:6px 6px 6px 12px; border-radius:14px; }
      .ov-field input { font-size:13.5px; }
      .ov-pill { padding:5px 6px; }
      .ov-pill b { width:20px; height:20px; font-size:12px; }
      .ov-notes .box { height:44px; }
      .ov-ans { min-height:76px; padding:10px 12px; }
      /* На телефоне панель во всю ширину окна, позицию всё так же считает JS. */
      .ov-set { width:calc(100vw - 20px); max-width:none; }
      .ov-card { margin:0 8px; }
      .ov-set-r em.pick { max-width:130px; }
    }
    /* Телефон. До 492px панель перестаёт упираться в max-width и начинает сжиматься
       вместе с экраном, а верхняя строка оверлея в один ряд не помещается: поле
       ввода схлопывалось до нуля (плейсхолдера не видно), а ⌘↵ и снимок экрана —
       flex:none — вылезали за скруглённый край поля на 24px, наезжая на кнопку
       свёртки. Отдаём полю собственный ряд; расстановка клиента сохраняется:
       транспорт слева, свёртка и настройки — у правого края. */
    @media (max-width: 480px) {
      .ov-top { flex-wrap:wrap; row-gap:10px; }
      .ov-field { order:1; flex-basis:100%; }
      /* Соседний по разметке — #ovCollapse; auto-отступ уводит его и шестерёнку
         вправо, как в клиенте. Селектор структурный, чтобы не связывать вёрстку
         с id, которые заняты под обработчики. */
      .ov-field + .ov-btn { margin-inline-start:auto; }
      /* Кнопки настоящие (свернуть, настройки, снимок), а 34px для пальца мало.
         Место в первом ряду освободилось — поднимаем до 40px. */
      .ov-btn { width:40px; height:40px; }
      .ov-shot { width:34px; height:34px; }
      .ov-nav { padding:8px 10px; }
      /* На узком экране высота панели упирается в окно, а не в макет клиента:
         600 px там попросту некуда положить. Верх задаёт JS, поэтому `top`
         здесь не трогаем — инлайновое значение всё равно сильнее. */
      .ov-set { max-height:calc(100vh - 24px); }
    }

    /* ── Живой перевод ───────────────────────────────────────────────────────
       Портрет `VoiceTranslationOverlay.swift`: пульсирующая точка с подписью и
       две плашки друг под другом. Ни карточек режимов, ни своих кнопок — в
       клиенте этого нет, а демонстрация его портрет. Управление живёт в панели
       настроек и пользуется её же строками (`.ov-set-r`, `.ov-hr`, `.pick`). */
    .ov-tr { margin-top:12px; display:flex; flex-direction:column; gap:8px; }
    .ov-tr-head { display:flex; align-items:center; gap:8px; font-size:12px; font-weight:600;
      color:rgba(255,255,255,.75); }

    /* То же наследство страницы: `.sol b` перекрашивал подпись в тёмный. */
    .demo .ov-tr-head b { font-weight:800; color:rgba(255,255,255,.75); }
    .ov-tr-dot { width:9px; height:9px; border-radius:50%; background:var(--ov-acc); flex:none;
      animation:ovTrPulse 1.8s ease-in-out infinite; }
    @keyframes ovTrPulse { 0%,100% { opacity:1; } 50% { opacity:.35; } }
    /* Часы прижаты к правому краю строки: слева — состояние, справа — сколько
       его осталось. Моноширинный, иначе цифры дёргают подпись при каждом тике. */
    .ov-tr-clock { margin-inline-start:auto; font-family:"JetBrains Mono",monospace;
      font-size:12px; font-weight:700; color:var(--ov-acc); }
    .ov-tr-caps { display:flex; flex-direction:column; gap:8px; }
    .ov-tr-cap { padding:10px; border-radius:12px; background:rgba(0,0,0,.35); min-width:0; }
    .ov-tr-cap p { margin-top:2px; font-size:14px; line-height:1.4; color:#fff;
      overflow-wrap:anywhere; }

    /* Строка причины под тумблером — то же место, что у `voice.lastError`.

       Селектор начинается с `.demo` не для красоты: на посадочных панель стоит
       ВНУТРИ `.sol`, а у той свои правила для элементов (`.sol p`, `.sol b`).
       По специфичности они бьют одиночный класс, и подпись получала кегль
       абзаца страницы вместо своего — замер показал 15.5px вместо 11px и
       тёмно-серый вместо белого. */
    .demo .ov-tr-note { margin-top:8px; font-size:11px; line-height:1.45;
      color:rgba(255,255,255,.55); }
    .demo .ov-tr-note.warn { color:#F2554C; }

    /* Подпись под строкой настроек — тот же приём, что у причины отказа перевода:
       объяснение стоит там, где стоит сама строка, а не в подсказке по наведению,
       которой на телефоне нет вовсе. Кегль и цвет общие с `.ov-tr-note`. */
    .demo .ov-note { margin:6px 0 2px; font-size:11px; line-height:1.45;
      color:rgba(255,255,255,.55); }

    /* ПОРТРЕТ, А НЕ УПРАВЛЕНИЕ. Страница про базу знаний показывает её строку
       включённой: человек пришёл смотреть именно на эту возможность, и строка в
       выключенном положении рассказывала бы про неё обратное.

       Отдельный класс, а не `locked`, ровно из-за приглушения: `locked`
       уменьшает непрозрачность до .45, и «включено» читалось бы как «сломано».
       Нажатие при этом по-прежнему не проходит — включать здесь нечего, — а
       почему, написано подписью под строкой (`.ov-note`), которая обязательна:
       без неё светящийся тумблер обещал бы живой ответ по чужим документам. */
    .demo .ov-set-r.shown { cursor:default; }
    .demo .ov-set-r.shown .ov-sw, .demo .ov-set-r.shown .pick { pointer-events:none; }

    /* Параметры перевода раскрываются только при включённом тумблере: в
       выключенном состоянии они ни на что не влияют и только шумят в панели. */
    .ov-tr-rows { margin-top:2px; }
    /* Пикер в строке настроек — системный <select> внутри той же «пилюли», что и
       у остальных строк. Список раскрывается виджетом ОС и рисуется в светлой
       палитре: без явного цвета пункты получались белым по белому. */
    .ov-set-r em.pick select { appearance:none; -webkit-appearance:none; border:0;
      background:transparent; color:inherit; font:inherit; cursor:pointer;
      max-width:150px; padding-inline-end:2px; }
    .ov-set-r em.pick select option { color:#111; background:#fff; }

    /* ── Режим переводчика ───────────────────────────────────────────────────
       Окно остаётся ОКНОМ: поле ввода, «Заметки» и счётчик страниц на месте —
       ровно так оно выглядит в клиенте, когда перевод идёт. Уходит только то,
       что относится к ОТВЕТУ: сам ответ, подсказка «попробуйте сами», лента
       распознавания и плашка снимка экрана. Их место занимают плашки перевода.

       Прячем разметкой, а не удалением: компонент один на все страницы, и своя
       копия ради одного режима разошлась бы с ним при первой же правке. */
    .demo--translate .ov-ans,
    .demo--translate .ov-try,
    .demo--translate .ov-transcript,
    .demo--translate .ov-shot-toast { display:none !important; }
    /* Панель перевода здесь — главное содержимое, и отступ сверху ей не нужен:
       над ней только строка транспорта. */
    .demo--translate .ov-tr { margin-top:10px; }
  
