/* ── /keysy/ — скрол-сцена на ScrollSmoother ────────────────────────────
   Надбудова НАД keys.css: звідти беруться змінні, шрифти, сабсет FA,
   шапка, футер і кнопка .btn-26. Тут — лише композиція сторінки-вітрини
   й усе, що потрібно анімації. keys.css НЕ чіпаємо: підстрінки кейсів
   лишаються як були.

   Порядок підключення обов'язковий:
     <link keys.css> потім <link keysy-scroll.css>
   Специфічність тут скрізь дорівнює або вища за keys.css, і перевизначення
   працюють лише другим файлом.
   ────────────────────────────────────────────────────────────────────── */

/* ☠ ScrollSmoother і `scroll-behavior:smooth` разом не працюють: браузер
   анімує скрол сам, смузер анімує трансформ — виходить смикання й
   промахи по якорях. keys.css вмикає smooth глобально, тут знімаємо. */
html{scroll-behavior:auto}

/* Смузер загортає сторінку в трансформований контейнер, а всередині
   такого контейнера `position:sticky` і `fixed` рахуються від нього,
   а не від вікна. Тому шапка винесена в розмітці ПОЗА #smooth-wrapper
   і тут стає fixed. */
.nav{position:fixed;left:0;right:0;top:0}

/* Смузер керує висотою сам — тілу лишається тільки не мати своїх полів. */
body{overflow-x:hidden}
#smooth-wrapper{overflow:hidden}

/* ── ФОН НА ВСЮ СТОРІНКУ ──────────────────────────────────────────────
   Одне фото під усіма чотирма кейсами: слайди проїжджають поверх
   нерухомого кадру, звідси глибина без жодного параллаксу.

   ☠ Шар лежить у розмітці ПОЗА `#smooth-wrapper` — усередині смузера
   `position:fixed` рахується від трансформованого контейнера, і фон
   поїхав би зі сторінкою.

   ⚠ Шар лежить на `z-index:0`, а не на -1 — чому саме так, розписано
   нижче біля самого правила. Коротко: на -1 його повністю накриває фон
   `body`, і фото не видно взагалі.

   Числа зібрані в змінних навмисно: панель `?bgtest=1` крутить саме їх,
   і те, що власник виставив, переноситься сюди один в один. */
/* ⚠ Затемнення тут ДВА і вони перемножуються: рівне `--bg-dim` і градієнт
   у `::after`. Перший захід ставив 0.72 + градієнт до 0.75 — разом ~0.93,
   і фото читалось як чорний прямокутник (заміряно на скріншоті: різниця
   з чистим `#001621` майже нульова). Тримати суму нижче ~0.75. */
/* ⚠ Саме фото темне: середня яскравість кадру 62 з 255 (заміряно по
   зменшеній копії). Тому тут є `--bg-bright` — без нього будь-яке
   затемнення перетворює його на рівну темну заливку. */
:root{
  --bg-dim:0.57;      /* сила затемнення поверх фото */
  --bg-blur:3px;      /* розмиття кадру */
  --bg-sat:0.45;       /* насиченість: 1 = як у джерелі */
  --bg-bright:0.7;    /* яскравість: 1 = як у джерелі */
  --bg-pos:50%;       /* вертикальна точка кадру */
  --bg-scale:1;       /* масштаб */
  --bg-move:16;       /* хід фону за всю сторінку, у vh (0 = стоїть намертво) */
  --bg-shift:0;       /* поточний зсув — його крутить скрол, руками не чіпати */

  /* ── Кадр фону: у кожної вітрини свій ───────────────────────────────
     Власник 05.09.2026: «все вкладки кейси имеют одну и ту же фотку фон,
     хотя я просил сделать разные фоны». Фон малювався одним правилом на
     чотири сторінки, тому й був однаковий.

     Тут стоїть значення за замовчуванням — воно для `/keysy/` (сайти).
     Три вітрини перевизначають ці дві змінні власним `<style>` у `<head>`,
     його ставить `gen_hubs.py` (ключ `bg` у `HUBS`).
     🛑 Правиш пару файлів — правь і генератор, інакше наступний прогін
     поверне спільний фон на всі три сторінки.
     ⚠ Панель `?bgtest=1` ці дві змінні НЕ чіпає: вона крутить тільки
     затемнення, розмиття, насиченість, яскравість, точку і хід. */
  --bg-img:url('/img/keysy/bg-desk.webp?v=1');
  --bg-img-mob:url('/img/keysy/bg-mob.webp?v=1');

  /* ── Читабельність абзаців поверх фото ──────────────────────────────
     Власник 05.09.2026: «мілкий текст під описом погано видно, напр.
     ПЛЕТІНКА і під нею текст — або світлішим, або підкладку».
     Підібрав сам панеллю `?txttest=1`.

     ⚠ Самі змінні (`--txt-*`) живуть у `keys.css`, а не тут: ними
     користується ще й `/pro-mene/`, який цього файлу не вантажить.
     Правити числа — ТАМ, в одному місці. Тут лише те, до яких
     елементів вітрини вони прикладені. */
}
/* Фон УВІМКНЕНИЙ за замовчуванням — власник дивиться його на бою.
   Вимикається адресою `?bg=0` (клас `bg-off` вішає інлайн-скрипт на
   початку body) — це швидкий спосіб порівняти «з фоном / без фону»,
   не чіпаючи файли.
   ⚠ Перший захід був навпаки: фон тільки за `?bg=1`. Власник відкрив
   голу адресу, фону не побачив — і правильно, так і було задумано.
   Не вмикати назад без його слова. */
