/* ==========================================================================
   Гав VPN — вариант V3.1 «Лента».
   Полная копия композиции V3, разложенная на 13 полноэкранных кадров:
   обёрток .frames, .spread и .hold больше нет, все кадры лежат прямо в .tape.
   Файл самостоятельный — v3.css не подключается и не правится.
   Файл правит только композицию: токены и база лежат в общих файлах.

   Композицию получили все тринадцать кадров. Общее правило у них одно:
   кадр равен окну ленты и укладывается в него содержимым целиком. Проверять
   это надо по содержимому, а не по scrollHeight: декоративная подсветка
   `.frame::before` уходит на 14 % ниже кадра и всегда даёт ложный перебор.
   ========================================================================== */

/* --- Переменные композиции. Значения цвета и ритма — из tokens.css. ------- */

[data-variant='v3.1'] {
  --col: 720px;                 /* колонка кадра: мобильный макет и на десктопе */
  --headroom: 64px;             /* высота залипающей шапки из base.css         */
  --rail: 96px;                 /* поле по бокам колонки на широком экране     */
  --glow-c: var(--lime);        /* подсветка под маскотом, меняется по кадру   */
}

/* Кегли заголовков — общие токены. Прежние собственные clamp были поправкой
   на широкую Unbounded: её кириллица занимала около 0.91em на букву и рвала
   строку. Дисплейная гарнитура сменилась на Onest (уже на четверть), поправка
   больше не нужна — заголовки идут тем же кеглем, что у соседних вариантов. */
.plate { font-size: var(--t-mega); }
.plate .mark { padding-inline: 0.09em; }
.frame .display { font-size: var(--t-d1); }

/* Слово длиннее своей колонки не должно распирать сетку: перенос разрешён
   в любом месте. При выбранных кеглях это страховка, а не рабочий режим. */
.mega, .display { overflow-wrap: anywhere; }

/* --- Окно ленты ----------------------------------------------------------
   Обёртка .tape охватывает <main> и <footer> — все 13 кадров. На широком
   экране и при выключенном движении она обычный блок в потоке страницы;
   собственное окно прокрутки ей даёт блок «Режим ленты» в конце файла. */

.tape { position: relative; display: block; }

/* --- Снап ----------------------------------------------------------------
   Только proximity: с mandatory длинный хвост не листается. И только там,
   где листают пальцем. Замер в Chrome: с мышью колесо на 120 px возвращалось
   к началу кадра шесть раз подряд — страница не двигалась вообще, потому что
   ближайшая точка привязки всегда оказывалась той же самой. Палец бросает
   экран далеко, мышь крутит понемногу, поэтому у точного указателя снапа
   нет: [65,65,65,65,65,65] против [120,240,360,480,600,720] без него. */

@media (pointer: coarse) {
  html { scroll-snap-type: y proximity; }
}
html.motion-off { scroll-snap-type: none; }

/* --- Иконки: один рисунок, одна толщина штриха, currentColor. ------------ */

.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.ico {
  width: 1em; height: 1em; flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- Ссылка «к содержанию» для клавиатуры -------------------------------- */

.skip {
  position: absolute; left: var(--s4); top: var(--s4); z-index: 200;
  padding: var(--s3) var(--s5);
  background: var(--lime); color: #0a0a0a;
  font-family: var(--f-ui); font-size: var(--t-label);
  letter-spacing: var(--tr-label); text-transform: uppercase;
  translate: 0 -200%;
}
.skip:focus-visible { translate: 0 0; }

/* --- Полоса прогресса: --sq считает shell.js по всему <main>. -------------
   Начало розовое, конец лаймовый: по цвету полосы видно, далеко ли ушли. */

.progress {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  height: 3px;
  background: color-mix(in oklab, var(--paper) 12%, transparent);
  pointer-events: none;
}
.progress__bar {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--pink), var(--lime) 46%, var(--lime-hi));
  transform: scaleX(var(--sq, 0));
  transform-origin: left center;
  will-change: transform;
}

/* --- Шапка на узком телефоне ---------------------------------------------
   На 360 px кнопка «ТРИ ДНЯ БЕСПЛАТНО» вылезала за правый край на 22 px и
   пряталась только за overflow-x: hidden у body. Поджимаем сам ряд: знак,
   зазор и поля кнопки, — вместо того чтобы прятать дефект. */

@media (max-width: 430px) {
  .site-head__in { gap: var(--s4); }
  .site-head .btn {
    padding-inline: var(--s4);
    font-size: var(--t-label);
    letter-spacing: 0.035em;
  }
  .logo { gap: var(--s2); }
  .logo__paw { width: 26px; height: 26px; }
  .logo__word { font-size: calc(0.9375rem * var(--kui)); }
}

/* ==========================================================================
   Кадр
   ========================================================================== */

/* Поля кадра держат две переменные, а не пять правил с разной специфичностью:
   низкое окно и запас под переключатель меняют ТОЛЬКО их. */
.frame {
  --pad-top: calc(var(--headroom) + var(--s5));
  --pad-bot: var(--s6);

  position: relative;
  min-height: 100svh;
  scroll-snap-align: start;
  display: grid;
  padding:
    var(--pad-top)
    var(--gutter)
    calc(var(--pad-bot) + env(safe-area-inset-bottom));
  overflow-x: clip;
}

/* --- Кадр без прижатого низа ---------------------------------------------
   `align-content: center` центрирует по коробке кадра, а её верхнее поле
   больше нижнего на высоту накладной шапки — пока есть запас, композиция
   уезжает вниз на пол-шапки (приёмка намерила 148–207 px черноты сверху при
   95–151 снизу). Поля выравниваем. Только кадрам с большим запасом: на
   заполненных лишние 56 px снизу выдавили бы содержимое.
   Два класса в селекторе намеренно, как у подвала: одноклассовое правило
   слабее правил «низкого окна» ниже по файлу, и те его перебивали. */
.frame.frame--devices, .frame.frame--quiet {
  --pad-bot: var(--pad-top);
}

/* Подсветка под маскотом: ярче всего, когда кадр стоит в экране. */
.frame::before {
  content: '';
  position: absolute; left: 50%; bottom: -14%;
  translate: -50% 0;
  width: min(150%, 980px); aspect-ratio: 1;
  background: radial-gradient(50% 50% at 50% 50%,
              color-mix(in oklab, var(--glow-c) 15%, transparent), transparent 68%);
  opacity: calc(1 - ((var(--sp, 0.5) - 0.5) * (var(--sp, 0.5) - 0.5) * 3.1));
  pointer-events: none;
}

/* Рельсы колонки: на широком экране кадр читается как высокая карточка. */
.frame::after {
  content: '';
  position: absolute; top: 0; bottom: 0; left: 50%;
  translate: -50% 0;
  width: min(100% - var(--gutter) * 2, calc(var(--col) + var(--rail)));
  border-inline: var(--bw) solid var(--line);
  opacity: 0;
  pointer-events: none;
}
@media (min-width: 1040px) { .frame::after { opacity: 1; } }

.frame__in {
  position: relative; z-index: 1;
  width: 100%; max-width: var(--col);
  margin-inline: auto;
  display: grid;
  gap: var(--s5);
  align-content: center;
  grid-template-columns: minmax(0, 1fr);
}

.frame__head { grid-area: head; display: grid; gap: var(--s4); }
.frame__body { grid-area: body; display: grid; gap: var(--s4); }
.frame__foot { grid-area: foot; display: grid; gap: var(--s4); justify-items: start; }
/* Обёртка карточки — сетка во всю ширину своей доли. Выравнивание задаём
   через justify-items, а не justify-self: сжатая по содержимому обёртка даёт
   круговую зависимость (ширина карточки считается от 100% обёртки). */
.frame__mascot { grid-area: dog; display: grid; justify-items: center; }

/* Параллакс: маскот отстаёт от заголовка. --sp даёт shell.js. */
.frame[data-scrub] .frame__head { transform: translateY(calc((var(--sp, 0.5) - 0.5) * -78px)); }
.frame[data-scrub] .frame__mascot { transform: translateY(calc((var(--sp, 0.5) - 0.5) * -40px)); }

/* Счётчик кадров: не украшение, а место в ленте. Текущий номер розовый —
   тот же сигнал, что у молний на куртке Гавчика. */
