/* Витрина казино-креативов. Одна таблица стилей, без сборки и без Node.
   Значения не выдуманы — они из замеренной системы родительского проекта
   (docs/08-design.md), и каждое ограничение ниже стоит за числом. */

/* ── ШРИФТЫ: самохостом, никогда fonts.googleapis.com (LG München Az. 3 O 17493/20).
   Два семейства, две подмножества каждое: 108 КБ на всё. `swap` — чтобы текст
   был виден сразу, а не через полсекунды. */
@font-face { font-family: Onest; src: url(/s/fonts/onest-latin.woff2) format("woff2");
  font-weight: 400 500; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+2000-206F, U+2192, U+2212; }
@font-face { font-family: Onest; src: url(/s/fonts/onest-cyrillic.woff2) format("woff2");
  font-weight: 400 500; font-display: swap; unicode-range: U+0400-045F, U+0490-0491; }
/* ЛАТИНИЦА РАСШИРЕННАЯ И ВЬЕТНАМСКАЯ. Не роскошь: на них набраны имена
   рекламодателей половины ЕС — `Michał Nowak`, `Casino Győr`, `Tenis živě`,
   `Markéta Benešová`, `Phương Chinh`. Без этих двух файлов такое имя приезжало
   не в Onest, а в системный шрифт браузера: в одной строке ленты стояли два
   разных начертания, и заметить это можно было только на польском или чешском
   рекламодателе. Замер 01.09.2026: 2.0 % живых имён (327 из 16 494), после
   добавления — 0.42 %, и остаток это греческий с ударениями и арабская вязь,
   которых у Onest нет вовсе. Диапазоны — как у самого Google Fonts (v11);
   браузер качает файл, только если такая буква на странице есть. */
@font-face { font-family: Onest; src: url(/s/fonts/onest-latin-ext.woff2) format("woff2");
  font-weight: 400 500; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
  U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
  U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: Onest; src: url(/s/fonts/onest-vietnamese.woff2) format("woff2");
  font-weight: 400 500; font-display: swap;
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
  U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
  U+1EA0-1EF9, U+20AB; }
@font-face { font-family: "Martian Mono"; src: url(/s/fonts/martianmono-latin.woff2) format("woff2");
  font-weight: 400; font-display: swap; unicode-range: U+0000-00FF, U+2000-206F; }
@font-face { font-family: "Martian Mono"; src: url(/s/fonts/martianmono-cyrillic.woff2) format("woff2");
  font-weight: 400; font-display: swap; unicode-range: U+0400-045F; }

:root {
  /* ПЯТЬ АХРОМАТИЧЕСКИХ ТОНОВ. Тон 65–77°, тёплый нейтральный: холодный серый
     рядом с пёстрой рекламой даёт грязь. Контрасты посчитаны, а не подобраны:
     кадр/бумага 1.16 (при 1.09 заливка перестаёт читаться как граница),
     волос 1.53, карандаш 4.86, чернила 13.96. */
  --paper:  #e9ebdf;   /* фон страницы И просветы между кадрами */
  --frame:  #fafbf6;   /* плита кадра, поля при object-fit: contain */
  --hair:   #bfc0b4;   /* линия, только там, где разделяет смыслы */
  --pencil: #63665b;   /* весь текст метаданных */
  --ink:    #1c1e17;   /* текст, заливка действия и активного состояния */
  /* ЕДИНСТВЕННЫЙ ХРОМАТИЧЕСКИЙ, и значение у него ровно одно: «в показе
     сейчас». 145° выбран по гистограмме корпуса: полоса 100–189° держит 6.87 %
     хроматической массы чужих реклам, а у оранжевого её 57 %. Цветной хром в
     такой сетке читался бы как тридцать первая реклама. */
  --live:   #1c6b3d;

  /* ШЕСТЬ КЕГЛЕЙ, между ними ничего. Отношение 44/11 = 4.0. Добавлять размер,
     чтобы починить вёрстку, запрещено — чинится вёрстка. */
  --t-sig: 30px; --t-prose: 16px; --t-name: 15px;
  --t-ui: 13px;  --t-cap: 12px;   --t-meta: 11px;

  --gap: 6px;          /* модуль: он же просвет плёнки. Всё кратно ему */
  --radius: 10px;      /* единственный радиус: шкале нечем разъехаться */
  /* Поле у края экрана: полтора модуля. Плитка добирает свои полмодуля изнутри,
     и кадр встаёт ровно в двух модулях от края. */
  --edge: calc(var(--gap) * 2 - var(--gap) / 2);
  /* ШИРИНА ПАНЕЛИ КРЕАТИВА, и знать её должны двое: сама панель и лента, которая
     освобождает под неё место. Два числа в двух местах разъедутся, и лента либо
     залезет под панель, либо оставит полосу пустой бумаги. 360 px — предел, ниже
     которого ссылка объявления ломается на четыре строки; 520 — тот, за которым
     панель начинает спорить с лентой за место, ради которого лента и есть. */
  --panel: clamp(360px, 25vw, 520px);
  /* Ряда в пикселях больше нет: он сороковая доля колонки и считается от
     ширины сетки — см. .sheet. */
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--paper); color: var(--ink);
  font: 400 var(--t-ui)/1.45 Onest, ui-sans-serif, system-ui, sans-serif;
  font-variant-numeric: tabular-nums;   /* числа не должны плясать по ширине */
}
a { color: inherit; text-decoration: none; }

/* ЕСТЬ, НО НЕ ВИДНО. Ровно одно применение: `h1` страницы. Шапка представляет
   продукт знаком, а не строкой текста, — но страница без заголовка не называет
   себя ничем ни экранному диктору, ни поисковику (на витрине не было ни одного
   `h1`, иерархия начиналась с `h3`).
   НЕ `display: none`, НЕ `hidden` и НЕ `opacity: 0`: первые два убирают элемент
   и из чтения диктором — то есть ровно оттуда, ради чего он тут, — а третий
   оставляет его и в отрисовке, и в проверке попаданий, то есть прозрачная
   строка ловила бы клики поверх ленты. Прямоугольник в один пиксель за пределами
   экрана не делает ни того, ни другого. */