/* ☠☠ `z-index:-1` ТУТ НЕ ПРАЦЮЄ — перевірено пікселями, не на око.
   Перший захід поклав шар на -1, розраховуючи, що фон `body` спливе на
   канву документа й малюватиметься нижче. Не спливає: у `body` стоїть
   `overflow-x:hidden` (і в keys.css, і тут), і Chrome лишає фон на самому
   боді. А фон боді — це фон звичайного блока в потоці, він малюється
   ПІЗНІШЕ за від'ємні шари, тобто накриває їх повністю.
   Доказ: знімок сторінки з фоном і з `?bg=0` збіглися піксель у піксель
   (максимальна різниця каналів 13 — і та лише в зоні макетів, які просто
   доїхали по-різному). Картинка при цьому чесно качалась — браузер її
   малював, її просто не було видно.

   Тому шар стоїть на `z-index:0`, а вміст піднято на 1. Це не залежить
   від спливання фону взагалі.
   🛑 Повернеш -1 — фон знову зникне, і знову буде схоже, що «не залилось». */
.ks-bg{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;
  background:var(--bg-primary)}
.bg-off .ks-bg{display:none}
/* Вміст — над фоном. Шапка (100) і полоска (101) і так вище. */
#smooth-wrapper{position:relative;z-index:1}
/* ☠ `inset:-22%` — це запас під хід фону, а не «щоб з полями».
   Кадр їде вгору на `--bg-move` vh за всю сторінку, і якщо хід більший
   за запас — з-під фото вилізе порожній край. Запас 22% висоти вікна,
   тому повзунок у панелі впирається в 20 vh. Збільшуєш хід — збільшуй
   і `inset`, інакше знизу з'явиться смуга. */
.ks-bg::before{content:'';position:absolute;inset:-22%;
  background:var(--bg-img) center var(--bg-pos)/cover no-repeat;
  filter:blur(var(--bg-blur)) saturate(var(--bg-sat)) brightness(var(--bg-bright));
  transform:translateY(calc(var(--bg-shift) * 1vh)) scale(var(--bg-scale));
  transform-origin:50% 50%;will-change:transform}
/* Підкладка: рівне затемнення плюс густіший низ, щоб текст і сірий
   `--text-secondary` не сідали по контрасту на світлих плямах неону. */
.ks-bg::after{content:'';position:absolute;inset:0;
  background:
    linear-gradient(180deg,rgba(0,22,33,0.45),rgba(0,22,33,0.15) 42%,rgba(0,22,33,0.55)),
    rgba(0,22,33,var(--bg-dim))}
/* ⚠ Фото ОДНЕ на обидві верстки — `pexels-mikhail-nilov-8107901`.
   Був захід поставити на десктоп інший кадр (8107819, горизонтальний),
   власник відмовився: «лиши і там і там як було фото». Різні тут лише
   обрізки одного знімка: горизонтальна верхівка на широкий екран і повний
   портрет на телефон.

   ☠☠ НА МОБІЛЦІ ФОН НЕ РУХАЄТЬСЯ І НЕ ДИХАЄ. Дві скарги власника
   04.09.2026 — і в обох винен той самий запас `inset:-22%`:

   1. «голова у дівчини вгорі обрізана». Запас піднімає кадр на 22% висоти
      вікна (на 390×844 це 186px), а голова на цьому знімку стоїть якраз
      на 15–25% висоти — тобто рівно в тій смузі, що поїхала за екран.
      Тут `inset:0` — кадр стоїть у вікні цілком, голова видно.

   2. «при прокрутці фон наближається і віддаляється». Це НЕ анімація —
      це адресний рядок браузера: він ховається й вилазить, висота вікна
      стрибає, `position:fixed; inset:0` слухняно міняє розмір, і `cover`
      перераховує масштаб фото. Виглядає як зум.
      Лікування — `height:100lvh`: «найбільша» висота вікна, вона НЕ
      залежить від того, сховався рядок чи ні. `bottom:auto` — бо `inset:0`
      інакше перебиває висоту.

   Хід фону тут вимкнено (`--bg-move:0`): власник попросив прибрати. Скрипт
   читає цю змінну на старті й просто не створює твін.
   🛑 Повернеш рух або від'ємний `inset` на мобілці — повернуться обидві
   скарги разом. */