.frame__no {
  position: absolute; z-index: 2;
  /* Ровно под шапкой, а не на уровне первой строки заголовка: метка стоит у
     правого края колонки, и широкий заголовок (кадры 01, 05, 07) печатался
     прямо сквозь неё. Между низом шапки (64 px) и верхом содержимого
     (64 + --s5) как раз помещаются её 17 px. */
  top: var(--headroom); left: 50%;
  translate: -50% 0;
  width: min(100% - var(--gutter) * 2, var(--col));
  text-align: right;
  font-family: var(--f-ui); font-weight: 500;
  font-size: var(--t-label); letter-spacing: var(--tr-label);
  color: var(--paper-mute);
}
.frame__no b { color: var(--pink); font-weight: 500; }
.frame__no i { font-style: normal; }

/* --- Плита-заголовок -----------------------------------------------------
   Лаймовая заливка во всю долю заголовка, жёсткие края, чёрный текст.
   Стоит через кадр (2, 4, 6): смена плоскости работает только чередованием,
   подряд залитые кадры снова читаются одинаково. */

.slab {
  display: block;
  width: fit-content;
  max-width: 100%;
  background: var(--lime);
  color: #0a0a0a;
  padding: 0.14em var(--gutter) 0.2em;
  margin-inline: calc(var(--gutter) * -1);
  text-wrap: balance;
}
/* У тихого кадра плита стоит по центру — вынос за колонку сделал бы её
   несимметричной. */
.frame--quiet .slab { margin-inline: auto; }

/* --- Карточка Гавчика внутри кадра --------------------------------------- */

/* Ширина считается от допустимой высоты и пропорций мастера, поэтому
   карточка никогда не выше кадра и никогда не шире своего мастера. */
.frame__mascot .mascot {
  width: min(100%, var(--card-dw, 720px), calc(var(--mh, 34svh) * var(--ar, 0.62)));
}

/* Вход карточки: маска раскрывается снизу вверх.
   Маска висит на <picture>, а не на самой <figure>: clip-path на элементе,
   за которым следит IntersectionObserver, обнуляет его пересечение с экраном,
   и появление не срабатывает никогда (проверено в Chrome — карточки
   оставались невидимыми). */
.mascot-rise picture {
  clip-path: inset(100% 0 0 0);
  transition: clip-path var(--d3) var(--e-out);
}
.mascot-rise.is-in picture { clip-path: inset(0 0 0 0); }
html.no-js .mascot-rise picture,
html.motion-off .mascot-rise picture { clip-path: none; transition: none; }

/* У кадров 1 и 7 текст стоит над карточкой, поэтому её рост считается от
   остатка высоты экрана: иначе на низком окне кадр выталкивает соседа.
   У первого кадра из остатка вычтена ещё и полоса пояснений. */
.m-hero    { --ar: 0.643; --card-dw: 1440px; --mh: min(40svh, max(110px, calc(100svh - 578px))); }
.m-run     { --ar: 0.655; --card-dw: 720px;  --mh: 33svh; }
.m-point   { --ar: 0.662; --card-dw: 720px;  --mh: 26svh; }
.m-laptop  { --ar: 0.857; --card-dw: 720px;  --mh: 19svh; }
/* Кадр 05: прежние 22svh оставляли под шагами 305 px пустоты, карточка была
   наклейкой в углу. Бюджет от остатка высоты — 300 px на 390x844. */
.m-phone   { --ar: 0.521; --card-dw: 720px;  --mh: min(36svh, max(120px, calc(100svh - 544px))); }
.m-hush    { --ar: 0.573; --card-dw: 720px;  --mh: 44svh; }
.m-jump    { --ar: 0.742; --card-dw: 720px;  --mh: min(36svh, max(110px, calc(100svh - 505px))); }
/* Кадры 09, 10 и 13 держат карточку в углу под содержимым, бюджет высоты у них
   небольшой. Кадр 08 — нет: прежние 20svh оставляли карточке угол, а запас
   уходил в зазоры между ярусами; теперь бюджет считается от остатка высоты
   экрана, как у кадров 01 и 07, и даёт 321 px на 390x844. */
.m-approve { --ar: 0.575; --card-dw: 720px;  --mh: min(38svh, max(120px, calc(100svh - 523px))); }
.m-guard   { --ar: 0.931; --card-dw: 720px;  --mh: 17svh; }
.m-friends { --ar: 0.645; --card-dw: 720px;  --mh: 26svh; }
.m-wave    { --ar: 0.733; --card-dw: 480px;  --mh: 15svh; }

/* ==========================================================================
   Кадр 1 · Пропуск
   ========================================================================== */

.frame--hero .frame__in {
  grid-template-areas: 'head' 'facts' 'dog' 'foot';
  align-content: space-between;
}
.frame--hero .frame__foot {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s4) var(--s6);
}
.frame--hero .lead { max-width: 34ch; }

/* Пояснения первого кадра: всплывают одно за другим по ходу прокрутки.
   Считаются от --sq — доли пройденного пути по всему <main>, которую даёт
   shell.js. Именно от неё, а не от --sp самого кадра: --sq растёт только
   вниз, поэтому появившееся пояснение дальше уже не гаснет. Пороги подобраны
   замером: пятое встаёт примерно на 350-м пикселе прокрутки, то есть внутри
   первого экрана. Без JS и при выключенном движении --sq равен 1 — тогда
   все пять видны сразу. */
.facts {
  grid-area: facts;
  display: flex; flex-wrap: wrap;
  gap: 6px;
}
.fact {
  --at: 0;
  --on: clamp(0, (var(--sq, 1) - var(--at)) * 500, 1);
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px var(--s2) 5px 7px;
  border: var(--bw) solid var(--line-strong);
  font-family: var(--f-ui); font-weight: 500;
  font-size: var(--t-ui-xs); letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--paper-dim);
  white-space: nowrap;
  opacity: var(--on);
  translate: 0 calc((1 - var(--on)) * 10px);
  transition: opacity var(--d1) linear, translate var(--d2) var(--e-out);
}
.fact::before {
  content: ''; flex: none;
  width: 5px; height: 5px;
  background: var(--pink);
}
.fact:nth-child(1) { --at: 0.0012; }
.fact:nth-child(2) { --at: 0.005; }
.fact:nth-child(3) { --at: 0.010; }
.fact:nth-child(4) { --at: 0.016; }
.fact:nth-child(5) { --at: 0.023; }
html.no-js .fact { opacity: 1; translate: none; }

.hint {
  display: inline-flex; align-items: center; gap: var(--s2);
  width: 100%;
  font-family: var(--f-ui); font-size: var(--t-label);
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--paper-dim);
}
.hint .ico { font-size: 1.1rem; color: var(--pink); animation: v3-bob 2.6s var(--e-soft) infinite; }
@keyframes v3-bob { 0%, 100% { translate: 0 0; } 50% { translate: 0 5px; } }
html.motion-off .hint .ico { animation: none; }

/* ==========================================================================
   Кадр 2 · Через нас — плита-заголовок, плотный список, маскот справа
   ========================================================================== */

.frame--thru { --glow-c: var(--lime); }
/* На телефоне карточка делит клетку со списком и стоит в его правом нижнем
   углу: восемь названий по слову в строке узкими не становятся, а отдельная
   строка под маскот выталкивала кадр за пределы экрана. */
.frame--thru .frame__in { grid-template-areas: 'head' 'body'; }
.frame--thru .frame__mascot { grid-area: body; align-self: end; justify-items: end; }
.frame--thru .frame__body { position: relative; z-index: 1; }
.frame--thru .lead { max-width: 34ch; }

/* Восемь названий расходятся по высоте кадра, а не стоят пачкой посередине:
   до правки запас в 358 px копился полями и приёмка увидела 207 px черноты
   над плитой. Строки при этом только выше, список читается как табло. */
@media (max-width: 619px) {
  .frame--thru .frame__in { grid-template-rows: auto minmax(0, 1fr); }
  .frame--thru .frame__body { grid-template-rows: auto minmax(0, 1fr); }
  .frame--thru .thru { align-content: space-between; }
  /* Плита поднялась к верху кадра и на 320 px наехала на метку номера — та
     печаталась розовым по лайму. Запас под метку, как у кадров 01 и 07. */
  .frame--thru .frame__head { padding-top: var(--s5); }
}

.way { gap: var(--s2); }
.way .ico { font-size: 1.05rem; }

.thru { display: grid; max-width: 62%; }
.thru li {
  font-family: var(--f-display); font-weight: var(--w-head);
  font-size: calc(clamp(1.3rem, 6.2vw, 2rem) * var(--khead));
  line-height: 1.28;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  border-bottom: var(--bw) solid var(--line);
}

/* ==========================================================================
   Кадр 3 · Напрямую — одно слово-плита, мелкие плашки, маскот слева
   ========================================================================== */