.vh {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* МЕСТО ПОД ПАНЕЛЬ ОСВОБОЖДАЕТ ВСЯ СТРАНИЦА, а не одна сетка. Сперва сужалась
   только лента — и под панель уехал правый край строки ввода: поле «стран до…»
   оказалось наполовину под листом. Отступ у страницы: ширина панели, её отступ
   от края и просвет до плёнки. Обёртка ленты — контейнер запросов, и колонки
   пересчитываются сами, потому что сужается и она. */
body.with-panel { padding-right: calc(var(--panel) + var(--gap) * 2); }
.mono { font-family: "Martian Mono", ui-monospace, monospace; letter-spacing: -.02em; }

/* ── ШАПКА ─────────────────────────────────────────────────────────────────── */
/* ШАПКА НЕ ЛИПКАЯ И НЕ ПО ЦЕНТРУ. Липкой она была ради числа-сигнатуры: то
   число менялось от фильтров, и его держали на виду при прокрутке. Числа нет,
   а один логотип занимать полосу экрана при каждой прокрутке ленты не должен —
   лента и есть продукт. Заодно ушли `z-index` и непрозрачный фон: они нужны
   были, только чтобы плитки проезжали ПОД шапкой. */
header {
  background: var(--paper);
  border-bottom: 1px solid var(--hair);
  padding: calc(var(--gap) * 2) calc(var(--gap) * 2);
  /* Знак стоит слева, по началу строки — там же, где начинается всё остальное
     на странице: полоса фильтров, сетка плиток, текст. Выключка по базовой
     линии ушла вместе с числом: выключать логотип больше не с чем, а
     `baseline` при единственном ребёнке молча ничего не значит. */
  display: flex; gap: calc(var(--gap) * 2); align-items: center; flex-wrap: wrap;
}
/* ЛОГОТИП. Высота задаётся ЕМУ, а не кеглю: у знака нет ни строчных, ни
   выносных, и «15px» на нём означало бы всю картинку целиком — прописная вышла
   бы вдвое ниже соседнего текста.
   ВЫСОТА ТЕКУЧАЯ, и это не украшение. Логотип широкий — 6.3 своих высоты, —
   поэтому на телефоне он упирался бы в края: 56 px это 353 px строки при 366
   доступных. `clamp` держит потолок на широком экране и сам сходит к 32 на
   390 px (6vw = 23, снизу подпирает 32). Медиазапроса для этого не нужно.
   Потолок 44, а не 56: при 56 знак вставал вровень с полосой фильтров под ним
   и спорил с ней за первый взгляд. Прописная при 44 px идёт 25 px, знак — 44:
   он выше прописной в 1.75 раза (MARK_CAP в tools/brand.py), и высота бокса это
   уже учитывает.
   `width: auto` вместе с `viewBox` держит пропорцию: второго числа о ширине в
   разметке нет и не должно быть — оно уже есть в самом SVG. */
.brand { color: var(--ink); display: inline-flex; align-items: center; }
/* Знак — единственное, что есть в шапке, и стоит он блоком: строчной картинке
   браузер оставляет снизу просвет под выносные буквы, которых в логотипе нет,
   и шапка получалась на четыре пикселя выше, чем нарисована.
   С 05.09.2026 знак приезжает ФАЙЛОМ (`/s/logo.svg`), а не разметкой: 17 001
   байт на каждой из полутора миллионов страниц — см. `_logo_box` в app.py.
   Ширина и высота стоят атрибутами в разметке (из самого файла), поэтому место
   под знак браузер держит ещё до того, как файл приехал, и шапка не дёргается. */
.brand .logo { height: clamp(32px, 6vw, 44px); width: auto; display: block; }
/* Стрелка «назад» — текст, и кегль у неё свой: она про действие, а не про имя. */
.brand .arr { font-size: var(--t-name); margin-right: var(--gap); }
/* Знак — ссылка на обеих страницах: с ленты на чистый адрес, с карточки назад в
   ленту. Отзыв на наведение общий, чтобы клик был виден до клика.
   ЦВЕТОМ ОН БОЛЬШЕ НЕ ОТВЕЧАЕТ, и это следствие переезда в файл: картинка не
   наследует `color` у страницы, поэтому слово в логотипе покрашено чернилами
   внутри самого SVG. Отзыв даёт прозрачность; стрелка «назад» — текст, и она
   по-прежнему меняет цвет вместе со ссылкой. */
.brand:hover { color: var(--pencil); }
.brand:hover .logo { opacity: .72; }
/* `.sig` и `.sub` жили здесь до 01.09.2026 — сигнатура-число и строка под ней.
   Убраны вместе с разметкой: правило без разметки живёт до первой чистки, а
   потом кто-нибудь возвращает по нему элемент, которого в продукте нет. */
/* Поиск — такая же ось, как страна или формат, просто вводимая с клавиатуры,
   и стоит он в одной строке с числом, а не отдельной полосой. Поле и кнопка
   склеены в одну фигуру: два отдельных прямоугольника с просветом читаются как
   два разных элемента управления. */
form.find { margin-left: auto; display: flex; align-items: stretch;
            background: var(--frame); border: 1px solid var(--hair);
            border-radius: var(--radius); overflow: hidden; }
form.find:focus-within { border-color: var(--pencil); }
input[type=search] {
  font: inherit; padding: 6px 11px; min-width: 260px;
  background: none; color: var(--ink); border: 0; border-radius: 0;
}
input[type=search]:focus { outline: none; }
/* Действие — чернильная заливка, а не цвет: хроматический тон в системе один,
   и он занят состоянием «в показе». */
form.find button {
  font: inherit; font-size: var(--t-cap); cursor: pointer;
  padding: 0 calc(var(--gap) * 2); border: 0;
  background: var(--ink); color: var(--paper);
}
form.find button:hover { background: var(--pencil); }

/* ── СТРОКА ВВОДА ──────────────────────────────────────────────────────────────
   Три поля и одна кнопка: текст, страна словами, ширина залива. Раскладка
   явными областями, а не потоком: поля разной важности и разной ширины, и на
   узком экране они должны переноситься по смыслу, а не как придётся. */
.top { display: grid; gap: var(--gap); align-items: center;
       grid-template-columns: auto 1fr;
       padding: var(--gap) calc(var(--gap) * 2) 0; }
/* Позиции ЯВНЫЕ: кнопка и поля — один ряд, панель — весь следующий.
   Автопоток положил бы панель в свободную ячейку первого ряда. */
.top > .f-btn { grid-row: 1; grid-column: 1; }
.top > .ask   { grid-row: 1; grid-column: 2; }
.top > .pan   { grid-row: 2; grid-column: 1 / -1; }

/* Переключатель панели: сам флажок не показывается никогда, его роль играет
   `label`. `display: none` ему нельзя — снятый с отрисовки флажок перестаёт
   принимать фокус с клавиатуры, и панель становится недоступной без мыши.
   Поэтому он размером в пиксель и спрятан за край. */
.f-toggle { position: absolute; width: 1px; height: 1px; opacity: 0;
            pointer-events: none; }
.f-toggle:not(:checked) ~ .top > .pan { display: none; }
.f-toggle:focus-visible ~ .top > .f-btn { border-color: var(--ink); }
/* Пять колонок с 08.09.2026: у домена своё поле (`spy/domdict.py`). Текстовому
   ужато с 5fr до 4fr — из него ушла целая обязанность, а домен длиннее страны и
   без своей доли колонки не читается. */
.ask { display: grid; gap: var(--gap);
       grid-template-columns: 4fr 3fr 3fr 2fr auto; }
@media (max-width: 1100px) { .ask { grid-template-columns: 1fr 1fr; }
                             .ask .f-go { grid-column: 1 / -1; } }
@media (max-width:  640px) { .ask { grid-template-columns: 1fr; } }
.ask input {
  font: inherit; font-size: var(--t-cap); min-width: 0; width: 100%;
  padding: 6px 10px; color: var(--ink);
  background: var(--frame); border: 1px solid var(--hair);
  border-radius: var(--radius);
}
.ask input:focus { outline: none; border-color: var(--pencil); }

/* ПОДСКАЗКА К ПОЛЮ СТРАНЫ. Выпадает поверх страницы, а не раздвигает её: панель
   под полем не должна прыгать от каждой буквы. Показывается только пока в поле
   фокус — иначе она перекрывала бы фильтры после ухода курсора. */
.f-cc, .f-dom { position: relative; display: block; min-width: 0; }
.cc-hint { position: absolute; z-index: 20; left: 0; right: 0; top: calc(100% + 3px);
           display: none; flex-direction: column;
           background: var(--frame); border: 1px solid var(--hair);
           border-radius: var(--radius);
           /* Список ПРОКРУЧИВАЕТСЯ: при пустом поле в нём все тридцать пять
              стран, и обрезать его молча нельзя — обрезка, которую не видно,
              врёт про данные. Десять строк по 28 px — предел, за которым
              список начинает закрывать собой панель фильтров. */
           max-height: 280px; overflow-y: auto; overscroll-behavior: contain;
           box-shadow: 0 4px 12px color-mix(in srgb, var(--ink) 12%, transparent); }
.f-cc:focus-within .cc-hint:not(:empty),
.f-dom:focus-within .cc-hint:not(:empty) { display: flex; }
.cc-item { display: flex; align-items: center; gap: var(--gap);
           padding: 5px 10px; font-size: var(--t-cap); color: var(--ink);
           white-space: nowrap; }
/* ДОМЕН ДЛИННЕЕ СТРАНЫ и в строку не всегда лезет: `casinocopenghagen.hollandjackpot.top`
   — тридцать семь знаков. Обрезаем многоточием, а не переносим: перенесённый
   домен занимает две строки, и список из двенадцати перестаёт помещаться на
   экран. Само имя целиком остаётся в `title` ссылки. */
.cc-item .dm { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.cc-item i + .dm, .cc-item .dm + i { flex: none; }
.cc-item:hover { background: var(--paper); }
/* Число уходит к правому краю: оно объясняет порядок списка, но читают его
   вторым — первым читают имя. */
.cc-item i { font-style: normal; color: var(--pencil); font-size: var(--t-meta);
             margin-left: auto; }
/* Флаг — эмодзи, и рисует его системный набор: своих шрифтов на 35 флагов мы не
   возим. Ширина задана, чтобы имена стояли одной колонкой и там, где флага
   нет вовсе. */
.fl { font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
      "Twemoji Mozilla", sans-serif;
      font-weight: 400; font-size: 14px; line-height: 1;
      width: 1.15em; flex: none; }
.cc-none { padding: 5px 10px; font-size: var(--t-meta); color: var(--pencil); }
/* Действие — чернильная заливка, а не цвет: хроматический тон в системе один,
   и он занят состоянием «в показе». */
.ask .f-go { font: inherit; font-size: var(--t-cap); cursor: pointer;
             padding: 6px calc(var(--gap) * 3); border: 1px solid var(--ink);
             border-radius: var(--radius); background: var(--ink); color: var(--paper); }
.ask .f-go:hover { background: var(--pencil); border-color: var(--pencil); }
/* «Сбросить» — не состояние, а выход из него, и потому пунктиром. Стоит он в
   конце полосы выбранного: у строки ввода он был четвёртым элементом ряда и не
   имел отношения ни к одному из соседей. */
.f-clr { font-size: var(--t-cap); color: var(--pencil);
         padding: 3px 10px; border: 1px dashed var(--hair);
         border-radius: var(--radius); }
.f-clr:hover { border-color: var(--pencil); color: var(--ink); }
.picked .f-clr { margin-left: auto; }

/* Что уже выбрано — полосой под вводом, а не внутри панели: свёрнутая панель не
   имеет права спрятать действующий фильтр. */
.picked { display: flex; flex-wrap: wrap; gap: var(--gap); align-items: center;
          padding: var(--gap) calc(var(--gap) * 2) 0; }

/* ── ЧИПС ОСИ: имя, значения, крестик ──────────────────────────────────────────
   Один чипс на ОСЬ, а не на значение. Россыпь по значению не говорит, к какой
   оси относится каждое, и снятие воронки с тремя галочками стоило трёх кликов
   с тремя перезагрузками ленты.

   Инверсии здесь нет намеренно, хотя чипс и означает включённое: в полосе стоит
   ТОЛЬКО включённое, и заливать чернилами всю строку значит спорить с лентой за
   внимание. Отличает её от выключенного не тон, а факт присутствия. */
.grp { display: inline-flex; align-items: baseline; gap: 6px;
       padding: 2px 2px 2px 8px; border-radius: var(--radius);
       border: 1px solid var(--hair); background: var(--frame);
       font-size: var(--t-cap); max-width: 100%; }
.grp-lbl { color: var(--pencil); font-size: var(--t-meta); flex: none; }
/* Значения — чернилами: имя оси человек прочитает вторым, а первым то, что по
   ней стоит. Ссылкой значение становится, только когда их несколько: при одном
   клик по нему и крестик делали бы одно и то же. */
.grp-v { color: var(--ink); overflow: hidden; text-overflow: ellipsis;
         white-space: nowrap; max-width: 22ch; }
/* Что снимет клик — показывает зачёркивание. Слово «снять» рядом с каждым
   значением заняло бы полосу целиком. */
a.grp-v:hover { text-decoration: line-through; }
/* Хвост длинной оси. Класс НЕ `more`: он занят кнопкой «ещё» в конце ленты
   (`grid-column: 1 / -1` и 24 px полей), и чипс языка от неё разъезжался в
   70 px высоты посреди полосы в 23. */
.grp-v.rest { color: var(--pencil); font-size: var(--t-meta); }
/* Крестик — снятие ВСЕЙ оси, и отделён линией: иначе он читается как часть
   последнего значения. Мишень крупнее знака: 8 px по бокам — это 24 px ширины
   при кегле в 11. */
.grp-x { color: var(--pencil); font-size: var(--t-meta); align-self: stretch;
         display: inline-flex; align-items: center; padding: 0 8px;
         border-left: 1px solid var(--hair); border-radius: 0 1px 1px 0; }
.grp-x:hover { background: var(--ink); color: var(--paper); }

/* ── ПОЛОСА НАД ЛЕНТОЙ ─────────────────────────────────────────────────────── */
.bar { display: flex; flex-wrap: wrap; gap: var(--gap); align-items: center;
       padding: calc(var(--gap) * 2) calc(var(--gap) * 2) 0; }
.found { color: var(--pencil); font-size: var(--t-cap); }
.found b { color: var(--ink); font-weight: 400; letter-spacing: -.02em; }
/* Порядок уходит к правому краю: он относится к ленте, а не к фильтрам. */
.sorts { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--gap);
         align-items: center; }

/* ── ПАНЕЛЬ ФИЛЬТРОВ ──────────────────────────────────────────────────────────
   Оси собраны в один блок и сгруппированы по вопросу, на который отвечают.
   Шесть плоских рядов чипсов занимали 232 px первого экрана — на 1920×1080 это
   пятая часть высоты, и лента начиналась ниже сгиба. */
/* Кнопка раскрытия — той же геометрии, что поля рядом: открыть фильтры и задать
   вопрос полем — соседние движения, и выглядеть они должны как соседние. */
.f-btn {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  padding: 6px 10px; user-select: none;
  border: 1px solid var(--hair); border-radius: var(--radius);
  background: var(--frame); font-size: var(--t-cap); color: var(--pencil);
}
.f-btn::before { content: "▸"; font-size: 9px; line-height: 1; }
.f-toggle:checked ~ .top > .f-btn::before { content: "▾"; }
.f-btn:hover { border-color: var(--pencil); }
/* Сколько осей выбрано. Свёрнутая панель обязана говорить о спрятанном под ней,
   иначе она врёт молчанием. */
.cnt { font-style: normal; background: var(--ink); color: var(--paper);
       border-radius: 999px; padding: 0 5px; font-size: var(--t-meta); }

/* Секции панели: одна карточка, разграфлённая на колонки. Рамка ОДНА, общая, —
   рамка у каждой секции дробила бы экран на клетки. */
/* ТРИ СЕКЦИИ, ТРИ ВОПРОСА: что показывают, куда ведёт, какого размаха. Ширина
   у них разная и заданная, а не подобранная потоком: у «что показывают» десять
   языков и пять форматов, у «куда ведёт» — три воронки. Автопоток на таких
   осях давал первый ряд забитым, а второй наполовину пустым.

   Рамка ОДНА, общая; секции разделены линией внутри неё — рамка у каждой
   дробила бы экран на клетки. */
/* `overflow: hidden` здесь БЫТЬ НЕ ДОЛЖНО, хотя просится: он стоял ради того,
   чтобы скругление обрезало внутренние линии секций. Обрезал он и выпадающий
   список оси — `.dd-list` всплывающий (`position: absolute`), и любой предок с
   `overflow` становится ему клеткой. Снаружи это выглядело так, будто список
   «заходит за линии»: нижняя строка (`dpa`) обрезана ровно по рамке панели.
   Обрезать нечего: у секций нет фона, только вертикальные линии в середине
   панели, а до скруглённых углов они не достают. */
.pan {
  display: grid; grid-template-columns: 5fr 3fr 4fr;
  background: var(--frame); border: 1px solid var(--hair);
  border-radius: var(--radius);
}
.sec { display: flex; flex-direction: column; gap: calc(var(--gap) * 2);
       padding: calc(var(--gap) * 2); min-width: 0;
       border-left: 1px solid var(--hair); }
.sec:first-child { border-left: 0; }
/* Заголовок секции — карандашом и тем же кеглем, что метки осей: он называет
   группу, а не спорит с ней за внимание. */
.sec h3 { margin: 0; font: 500 var(--t-cap)/1 Onest, sans-serif; color: var(--ink);
          display: flex; align-items: baseline; gap: var(--gap); }
/* «Сбросить» у заголовка снимает весь вопрос секции разом — воронку вместе с
   кнопкой. Уходит к правому краю и печатается карандашом: это выход из
   состояния, а не ещё одна ось. Появляется, только когда снимать есть что. */
.sec h3 .clr { margin-left: auto; font-weight: 400; font-size: var(--t-meta);
               color: var(--pencil); text-decoration: underline dotted; }
.sec h3 .clr:hover { color: var(--ink); }
@media (max-width: 1200px) {
  .pan { grid-template-columns: 1fr 1fr; }
  .sec:nth-child(2) { border-left: 1px solid var(--hair); }
  .sec:nth-child(3) { grid-column: 1 / -1; border-left: 0;
                      border-top: 1px solid var(--hair); }
}
@media (max-width: 640px) {
  .pan { grid-template-columns: 1fr; max-height: 60vh; overflow-y: auto; }
  .sec { border-left: 0; border-top: 1px solid var(--hair); }
  .sec:first-child { border-top: 0; }
  .sec:nth-child(2) { border-left: 0; }
  /* Панель здесь прокручивается, а `overflow-y: auto` — такая же клетка для
     всплывающего списка, как `hidden`. Поэтому на узком экране список не
     всплывает, а встаёт В ПОТОК: раздвигает секцию и едет вместе с панелью.
     Тень и своя прокрутка ему тогда не нужны — прокрутка уже есть у панели. */
  .dd-list { position: static; max-height: none; overflow: visible;
             box-shadow: none; margin-top: 3px; }
}
.pan .box.picked { border-top: 1px solid var(--hair); padding-top: calc(var(--gap) * 2);
                   flex-direction: row; flex-wrap: wrap; gap: calc(var(--gap) * 3); }
/* ── ОСЬ КАК ПОЛЕ СО СПИСКОМ ───────────────────────────────────────────────────
   Россыпь чипсов показывала все значения всех осей разом — тридцать шесть
   кнопок на экране, и ни одна не выглядит важнее прочих. Закрытое поле говорит
   ровно две вещи: как ось называется и что по ней выбрано.

   Раскрытие — флажком через соседний селектор, без единой строки кода. Флажок
   спрятан размером в пиксель, а не `display: none`: снятый с отрисовки, он
   теряет фокус с клавиатуры, и список становится недоступен без мыши. */
/* ОТКРЫТ ВСЕГДА ОДИН СПИСОК: у всех `details` одно имя группы, и браузер сам
   закрывает соседний. Два раскрытых перекрывали друг друга и всё под ними, а
   сводить их скриптом значило бы держать состояние в двух местах — в разметке и
   в коде. Там, где группы ещё нет (браузер старше), откроется несколько: это
   ровно прежнее поведение, а не поломка. */
.dd { position: relative; min-width: 0; }
.dd-head {
  display: flex; align-items: baseline; gap: 6px; cursor: pointer;
  padding: 6px 24px 6px 10px; user-select: none; list-style: none;
  border: 1px solid var(--hair); border-radius: var(--radius);
  background: var(--paper); font-size: var(--t-cap);
}
.dd-head::-webkit-details-marker { display: none; }
.dd-head::after { content: "▾"; position: absolute; right: 9px; top: 7px;
                  font-size: 9px; color: var(--pencil); }
.dd[open] > .dd-head::after { content: "▴"; }
.dd-head:hover, .dd-head:focus-visible { border-color: var(--pencil); }
.dd-lbl { color: var(--pencil); font-size: var(--t-meta); flex: none; }
/* Что выбрано — чернилами: это единственное, что поле сообщает в закрытом виде,
   и оно должно читаться первым. «Любой» карандашом — это не выбор, а его
   отсутствие. */
.dd-val { color: var(--ink); overflow: hidden; text-overflow: ellipsis;
          white-space: nowrap; }
/* «Любой» — карандашом: это не выбор, а его отсутствие. Точный диапазон галочки
   не зажигает, поэтому у оси с ним стоит `.set`: без неё «5000 – 50000» было бы
   написано тем же тоном, что и «любой», то есть выбор выглядел бы его
   отсутствием. */
.dd:not(:has(.opt.on)):not(.set) .dd-val { color: var(--pencil); }

.dd-list { position: absolute; z-index: 15; left: 0; right: 0; top: calc(100% + 3px);
           display: flex; flex-direction: column;
           max-height: 260px; overflow-y: auto; overscroll-behavior: contain;
           background: var(--frame); border: 1px solid var(--hair);
           border-radius: var(--radius);
           box-shadow: 0 4px 12px color-mix(in srgb, var(--ink) 12%, transparent); }

/* Пункт списка — ССЫЛКА, а не `input`: выбор значения это переход к другой
   выдаче, и он обязан работать с адресом, «назад» и открытием в новой вкладке.
   Галочка нарисована квадратиком, потому что от неё ждут именно этого. */
.opt { display: flex; align-items: center; gap: 7px; padding: 5px 10px;
       font-size: var(--t-cap); color: var(--ink); white-space: nowrap; }
.opt:hover { background: var(--paper); }
.opt .box { width: 12px; height: 12px; flex: none; border-radius: 2px;
            border: 1px solid var(--hair); background: var(--frame); }
.opt.on .box { background: var(--ink); border-color: var(--ink);
               box-shadow: inset 0 0 0 2px var(--frame); }
.opt.on { font-weight: 500; }
.opt b { margin-left: auto; font-weight: 400; color: var(--pencil);
         font-size: var(--t-meta); }
/* «Любой» — снятие оси целиком, первой строкой списка. Отделено линией: это не
   ещё одно значение, а выход из выбора, и стоять вровень с галочками оно не
   должно. Слово то же, каким закрытое поле называет невыбранную ось. */
.opt.clr { color: var(--pencil); border-bottom: 1px solid var(--hair); }
.opt.clr:hover { color: var(--ink); }
.opt.clr b { font-size: var(--t-meta); }
/* Флаг у языка — ПРИМЕТА, по которой строка находится глазами, а не
   утверждение: по-немецки говорят и в Австрии, у арабского своей страны нет
   вовсе. Поэтому он никогда не один — рядом всегда имя языка.

   Тег `i`, а не `b`, и это не вкус: `.opt b` — это СЧЁТЧИК справа, у него
   `margin-left: auto`. Флаг в `b` подхватывал его и уезжал к правому краю,
   унося имя языка за собой — список читался колонкой по центру. */
.opt .fl { font-style: normal; }

/* ── ПОИСК ВНУТРИ СПИСКА ОСИ ──────────────────────────────────────────────────
   Языков тридцать один, кнопок двадцать восемь (замер 01.09.2026). Раньше в
   списке стояло двенадцать и восемь — остальные нельзя было выбрать вовсе;
   теперь все, а находятся они набором. Поле ЛИПКОЕ: под ним тридцать строк прокрутки, и
   уехавшая наверх строка поиска читалась бы как её отсутствие. */
.dd-find { position: sticky; top: 0; z-index: 1; padding: var(--gap) 10px;
           background: var(--frame); border-bottom: 1px solid var(--hair); }
.dd-find input { font: inherit; font-size: var(--t-cap); width: 100%; min-width: 0;
                 padding: 4px 8px; color: var(--ink);
                 background: var(--paper); border: 1px solid var(--hair);
                 border-radius: var(--radius); }
.dd-find input:focus { outline: none; border-color: var(--pencil); }
.dd-none { padding: 5px 10px; font-size: var(--t-meta); color: var(--pencil); }
/* `hidden` у строки списка перебивается её же `display: flex` — правило UA
   слабее. Без этой строки поиск бы «работал», ничего не пряча. */
.opt[hidden], .dd-none[hidden] { display: none; }

/* ── «ОТ» И «ДО»: ВТОРОЙ ВХОД В ОСЬ ОХВАТА И ДНЕЙ ─────────────────────────────
   Полосы отвечают крупно, а спрашивают часто мелко: замер 01.09.2026 на
   1 352 736 креативах — 77.6 % корпуса лежит в одной нижней полосе охвата,
   65.1 % в одной полосе дней, и внутри них фильтра нет вовсе.

   Форма стоит ПЕРВОЙ строкой списка и выглядит так же, как строка ввода наверху
   (`.ask`): те же поля, та же чернильная кнопка. Ничего, чего человек не видел
   бы в любой форме на свете, и ни строчки скрипта — обычный `method=get`. */
.rng { display: flex; align-items: center; gap: var(--gap);
       padding: var(--gap) 10px; border-bottom: 1px solid var(--hair); }
/* Метка и поле одной строкой: «от» перед полем читается как часть фразы, а
   сверху занимало бы вдвое больше высоты у списка, который и так выпадающий. */
.rng label { display: flex; align-items: center; gap: 5px; min-width: 0; flex: 1;
             color: var(--pencil); font-size: var(--t-meta); }
.rng input { font: inherit; font-size: var(--t-cap); min-width: 0; width: 100%;
             padding: 4px 8px; color: var(--ink);
             background: var(--paper); border: 1px solid var(--hair);
             border-radius: var(--radius); }
.rng input:focus { outline: none; border-color: var(--pencil); }
/* Единица измерения — карандашом и после полей: она объясняет число, а не
   спорит с ним за первое чтение. */
.rng .unit { color: var(--pencil); font-size: var(--t-meta); flex: none; }
/* Стрелочки числового поля убраны: они дают шаг в единицу, а шаг здесь — тысячи.
   Нажимать их до пяти тысяч никто не станет, а место в узкой строке они едят. */
.rng input::-webkit-outer-spin-button,
.rng input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.rng input[type=number] { -moz-appearance: textfield; appearance: textfield; }
.rng button { font: inherit; font-size: var(--t-cap); cursor: pointer; flex: none;
              padding: 4px calc(var(--gap) * 2); border: 1px solid var(--ink);
              border-radius: var(--radius); background: var(--ink); color: var(--paper); }
.rng button:hover { background: var(--pencil); border-color: var(--pencil); }
/* Значение, дающее с уже выбранным ноль. Не ссылка — и выглядеть должно так же,
   иначе клик обещает выдачу и приводит на пустой экран. */
.opt.off { color: var(--hair); cursor: default; }
.opt.off .box { border-color: var(--frame); background: transparent; }

.ax { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.ax .row { display: flex; flex-wrap: wrap; gap: var(--gap); }
/* Своё число стран — тот же чипс по геометрии, что и готовые «не больше 3»:
   поле ввода не должно выглядеть гостем среди них. */
.num { display: inline-flex; align-items: stretch; overflow: hidden;
       border: 1px solid var(--hair); border-radius: var(--radius);
       background: var(--frame); }
.num:focus-within { border-color: var(--pencil); }
.num input { font: inherit; font-size: var(--t-cap); width: 62px;
             padding: 2px 6px; border: 0; background: none; color: var(--ink); }
.num input:focus { outline: none; }
.num button { font: inherit; font-size: var(--t-meta); cursor: pointer;
              border: 0; padding: 0 7px; background: var(--ink); color: var(--paper); }
.num button:hover { background: var(--pencil); }
/* Страна словами — то же поле, что и «своё число», только шире: в него пишут
   «Германия», а не «3». */
.named { display: inline-flex; align-items: stretch; overflow: hidden;
         border: 1px solid var(--hair); border-radius: var(--radius);
         background: var(--frame); }
.named:focus-within { border-color: var(--pencil); }
.named { max-width: 100%; }
.named input { font: inherit; font-size: var(--t-cap); width: 240px;
               max-width: 100%; min-width: 0;
               padding: 2px 8px; border: 0; background: none; color: var(--ink); }
.named input:focus { outline: none; }
.named button { font: inherit; font-size: var(--t-cap); cursor: pointer;
                border: 0; padding: 0 9px; background: var(--ink); color: var(--paper); }
.named button:hover { background: var(--pencil); }
/* Что разбор понял. Карандашом, а не чипсом: это не состояние фильтра, а ответ
   на ввод, и жить ему до следующего клика. */
.note { align-self: center; color: var(--pencil); font-size: var(--t-meta); }
.note.bad { text-decoration: underline dotted; }

/* ── ЧИПСЫ: единственный носитель состояния фильтра ────────────────────────── */
.chips { display: flex; flex-wrap: wrap; gap: var(--gap);
         padding: var(--gap) calc(var(--gap) * 2); }
.chip {
  display: inline-flex; gap: 5px; align-items: baseline;
  padding: 3px 8px; border-radius: var(--radius);
  border: 1px solid var(--hair); background: var(--frame);
  font-size: var(--t-cap); white-space: nowrap;
}
/* Активное состояние — ИНВЕРСИЯ, а не цвет. Хроматический тон в системе один,
   и он занят состоянием «в показе». */
.chip.on { background: var(--ink); color: var(--paper); border-color: var(--ink); }
/* ГРАНИЦА БАЗЫ, А НЕ ФИЛЬТР: «iGaming» и «ЕС» в полосе над лентой. Стоят они
   всегда и снимаются ничем — другой вертикали и другого гео в базе нет вовсе
   (почему именно так — в feed.html над разметкой). Вид тот же инверсный: они
   действительно поставлены, и выглядеть иначе, чем поставленный фильтр, им не
   за что. Курсор при этом обычный: `pointer` обещал бы клик, а кликать не по
   чему — крестика у них тоже нет намеренно.
   `align-items` перебивается на `center`: у чипса он `baseline`, а базовая
   линия встроенного SVG — его нижний край, и знак садился бы на строку. */
.chip.fix { background: var(--ink); color: var(--paper); border-color: var(--ink);
            align-items: center; cursor: default; }
/* Знак берёт цвет у чипса (`currentColor`) и на инверсии становится бумажным
   сам. 20 px — выбрано по раскладке 16/18/20/22 в chromium на 3×, 06.09.2026.
   16 px не растит чипс вовсе (строка в нём 17.4 px = 12 × 1.45), но луч звезды
   ЕС там 3.4 px и кольцо читается россыпью точек; 20 px даёт 4.3 px, и звезда
   становится звездой. Цена — чипс на 3 px выше соседних, и это принято: два
   постоянных чипса не снимаются, и отличаться от снимаемых им положено.
   `flex: none` — иначе флекс сожмёт знак под длинную подпись. */
.chip.fix .ico { flex: none; width: 20px; height: 20px; }
.chip i { font-style: normal; color: var(--pencil); font-size: var(--t-meta); }
.chip.on i { color: var(--paper); opacity: .7; }
/* Чипс, который при уже выбранном даёт ноль. Он не ссылка (в разметке `span`),
   и выглядеть должен так же — иначе клик обещает выдачу и приводит на пустой
   экран. Гасится ТОНОМ, а не цветом: хроматический в системе один, и он занят.
   Прозрачностью не гасим — прозрачный элемент остаётся в проверке попаданий. */
.chip.off { color: var(--pencil); border-color: var(--frame);
            background: transparent; cursor: default; }
.chips .lbl, .bar .lbl { color: var(--pencil); font-size: var(--t-meta);
              align-self: center; padding-right: 2px; }
/* Заголовок оси в панели — над чипсами, а не перед ними: слева он съедал бы
   ширину колонки, а её и так минимум 260 px. */
.ax .lbl { color: var(--pencil); font-size: var(--t-meta); }

/* ── КОНТАКТНЫЙ ЛИСТ ───────────────────────────────────────────────────────────
   Место плитки решается СЕРВЕРОМ (`app.span`), клиент геометрию не читает
   вовсе. Замер родителя: чтение getBoundingClientRect по частому событию
   стоило 2 255 принудительных пересчётов раскладки за прокрутку.

   Ряд — сороковая доля колонки, и плитка занимает столько рядов, сколько
   требует ЕЁ пропорция. Прежде форм было две — вертикаль в два ряда по 210 px и
   всё остальное в один, — а в корпусе их пять: 9:16 44 %, 1:1 38 %, 4:5 8.9 %,
   3:4 2 %, 16:9 1.1 % (замер 30.08.2026, 747 357 кадров). Квадрат в вертикальной
   ячейке получал поля с боков, широкое — сверху и снизу:

       кадром занято плёнки     77 %  →  95 %
       дыр внизу колонок       1.9 %  →  0.00 %  (600 плиток, 4/6/8 колонок)

   Число колонок ФИКСИРОВАНО в брейкпоинте, а не набирается `auto-fill`: высота
   ряда выражается через ширину колонки, а её без известного числа колонок не
   вычислить. `dense` не ставим — он ничего не даёт: при плитках шириной ровно в
   колонку обычная раскладка сама ведёт себя как жадное masonry, результат до
   пикселя тот же (проверено на стенде, tools/grid_stand.py).

   Вертикальный `gap` здесь НУЛЕВОЙ. При мелком ряде он умножается на число
   рядов — у 9:16 это 71 ряд, то есть 426 px лишней высоты, — и раскладка
   расходится с расчётом. Просвет живёт внутри ячейки отступом. */
/* ОБЁРТКА ЛЕНТЫ — КОНТЕЙНЕР ЗАПРОСОВ, и число колонок считается ОТ НЕЁ, а не от
   ширины окна. Разница видна ровно в одном месте, зато в важном: панель креатива
   ОСВОБОЖДАЕТ СЕБЕ МЕСТО, а не ложится поверх ленты, и лента должна пересчитать
   колонки под оставшуюся ширину. С брейкпоинтами по окну для этого пришлось бы
   держать вторую таблицу порогов, сдвинутую на ширину панели, — и разъезжались
   бы они на первой же правке.

   Сперва панель именно накладывалась: пересборка сетки казалась слишком дорогой.
   Замер 30.08.2026 на 800 плитках это опроверг — 24 мс, полтора кадра:

       наложение (ничего не двигаем)     0.4 мс
       сузить ленту на ширину панели    23.3–34.9 мс (шесть переключений)

   За 24 мс на клик покупается то, ради чего лента и существует: кадры остаются
   видны все. Правый столбец больше не уезжает под панель.

   При открытой панели колонок становится меньше, а ШИРИНА их не меняется: 2 506
   px дают 9 колонок по 276 px, с панелью — 7 колонок по тем же 276. Значит и
   высоты плиток те же, и пересборка выходит перестановкой, а не пересчётом
   размеров.

   Брейкпоинты подобраны так, чтобы колонка везде оставалась 236–280 px: у́же — и
   текст на чужом креативе перестаёт читаться, шире — на экран влезает меньше
   тридцати кадров, а контактный лист тем и живёт, что их видно много. */
.wrap { container-type: inline-size; padding: 0 var(--edge); }

.sheet {
  --n: 9;
  display: grid; gap: 0; align-content: start;
  grid-template-columns: repeat(var(--n), minmax(0, 1fr));
  grid-auto-rows: calc(100cqw / var(--n) / 40);
  padding: calc(var(--gap) / 2) 0 calc(var(--gap) * 8);
}
@container (max-width: 2200px) { .sheet { --n: 8; } }
@container (max-width: 1900px) { .sheet { --n: 7; } }
@container (max-width: 1660px) { .sheet { --n: 6; } }
@container (max-width: 1420px) { .sheet { --n: 5; } }
@container (max-width: 1180px) { .sheet { --n: 4; } }
@container (max-width:  940px) { .sheet { --n: 3; } }
@container (max-width:  700px) { .sheet { --n: 2; } }

/* contain: плитка не влияет на раскладку соседей и не рисует за краями.
   Замер: Paint 1 977 → 1 349 мс на 800 плитках. `content-visibility: auto`
   сюда НЕЛЬЗЯ — раскладка 2 434 → 16 908 мс: размер задаёт сетка, а не
   содержимое. */
.tile { grid-row: span var(--s); padding: calc(var(--gap) / 2);
        contain: layout paint; }

/* Кадр забирает ячейку целиком. Тени нет — кадры разделяет просвет бумаги.
   А вот КРАЙ есть, и это не украшение: у 5.2 % креативов поля белые или почти
   белые (замер 30.08.2026 на 600 кадрах — 2.7 % ярче 245, ещё 2.5 % ярче 225).
   Такой кадр сливается и с плитой, и с бумагой, и с соседом; на экране из
   сорока плиток их пара, и читаются они ровно как дыра в сетке — та самая, на
   которую жаловались глазами, когда дыр в раскладке не было ни одной.
   `outline` со сдвигом внутрь, а не рамка: она рисуется ПОВЕРХ кадра, не
   занимает места и на тёмном креативе не видна вовсе. */
.frame { position: relative; width: 100%; height: 100%; overflow: hidden;
         background: var(--frame); border-radius: var(--radius);
         outline: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
         outline-offset: -1px; }
.shot { position: absolute; inset: 0; display: block; }
/* cover, а не contain — и это стало честным ровно тогда, когда ячейка перестала
   быть чужой формой: бокс отличается от пропорции кадра не больше чем на шаг
   ряда, срезается 0.69 % кадра, полтора пикселя с каждой стороны (замер на
   20 000 кадрах). В старой сетке из двух форм cover резал бы до трети — там он
   и был запрещён. */
.shot img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* <video> в разметке НЕТ — его создаёт tiles.js по наведению. Замер: с роликами
   в разметке к 800 плиткам накапливалось 259 плееров, каждый со своим слоем
   композитора, и Chrome тянет первые байты даже при preload="none". */
.clip { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%;
        object-fit: contain; pointer-events: none;
        visibility: hidden; opacity: 0;
        transition: opacity .15s ease, visibility 0s linear .15s; }
.clip.on { visibility: visible; opacity: 1; transition-delay: 0s; }

/* Полоса перемотки: видимая линия 2 px по низу кадра. Во всю ширину кадра её
   не делаем — закрывала бы до 45 % картинки ровно там, куда смотрят. */
.scrub { position: absolute; inset: auto 0 0 0; z-index: 2; height: 2px;
         background: color-mix(in srgb, var(--ink) 30%, transparent);
         visibility: hidden; }
.scrub > i { display: block; height: 100%; width: 100%; background: var(--ink);
             transform: scaleX(0); transform-origin: 0 50%; }
.tile.scrubbing .scrub { visibility: visible; }

/* Метка «видео» — форма, а не цвет: цвет в системе занят состоянием показа.
   Треугольник лежит в тёмном кружке, а не сам по себе: голый он читался только
   на светлых кадрах, а обводка ему стоила `drop-shadow` — фильтра на каждой
   плитке с роликом, то есть на 68 % ленты. Кружок даёт тот же контраст
   заливкой, которая не стоит слоя. */
.play { position: absolute; left: var(--gap); top: var(--gap); z-index: 2;
        width: 18px; height: 18px; border-radius: 50%;
        background: color-mix(in srgb, var(--ink) 52%, transparent); }
.play::after { content: ""; position: absolute; left: 6px; top: 4px;
        border-style: solid; border-width: 5px 0 5px 8px;
        border-color: transparent transparent transparent var(--frame); }
.tile.scrubbing .play { visibility: hidden; }

/* ── ДВА ЧИСЛА: ПЛАШКА ПО НАВЕДЕНИЮ ────────────────────────────────────────────
   В покое на плитке нет ничего, кроме кадра. Подпись под каждым кадром держала
   строку в 22 px — при колонке 240 это девятая часть плёнки, отданная тексту,
   который на контактном листе читают у одной плитки из тридцати. Числа нужны,
   когда на кадр УЖЕ смотрят, — тогда плашка и выезжает.

   Прячется `visibility`, а не одной прозрачностью: прозрачный элемент остаётся
   и в отрисовке, и в проверке попаданий. `backdrop-filter` здесь запрещён —
   два размывающих слоя на плитку дали родителю 1 536 слоёв композитора и 3 с
   сборки слоёв на 800 плитках. Заливка сплошная. */
.meta { position: absolute; inset: auto var(--gap) var(--gap); z-index: 3;
        margin: 0; display: flex; align-items: center; gap: 5px;
        padding: 4px 6px; border-radius: var(--radius);
        background: color-mix(in srgb, var(--ink) 82%, transparent);
        color: var(--paper); font-size: var(--t-meta); line-height: 1;
        white-space: nowrap; overflow: hidden;
        visibility: hidden; opacity: 0;
        transition: opacity .12s ease, visibility 0s linear .12s; }
.tile:hover .meta, .tile:focus-within .meta {
        visibility: visible; opacity: 1; transition-delay: 0s; }
.meta b { font-weight: 400; font-family: "Martian Mono", monospace;
          letter-spacing: -.03em; }
.nd { color: color-mix(in srgb, var(--paper) 60%, transparent); }
/* Имя рекламодателя — ссылка на его же выдачу, но выглядит как подпись:
   действие в системе не красится, оно подчёркивается при наведении. */
.who { overflow: hidden; text-overflow: ellipsis; color: inherit;
       text-decoration: none; min-width: 0;
       color: color-mix(in srgb, var(--paper) 72%, transparent); }
.who:hover { text-decoration: underline; }

/* ── «В ПОКАЗЕ СЕЙЧАС» — ЗЕЛЁНЫЙ КРУГ СЛЕВА ВНИЗУ ──────────────────────────────
   Единственный хроматический тон системы и единственное, что стоит на плитке
   постоянно. Погасшее не помечается ничем: отсутствие круга и есть метка.

   Внизу слева, а не вверху: верхние углы у чужой рекламы заняты — там логотип
   рекламодателя и значок 18+/GambleAware, и метка в них тонет. Светлая обводка
   не украшение: без неё круг пропадает на тёмно-зелёном креативе, а зелёного в
   казино-рекламе своего хватает. */
.dot { position: absolute; left: var(--gap); bottom: var(--gap); z-index: 2;
       width: 10px; height: 10px; border-radius: 50%; background: var(--live);
       /* Два кольца: светлое отделяет от тёмного кадра, тень — от светлого.
          Оба без размытия по радиусу больше двух пикселей: размытые тени в
          сетке из восьмисот плиток родитель уже мерил, они стоят слоёв. */
       box-shadow: 0 0 0 2px color-mix(in srgb, var(--frame) 88%, transparent),
                   0 1px 2px color-mix(in srgb, var(--ink) 35%, transparent); }
/* Плашка с числами выезжает в ТОТ ЖЕ угол, и круг под ней читался бы как
   соринка на буквах. Поэтому на время наведения круг уходит, а состояние
   переезжает внутрь плашки — тем же тоном, той же формой, перед числами. */
.tile:hover .dot, .tile:focus-within .dot { visibility: hidden; }
.meta .live { width: 7px; height: 7px; border-radius: 50%; flex: none;
              background: var(--live); }
/* «Снять всё» — тоже чипс, но пунктирный: он не состояние, а выход из него. */
.chip.clear { border-style: dashed; }

/* ── ПОДВАЛ ЛЕНТЫ ──────────────────────────────────────────────────────────────
   Здесь человек упирается в конец ленты, и здесь же названы причины, по которым
   он упёрся. НАЗВАННАЯ ПРИЧИНА ОБЯЗАНА ИМЕТЬ ВЫХОД, А ВЫХОД — БЫТЬ ВИДЕН: пока
   «показать все» стоял ссылкой внутри той же карандашной строки в 12 px, ничем
   не отличаясь от объяснения, отличить его было нечем. Замер 01.09.2026 на
   `?funnel=стор`: лента отдаёт 543 плитки, счётчик обещает 1006, и дорога к
   оставшимся 463 была неотличима от текста про них.

   Объяснение остаётся карандашом, выход становится действием — чернильной
   заливкой, как кнопка поиска и кнопка разбора страны. Цветом не красим:
   хроматический тон в системе один, и он занят состоянием «в показе». */
.more { grid-column: 1 / -1; padding: calc(var(--gap) * 4) 0;
        display: flex; flex-direction: column; align-items: center;
        gap: calc(var(--gap) * 2);
        text-align: center; color: var(--pencil); font-size: var(--t-cap); }
/* Каждая причина — своей строкой, а не в скобках за предыдущей: причин бывает
   две (архив и прореживание), и читать их надо порознь. Ширина по строке текста,
   а не по восьми колонкам сетки: объяснение в одну строку через весь экран
   читается как подпись к плиткам, а не как обращение к читателю. */
.more .why { margin: 0; max-width: 44ch; }
/* Число внутри объяснения — чернилами: оно и есть то, ради чего строка написана. */
.more .why b { color: var(--ink); font-weight: 400; }
.act { display: inline-block; font-size: var(--t-name);
       padding: calc(var(--gap) * 1.5) calc(var(--gap) * 3);
       border: 1px solid var(--ink); border-radius: var(--radius);
       background: var(--ink); color: var(--paper); }
.act:hover { background: var(--pencil); border-color: var(--pencil); }
/* ВОЗВРАТ К УМОЛЧАНИЮ — не то же самое, что выход из него: вперёд он не ведёт.
   Обводка вместо заливки, тем же приёмом, что у пунктирного «снять всё». */
.act.back { background: transparent; color: var(--ink); border-color: var(--hair); }
.act.back:hover { background: transparent; border-color: var(--ink); }
/* ПОКА ПОРЦИЯ ЕДЕТ, СТРОКА ГАСНЕТ. Кнопка теперь срабатывает сама, когда
   доехала до экрана (`_tiles.html`), и «показать ещё» в этот момент обещает
   действие, которого от человека не требуется, — а человек, домотавший до низа,
   читает её как «это всё». Гашение до тона линии и есть ответ «идёт»: нового
   значения система при этом не заводит, тон уже в ней есть. */
.more a.htmx-request { color: var(--hair); }
.watch { position: absolute; inset-inline: 0; top: 0; height: 1px;
         pointer-events: none; }
.empty { padding: calc(var(--gap) * 8) calc(var(--gap) * 2);
         color: var(--pencil); font-size: var(--t-prose); }
/* СТРАНИЦА, КОТОРОЙ НЕТ. Кегль заголовка — тот же, каким печаталось число в
   шапке (`--t-sig`): новой ступени в шкалу ради одной страницы не заводим.
   Ширина ограничена мерой строки, а не окном: два предложения во всю ширину
   монитора читаются хуже, чем в шестьдесят знаков. */
.lost { padding: calc(var(--gap) * 12) calc(var(--gap) * 2); max-width: 62ch; }
.lost h1 { margin: 0; font-size: var(--t-sig); font-weight: 500; line-height: 1.1; }
.lost p { margin: calc(var(--gap) * 3) 0 calc(var(--gap) * 5);
          color: var(--pencil); font-size: var(--t-prose); }

/* ── ПАНЕЛЬ КРЕАТИВА ──────────────────────────────────────────────────────────
   Одни и те же правила красят и панель справа от ленты, и страницу `/c/<key>`:
   разметка у них общая (`_panel.html`), и держать ей два вида значило бы
   получить два разных креатива из одной записи.

   ПАНЕЛЬ ОСВОБОЖДАЕТ СЕБЕ МЕСТО, а не ложится поверх ленты: место ей даёт
   `body.with-panel .wrap` (см. контактный лист). Сперва было наложение — из
   опасения, что пересборка сетки дорога, — и правый столбец кадров уезжал под
   панель. Замер на 800 плитках: 24 мс, полтора кадра. За это лента остаётся
   видна целиком, и цена дешевле спора.

   ПАНЕЛЬ — ЛИСТ, ЛЕЖАЩИЙ НА ЛЕНТЕ, а не отрезанный от неё край экрана. Раньше
   она прижималась к трём краям вплотную и делила с лентой границу: на стене из
   тридцати чужих реклам такая граница не читается вовсе — глазу не за что
   зацепиться, и появление панели проходит незамеченным. Просвет бумаги по
   кругу, скругление и мягкая тень говорят «это слой выше» одним взглядом и без
   единого цветного пикселя.

   Тон — `--frame`, тот же, что у плиты кадра: панель это поднятая поверхность,
   а не второй фон. Рамки вокруг блоков внутри запрещены — они дробят узкую
   колонку на клетки.

   Ширина живёт в `--panel` и берётся ОТТУДА же лентой: два числа в двух местах
   разъедутся, и лента либо залезет под панель, либо оставит полосу пустой
   бумаги. */
.panel {
  position: fixed; z-index: 30;
  top: var(--gap); right: var(--gap); bottom: var(--gap);
  width: var(--panel);
  background: var(--frame);
  border: 1px solid var(--hair); border-radius: var(--radius);
  overflow-y: auto; overscroll-behavior: contain;
  padding: calc(var(--gap) * 2);
  /* Две тени, а не одна: дальняя мягкая даёт высоту над пёстрой лентой, ближняя
     жёсткая держит край. Обе без цвета — это чернила с прозрачностью. */
  box-shadow: 0 8px 28px color-mix(in srgb, var(--ink) 14%, transparent),
              0 1px 3px color-mix(in srgb, var(--ink) 10%, transparent);

  /* ВЫЕЗД. Панель приезжает из-за правого края и уезжает туда же — движение
     называет, откуда взялся лист, и куда он делся. Всё, что здесь есть, выбрано
     против «утомляет глаз»:

       * едет ТОЛЬКО `transform` — композитор, без пересчёта раскладки ленты.
         Анимировать `width` или `right` значило бы гонять раскладку 60 раз в
         секунду поверх сотен плиток;
       * 0.24 с — движение видно, но не приходится ждать. Ниже 0.15 с оно
         читается как рывок (то самое «появилось незаметно»), выше 0.3 с
         начинает раздражать на втором же кадре, а кадров за сеанс смотрят
         десятки;
       * кривая с быстрым началом и мягким торможением: лист «долетает» и
         останавливается, а не тормозит рывком у края;
       * гасится `visibility` с задержкой в длину выезда, а не `opacity`:
         прозрачная панель осталась бы и в отрисовке, и в проверке попаданий —
         невидимый лист поверх правой колонки ленты ловил бы клики по плиткам. */
  transform: translateX(calc(100% + var(--gap) * 3));
  visibility: hidden;
  transition: transform .24s cubic-bezier(.22, .61, .36, 1),
              visibility 0s linear .24s;
}
.panel.on { transform: none; visibility: visible; transition-delay: 0s; }
.panel:focus { outline: none; }

/* КАКАЯ ИМЕННО ПЛИТКА СЕЙЧАС В ПАНЕЛИ. На экране тридцать кадров, и уже через
   два клика непонятно, чей креатив справа: панель отвечает на вопрос, а связи с
   вопросом не видно.

   Обводка стоит В ПРОСВЕТЕ ПЛЁНКИ, а не поверх кадра, и это не вкус: чернила
   поверх чужой рекламы пропадают на первом же тёмном креативе, а в просвете они
   лежат на бумаге и читаются на любом. Ширина ровно в просвет (3 px — половина
   модуля), места метка не занимает и слоя не стоит: `outline` рисуется поверх и
   в раскладке не участвует. */
.tile.open .frame { outline: 3px solid var(--ink); outline-offset: 0; }

/* Содержимое ПРОЯВЛЯЕТСЯ, а не подменяется рывком. Нужно это не первому показу
   (там едет сама панель), а перебору соседей: панель на месте, и без проявления
   один креатив мгновенно заменяется другим — глаз читает это как мигание.
   Только прозрачность: `transform` на детях сломал бы липкую шапку, которая на
   время анимации получила бы новый содержащий блок. */
@keyframes p-in { from { opacity: 0 } to { opacity: 1 } }
.panel > * { animation: p-in .18s ease-out both; }

/* Кому движение мешает — тому его нет. Панель при этом работает ровно так же. */
@media (prefers-reduced-motion: reduce) {
  .panel { transition: transform 0s, visibility 0s; }
  .panel > * { animation: none; }
}
/* Страница того же креатива: колонка та же по ширине, что и панель, — читается
   она одинаково, и правило вёрстки на них одно. */
.card { max-width: 560px; margin: 0 auto; padding: calc(var(--gap) * 3); }

.p-top { display: flex; align-items: flex-start; gap: var(--gap);
         margin: calc(var(--gap) * -2) calc(var(--gap) * -2) 0;
         padding: calc(var(--gap) * 2);
         background: var(--frame); border-bottom: 1px solid var(--hair); }
/* Шапка ЛИПКАЯ только в панели: пролистав ролик и текст, надо помнить, чей это
   креатив, и иметь чем закрыть. На странице липнуть ей не к чему — там уже есть
   своя шапка со стрелкой, и вторая липкая полоса под ней читалась бы как сбой. */
.panel .p-top { position: sticky; top: calc(var(--gap) * -2); z-index: 2; }
.p-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.p-name { font-size: var(--t-name); font-weight: 500;
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.p-name:hover { text-decoration: underline; }
.p-when { color: var(--pencil); font-size: var(--t-meta); }
/* Действия шапки панели держатся вместе и прижаты вправо одним `auto` на всю
   группу: два отдельных `margin-left: auto` поделили бы просвет пополам и
   поставили бы «ссылку» посреди строки. */
.p-act { margin-left: auto; flex: none; display: flex; gap: var(--gap);
         align-items: flex-start; }
/* ССЫЛКА В БУФЕР. Цвета у кнопки нет: действие в системе — чернильная заливка,
   а «сделано» — инверсия (docs/08-design.md). Ширина фиксирована по самому
   длинному из двух своих слов: без этого шапка дёргалась бы на каждый клик,
   когда «ссылка» становится «скопировано». */
.p-copy { flex: none; cursor: pointer; min-width: 88px; height: 26px; padding: 0;
          font: 400 var(--t-meta)/24px Onest, sans-serif; color: var(--pencil);
          background: none; border: 1px solid var(--hair); border-radius: var(--radius); }
.p-copy:hover { border-color: var(--pencil); color: var(--ink); }
.p-copy.done { background: var(--ink); color: var(--paper); border-color: var(--ink); }

/* СКАЧАТЬ — та же кнопка, что «ссылка», и это не лень, а система: действие
   здесь не красится ничем, единственный хроматический тон означает «в показе».
   Стрелка вниз стоит слева от слова, потому что слово одно и короткое: без
   знака ряд из двух одинаковых кнопок читается как «ссылка ссылка».

   Ширина зафиксирована и считается по самому длинному состоянию («скачать» и
   «99 %» короче, чем «готово»): без этого кнопка дёргала бы шапку на каждом
   проценте, а рядом с ней стоит крестик, по которому в этот момент целятся. */
.p-get { flex: none; cursor: pointer; min-width: 106px; height: 26px; padding: 0;
         display: inline-flex; align-items: center; justify-content: center; gap: 5px;
         font: 400 var(--t-meta)/24px Onest, sans-serif; color: var(--pencil);
         background: none; border: 1px solid var(--hair); border-radius: var(--radius); }
.p-get:hover { border-color: var(--pencil); color: var(--ink); }
.p-get svg { flex: none; }
/* Идёт закачка: стрелка убирается, на её месте число процентов. Прятать её
   `opacity` нельзя — прозрачный знак остаётся и в отрисовке, и в попаданиях. */
.p-get.busy svg, .p-get.done svg { display: none; }
.p-get.busy { color: var(--ink); border-color: var(--pencil); cursor: progress; }
/* ПОЛОСА ХОДА — заливкой самой кнопки, а не отдельным элементом: второй
   элемент в шапке панели означал бы третью рамку в ряду из двух. Доля приходит
   из `tiles.js` переменной `--p`; фон резкий (`0px`-переход), потому что это
   мера, а не украшение. */
.p-get.busy { background: linear-gradient(to right,
              var(--hair) 0 var(--p, 0%), transparent var(--p, 0%) 100%); }
.p-get.done { background: var(--ink); color: var(--paper); border-color: var(--ink); }

/* Закрыть — не цвет и не рамка, а знак: действие в системе не красится. */
.p-x { flex: none; cursor: pointer;
       width: 26px; height: 26px; padding: 0; line-height: 24px; text-align: center;
       font: 400 var(--t-prose)/24px Onest, sans-serif; color: var(--pencil);
       background: none; border: 1px solid var(--hair); border-radius: var(--radius); }
.p-x:hover { border-color: var(--pencil); color: var(--ink); }

/* ПОЛНОЕ МЕДИА — хотлинком с fbcdn и только здесь. `contain`, а не `cover`:
   в ленте кадр стоит в своей ячейке и режется на 0.69 %, а тут его смотрят,
   и резать его нечем. */
/* Под полным медиа лежит НАШ кадр (`--shot` из разметки): оригинал у Facebook
   весит до 2.47 МБ, и пока он едет, здесь была дыра в пол-панели. Плитка уже в
   кеше браузера — её только что показала лента, — и появляется она мгновенно.
   `contain` у фона и у самого медиа считается по одному боксу, поэтому когда
   полное доедет, оно ляжет ровно поверх, без сдвига на пиксель. */
.p-media { margin: calc(var(--gap) * 2) 0;
           background: var(--paper) var(--shot) center / contain no-repeat;
           border-radius: var(--radius);
           text-align: center; overflow: hidden; }
.p-media img, .p-media video { display: block; width: 100%; height: auto;
                               max-height: 62vh; object-fit: contain; }
.p-note { margin: 0; padding: var(--gap); color: var(--pencil);
          font-size: var(--t-meta); }

/* Два числа Facebook. Полосы «в показе» они не делят ни с чем: круг стоит перед
   числом дней, потому что это одно утверждение — «идёт столько-то дней». */
.nums { display: flex; gap: calc(var(--gap) * 5); flex-wrap: wrap;
        margin: calc(var(--gap) * 3) 0; }
.nums div { display: flex; flex-direction: column; gap: 2px; }
.nums span { color: var(--pencil); font-size: var(--t-meta); }
.nums b { display: flex; align-items: center; gap: 5px; white-space: nowrap;
          font: 400 var(--t-sig)/1 "Martian Mono", monospace; letter-spacing: -.02em; }
.nums b .live { width: 8px; height: 8px; flex: none;
                border-radius: 50%; background: var(--live); }
/* «Не раскрыт» — это СЛОВО, а не число, и кеглем оно спорить с числами не
   должно: колонка охвата вне ЕС пуста, и врать нулём там нечем. */
.nums b em { font: 400 var(--t-ui)/1.2 Onest, sans-serif; color: var(--pencil); }
/* В ПАНЕЛИ ЧИСЛА СТОЯТ КОЛОНКАМИ, а не потоком. Подписи у них разной длины
   («в показе» против «охват — число Facebook, только ЕС»), и во flex-строке
   третье число сваливалось на свою строку, а числа переставали стоять в ряд —
   читались они тогда как три разных утверждения, а не как три числа об одном
   креативе. `end` держит их на общей линии, сколько бы строк ни заняла подпись. */
.p-nums { display: grid; grid-template-columns: repeat(3, 1fr);
          align-items: end; gap: calc(var(--gap) * 2);
          margin: calc(var(--gap) * 2) 0; }

/* Блок панели: подпись карандашом и содержимое под ней. Линия сверху разделяет
   смыслы — рамка обвела бы каждый блок в клетку. */
.p-block { display: flex; flex-direction: column; gap: var(--gap);
           align-items: flex-start;
           padding-top: calc(var(--gap) * 2); margin-top: calc(var(--gap) * 2);
           border-top: 1px solid var(--hair); }
.p-lbl { color: var(--pencil); font-size: var(--t-meta); }
.p-sub { color: var(--pencil); font-size: var(--t-cap); }
/* Ссылка объявления печатается ЦЕЛИКОМ: это адрес именно этого креатива, а не
   «сайт оффера», и обрезать его значит показать чужой путь своим. Ломается по
   любому знаку — трекерные адреса длиннее строки почти всегда. */
.p-link { display: block; width: 100%; padding: var(--gap) calc(var(--gap) * 1.5);
          background: var(--paper); border-radius: var(--radius);
          overflow-wrap: anywhere; }
.p-link:hover { outline: 1px solid var(--hair); }
.p-link b { display: block; font-weight: 500; font-size: var(--t-cap); }
.p-link span { color: var(--pencil); font-size: var(--t-meta); }
.p-ccs { display: flex; flex-wrap: wrap; gap: 5px; }
.p-title { margin: calc(var(--gap) * 2) 0 0; font-size: var(--t-name);
           font-weight: 500; }
.p-text { margin: var(--gap) 0 0; font-size: var(--t-cap); line-height: 1.6;
          white-space: pre-wrap; overflow-wrap: anywhere; color: var(--pencil); }
.p-more { flex-direction: row; flex-wrap: wrap; align-items: baseline;
          gap: calc(var(--gap) * 2); font-size: var(--t-cap); }
.p-more a { text-decoration: underline; text-decoration-color: var(--hair); }
.p-more a:hover { text-decoration-color: var(--ink); }

/* ── РЯДОМ ────────────────────────────────────────────────────────────────────
   Соседи по личности. Кадры квадратные и одинаковые — здесь их не смотрят, а
   пересчитывают взглядом: это ответ на «сколько ещё такого», а не второй
   контактный лист. */
.near { padding-top: calc(var(--gap) * 2); margin-top: calc(var(--gap) * 2);
        border-top: 1px solid var(--hair); }
.near-top { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.near-top .p-lbl { margin-right: var(--gap); }
.near-grid { display: grid; grid-template-columns: repeat(4, 1fr);
             gap: var(--gap); margin-top: calc(var(--gap) * 2); }
.n-tile { position: relative; display: block; aspect-ratio: 1;
          border-radius: var(--radius); overflow: hidden; background: var(--paper);
          outline: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
          outline-offset: -1px; }
.n-tile img { display: block; width: 100%; height: 100%; object-fit: cover; }
.n-tile .dot { width: 8px; height: 8px; left: 4px; bottom: 4px; }
.p-all { display: inline-block; margin-top: calc(var(--gap) * 2);
         font-size: var(--t-cap); color: var(--pencil); }
.p-all:hover { color: var(--ink); }

/* Узкий экран: панель занимает его целиком — лист с просветом по краям рядом с
   двумя колонками плиток не оставляет ни панели, ни ленты. Выезд остаётся: он
   тут единственное, что говорит, откуда взялся экран и куда денется.

   Место под панель лента здесь НЕ освобождает: освобождать нечего — панель
   закрывает экран целиком, а сужение ленты под ней означало бы пересборку
   сетки, которой никто не увидит. */
@media (max-width: 700px) {
  .panel { inset: 0; width: 100vw; border: 0; border-radius: 0;
           box-shadow: none; }
  body.with-panel { padding-right: 0; }
  .near-grid { grid-template-columns: repeat(3, 1fr); }
}