@media(max-width:900px){
  :root{--bg-move:0}
  .ks-bg{bottom:auto;height:100lvh}
  .ks-bg::before{inset:0;transform:none;
    background-image:var(--bg-img-mob)}
}

/* Полоска прогресу під шапкою: єдиний елемент, що показує, скільки
   лишилось. Ширину крутить GSAP через scaleX. */
.ks-progress{position:fixed;top:0;left:0;right:0;height:2px;z-index:101;
  background:linear-gradient(90deg,var(--accent),#ff8a3d);
  transform:scaleX(0);transform-origin:0 50%;pointer-events:none}

/* ── Стан «js увімкнено» ──────────────────────────────────────────────
   Як у прототипі: до ініціалізації ховається ВСЯ сцена (`.stage`
   у прототипі), а не окремі елементи. Так стартові позиції твінів ніхто
   не бачить. Клас `js-anim` вішає інлайн-рядок на початку body — немає
   js, немає й приховування, сторінка читається повністю. Це ж рятує від
   білого екрана, якщо gsap не приїхав (CSP, блокувальник, 404). */
.js-anim .ks-stage{visibility:hidden}

/* ☠ Блоку `@media(prefers-reduced-motion:reduce)` тут НЕМАЄ — як у
   прототипі. Він показував сцену до ініціалізації, і разом із такою ж
   гілкою в скрипті вбивав анімацію в сеансі RDP (той гасить анімації
   Windows, і Chrome віддає `reduce`). Подробиці — коментарем у скрипті. */

/* ── ІНТРО ───────────────────────────────────────────────────────────── */
/* ⚠ overflow-x:clip, а не hidden: clip не створює контейнер прокрутки й
   не ламає sticky/фіксовані нащадки. Тут він страхує від горизонтальної
   смуги, коли елемент стоїть у стартовій позиції твіна (зсунутий по x). */
.ks-intro{min-height:100svh;display:flex;flex-direction:column;justify-content:center;
  padding:120px 0 80px;position:relative;border-top:none;overflow-x:clip}
.ks-intro::before{content:'';position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:radial-gradient(900px 420px at 10% -10%,rgba(255,65,3,0.16),transparent 62%)}
.ks-intro .wrap-wide{width:100%}

.ks-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:2.5px;
  color:var(--accent);text-transform:uppercase;margin-bottom:14px}

/* ⚠ Розмір інтро-заголовка живе ТУТ, а не в загальному h1 з keys.css:
   там 42px стеля на всі сторінки кейсів, а вітрині потрібен плакат. */
/* ☠ overflow:hidden тут — це і є маска заголовка з прототипу
   (`.intro__title{overflow:hidden}`). Рядки їдуть знизу на 400px, і
   ховає їх саме ця коробка. Знімеш — рядки поповзуть по всій сторінці. */
.ks-intro h1{font-size:clamp(34px,7.2vw,86px);line-height:1.03;font-weight:700;
  letter-spacing:-0.5px;max-width:15ch;margin:0 0 22px;overflow:hidden}
/* ⚠ Абзаци на цій сторінці СВІТЛІШІ за загальний `--text-secondary`
   (#858da5). Власник 04.09.2026, коли з'явився фон: «текста тепер погано
   видно, їх світлішими зробити треба, но не намного».
   Крок один: #858da5 → #a5adc1. Змінну НЕ чіпаємо — вона працює на всіх
   сторінках кейсів, а фото лежить тільки під вітриною. */
/* ⚠ Крок два, 05.09.2026: колір переїхав у змінну `--txt-color`, бо його
   тепер крутить панель `?txttest=1`. Значення за замовчуванням те саме
   #a5adc1, просто записане як hsl(223 18% 70%). */
.ks-sub{font-size:clamp(16px,1.7vw,19px);color:var(--txt-color);max-width:46ch;margin:0;
  position:relative;z-index:0;
  text-shadow:0 2px 14px rgba(0,0,0,var(--txt-shadow))}
/* Підкладка під вступним абзацом. Два шари в одному псевдоелементі:
   м'яка пляма (`--txt-scrim`) і плашка з краями (`--txt-plate`).
   Обидва за замовчуванням 0 — псевдоелемент є, але прозорий наскрізь,
   тобто відвідувач бачить рівно те, що бачив до правки.
   ⚠ `z-index:0` на самому абзаці — не прикраса: він створює контекст
   накладання, і мінус-перший шар лишається ПІД текстом, але НАД фото.
   Без нього псевдоелемент провалюється під `.ks-bg` (та сама граблі, що
   вже ловилась на фоні сторінки). */