.frame--direct { --glow-c: var(--pink); }
.frame--direct .frame__in { grid-template-areas: 'head' 'body' 'dog' 'foot'; }
.frame--direct .frame__mascot { align-self: end; justify-items: start; }

.direct { display: flex; flex-wrap: wrap; gap: var(--s2); }
.direct li {
  padding: 7px var(--s3);
  border: var(--bw) solid var(--line-strong);
  font-family: var(--f-ui); font-weight: 500;
  font-size: var(--t-btn-sm); letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--paper-dim);
}

/* Сноска идёт во всю ширину колонки под маскотом. Прежние 50 % ширины
   превращали её в столбик по три слова в строке. */
.note { font-size: var(--t-sm); line-height: 1.45; color: var(--paper-dim); }

/* ==========================================================================
   Кадр 4 · Устройства — плита-заголовок, три рисованных силуэта
   ========================================================================== */

.frame--devices .frame__in { grid-template-areas: 'head' 'body' 'dog' 'foot'; }
.frame--devices .frame__mascot { align-self: end; justify-items: start; }

.kinds { display: grid; gap: var(--s4); }
.kinds li {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  grid-template-areas: 'ico title' 'ico text';
  gap: 2px var(--s3);
  padding-bottom: var(--s4);
  border-bottom: var(--bw) solid var(--line);
}
.kinds__ico { grid-area: ico; font-size: 1.4rem; color: var(--lime); align-self: start; }
.kinds h3 { grid-area: title; }
.kinds p { grid-area: text; font-size: var(--t-sm); color: var(--paper-dim); }

/* ==========================================================================
   Кадр 5 · Три шага — крупные цифры, маскот в углу того же поля
   ========================================================================== */

.frame--steps { --glow-c: var(--pink); }
.frame--steps .frame__in { grid-template-areas: 'head' 'body' 'dog'; }
.frame--steps .frame__mascot { align-self: end; justify-items: end; }
.frame--steps .frame__body { position: relative; z-index: 1; }

.steps { display: grid; gap: var(--s5); }
.steps li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: 'n title' 'n text';
  gap: 2px var(--s4);
  align-items: baseline;
}
.steps__n {
  grid-area: n;
  font-family: var(--f-display); font-weight: var(--w-head);
  font-size: calc(clamp(2rem, 9vw, 3rem) * var(--khead));
  line-height: 0.9;
  letter-spacing: -0.05em;
  color: var(--lime);
  font-variant-numeric: tabular-nums;
}
.steps h3 { grid-area: title; }
.steps p { grid-area: text; font-size: var(--t-sm); color: var(--paper-dim); }

/* ==========================================================================
   Кадр 6 · Карту не привязываем — тихий кадр, максимум воздуха
   ========================================================================== */

.frame--quiet .frame__in { grid-template-areas: 'head' 'dog'; gap: var(--s7); }
.frame--quiet .frame__head { justify-items: center; text-align: center; }
.frame--quiet .lead { max-width: 26ch; }

/* ==========================================================================
   Кадр 7 · Три дня бесплатно
   ========================================================================== */

.frame--trial .frame__in { grid-template-areas: 'head' 'dog' 'foot'; align-content: space-between; }
.frame--trial .frame__foot { justify-items: stretch; }
.frame--trial .frame__foot .btn { justify-self: start; }
.frame--trial .lead { max-width: 30ch; }
.frame--trial .link { justify-self: start; }

/* ==========================================================================
   Узкий экран и низкий экран: кадр обязан остаться в своих границах
   ========================================================================== */

/* Крупная кнопка с длинной подписью на 360 px не помещается в строку. */
@media (max-width: 470px) {
  .btn--lg { padding-inline: var(--s5); font-size: var(--t-btn); }
  .frame__foot .btn, .foot-cta .btn { max-width: 100%; white-space: normal; text-align: center; }
}

/* ==========================================================================
   Кадры шире 620 px: колонка та же, композиция раскладывается на две доли
   ========================================================================== */

@media (min-width: 620px) {
  .frame__in { gap: var(--s6); }

  .frame--thru .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);
    grid-template-areas: 'head head' 'body dog';
    align-items: end;
  }
  /* Долю карточки надо назначить явно: без этого маскот остаётся в клетке
     списка и наезжает на названия сервисов. */
  .frame--thru .frame__mascot { grid-area: dog; justify-items: center; }
  /* Список раскрывается во всю долю, поэтому строки идут плотнее: восемь
     строк с прежним интерлиньяжем выталкивали кадр за высоту экрана. */
  .frame--thru .thru { max-width: none; }
  .frame--thru .thru li { line-height: 1.2; }

  /* Плита в одно слово требует всю ширину колонки — заголовок идёт над
     обеими долями, а маскот встаёт слева от плашек. */
  .frame--direct .frame__in {
    grid-template-columns: minmax(0, 0.62fr) minmax(0, 1fr);
    grid-template-areas: 'head head' 'dog body' 'dog foot';
    align-items: start;
  }
  .frame--direct .frame__mascot { align-self: end; justify-items: center; }
  /* Сноска садится на одну линию с лапами Гавчика, а не повисает в середине
     пустой доли. */
  .frame--direct .frame__foot { align-self: end; }
  .frame--direct .note { max-width: 46ch; }

  .frame--devices .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.56fr);
    grid-template-areas: 'head head' 'body body' 'foot dog';
    align-items: end;
  }
  .frame--devices .kinds { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .frame--devices .kinds li { grid-template-columns: minmax(0, 1fr); grid-template-areas: 'ico' 'title' 'text'; gap: var(--s2); }
  .frame--devices .frame__mascot { justify-items: end; }
  .frame--devices .note { max-width: 46ch; }

  .frame--steps .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.5fr);
    grid-template-areas: 'head head' 'body dog';
    align-items: end;
  }
  .frame--steps .frame__mascot { grid-area: dog; justify-items: center; }

  .m-hero { --mh: min(48svh, max(140px, calc(100svh - 555px))); }
  .frame--hero .lead { max-width: 44ch; }
  .m-jump { --mh: min(38svh, max(130px, calc(100svh - 585px))); }
  .m-hush { --mh: 52svh; }
  .m-run { --mh: 40svh; }
  .m-point { --mh: 40svh; }
  .m-phone { --mh: 40svh; }
  .m-laptop { --mh: 28svh; }
}

/* ==========================================================================
   Кадры шире 1100 px: колонка расширяется, кадр разводится на две доли.
   До этой правки контент стоял в полосе 720 px, а по бокам оставались две
   пустые обочины по 300 px с каждой стороны.
   ========================================================================== */

@media (min-width: 1100px) {
  [data-variant='v3.1'] { --col: min(1200px, calc(100vw - 200px)); }

  .frame__in { column-gap: var(--s9); }

  /* У кадров 1 и 7 плита стоит в левой доле, а не во всей колонке: на полном
     кегле «ИНТЕРНЕТ» и «БЕСПЛАТНО» не помещались в строку и рвались посреди
     слова. Потолок 6rem — тот же, что держит остальная страница. */
  .frame--hero .plate, .frame--trial .plate { font-size: min(var(--t-mega), 6rem); }

  /* Кадр 1: текст слева, пояснения под ним, огромный Гавчик справа. */
  .frame--hero .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.86fr);
    grid-template-areas: 'head dog' 'facts dog' 'foot dog';
    align-content: center;
    align-items: start;
    row-gap: var(--s7);
  }
  .frame--hero .frame__mascot { align-self: center; justify-items: center; }
  .frame--hero .facts { flex-direction: column; align-items: flex-start; gap: var(--s2); }
  .frame--hero .fact { font-size: var(--t-label); padding: 7px var(--s3) 7px var(--s2); }
  .m-hero { --mh: min(74svh, calc(100svh - 200px)); }

  /* Кадр 2: список во всю левую долю, карточка крупнее. */
  .frame--thru .frame__in { grid-template-columns: minmax(0, 1fr) minmax(0, 0.62fr); }
  .m-run { --mh: 50svh; }

  /* Кадр 3: плита слева над плашками, маскот держит левую долю. */
  .frame--direct .frame__in { grid-template-columns: minmax(0, 0.52fr) minmax(0, 1fr); }
  .m-point { --mh: 52svh; }

  /* Кадр 4: три силуэта во всю ширину, сноска и маскот — нижняя строка. */
  .frame--devices .frame__in { grid-template-columns: minmax(0, 1fr) minmax(0, 0.44fr); }
  .m-laptop { --mh: 34svh; }

  /* Кадр 5: шаги слева, Гавчик с телефоном справа во весь рост доли. */
  .frame--steps .frame__in { grid-template-columns: minmax(0, 1fr) minmax(0, 0.46fr); }
  .m-phone { --mh: 56svh; }

  /* Кадр 6: тихий кадр тоже перестаёт быть колонкой по центру. */
  .frame--quiet .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
    grid-template-areas: 'head dog';
    align-items: center;
    gap: var(--s6) var(--s9);
  }
  .frame--quiet .frame__head { justify-items: start; text-align: left; }
  .frame--quiet .slab { margin-inline: calc(var(--gutter) * -1) 0; }
  .frame--quiet .lead { max-width: 30ch; }
  .m-hush { --mh: 72svh; }

  /* Кадр 7: призыв слева, прыжок справа. */
  .frame--trial .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
    grid-template-areas: 'head dog' 'foot dog';
    align-content: center; align-items: start;
    row-gap: var(--s7);
  }
  .frame--trial .frame__mascot { align-self: center; }
  .m-jump { --mh: min(62svh, calc(100svh - 240px)); }
}

/* ==========================================================================
   Низкое окно (короткий телефон, ноутбук, альбомный планшет): поджимаем
   воздух, кегль и рост карточки, чтобы кадр укладывался в свою высоту.
   Блок идёт последним намеренно: ограничение по высоте должно перебивать
   раскладку по ширине, иначе на ноутбуке 1280×720 кадр вылезает за экран.
   ========================================================================== */

@media (max-height: 820px) {
  .frame {
    --pad-top: calc(var(--headroom) + var(--s3));
    --pad-bot: var(--s5);
  }
  .frame__in { gap: var(--s4); }
  .frame--quiet .frame__in { gap: var(--s5); }
  .frame--hero .frame__in, .frame--trial .frame__in { row-gap: var(--s4); }
  .frame .display { font-size: calc(clamp(1.75rem, 6.4vw, 2.6rem) * var(--khead)); }
  .frame .plate { font-size: calc(clamp(2.2rem, 9.4vw, 3.8rem) * var(--khead)); }
  .thru li { font-size: calc(clamp(1.15rem, 4.4vw, 1.5rem) * var(--khead)); }
  .m-hero { --mh: min(40svh, max(104px, calc(100svh - 530px))); }
  .m-run { --mh: 34svh; }
  .m-point { --mh: 30svh; }
  .m-laptop { --mh: 22svh; }
  .m-phone { --mh: 24svh; }
  .m-hush { --mh: 46svh; }
  .m-jump { --mh: min(38svh, max(104px, calc(100svh - 470px))); }
  .btn--lg { min-height: 54px; }
  .steps { gap: var(--s4); }
  .facts { gap: 4px; }
}

@media (max-height: 820px) and (max-width: 619px) {
  .m-hero { --mh: min(34svh, max(96px, calc(100svh - 500px))); }
  .m-run { --mh: 25svh; }
  .m-point { --mh: 19svh; }
  .m-laptop { --mh: 16svh; }
  /* Тот же расчёт от короткого окна: 185 px на 360x740 вместо прежних 133. */
  .m-phone { --mh: min(25svh, max(100px, calc(100svh - 555px))); }
  .m-hush { --mh: 34svh; }
  .fact { font-size: calc(0.5625rem * var(--kui)); padding-inline: 6px 6px; }
}

/* ==========================================================================
   Кадры 08–11: тарифы, честно, друзья, вопросы.

   Обёрток .spread и .hold в разметке больше нет: это такие же кадры ленты,
   как 01–07, — та же коробка .frame__in, те же доли head / body / dog / foot.
   Верхнюю линию, которую раньше рисовала .spread, берёт на себя первый из
   них — иначе кадр 07 сливается с тарифами.
   ========================================================================== */

.frame--tariffs { border-top: var(--bw) solid var(--line); }

/* --- Кадр 8 · Тарифы -----------------------------------------------------
   Четыре срока — четыре полосы во всю ширину. Цель нажатия это ВСЯ полоса,
   а не подпись в её углу: на телефоне палец бьёт по строке целиком, и
   промахнуться некуда. Полосы разделены волосяной линией и не скруглены —
   это контентная поверхность, а не пилюля действия. */

.frame--tariffs .frame__in {
  grid-template-areas: 'head' 'body' 'foot' 'dog';
  /* Ярусы разъезжаются по высоте кадра: выбор сразу под заголовком, Гавчик
     внизу. На низком экране разъезжаться нечему, и правило не мешает. */
  align-content: space-between;
  gap: var(--s5);
}
.frame--tariffs .frame__body { align-content: start; }
.frame--tariffs .lead { max-width: 34ch; }

.plans { display: grid; }
.plans li { border-top: var(--bw) solid var(--line); }
.plans li:last-child { border-bottom: var(--bw) solid var(--line); }

.plan {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--s4);
  /* Цель нажатия: полоса не ниже 44 px даже при самом мелком кегле. */
  min-height: 52px;
  padding-block: var(--s3);
  transition: background-color var(--d1) linear, color var(--d1) linear;
}
.plan:hover, .plan:focus-visible {
  background: color-mix(in oklab, var(--paper) 6%, transparent);
}
/* Срок всегда в одну строку. «12 МЕСЯЦЕВ» просит 83 px, а на 320 px его доле
   оставалось 76: слово переносилось и годовая полоса становилась вдвое выше
   остальных. Недостающие 16 px даёт поджатый зазор полосы ниже. */
.plan__term {
  font-family: var(--f-display); font-weight: var(--w-head);
  font-size: var(--t-h); text-transform: uppercase; letter-spacing: -0.02em;
  line-height: 1.1;
  white-space: nowrap;
}
.plan__price {
  font-family: var(--f-ui); font-size: calc(1.0625rem * var(--kui));
  color: var(--lime);
  white-space: nowrap;
}
.plan__go {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-family: var(--f-ui); font-size: var(--t-btn-sm);
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--paper-mute);
}
/* Слово «Оплатить» на телефоне лишнее: полоса и так вся кликается, а место
   нужно цене. На четырёх колонках оно возвращается. */
.plan__go i { display: none; font-style: normal; }
.plan:hover .plan__go { color: var(--pink); }
.plan:hover .plan__go .ico { transform: translateX(4px); }
.plan__go .ico { font-size: 1.1rem; transition: transform var(--d2) var(--e-out); }

/* Годовая полоса — выгодная: лаймом помечены и кромка, и срок. Второго
   сигнала (значка, подписи, цифры) на ней нет, иначе это уже третий акцент. */
.plan-row--year { border-left: var(--bw-strong) solid var(--lime); }
.plan-row--year .plan { padding-left: var(--s3); }
.plan-row--year .plan__term { color: var(--lime); }

.ph {
  font-family: var(--f-mono); font-weight: 500;
  font-size: 0.95em;
  padding: 1px 5px;
  border: var(--bw) dashed var(--line-strong);
  color: inherit;
  /* nowrap только там, где хватает места: на 320 px {{ОТВЕТ-ПРО-ТРАФИК}}
     вылезал за край, и прокрутки не было лишь из-за overflow-x: hidden. */
  white-space: nowrap;
}
@media (max-width: 400px) {
  .ph { white-space: normal; overflow-wrap: anywhere; }
  /* Зазор полосы тарифа: 16 px между сроком и ценой на 320 px — роскошь,
     из-за которой сроку не хватало ровно 7 px на строку. */
  .plan { gap: var(--s2); }
}
.plan .ph { white-space: normal; overflow-wrap: anywhere; max-width: 100%; }

/* Карточка Гавчика стоит справа внизу — под выбором, а не над ним. */
.frame--tariffs .frame__mascot { justify-items: end; align-self: end; }

/* --- Кадр 9 · Честно о сервисе ------------------------------------------- */

.frame--honest { --glow-c: var(--pink); }
.frame--honest .frame__in { grid-template-areas: 'head' 'body' 'dog'; align-content: space-between; }
.frame--honest .frame__mascot { justify-items: end; align-self: end; }
.honest__list { display: grid; }
.honest__list li {
  display: grid; gap: var(--s1);
  padding-block: var(--s4);
  border-top: var(--bw) solid var(--line);
}
.honest__list li:last-child { border-bottom: var(--bw) solid var(--line); }
.honest__list p { font-size: var(--t-sm); color: var(--paper-dim); }

/* --- Кадр 10 · Друзья ---------------------------------------------------- */

.frame--friends .frame__in { grid-template-areas: 'head' 'dog' 'foot'; align-content: space-between; }
.frame--friends .frame__mascot { justify-items: center; }
.frame--friends .frame__foot { justify-items: start; }