.ks-sub::before{content:'';position:absolute;z-index:-1;pointer-events:none;
  inset:-18px -26px;border-radius:16px;
  background:
    radial-gradient(120% 130% at 8% 50%,rgba(2,10,20,var(--txt-scrim)),rgba(2,10,20,0) 72%),
    rgba(2,10,20,var(--txt-plate))}
/* Розмиття фону під плашкою вмикається класом, а не самою змінною:
   `backdrop-filter:blur(0px)` — це вже окремий шар композитора на кожному
   слайді, платити за нього при вимкненій плашці нема за що.
   Клас вішає панель `?txttest=1`, коли повзунок розмиття не нуль. */
.txt-blur .ks-sub::before{backdrop-filter:blur(var(--txt-plate-blur))}

/* Рядок, який нарізав SplitText. Один в один із `.intro-line` прототипу. */
.ks-line{display:block;overflow:hidden}

/* ── Перемикач вітрин ─────────────────────────────────────────────────
   Чотири розділи кейсів: сайти, боти, mini apps, агенти. Один ряд на всіх
   чотирьох сторінках, ставить його генератор `_витрини-инструменти\
   gen_hubs.py` — руками в розмітку не дописувати, наступна збірка затре.

   Поточний розділ — `<span class="ks-tab is-on">`, а не посилання: клікати
   по сторінці, на якій уже стоїш, нема куди.

   ⚠ Ряд стоїть у вступі, тобто в тому самому екрані, де макети праворуч,
   і додає до колонки тексту ~51px. Заміряно на 1467×678 (низьке вікно
   власника, вітрина ботів): вступ виріс до 731px при вікні 678 — тобто
   на 53px нижче згину. Видно при цьому ВСЕ: вкладки 618…651, макети
   161…669 при ширині 210px, до шапки 99px. Просто перший слайд
   починається трохи нижче згину, а не рівно на ньому.
   🛑 Ростиш `margin-top` або кегль вкладок — перемір саме на низькому
   вікні. На 940px висоти цього не видно взагалі. */
.ks-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
/* ☠ 08.09.2026, контраст. Тут стояв `var(--text-secondary)` = #858DA5.
   На власній плашці `rgba(255,255,255,.03)` над фоновим фото це дає
   3.06:1 при порозі 4.5, а шрифт 11 px вагою 400 під виняток для
   великого тексту не підпадає. `#B4BCD4` дає 5.34:1.

   Заміряно по НАМАЛЬОВАНИХ пікселях: текст ховається `color:transparent`,
   знімається кадр підкладок, береться найгірша точка по внутрішній
   області вкладки. ⚠ Замір по всій коробці дає 2.29 — це ловиться
   рамка 1 px, а не фон під літерами.

   `--text-secondary` не чіпаю: він стоїть ще в кількох місцях, де фон
   інший і контраст свій. Правка адресна, тільки вкладкам.
   Блок `.ks-tabs` є рівно на чотирьох вітринах: `/keysy/`, `/boty/`,
   `/mini-apps/`, `/ai-agenty/`. Більше ніде його немає. */
.ks-tab{display:inline-block;padding:7px 15px;border-radius:999px;
  font-family:var(--font-mono);font-size:11px;letter-spacing:1.6px;
  text-transform:uppercase;text-decoration:none;white-space:nowrap;
  color:#B4BCD4;border:1px solid rgba(255,255,255,0.14);
  background:rgba(255,255,255,0.03);
  transition:color .25s ease,border-color .25s ease,background .25s ease}
.ks-tab:hover{color:var(--text-primary);border-color:rgba(255,255,255,0.32)}
.ks-tab:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
/* ⚠ 06.09.2026: активна вкладка була помаранчева (`var(--accent)`) — власник
   попросив білу заливку. Текст лишився той самий темний, контраст на білому
   тільки виріс. Помаранчевий у ряду більше не з'являється ніде. */
.ks-tab.is-on{color:#001621;background:#fff;border-color:#fff;
  font-weight:700}

/* Два скріншоти праворуч. Ховаються раніше, ніж стають тісними:
   на 1100px вони вже налазять на заголовок. */
/* ☠ `align-items:flex-start` — не косметика. Без нього flex РОЗТЯГУЄ макети
   по висоті найвищого рядка, а рядок нерівний через сходинку (`margin-top`
   -40 / +70). Заміряно на 390px: три однакові знімки давали 261, 247 і
   227px замість 230 — тобто картинки їхали по пропорції, кожна по-своєму.
   На десктопі та сама біда була, просто менш помітна. */