/* --- Кадр 11 · Вопросы ---------------------------------------------------
   Строка вопроса: мордочка 40 px слева, текст справа, знак у правого края.
   Вся строка — цель нажатия, поэтому у неё своя минимальная высота. */

.frame--faq .frame__in { grid-template-areas: 'head' 'body'; align-content: start; }
.frame--faq .frame__body { align-content: start; }

.qas { display: grid; }
.qa { border-top: var(--bw) solid var(--line); }
.qa:last-child { border-bottom: var(--bw) solid var(--line); }
.qa__q {
  display: flex; align-items: center; gap: var(--s3);
  min-height: 48px;
  padding-block: var(--s2);
  cursor: pointer;
  list-style: none;
}
.qa__q::-webkit-details-marker { display: none; }
.qa__face { flex: none; width: 40px; }
.qa__face .mascot img { width: 40px; height: auto; }
.qa__t { font-weight: 600; font-size: var(--t-body); line-height: 1.25; }
.qa__sign {
  margin-left: auto; flex: none;
  font-size: 1.15rem;
  color: var(--lime);
  transition: rotate var(--d2) var(--e-out);
}
/* Раскрытый вопрос — единственный активный элемент списка, и он подписан
   розовым: заголовок, знак и верхняя линия. Без JS ответ раскрывается прямо
   в строке — так страница остаётся читаемой, когда шторки нет. */
.qa[open] { border-top-color: color-mix(in oklab, var(--pink) 55%, transparent); }
.qa[open] .qa__t { color: var(--pink); }
.qa[open] .qa__sign { rotate: 45deg; color: var(--pink); }
.qa__a {
  padding: 0 0 var(--s4) calc(40px + var(--s3));
  font-size: var(--t-sm);
  color: var(--paper-dim);
  max-width: 62ch;
}

/* --- Текстовая ссылка: подчёркивание розовое ----------------------------- */

.link::after { background: var(--pink); }
.link:hover { color: var(--pink); }

/* ==========================================================================
   Кадры 08–11 на широком экране
   ========================================================================== */

@media (min-width: 620px) {
  .frame--honest .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.42fr);
    grid-template-areas: 'head head' 'body dog';
    align-items: end;
  }
  .frame--honest .frame__mascot { grid-area: dog; justify-items: center; }
  .frame--friends .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.6fr);
    grid-template-areas: 'head dog' 'foot dog';
    align-content: center; align-items: start;
  }
  .frame--friends .frame__mascot { align-self: center; }
  .frame--friends .lead { max-width: 46ch; }
  .m-approve { --mh: 26svh; }
  .qa__t { font-size: var(--t-h); }
}

/* Четыре колонки: владелец решил, что на десктопе четыре полосы в столбик
   выглядят бедно. Полоса снова становится карточкой — той же, что в v3:
   лаймовая верхняя кромка, прямые углы, подпись «Оплатить» на месте. */
@media (min-width: 980px) {
  .plans { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s4); }
  .plans li, .plans li:last-child { border: 0; }
  .plan-row--year { border: 0; }
  .plan {
    grid-template-columns: minmax(0, 1fr);
    align-content: start; align-items: start;
    height: 100%;
    gap: var(--s3);
    padding: var(--s5);
    background: var(--raise);
    border: var(--bw) solid var(--line-strong);
    border-top: var(--bw-strong) solid var(--lime);
    transition: border-color var(--d1) linear, background-color var(--d1) linear,
                transform var(--d2) var(--e-out);
  }
  .plan-row--year .plan { padding-left: var(--s5); }
  .plan:hover, .plan:focus-visible {
    background: var(--raise);
    border-color: var(--pink);
    transform: translateY(-3px);
  }
  .plan__price { font-size: calc(1.25rem * var(--kui)); }
  .plan__go { margin-top: var(--s2); }
  .plan__go i { display: inline; }
}

@media (min-width: 1100px) {
  .frame--tariffs .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.34fr);
    grid-template-areas: 'head dog' 'body body' 'foot foot';
    align-items: end;
  }
  .frame--tariffs .frame__mascot { grid-area: dog; justify-items: end; }
  /* На широком экране разносить нечего: четыре колонки коротки, и кадр
     читается как одна плотная группа по центру. */
  .frame--tariffs .frame__in { align-content: center; }
  .m-approve { --mh: 34svh; }
  .frame--honest .frame__in { grid-template-columns: minmax(0, 1fr) minmax(0, 0.36fr); }
  .frame--friends .frame__in { grid-template-columns: minmax(0, 1fr) minmax(0, 0.52fr); }
}

/* ==========================================================================
   Телефон: кадры 05 и 08 с крупным Гавчиком, кадр 09 — без него

   Замер: карточка занимала 97 / 97 / 134 px по ширине на 390x844 и 38 / 45 / 45
   на 320x600. Обрезки нет (проверено увеличением снимка), но фигурка в углу —
   иллюстрация, а не объект кадра. Решение по каждому кадру своё:
   · 05 и 08 — запас есть (305 и 260 px), он просто уходил в зазоры между
     ярусами, а не карточке. Гавчик растёт, см. --mh у .m-phone и .m-approve;
   · 09 — запаса 42 px, расти некуда, на телефоне карточки нет; с 620 px у неё
     своя доля и она возвращается. Поза landing/honest-guardian с телефона
     уходит, повторов от этого не появилось.
   Освободившееся забирают сами пункты — делят кадр поровну, текст по центру
   полосы. Тот же приём уже применён к строкам вопросов на высоком экране.
   ========================================================================== */

@media (max-width: 619px) {
  .frame--honest .frame__mascot { display: none; }

  /* Клетку 'dog' убираем из сетки, а не оставляем пустой: высота пустого ряда
     нулевая, но зазор вокруг него остаётся. */
  .frame--honest .frame__in {
    grid-template-areas: 'head' 'body';
    grid-template-rows: auto minmax(0, 1fr);
  }
  .frame--honest .honest__list { align-content: stretch; }
  .frame--honest .honest__list li { align-content: center; }
}

/* Низкое окно (320x600 и короче): запаса нет и у кадров 05 и 08 — карточка
   съезжает до 72 и 78 px, снова наклейка. Гавчика снимаем с обоих, высоту
   делят сами строки: шаги расходятся по кадру, четыре полосы тарифа берут по
   равной доле — цель нажатия только растёт, нижняя граница прежняя, 52 px. */
@media (max-width: 619px) and (max-height: 700px) {
  .frame--steps .frame__mascot,
  .frame--tariffs .frame__mascot { display: none; }

  .frame--steps .frame__in {
    grid-template-areas: 'head' 'body';
    grid-template-rows: auto minmax(0, 1fr);
  }
  .frame--steps .steps { align-content: space-between; }

  .frame--tariffs .frame__in {
    grid-template-areas: 'head' 'body' 'foot';
    grid-template-rows: auto minmax(0, 1fr) auto;
  }
  /* `align-content: start` у доли снимаем: с ним список не растягивается и
     запас уходит в дыру под последней полосой. */
  .frame--tariffs .frame__body { align-content: stretch; }
  .frame--tariffs .plans { align-content: stretch; grid-auto-rows: minmax(52px, 1fr); }
  .frame--tariffs .plan { height: 100%; }
}

/* ==========================================================================
   Кадр 13 · Подвал: три яруса в один экран

   Ярус сверху — лаймовая плита с призывом, кнопкой и машущим Гавчиком;
   середина — ссылки двумя колонками; низ — правовая строка и служебная
   полоса. Кадр целиком остаётся полем цвета только в верхней трети: залить
   весь подвал лаймом значило бы поставить рядом два кадра одной плоскости.
   ========================================================================== */

.frame.frame--foot {
  /* Два класса в селекторе намеренно: одноклассовое `.frame--foot` слабее
     правил «низкого окна», и те возвращали подвалу верхнее поле в 68 px —
     лаймовая плита переставала доходить до верха кадра.
     Своя геометрия вместо общей: у подвала нет колонки .frame__in, ярусы
     идут во всю ширину и делят высоту кадра между собой.

     Ряды именно по содержимому, а не `minmax(0, 1fr)` у среднего: с нулевым
     минимумом на 320×600 средний ряд схлопывался в ноль, и колонки ссылок
     наезжали на правовую строку. Замер по нижнему краю этого не ловит —
     видно только на снимке. Остаток высоты раздаёт space-between. */
  grid-template-rows: auto auto auto;
  align-content: space-between;
  padding: 0 0 env(safe-area-inset-bottom);
}
/* Ярусы сами держат свои поля: у кадра их нет, иначе плита перестанет быть
   плитой во всю ширину. */