/* ☠ Ряд стоїть у СМУЗІ ПІД ШАПКОЮ, а не по центру вікна. Власник
   04.09.2026 по скріншоту: «картинка залазить під шапку, треба всі три
   опустити нижче, щоб при закритті анімації вона не торкалась шапки».

   Було `top:50%` + `translateY(-50%)`: ряд центрувався по ВСЬОМУ вікну,
   шапки він не бачив узагалі. Ряд високий (макет 455px плюс сходинка
   −40/+70 = смуга 565px), і на невисокому екрані верхній макет просто
   виїжджав під шапку.

   Стало: смуга від 118px (шапка 74px + запас) до 34px від низу, а всередині
   `align-items:center`. Тепер вертикальний центр рахується від вільного
   місця, і верх ряду фізично не може опинитись вище 118px.

   ⚠ Плюс стеля по висоті. Без неї на низькому вікні ряд переростає смугу
   й вилазить назад під шапку. Рахується від ширини, бо висота йде за
   пропорцією 397×860 — так кадр не спотворюється.

   ☠☠ ПЕРША СТЕЛЯ БУЛА `23vh` — І ЦЕ БУЛО ГРУБО ЗАНИЗЬКО. Власник
   04.09.2026: «я просил чуть-чуть уменьшить по высоте, а ты в 2 раза
   уменьшил фотки». Він має рацію, і ось чому я цього не побачив:
   я міряв на вікні 1859×940, де `23vh` = 216px — більше за стелю clamp
   (210px), тобто правило НЕ спрацьовувало взагалі й макети лишались
   повного розміру. А в нього вікно 1467×678 CSS-пікселів (DPR 1.25,
   заміряно по Screenshot_4: макет 259 фізичних пікселів = 207 CSS).
   На 678px `23vh` = 156px — макет схуд 210 → 156, висота 455 → 338.
   🛑 Мораль: стелю, прив'язану до висоти вікна, перевіряти на НИЗЬКОМУ
   вікні. На високому вона мовчить, і здається, що її немає.

   Тепер стеля порахована з того, що нам насправді треба — щоб ВЕРХ ряду
   не піднявся під шапку. Не з того, щоб ряд цілком уліз у смугу:
     смуга        C  ≥ 100vh − 118 − 34 = 100vh − 152 (це нижня межа:
                       `.ks-intro` має `min-height:100vh` і буває вищим);
     висота ряду  = h + 55 (крок −40/+70 при `align-items:center`
                       додає 20 зверху і 35 знизу);
     переріс смугу — `center` виштовхує ряд НАРІВНО в обидва боки,
       тобто верх іде вище 118 на (h+55−C)/2;
     низ шапки 62px, лишаємо 20px запасу → (h+55−C)/2 ≤ 36;
     звідси h ≤ 100vh − 135, ширина = h × 397/860 ≈ 0.4616h
       →  ширина ≤ 46vh − 64px.
   На 678px це 248px — більше за 210, стеля мовчить, макети повного
   розміру. Кусається вона лише нижче ~594px висоти вікна.
   ⚠ Заміряно на 1467×678 (його вікно) і на 1467×620: у першому 210px,
   зазор до шапки 73px; у другому 210px, ряд уліз у смугу цілком. */
.ks-shots{position:absolute;right:max(20px,4vw);top:118px;bottom:34px;
  display:flex;align-items:center;gap:22px;z-index:-1;opacity:.9}
.ks-shot{width:min(clamp(150px,15vw,210px),calc(46vh - 64px));height:auto;border-radius:10px;
  border:1px solid rgba(255,255,255,0.1);box-shadow:0 30px 60px rgba(0,0,0,0.45)}
.ks-shot--2{margin-top:70px}
/* Третій макет (STWBH) стоїть лівим і трохи ВИЩЕ за перший — сходинка
   веде вгору-вліво, як власник накреслив на скріншоті 04.09.2026. */
.ks-shot--3{margin-top:-40px}
/* ⚠ Поріг піднято 1100px → 1400px разом із третім макетом.
   Ряд став ширшим рівно на одну ширину + gap: на 1100–1400px три штуки
   вже налазять на заголовок, хоча дві там ще вміщались. Число підібрано
   заміром перекриття, не на око. */
@media(max-width:1400px){.ks-shots{display:none}}

/* Стрілка «крути далі» — єдина підказка, що сторінка не закінчилась. */
.ks-hint{position:absolute;left:max(20px,4vw);bottom:34px;display:flex;align-items:center;gap:12px;
  font-family:var(--font-mono);font-size:10px;letter-spacing:2px;text-transform:uppercase;
  color:var(--text-muted)}
.ks-hint-line{display:block;width:52px;height:1px;background:var(--accent);transform-origin:0 50%}

/* ── СЛАЙД КЕЙСУ ─────────────────────────────────────────────────────── */
/* keys.css задає section{padding:44px 0;border-top:…} — перебиваємо. */
.ks-slide{min-height:100svh;display:grid;grid-template-columns:1.05fr 0.95fr;
  align-items:center;gap:min(5vw,64px);padding:110px 0 96px;position:relative;
  border-top:1px solid rgba(255,255,255,0.07);overflow-x:clip}

/* Слайд сам собі сітка — без .wrap-wide усередині. Поля рахуються від
   тієї ж смуги 1140px, що й решта сайту, але картинка при цьому
   лишається на всю праву половину екрана. */
.ks-col{position:relative;z-index:2;padding-left:max(20px,calc((100vw - 1140px)/2 + 20px))}

/* Маска номера кейсу — `.slide__eyebrow-mask` з прототипу. Всередині
   рядок їде знизу на 200px, і обрізає його саме ця обгортка. */
.ks-num-mask{overflow:hidden;margin-bottom:16px}
.ks-num{font-family:var(--font-mono);font-size:11px;letter-spacing:2.4px;text-transform:uppercase;
  color:var(--accent);display:block}
.ks-slide h2{font-size:clamp(26px,3.9vw,52px);line-height:1.08;font-weight:700;
  letter-spacing:-0.3px;margin:0 0 16px}
/* Той самий крок, що й у `.ks-sub` вище — це той самий абзац, який власник
   і показав («Борошно для пекарень…»). Правити разом, порізно розійдуться. */
.ks-txt{color:var(--txt-color);max-width:44ch;margin:0 0 30px;font-size:clamp(15px,1.5vw,18px);
  text-shadow:0 2px 14px rgba(0,0,0,var(--txt-shadow))}

/* Підкладка на слайді лягає під УСЮ колонку — номер, заголовок, абзац і
   посилання, — а не під сам абзац: плашка на півколонки читається як
   випадковий прямокутник. Шари й дефолти ті самі, що в `.ks-sub::before`.
   `.ks-col` уже має `z-index:2`, тобто контекст накладання, тому окремої
   страховки, як у `.ks-sub`, тут не треба. */
/* ⚠ Ліворуч підкладка йде ДО краю екрана (`left:0`), а не відступає на
   34px: `.ks-col` починається на x=0, і весь відступ до тексту — це його
   власний `padding-left`. Зроби симетричну рамку — і вертикальний край
   плашки стане посеред фото ліворуч від тексту. Тому скруглення тільки
   праворуч, як у смуги, що виходить із-за краю. */
.ks-col::before{content:'';position:absolute;z-index:-1;pointer-events:none;
  inset:-30px -34px -30px 0;border-radius:0 20px 20px 0;
  background:
    radial-gradient(120% 130% at 8% 50%,rgba(2,10,20,var(--txt-scrim)),rgba(2,10,20,0) 74%),
    rgba(2,10,20,var(--txt-plate))}
.txt-blur .ks-col::before{backdrop-filter:blur(var(--txt-plate-blur))}

/* ── УВЕСЬ ІНШИЙ ТЕКСТ ВІТРИНИ ─────────────────────────────────────────
   Власник 05.09.2026: «зроби ці налаштування для всього тексту на
   сторінках кейсів, на всіх сторінках кейсів, тільки без підкладок —
   підкладки лише там, де я показав». Названі ним блоки — «Що ще є в
   портфоліо» і абзац під «Наступний кейс може бути вашим».

   Тому тут ЛИШЕ колір і тінь. `--txt-scrim` навмисно не задіяний:
   підкладка лишається двома псевдоелементами вище (`.ks-col`, `.ks-sub`).

   ⚠ Заголовки не чіпаємо: вони вже світліші за абзаци (#e4e7ef проти
   #dee1e8), і `--txt-color` зробив би їх ТЕМНІШИМИ, а не навпаки.
   ⚠ Посилання в списку «Що ще є» мають інлайновий бірюзовий колір —
   він перебиває це правило, і так і треба: колір посилання тут окремий
   сигнал, а не «текст».
   ⚠ `.ks-top` нижче має свій `:hover` — він стоїть далі за файлом і
   тому перемагає, стан наведення не зламано. */
.ks-extra p, .ks-extra ul.plain li,
.ks-cta p,
.ks-intro .crumbs, .ks-intro .crumbs a,
.ks-hint, .ks-top{
  color:var(--txt-color);
  text-shadow:0 2px 14px rgba(0,0,0,var(--txt-shadow))}

/* Посилання-рядок: смужка + текст. Довжину смужки на hover крутить GSAP. */
.ks-link{display:inline-flex;align-items:center;gap:14px;color:var(--text-primary);
  text-decoration:none;font-weight:700;font-size:15px;letter-spacing:.6px;
  text-transform:uppercase;font-family:var(--font-mono);padding:10px 2px}