.frame--foot .wrap { position: relative; z-index: 1; }

.foot-cta { background: var(--lime); color: #0b0d05; }
/* Запас сверху — под накладную шапку: под ней тут не тёмное поле, а плита. */
.foot-cta .site-foot__cta {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s4);
  padding-block: calc(var(--headroom) + var(--s4)) var(--s6);
}
.foot-cta__say { display: grid; gap: var(--s5); justify-items: start; }
.frame--foot .plate { font-size: var(--t-d1); }
.foot-cta__row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s4) var(--s6); }
.foot-cta .btn { --btn-bg: #0b0d05; --btn-fg: var(--lime); }
.foot-cta .btn:hover { box-shadow: 0 12px 32px -12px rgb(0 0 0 / 0.55); }
.foot-cta .link { color: #2a3305; }
.foot-cta .link::after { background: currentColor; }
.foot-cta .link:hover { color: #0b0d05; }
.foot-cta :focus-visible { outline-color: #0b0d05; }
/* Ширину доли задаём числом, а не содержимым. Доля `auto` резолвит `100%`
   карточки по своему максимальному размеру, и колонка выходила вдвое шире
   самой картинки (замер на 320 px: доля 96 px при карточке 44 px). Отнятое
   уходило у призыва, тот переносился лишней строкой, и подвал переставал
   помещаться в кадр на 26 px. */
.foot-cta__dog {
  display: grid; justify-items: end; align-self: end;
  width: min(var(--card-dw, 480px), calc(var(--mh, 15svh) * var(--ar, 0.733)));
}
.foot-cta__dog .mascot { width: 100%; }

.site-foot__mid { display: grid; align-content: center; }
.site-foot__cols { padding-block: var(--s5); border-top: 0; }
.foot-list { display: grid; gap: var(--s2); margin-top: var(--s3); }
.foot-list a { color: var(--paper-dim); transition: color var(--d1) linear; }
/* Адрес почты длиннее колонки, и прежнее `overflow-wrap: anywhere` рвало его
   посреди домена: приёмка увидела `support@gavdog.co` / `m` на 390 px и
   `support@gavdog` / `.com` на 320 px. Место переноса теперь задаёт разметка —
   `<wbr>` после собаки: строки «support@» и «gavdog.com», 76 и 96 px, обе
   влезают даже на 320 px. Разрешение рвать где угодно больше не нужно. */
.foot-list a { overflow-wrap: normal; hyphens: none; }
.foot-list a:hover { color: var(--lime); }
.site-foot__end .legal { padding-block: var(--s3) 0; }
.site-foot__bar { padding-block: var(--s4); }
.site-foot__bar .logo__paw { width: 22px; height: 22px; }
.site-foot__bar .logo__word { font-size: calc(0.875rem * var(--kui)); }
.site-foot__bar .spacer { display: inline-flex; align-items: center; gap: var(--s2); }
.site-foot__bar .ico { font-size: 1rem; }

/* Две колонки на любой ширине, включая 320 px: восемь ссылок в один столбик
   — это 344 px высоты вместо 170, и подвал перестаёт помещаться в кадр.
   Колонка в 130 px держит все подписи; адрес почты переносится по разрешению
   выше. Правило вне медиазапроса: у base.css с 760 px стоят три колонки, а
   третьей у нас больше нет. */
.site-foot__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); }
@media (min-width: 400px) { .site-foot__cols { gap: var(--s5); } }

/* ==========================================================================
   Кадр 12 · Финал: один месяц
   ========================================================================== */

.frame--final { --glow-c: var(--lime); }
.frame--final .frame__in { grid-template-areas: 'head' 'dog' 'foot'; align-content: space-between; }
.frame--final .frame__foot { justify-items: start; }
.frame--final .lead { max-width: 30ch; }

/* Срок и цена стоят одной строкой: срок — дисплейной гарнитурой, цена —
   лаймом, как в карточках тарифов. Кегли — из общей лестницы. */
.final__price {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s2) var(--s4);
  font-family: var(--f-ui);
  font-size: var(--t-d2);
  color: var(--lime);
}
.final__term {
  font-family: var(--f-display); font-weight: var(--w-head);
  text-transform: uppercase; letter-spacing: -0.02em;
  color: var(--paper);
}

.m-triumph { --ar: 0.55; --card-dw: 720px; --mh: min(36svh, max(110px, calc(100svh - 505px))); }

@media (min-width: 620px) {
  .m-triumph { --mh: min(38svh, max(130px, calc(100svh - 585px))); }
}

@media (min-width: 1100px) {
  /* Кадр 12: призыв слева, триумф справа — тот же разворот, что у кадра 7. */
  .frame--final .frame__in {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
    grid-template-areas: 'head dog' 'foot dog';
    align-content: center; align-items: start;
    row-gap: var(--s7);
  }
  .frame--final .frame__mascot { align-self: center; }
  .frame--final .plate { font-size: min(var(--t-mega), 6rem); }
  .m-triumph { --mh: min(62svh, calc(100svh - 240px)); }
}

@media (max-height: 820px) {
  .m-triumph { --mh: min(38svh, max(104px, calc(100svh - 470px))); }
}

/* --------------------------------------------------------------------------
   Совсем низкое окно (высота 700 px и меньше — короткий телефон, альбомная
   ориентация): кадр обязан уложиться и здесь.

   Ужимается ровно то, что разрешено ужимать: воздух между долями, поля
   кадра, межстрочный и рост карточки Гавчика. Кегль текста и сами тексты не
   трогаются — читать на маленьком экране должно быть не труднее, а легче.
   Замер, ради которого блок написан: на 320×600 кадры 01, 03 и 05
   переливались на соседний на 28, 5 и 49 px.
   -------------------------------------------------------------------------- */

@media (max-height: 700px) {
  .frame {
    --pad-top: calc(var(--headroom) + var(--s1));
    --pad-bot: var(--s3);
  }
  .frame__in,
  .frame--quiet .frame__in,
  .frame--hero .frame__in, .frame--trial .frame__in { gap: var(--s3); }
  .frame__head, .frame__body, .frame__foot { gap: var(--s3); }

  /* Межстрочный, а не кегль: буква остаётся прежнего размера. */
  .lead { line-height: 1.4; }
  .steps p, .kinds p, .honest__list p, .note, .qa__a { line-height: 1.4; }

  .steps { gap: var(--s3); }
  .kinds { gap: var(--s3); }
  .kinds li { padding-bottom: var(--s3); }
  .honest__list li { padding-block: var(--s3); }
  .qa__q { min-height: 44px; padding-block: var(--s1); }
  .plan { min-height: 44px; padding-block: var(--s2); }
  .btn--lg { min-height: 48px; font-size: var(--t-btn); }
  .facts { gap: 3px; }
  .fact { padding-block: 3px; }

  /* Поле кадра ужато до 4 px, и слово-плита первого и седьмого кадров снова
     доросла до метки номера в правом верхнем углу. Отступ даётся только этим
     двум заголовкам: у остальных первая строка до правого края не достаёт. */
  .frame--hero .frame__head, .frame--trial .frame__head,
  .frame--honest .frame__head, .frame--steps .frame__head,
  .frame--friends .frame__head { padding-top: var(--s4); }

  .m-hero    { --mh: min(24svh, max(60px, calc(100svh - 542px))); }
  .m-run     { --mh: 20svh; }
  .m-point   { --mh: 15svh; }
  .m-laptop  { --mh: 13svh; }
  .m-phone   { --mh: 12svh; }
  .m-hush    { --mh: 30svh; }
  .m-jump    { --mh: min(30svh, max(80px, calc(100svh - 480px))); }
  .m-approve { --mh: 13svh; }
  .m-guard   { --mh: 8svh; }
  .m-friends { --mh: 17svh; }
  .m-triumph { --mh: min(30svh, max(80px, calc(100svh - 462px))); }
  .m-wave    { --mh: 10svh; }

  /* Плюс --s4 сверху: метка «13/13» стоит сразу под шапкой, и призыв подвала
     печатался сквозь неё. Отыграно на полях двух нижних ярусов. */
  .foot-cta .site-foot__cta { padding-block: calc(var(--headroom) + var(--s5)) var(--s4); }
  .foot-cta__say { gap: var(--s2); }
  /* Призыв подвала — на ступень мельче: три строки плиты на низком экране
     съедали половину кадра, и служебному ярусу не оставалось места. */
  .frame--foot .plate { font-size: var(--t-d2); }
  .site-foot__cols { padding-block: var(--s2); }
  .site-foot__end .legal { padding-block: var(--s2) 0; }
  .foot-list { gap: var(--s1); margin-top: var(--s2); }
  .frame--foot .foot-list,
  .frame--foot .legal,
  .frame--foot .site-foot__bar { line-height: 1.4; }
  .site-foot__bar { padding-block: var(--s2); gap: var(--s2) var(--s5); }
}

/* --------------------------------------------------------------------------
   Запас под переключатель вариантов

   Переключатель — обвязка примерки: закреплённая пилюля в правом нижнем
   углу шириной почти во весь узкий экран (замер: 56…308 из 320). Тихая
   ссылка «Смотреть тарифы» и подсказка «Листайте» стоят у нижнего края
   первого кадра и оказывались прямо под ней. Сам переключатель не трогаем —
   отступает кадр. Запас нужен только там, где листают пальцем: на десктопе
   переключатель уезжает по прокрутке сам.
   -------------------------------------------------------------------------- */

@media (pointer: coarse) and (max-width: 620px) {
  .frame--hero { --pad-bot: 66px; }
  /* Подсказка перестаёт занимать свою строку: на низком экране она встаёт
     рядом со ссылкой, и нижний ряд кадра становится вдвое короче. */
  .frame--hero .hint { width: auto; }
}


/* Композицию получили все тринадцать кадров: блок, снимавший геометрию с
   кадров 08–11 и 13, убран вместе с классом .sec из разметки.

   В подвале номер кадра ложится на лаймовую плиту — там светлая метка не
   читается, поэтому знаки берут цвет самой плиты. */
.frame--foot > .frame__no { color: #2a3305; }
.frame--foot > .frame__no b { color: #0b0d05; }

/* ==========================================================================
   Шторка ответа (кадр 11)

   Ответ на вопрос не раскрывается внутри строки: кадр обязан остаться ровно
   в экране, а самый длинный ответ выше него вдвое. Поэтому ответ показывает
   лист, выезжающий снизу поверх кадра. Разметку строит frames.js — без него
   шторки нет вовсе и работает штатное раскрытие <details>.

   Повадки взяты у панели твиков (base.css): выезд снизу, затемнение,
   `overscroll-behavior: contain`, закрытие по Escape и по крестику. Добавлен
   свайп вниз — на телефоне это первый жест, которым закрывают лист.
   ========================================================================== */

/* Пока шторка работает, ответ в строке не нужен и не должен занимать место.
   Убирать его надо явно: закрытый <details> прячет содержимое от глаза, но
   Chrome всё равно отдаёт ему коробку — замер на 320×600 показал у кадра 11
   лишние 146 px «переполнения», которых на экране не видно. Атрибут ставит
   frames.js, поэтому без JS правило не действует и вопрос раскрывается сам. */
html[data-sheet='on'] .qas .qa__a { display: none; }

/* Скрипт не поднялся — шторки нет, <details> раскрывается прямо в строке, а
   кадру расти некуда: он равен окну, и `scroll-snap-type: y mandatory` не даёт
   остановиться между кадрами (замер: ответ уходил на 176 px ниже границы, и
   дочитать его было нельзя). Без шторки кадр вопросов получает свою прокрутку:
   ряд становится ровно окном — `minmax(0, 1fr)`, иначе ряд по содержимому
   растёт вместе с ответом и прокручивать нечего.
   При работающей шторке стоит `data-sheet='on'` и правило выключено: вложенных
   прокруток не бывает и спорить со снапом нечему. `overscroll-behavior-y`
   держит жест внутри ответа — иначе лента уезжает посреди чтения.

   КАК ЭТО МЕРИТЬ (приёмка 10.08.2026 ошиблась дважды подряд):
   · окно прокрутки — `.frame__in`, а не сам кадр. У `.frame--faq` вычисленный
     `overflow-y` так и остаётся `visible`, и это НЕ признак, что правило не
     применилось. Признак обратный: `clientHeight` кадра равен окну (844 при
     390×844) — без правила кадр вырос бы до 1621 px под раскрытые ответы.
   · `scrollHeight` кадра 962 при `clientHeight` 844 — те самые 14 % от
     декоративной подсветки `.frame::before { bottom: -14% }`, а не перелив.
   · содержимое ниже сгиба ВНУТРИ окна прокрутки переливом не является:
     `.frame__body` лежит в `.frame__in`, и его нижний край считать надо от
     нижнего края `.frame__in`. Замер настоящим касанием, семь ответов
     раскрыты, без JS: кадр стоит (`tape.scrollTop` не меняется), четыре тяги
     пальцем внутри кадра доводят `.frame__in.scrollTop` до 777 из 777 на
     390×844, 898 из 898 на 360×740, 924 из 924 на 320×600 — последний ответ
     виден целиком, за пределы кадра не выходит ничего. */
@media (pointer: coarse) and (max-width: 620px) and (prefers-reduced-motion: no-preference) {
  html:not([data-sheet='on']):not(.motion-off) .frame--faq {
    grid-template-rows: minmax(0, 1fr);
  }
  html:not([data-sheet='on']):not(.motion-off) .frame--faq .frame__in {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    scrollbar-width: thin;
  }
}

/* Семь строк на высоком экране занимают две трети кадра, и остаток некуда
   деть: раздвигаем сами строки, а не добавляем блок. Цель нажатия при этом
   только растёт. Ниже 740 px правило не действует — там место в дефиците. */
@media (min-height: 740px) {
  .qa__q { padding-block: var(--s3); }
}
@media (min-height: 860px) {
  .qa__q { padding-block: var(--s4); }
}

.sheet {
  position: fixed; inset: 0;
  /* Над закреплённым переключателем: он живёт на --z-switch и иначе висел бы
     поверх ответа — ровно так же приподнимается открытая панель твиков. */
  z-index: calc(var(--z-switch) + 1);
  display: grid; align-items: end;
  visibility: hidden;
  transition: visibility var(--d3);
}
.sheet[data-open='true'] { visibility: visible; }

.sheet__scrim {
  position: absolute; inset: 0;
  background: color-mix(in oklab, #000 66%, transparent);
  opacity: 0;
  transition: opacity var(--d2) linear;
}
.sheet[data-open='true'] .sheet__scrim { opacity: 1; }

.sheet__panel {
  position: relative;
  max-height: min(80%, 640px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s3) var(--gutter) calc(var(--s6) + env(safe-area-inset-bottom));
  background: color-mix(in oklab, var(--ink) 96%, transparent);
  backdrop-filter: blur(20px);
  /* Кромка розовая — тем же сигналом, каким помечен раскрытый вопрос. */
  border-top: var(--bw-strong) solid var(--pink);
  box-shadow: var(--sh-3);
  translate: 0 100%;
  transition: translate var(--d3) var(--e-out);
}
.sheet[data-open='true'] .sheet__panel { translate: 0 var(--drag, 0px); }
/* Пока палец тянет лист, переход выключен: иначе он догоняет палец с
   задержкой в треть секунды и жест перестаёт быть прямым. */
.sheet[data-drag='true'] .sheet__panel { transition: none; }

/* Ручка: волосяная черта, а не пилюля. Круглая форма в системе закреплена
   за действием, и заводить вторую мягкую форму ради украшения нельзя. */
.sheet__grip {
  width: 44px; height: 2px;
  margin: var(--s2) auto var(--s4);
  background: var(--line-strong);
}
.sheet__head {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) 44px;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s4);
}
.sheet__face .mascot img { width: 64px; height: auto; }
.sheet__q { font-weight: 600; font-size: var(--t-h); line-height: 1.2; }
.sheet__close {
  width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border: var(--bw) solid var(--line-strong);
  border-radius: var(--pill);
  color: var(--paper-dim);
  /* Кегль через --kui: ручка крупности текста обязана доставать и сюда,
     иначе приёмка находит «неподвижную» строку. */
  font-size: calc(1.25rem * var(--kui));
  transition: color var(--d1) linear, border-color var(--d1) linear;
}
.sheet__close:hover { color: var(--pink); border-color: var(--pink); }
.sheet__body { color: var(--paper-dim); max-width: 62ch; }
/* Перенесённый ответ теряет отступ строки списка: в шторке он единственный. */
.sheet__body .qa__a { padding: 0; max-width: none; font-size: var(--t-body); }

html.motion-off .sheet__panel,
html.motion-off .sheet__scrim { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .sheet__panel, .sheet__scrim { transition: none; }
}

/* ==========================================================================
   Движение выключено твиком или настройкой системы: страница остаётся целой
   ========================================================================== */

html.motion-off .frame[data-scrub] .frame__head,
html.motion-off .frame[data-scrub] .frame__mascot { transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-snap-type: none; }
  .progress { display: none; }
  .frame[data-scrub] .frame__head,
  .frame[data-scrub] .frame__mascot { transform: none; }
  .mascot-rise picture { clip-path: none; }
  .frame::before { opacity: 1; }
  /* Скраб не работает — пояснения первого кадра просто видны. */
  .fact { opacity: 1; translate: none; }
}

/* ==========================================================================
   Режим ленты: телефон листает кадры как видео

   Включается двумя путями сразу, и это не дублирование ради надёжности,
   а два разных случая:

     · `html[data-tape='on']` ставит frames.js — только скрипт умеет снять
       режим по твику «движение выключено»;
     · `html:not([data-tape])` держит режим, пока скрипт не сказал своё слово:
       без JS атрибута не появится никогда, и лента обязана работать так же.

   Оба пути живут внутри одного медиазапроса намеренно: frames.js ставит
   атрибут ровно по этому же условию, поэтому за его пределами `data-tape`
   не встречается. Условие `prefers-reduced-motion: no-preference` и
   `:not(.motion-off)` стоят прямо в селекторе, чтобы режим снимался
   мгновенно — до того, как наблюдатель в скрипте успеет проснуться.

   Почему фиксированное окно, а не `100dvh` на кадре: `svh` оставляет полосу
   под ушедшей адресной строкой, `dvh` дёргает ленту в момент её ухода.
   Собственное окно снимает вопрос целиком — адресная строка перестаёт ездить.
   ========================================================================== */

@media (pointer: coarse) and (max-width: 620px) and (prefers-reduced-motion: no-preference) {

  /* Страница под лентой не прокручивается вообще: всё, что на ней осталось,
     — накладная шапка, полоса прогресса и служебный переключатель. */
  html[data-tape='on']:not(.motion-off),
  html:not([data-tape]):not(.motion-off) {
    height: 100%;
    overflow: hidden;
    scroll-snap-type: none;
  }

  /* Запас в 64 px под переключатель из base.css в ленте вреден: он добавляет
     странице высоту, которой неоткуда взяться в фиксированном окне. */
  html[data-tape='on']:not(.motion-off) body,
  html:not([data-tape]):not(.motion-off) body {
    height: 100%;
    overflow: hidden;
    padding-bottom: 0;
  }

  html[data-tape='on']:not(.motion-off) .tape,
  html:not([data-tape]):not(.motion-off) .tape {
    position: fixed;
    inset: 0;
    overflow-y: scroll;
    overflow-x: hidden;
    scroll-snap-type: y mandatory;
    overscroll-behavior-y: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  html[data-tape='on']:not(.motion-off) .tape::-webkit-scrollbar,
  html:not([data-tape]):not(.motion-off) .tape::-webkit-scrollbar {
    width: 0;
    height: 0;
  }

  /* <main> отдаёт свою коробку: без этого `height: 100%` у кадра считается от
     высоты <main>, а она у него по содержимому — то есть не задана, и высота
     кадра молча падает обратно на содержимое. Проверено замером: кадры давали
     607–918 px при окне 844. `display: contents` убирает лишнее звено, и
     кадры становятся прямыми детьми окна прокрутки. Подвал им и так является. */
  html[data-tape='on']:not(.motion-off) .tape > main,
  html:not([data-tape]):not(.motion-off) .tape > main {
    display: contents;
  }

  /* Кадр равен окну ленты, а не экрану: высота считается от .tape, поэтому
     она не меняется, когда браузер прячет свою адресную строку.
     `scroll-snap-stop: always` — половина ограничителя свайпа: он не даёт
     инерции пролететь мимо соседа. Вторая половина в frames.js. */
  html[data-tape='on']:not(.motion-off) .frame,
  html:not([data-tape]):not(.motion-off) .frame {
    height: 100%;
    min-height: 0;
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }

  /* Вырез камеры и скруглённые углы: поля кадра не уже вырезов экрана.
     Подвал сюда не входит: его ярусы держат поля сами, а лаймовая плита
     обязана доходить до края экрана. */
  html[data-tape='on']:not(.motion-off) .frame:not(.frame--foot),
  html:not([data-tape]):not(.motion-off) .frame:not(.frame--foot) {
    padding-right: max(var(--gutter), env(safe-area-inset-right));
    padding-left: max(var(--gutter), env(safe-area-inset-left));
  }

  /* --- Накладная шапка ---------------------------------------------------
     Высоту у кадра шапка больше не отъедает: она лежит поверх и уезжает по
     листанию вниз. Фон и размытие сняты — под ними всё равно кадр, а не
     прокручивающийся текст. Направление считает frames.js. */
  html[data-tape='on']:not(.motion-off) .site-head,
  html:not([data-tape]):not(.motion-off) .site-head {
    position: fixed;
    inset: 0 0 auto 0;
    background: none;
    backdrop-filter: none;
    border-bottom: 0;
    padding-top: env(safe-area-inset-top);
    transition: translate var(--d2) var(--e-out);
  }
  /* data-head ставит только frames.js, и только вместе с data-tape='on'. */
  html[data-tape='on']:not(.motion-off)[data-head='hidden'] .site-head {
    translate: 0 -110%;
  }

  /* --- Никакого движения на телефоне -------------------------------------
     Кадр сменился — содержимое уже на месте. Появление, скраб и параллакс
     на ленте не работают по замыслу: они рассчитаны на медленную прокрутку
     страницы, а здесь кадр приезжает целиком за один жест.
     `!important` у переменных — не украшение: shell.js пишет --sp и --sq
     инлайновым стилем, и обычное правило их не перебивает. */
  html[data-tape='on']:not(.motion-off) .tape [data-scrub],
  html:not([data-tape]):not(.motion-off) .tape [data-scrub] {
    --sp: 1 !important;
    --sq: 1 !important;
  }
  html[data-tape='on']:not(.motion-off) .reveal,
  html:not([data-tape]):not(.motion-off) .reveal {
    opacity: 1;
    transform: none;
    filter: none;
    clip-path: none;
    transition: none;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }
  html[data-tape='on']:not(.motion-off) .mascot-rise picture,
  html:not([data-tape]):not(.motion-off) .mascot-rise picture {
    clip-path: none;
    transition: none;
  }
  html[data-tape='on']:not(.motion-off) .frame[data-scrub] .frame__head,
  html[data-tape='on']:not(.motion-off) .frame[data-scrub] .frame__mascot,
  html:not([data-tape]):not(.motion-off) .frame[data-scrub] .frame__head,
  html:not([data-tape]):not(.motion-off) .frame[data-scrub] .frame__mascot {
    transform: none;
  }
  /* Подсветка под маскотом больше не считается от положения кадра в экране:
     кадр в экране всегда, значит и свет всегда полный. */
  html[data-tape='on']:not(.motion-off) .frame::before,
  html:not([data-tape]):not(.motion-off) .frame::before {
    opacity: 1;
  }
}

/* Высокий телефон, лента: кадр 01 перестал расти под содержимое, и прежний
   рост карточки перебирал 42 px — подсказка «Листайте» свисала на соседний
   кадр. Бюджет высоты Гавчика ужат ровно настолько, чтобы кадр закрылся с
   запасом. Правило вынесено в свой медиазапрос: ниже 821 px действует таблица
   «низкого окна», и трогать её этим значением нельзя.

   Вычитаемое выросло с 640 до 664 px, когда нижний ряд кадра получил запас
   под переключатель вариантов: на 390×844 содержимое перебирало коробку на
   12 px, и тихая ссылка «Смотреть тарифы» снова оказывалась под пилюлей. */
@media (pointer: coarse) and (max-width: 620px) and (min-height: 821px)
   and (prefers-reduced-motion: no-preference) {
  html[data-tape='on']:not(.motion-off) .m-hero,
  html:not([data-tape]):not(.motion-off) .m-hero {
    --mh: min(36svh, max(96px, calc(100svh - 664px)));
  }
  html[data-tape='on']:not(.motion-off) .m-phone,
  html:not([data-tape]):not(.motion-off) .m-phone {
    --mh: min(36svh, max(120px, calc(100svh - 592px)));
  }
}

/* Печать: кадры не растягиваем на страницу, снап не нужен. */
@media print {
  html { scroll-snap-type: none; }
  .progress, .hint, .frame__no { display: none; }
  .frame { min-height: 0; padding-block: var(--s6); }
  .fact { opacity: 1; translate: none; }
}