.ks-link-line{width:46px;height:1px;background:var(--accent);display:inline-block;flex:none}
.ks-link:hover{color:var(--accent-light)}
.ks-link:focus-visible{outline:2px solid var(--accent);outline-offset:6px}

/* Картинка з параллаксом. Кадр обрізає, всередині знімок вищий за кадр —
   інакше під час зсуву згори/знизу лізе порожнеча.

   ⚠ Пропорція кадру мусить бути близька до пропорції знімка (1400×900).
   Перший захід ставив фіксовану висоту 560px: у вузькій колонці виходив
   портретний кадр під альбомний скрін, і object-fit:cover з'їдав третину
   сайту, заради якого кейс і зроблено. Тому aspect-ratio, а не height.

   Кадр навмисно виходить у правий край екрана — так знімок більший, а
   макет читається як розворот, а не як картка. */
/* ⚠ Кадру дано рівно пропорцію знімка 1400×900, а <img> заповнює його
   один в один — object-fit:cover не ріже нічого.
   ☠ Почнеш рухати саму картинку замість кадру — доведеться вертати
   overscale, і cover одразу з'їсть боки (на AgroLoaf це половина
   заголовка, вже ловив).
   ☠ `max-height:76vh` — не смак, а стеля наїзду. Кадр тепер їде по
   горизонталі, а вертикально стоїть у центрі слайда 100svh: чим він
   вищий, тим менше повітря до сусіднього вікна. На 940px кадр 544px
   лишає по 198px з боків. Піднімаєш стелю — перевір заміром, що сусідні
   `.ks-media` не перетинають вікно, коли слайд рівно заповнює екран. */
.ks-media{position:relative;overflow:hidden;aspect-ratio:var(--ks-ar,1400/900);max-height:76vh;
  border:1px solid rgba(255,255,255,0.09);border-right:none;
  border-radius:14px 0 0 14px}
/* ☠ Пропорція кадру — ЗМІННА, а не одне число на всі слайди.
   Власник 05.09.2026 почав міняти знімки телефона на знімки комп-версії:
   «фотки кейсов которые больший справа, нужно ставить комп версии».
   Вікно браузера — це ~2.16, а не 1.56. У кадрі 1400/900 такий знімок
   втрачає по 28% з кожного боку (заміряно: 1838px ширини проти 1327px,
   що влазять у кадр) — саме та грабля, що описана вище.
   Тому широкий знімок отримує свій кадр, а слайди зі старим знімком
   лишаються в 1400/900, поки їх не переспимуть.
   ⚠ Класи ставить `gen_hubs.py` — ключ `wide` у слайді. Міняєш знімок на
   широкий — став `wide: True`, інакше кадр обріже його мовчки. */
.ks-media--wide{--ks-ar:1880/870}
/* ⚠ Широкому знімку віддано більше ширини — інакше він виходить надто
   низьким і сайт на ньому не читається. Заміряно на 1467×678: у колонці
   0.95fr кадр був 659×305, у 1.12fr став 777×360.
   Текст від цього НЕ постраждав — заміряно там само: `.ks-txt` має свою
   стелю ширини, рядок і в широкому, і в звичайному слайді один і той
   самий, 418px. Змінюється лише повітря праворуч від нього.
   🛑 Далі 1.12fr не розганяти: `.ks-col` має фіксований відступ зліва
   (смуга 1140px), і колонка почне тиснути вже сам відступ. */
.ks-slide--wide{grid-template-columns:0.88fr 1.12fr}
.ks-media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  max-width:none}
.ks-media::after{content:'';position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,transparent 55%,rgba(0,22,33,0.55))}

/* «Далі» в правому нижньому куті слайда. overflow:hidden — як у
   `.slide__scroll-link` прототипу. */
.ks-next{position:absolute;right:max(20px,4vw);bottom:38px;color:var(--text-muted);
  text-decoration:none;font-family:var(--font-mono);font-size:10px;letter-spacing:2px;
  text-transform:uppercase;display:block;padding-top:14px;z-index:3;overflow:hidden}
.ks-next-line{position:absolute;top:0;left:0;display:block;width:100%;height:1px;
  background:var(--accent);transform-origin:0 50%}
.ks-next:hover{color:var(--text-primary)}
.ks-next:focus-visible{outline:2px solid var(--accent);outline-offset:6px}

/* ── ХВІСТ СТОРІНКИ ──────────────────────────────────────────────────── */
.ks-extra{padding:90px 0}
.ks-extra h2{margin-bottom:10px}
.ks-cta{padding:100px 0 110px;text-align:center;border-top:1px solid rgba(255,255,255,0.07)}
.ks-cta h2{font-size:clamp(28px,5vw,58px);line-height:1.06;margin-bottom:18px}
.ks-cta p{max-width:52ch;margin:0 auto 34px}
.ks-cta-row{display:flex;gap:14px;justify-content:center;flex-wrap:wrap}

.ks-top{display:inline-block;position:relative;margin-top:56px;color:var(--text-muted);
  text-decoration:none;font-family:var(--font-mono);font-size:10px;letter-spacing:2px;
  text-transform:uppercase;padding-top:40px}
.ks-top-line{position:absolute;top:0;left:50%;width:1px;height:30px;background:var(--accent);
  transform:translateX(-50%);transform-origin:50% 100%}
.ks-top:hover{color:var(--text-primary)}

/* ── МОБІЛКА ─────────────────────────────────────────────────────────── */
/* Одна колонка, поля як у решти сайту. Анімація та сама, що й на
   десктопі: кадр виїжджає справа наліво.

   ☠ 04.09.2026: слайд тут став ПОВНИМ ЕКРАНОМ, як на десктопі — власник
   попросив «на мобілці теж» один сайт на одне видиме вікно.
   Було `min-height:auto` і `gap:72px` (запас під вертикальний параллакс,
   який знято того ж дня). Слайд виходив 839px при вікні 844 — тобто майже
   екран, але випадково: висота залежала від довжини тексту, і на іншому
   телефоні збіглась би інакше.

   Тепер геометрія задана, а не випадкова:
     min-height:100svh   слайд рівно екран (svh, не vh — на iOS адресний
                         рядок з'їдає різницю й vh дає обрізку);
     align-content:center вміст стоїть по центру, надлишок іде в повітря;
     gap 22px            замість 72 — запас під параллакс більше не потрібен;
     .ks-next статичний  третім рядком сітки, а не поверх фото.

   ⚠ `.ks-media` тут БЕЗ фіксованої висоти — тільки стеля 46svh. Кадр мусить
   лишатись у своїй пропорції 1385×900: даси йому висоту рядка, він стане
   портретним під альбомний знімок, і `object-fit:cover` з'їсть боки —
   рівно та грабля, що описана вище біля `.ks-media`.

   Заміряно на 390×844: 92 (поле) + 224 (текст) + 22 + 227 (кадр) + 22 +
   24 («Далі») + 56 = 667 із 844. Решта — повітря, слайд рівно один екран.
   ⚠ На короткому екрані (нижче ~700px) сам текст уже не лишає місця —
   там окремий блок нижче тисне поля. Ідеально не влізе все одно: заголовок
   із абзацом і кадр фізично не поміщаються в 640px. */
@media(max-width:900px){
  .ks-intro{min-height:100svh;padding:104px 0 64px}

  /* ☠ Три макети повертаються на телефон. Власник 04.09.2026: «на мобілці
     ти не вставив у перший слайд три фотки проектів, як на комп-версії».
     Вони ховались усім діапазоном `max-width:1400px` — правило вище, —
     бо на десктопі вужче 1400px налазили на заголовок. На телефоні цієї
     біди немає: ряд стає В ПОТОЦІ під текстом, а не абсолютом праворуч.

     ⚠ Ширина рахується від екрана, а не фіксована: три однакові колонки
     мінус два проміжки. Стеля 118px — щоб на широкому телефоні макети не
     розповзались і слайд лишався в одному екрані.
     ⚠ `z-index:auto` обов'язковий: на десктопі в них -1, щоб ховатись за
     заголовок, а тут вони самостійний блок і мають бути видимі.
     Сходинка збережена, тільки дрібніша: -14px / +20px замість -40 / +70. */
  .ks-shots{display:flex;position:static;transform:none;z-index:auto;
    margin:26px 20px 0;gap:9px;opacity:.92}
  .ks-shot{width:calc((100% - 18px) / 3);max-width:118px;border-radius:8px;
    box-shadow:0 14px 30px rgba(0,0,0,0.45)}
  .ks-shot--3{margin-top:-14px}
  .ks-shot--2{margin-top:20px}
  .ks-slide{grid-template-columns:1fr;min-height:100svh;align-content:center;
    padding:92px 0 56px;gap:22px}
  .ks-col{padding-left:20px;padding-right:20px}
  .ks-media{max-height:46svh;margin:0 0 0 20px}
  .ks-next{position:static;display:inline-block;margin:0 0 0 20px;justify-self:start}
  .ks-hint{display:none}
  .ks-extra{padding:56px 0}
  .ks-cta{padding:64px 0 72px}
}

/* Низькі екрани: тиснемо поля, щоб слайд не переростав вікно. */
@media(max-width:900px) and (max-height:720px){
  .ks-slide{padding:76px 0 40px;gap:16px}
  .ks-media{max-height:38svh}
}
