/* ==========================================================================
   Fluviaweb component layer — the pieces a SaaS product needs, styled to the
   brand. Requires tokens.css. Everything reads the semantic tokens, so it
   flips light/dark and amber/lime automatically. Class-based, framework-free;
   compose in plain HTML, Blade, JSX, Vue — anywhere.
   ========================================================================== */

/* ---- Buttons -------------------------------------------------------------
   Виміряно з живої кнопки на fluviaweb.com, а не підібрано на око:
   Golos Text 600, 13.6px (.85rem), padding 8×14, пігулка, підйом на 1px при
   наведенні. Розміри — та сама шкала, що на головному домені: sm .85rem,
   базовий .95rem, lg 1.02rem.

   Кнопки набираються ТЕКСТОВИМ шрифтом, не заголовковим. Manrope у дрібному
   кеглі на акцентному тлі читається тонко — саме тому головний домен ставить
   тут Golos Text.
   -------------------------------------------------------------------------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2);
  font-family:var(--font-body);font-weight:600;font-size:.95rem;line-height:1.2;
  padding:11px 18px;border-radius:var(--r-pill);border:1px solid transparent;
  cursor:pointer;white-space:nowrap;transition:background var(--dur-1) var(--ease-out),
  color var(--dur-1) var(--ease-out),border-color var(--dur-1) var(--ease-out),box-shadow var(--dur-1) var(--ease-out),transform var(--dur-1) var(--ease-out);
}
.btn:hover{transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
/* Primary — the amber/lime accent. Ink text (accent is too bright for white). */
.btn-accent{background:var(--accent);color:var(--accent-ink);box-shadow:var(--shadow-accent)}
.btn-accent:hover{background:var(--accent-hover);color:var(--accent-ink)}
/* Secondary — glass outline */
.btn-ghost{background:transparent;border-color:var(--border-strong);color:var(--text-main)}
.btn-ghost:hover{background:var(--bg-tint);color:var(--text-strong)}
/* Quiet — text only */
.btn-quiet{background:transparent;color:var(--text-soft);padding:8px 12px}
.btn-quiet:hover{color:var(--text-strong);background:var(--bg-tint)}
.btn-sm{padding:8px 14px;font-size:.85rem}
.btn-lg{padding:14px 22px;font-size:1.02rem}
.btn-block{width:100%}

/* ---- Cards & surfaces ---------------------------------------------------- */
/* Блоки без рамки: поверхню тримають фон і тінь. Обведення навколо картки
   додає другу лінію поруч із межею секції й дробить сторінку.

   Тінь — --shadow-2, як на fluviaweb.com: велика й м'яка. --shadow-1 щільна
   й коротка, у темній темі вона читається як чорна лінія під блоком; лишай її
   для щільних поверхонь на кшталт рядків таблиць. */
.card{position:relative;background:var(--surface-card);border-radius:var(--r-lg);box-shadow:var(--shadow-2);padding:clamp(18px,2.5vw,28px)}
.card-glass{border-radius:var(--r-lg);padding:clamp(18px,2.5vw,28px)} /* add class="card-glass glass" */
.panel{background:var(--surface-card);border-radius:var(--r-md);box-shadow:var(--shadow-2)}

/* ---- Text helpers -------------------------------------------------------- */
.eyebrow{font-family:var(--font-mono);font-size:var(--text-xs);letter-spacing:var(--ls-eyebrow);text-transform:uppercase;color:var(--accent)}
.title-hero{font-size:var(--text-hero);line-height:var(--lh-tight)}
.title-h1{font-size:var(--text-h1)}
.title-h2{font-size:var(--text-h2)}
.lead{font-size:var(--text-lead);color:var(--text-soft)}
.muted{color:var(--text-soft)}
.mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums}

/* ---- Form controls ------------------------------------------------------- */
.field{display:flex;flex-direction:column;gap:var(--sp-2)}
.label{font-size:var(--text-sm);font-weight:600;color:var(--text-strong)}
.input,.select,.textarea{
  width:100%;font:inherit;color:var(--text-strong);background:var(--surface-card);
  border:1px solid var(--border-strong);border-radius:var(--r-sm);
  padding:.62rem .8rem;transition:border-color var(--dur-1) var(--ease-out),box-shadow var(--dur-1) var(--ease-out);
}
.input:focus,.select:focus,.textarea:focus{outline:none;border-color:var(--accent);box-shadow:var(--ring)}
.textarea{min-height:6rem;resize:vertical;line-height:var(--lh-body)}
.hint{font-size:var(--text-xs);color:var(--text-faint)}
.field-error .input,.field-error .select,.field-error .textarea{border-color:var(--err-600)}
.field-error .hint{color:var(--err-600)}

/* ---- Badges & tags -------------------------------------------------------
   Бейдж — це статус, який мають прочитати з відстані, а не декоративна
   позначка. Тому насичена підкладка (колір стану, а не блідий -100), жирне
   накреслення й власні відступи: бейдж часто стоїть упритул до тексту в
   комірці таблиці, і без них він злипається із сусіднім словом.
   -------------------------------------------------------------------------- */
.badge{
  display:inline-flex;align-items:center;gap:.35rem;vertical-align:middle;
  font-family:var(--font-mono);font-size:var(--text-xs);font-weight:700;
  padding:.25rem .6rem;margin-inline:.2rem;border-radius:var(--r-pill);
  background:color-mix(in srgb, var(--ink-500) 16%, transparent);color:var(--text-strong);
}
/* Статуси — суцільна заливка, а не бліда підкладка: бейдж читають з відстані,
   а напівпрозорий колір на світлій і темній темі дає два різні бейджі.

   badge-ok — лайм #9BDD00 із білим текстом. Це фірмове рішення, і воно
   свідомо йде проти контрастної арифметики: білий на лаймі дає 1.65:1. Тому
   в цьому бейджі короткі слова, і він ніколи не є єдиним носієм змісту —
   поруч завжди є текст, який каже те саме.

   Решта — з розрахунку: акцент надто світлий для білого, там --accent-ink
   (11:1), червоний навпаки темний, там білий (4.7:1). */
.badge-accent{background:var(--accent);color:var(--accent-ink)}
.badge-ok{background:var(--state-ok);color:#fff}
.badge-warn{background:var(--state-warn);color:var(--accent-ink)}
.badge-err{background:var(--err-600);color:#fff}

/* Бейдж у кутку картки: статус не мусить лізти в текстовий рядок і зсувати
   те, що поруч. Вимагає position:relative на самій картці — він уже там. */
.badge-corner{position:absolute;top:var(--sp-4);inset-inline-end:var(--sp-4);margin:0;z-index:1}
/* Бейдж поруч із сусіднім елементом не має злипатися з ним. */
.badge:first-child{margin-inline-start:0}
.badge:last-child{margin-inline-end:0}

/* ---- Pager ---------------------------------------------------------------
   Посторінковий список без оформлення виглядає як недороблена сторінка, а не
   як навігація. Номери моноширинні: інакше «9» і «10» стрибають по ширині, і
   ряд кнопок хитається під час гортання.

   Стрілки поруч із номерами, а не замість них: у довгому списку потрібно і
   перегортати по одній, і стрибати відразу далеко.
   -------------------------------------------------------------------------- */
.pager{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-2);margin-top:var(--sp-5)}
.pager__link,.pager__gap{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:2.25rem;height:2.25rem;padding:0 .55rem;
  font-family:var(--font-mono);font-size:var(--text-sm);font-variant-numeric:tabular-nums;
  border-radius:var(--r-pill);color:var(--text-soft);text-decoration:none;
  transition:background var(--dur-1) var(--ease-out),color var(--dur-1) var(--ease-out);
}
.pager__link:hover{background:var(--bg-tint);color:var(--text-strong)}
/* Поточна сторінка — єдине акцентне місце в ряду. */
.pager__link[aria-current]{background:var(--accent);color:var(--accent-ink);font-weight:700}
/* Стрілка в кінці списку лишається на місці: зникнувши, вона зсувала б увесь
   ряд саме тоді, коли по ній ціляться. */
.pager__link--off{opacity:.35}
.pager__gap{color:var(--text-faint);min-width:auto;padding:0 .15rem}
.pager__count{margin-inline-start:auto;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-faint)}
@media (max-width:640px){.pager__count{margin-inline-start:0;width:100%}}

/* ---- Table --------------------------------------------------------------- */
.table{width:100%;border-collapse:collapse;font-size:var(--text-sm)}
.table th{text-align:left;font-family:var(--font-mono);font-size:var(--text-xs);letter-spacing:.04em;text-transform:uppercase;color:var(--text-faint);font-weight:500;padding:.6rem .75rem;border-bottom:1px solid var(--border-soft)}
.table td{padding:.7rem .75rem;border-bottom:1px solid var(--border-soft);color:var(--text-main)}
/* Останній рядок без лінії знизу — вона дублює межу блока. Це саме правило
   прибирає лінію і в таблиці з одного рядка. */
.table tbody tr:last-child td{border-bottom:0}
.table tbody tr:hover{background:var(--bg-tint)}
.table td.num,.table th.num{text-align:right;font-family:var(--font-mono);font-variant-numeric:tabular-nums}

/* Рядок-посилання: коли рядок описує один об'єкт і в ньому є посилання на
   нього, натискати треба весь рядок, а не влучати в маленький бейдж.
   Саме посилання лишається в розмітці — воно потрібне клавіатурі, пошуковику
   й «відкрити в новій вкладці». Скрипт — у patterns.md. */
.table tbody tr.row-link{cursor:pointer}
.table tbody tr.row-link:focus-within{background:var(--bg-tint)}

/* ---- Stat tile (KPI) ----------------------------------------------------- */
.stat{background:var(--surface-card);border-radius:var(--r-md);box-shadow:var(--shadow-2);padding:var(--sp-5)}
.stat__label{font-family:var(--font-mono);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.06em;color:var(--text-faint)}
.stat__value{font-family:var(--font-display);font-weight:700;font-size:1.9rem;color:var(--text-strong);line-height:1.1;margin-top:.35rem;font-variant-numeric:tabular-nums}
.stat__delta{font-size:var(--text-xs);color:var(--text-soft);margin-top:.25rem}

/* ---- Tabs ---------------------------------------------------------------- */
.tabs{display:flex;gap:var(--sp-1);border-bottom:1px solid var(--border-soft)}
.tab{padding:.6rem .9rem;font-size:var(--text-sm);font-weight:600;color:var(--text-soft);border-bottom:2px solid transparent;cursor:pointer}
.tab:hover{color:var(--text-strong)}
.tab.is-active{color:var(--text-strong);border-bottom-color:var(--accent)}

/* ---- Empty state --------------------------------------------------------- */
.empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--sp-3);padding:var(--sp-16) var(--sp-6);color:var(--text-soft)}
.empty__title{font-family:var(--font-display);font-weight:600;color:var(--text-strong);font-size:1.1rem}

/* ---- Note: стан, а не подія ----------------------------------------------
   Тост повідомляє про **дію**: збережено, не вдалося. Нотатка описує **стан
   об'єкта**: цей аудит перервано, цей тариф не має наблюдения, цей сервіс не
   налаштований.

   Різниця не косметична. Тост зникає — і має зникати, бо дія скінчилася.
   Стан лишається: відкрий той самий екран завтра, і причина збою має бути
   там само. Показати її тостом означає, що людина, яка прийшла за
   посиланням, побачить порожній екран без пояснень.

   Тому нотатка живе в потоці сторінки, поруч із тим, що описує. --------- */
.note{
  border-inline-start:3px solid var(--border-strong);
  border-radius:var(--r-md);
  background:var(--bg-tint);color:var(--text-soft);
  padding:var(--sp-3) var(--sp-4);font-size:var(--text-sm);line-height:var(--lh-body);
}
.note strong{color:var(--text-strong)}
.note-ok{border-inline-start-color:var(--state-ok)}
.note-warn{border-inline-start-color:var(--state-warn)}
.note-err{border-inline-start-color:var(--err-600)}

/* ---- Toast ---------------------------------------------------------------
   Підсумок дії: «збережено», «не вдалося», «зверніть увагу». Живе у правому
   верхньому куті, одразу під шапкою, і кілька повідомлень стають одне під
   одним.

   Чому не всередині сторінки. Плашка в потоці зсуває вміст рівно тоді, коли
   людина вже дивиться на нього, і зникає з очей, щойно сторінка прокрутиться.
   Тост не рухає нічого й лишається там, куди звик дивитися погляд.

   Це НЕ заміна помилкам полів: «пошта зайнята» має стояти під полем пошти.
   Тост — про результат дії цілком.
   -------------------------------------------------------------------------- */
.toast-stack{
  position:fixed;z-index:120;
  /* Під шапкою: 68px — її висота, далі звичайний відступ. */
  top:calc(68px + var(--sp-4));inset-inline-end:var(--gutter);
  display:flex;flex-direction:column;gap:var(--sp-3);
  width:min(24rem,calc(100vw - 2 * var(--gutter)));
  pointer-events:none;
}
/* Стек не ловить кліки — інакше прозора колонка на півекрана перехоплювала б
   натискання по тому, що під нею. Ловлять самі тости. */
.toast-stack > *{pointer-events:auto}

.toast{
  display:flex;align-items:flex-start;gap:var(--sp-3);
  background:var(--surface-card);border-radius:var(--r-md);
  box-shadow:var(--shadow-3);padding:var(--sp-4);
  border-inline-start:3px solid var(--ink-400);
  animation:toast-in var(--dur-2) var(--ease-out);
}
.toast__body{flex:1;min-width:0}
.toast__title{font-weight:600;color:var(--text-strong);font-size:var(--text-sm)}
.toast__text{color:var(--text-soft);font-size:var(--text-sm);line-height:var(--lh-body);overflow-wrap:anywhere}

/* Колір несе смугу й іконку, не тло: заливка на всю ширину поруч із карткою
   читається як ще один блок сторінки, а не як повідомлення про дію. */
.toast-ok{border-inline-start-color:var(--state-ok)}
.toast-warn{border-inline-start-color:var(--state-warn)}
.toast-err{border-inline-start-color:var(--err-600)}
.toast-ok .toast__icon{color:var(--state-ok)}
.toast-warn .toast__icon{color:var(--state-warn)}
.toast-err .toast__icon{color:var(--err-600)}
.toast__icon{flex:none;margin-top:1px}

.toast__close{
  flex:none;background:none;border:0;padding:2px;cursor:pointer;
  color:var(--text-faint);line-height:0;border-radius:var(--r-xs);
}
.toast__close:hover{color:var(--text-strong)}

.toast.is-going{opacity:0;transform:translateX(12px);transition:opacity var(--dur-2) var(--ease-out),transform var(--dur-2) var(--ease-out)}

@keyframes toast-in{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:none}}

@media (prefers-reduced-motion:reduce){
  .toast{animation:none}
  .toast.is-going{transition:none}
}

/* ==========================================================================
   Brand vertical logo lockup — each product ships its own logo, shown large
   and vertical on auth / splash / empty app, small in the app chrome.
   Provide two files per product: logo-light.svg (dark artwork, for light
   theme) and logo-dark.svg (light artwork, for dark theme). Swap by theme.
   ========================================================================== */
/* ==========================================================================
   Vertical brand rail — THE Fluviaweb logo. Fixed to the left edge, the word
   set vertically, "WEB" in the accent. Identical markup and CSS on every domain
   and subdomain: this is the brand, not a per-product decoration.

   Shows from 1024px; below that it disappears and the header falls back to the
   small horizontal wordmark. Ported verbatim from fluviaweb.com — do not
   "improve" the numbers here, they must match across properties.
   ========================================================================== */
:root{--sidelogo-w:0px}
@media (min-width:1024px){:root{--sidelogo-w:9.5rem}}
body{padding-left:var(--sidelogo-w)}

.fit-rail{
  position:fixed;left:0;top:0;width:var(--sidelogo-w);height:100vh;
  display:none;flex-direction:column;align-items:center;justify-content:center;gap:0;
  z-index:60;text-decoration:none;
}
@media (min-width:1024px){.fit-rail{display:flex}}

.fit-rail-word{
  writing-mode:vertical-rl;transform:rotate(180deg);
  font-family:var(--font-display);font-weight:900;
  font-size:clamp(3.5rem,16.5vh,10rem);letter-spacing:normal;line-height:1;
  color:var(--ink-950);
}
.text-flow{color:var(--azul-600)}   /* the "WEB" inside FLUVIAWEB */

/* Second property under the wordmark — the fluviaweb.com/journal precedent.
   Use it only when the rail must name a property; on product subdomains the
   product is named in the header instead. */
.fit-rail-suffix{
  writing-mode:vertical-rl;transform:rotate(180deg);margin-top:10px;
  font-family:var(--font-mono);font-size:clamp(.6rem,.85vw,.78rem);font-weight:600;
  text-transform:uppercase;letter-spacing:.22em;color:var(--text-faint);
}

/* The small header wordmark exists only while the rail is hidden. Wrapping it
   keeps this rule independent of the theme-swap display rules below.
   inline-flex, not a bare span: an inline box adds leading above the image and
   the logo ends up sitting higher than the product name next to it. */
.brand-compact{display:inline-flex;align-items:center}
@media (min-width:1024px){.brand-compact{display:none}}

/* ==========================================================================
   Site header — the public shell's chrome. It lives in the design system
   because every product needs the same one, and because it has three traps
   that each product would otherwise walk into on its own:

   1. It must span the rail (negative margin + width), or the glass strip
      starts 150px in and the top-left corner looks broken.
   2. Its inner padding is gutter PLUS rail width. Setting only the rail width
      overrides the .wrap gutter, and below 1024px (rail = 0) the logo ends up
      glued to the screen edge.
   3. Below ~900px the links do not fit. The nav becomes a drawer behind a
      burger — desktop items squeezed onto a phone is the single most common
      way this header breaks.
   ========================================================================== */
.site-header{
  position:sticky;top:0;z-index:70;
  background:var(--glass-bg-strong);
  -webkit-backdrop-filter:blur(var(--glass-blur));backdrop-filter:blur(var(--glass-blur));

  /* Скло тягнеться до краю екрана, під рейку — як на fluviaweb.com. */
  margin-left:calc(-1 * var(--sidelogo-w));
  width:calc(100% + var(--sidelogo-w));
  /* …але контентна коробка починається після рейки, тому вкладений .wrap
     центрується так само, як .wrap у секціях сторінки. Без цього padding
     від'ємний margin змінює базу центрування, і колонка шапки їде вбік. */
  padding-left:var(--sidelogo-w);
}
/* Смуга шапки. Висота, ритм і брейкпоінти — перенесені з fluviaweb.com;
   `.wrap` замість власного padding — єдина свідома відмінність, щоб колонка
   шапки збігалася з колонкою контенту під нею. */
.site-header__inner{position:relative;display:flex;align-items:center;gap:var(--sp-4);height:68px}

/* Три групи: бренд ліворуч, меню по центру смуги, контроли й головна дія
   праворуч однією групою.

   Меню центрується абсолютно, а не залишком місця. Через space-between чи
   flex:1 воно центрується всередині того, що лишилося від бренду й контролів,
   тому будь-яка зміна ширини назви продукту або підпису кнопки зсуває меню —
   і воно перестає збігатися з центром сторінки. */
.site-nav{position:absolute;left:50%;transform:translateX(-50%)}
.site-nav__actions{margin-left:auto}

/* Пункти меню з'являються від 768px — так само, як на головному домені. */
.site-nav{display:none;align-items:center;gap:4px}
@media (min-width:768px){.site-nav{display:flex}}
.site-nav__link{
  padding:8px 14px;border-radius:var(--r-pill);
  font-size:var(--text-sm);font-weight:500;color:var(--ink-700);
  text-decoration:none;white-space:nowrap;
  transition:color var(--dur-1) var(--ease-out);
}
.site-nav__link:hover,.site-nav__link[aria-current="page"]{color:var(--accent)}

/* Контроли завжди в смузі: тема, мова, дія. Перемикач мов ховається на
   найвужчих екранах і дублюється в мобільній панелі — точно як на
   fluviaweb.com, де в панелі стоїть той самий компонент. */
.site-nav__actions{display:flex;align-items:center;gap:10px}
@media (max-width:639px){.site-nav__actions > .lang-switcher{display:none}}

/* ---- Перемикач мов --------------------------------------------------------
   На головному домені він випадний: глобус, поточний код, стрілка — і список
   із рідними назвами мов. Там його форма задана утилітами Tailwind, тому в
   системі вона мусить бути виписана явно, інакше кожен продукт малює свій.
   Потрібен Alpine (x-data/x-show) — той самий, що на fluviaweb.com. */
/* Порт із fluviaweb.com, розмір за розміром. Там форма зібрана утилітами
   Tailwind поверх чотирьох власних правил, тому тут вона виписана явно —
   інакше кожен продукт домалює свою.

   Числа не підбирати: 126×40 у кнопки — фіксована ширина, щоб перемикач не
   стрибав при зміні коду мови; 144px і 8px відступу — меню; 0.06em — розрядка,
   без якої моношрифт капітеллю злипається. */
.lang-switcher{position:relative}
.lang-switcher__toggle{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  width:126px;height:40px;padding:0;border:0;cursor:pointer;
  border-radius:var(--r-pill);
  font-family:var(--font-mono);font-size:var(--text-sm);font-weight:600;
  text-transform:uppercase;letter-spacing:.06em;color:var(--ink-700);
}
.lang-switcher__toggle:focus-visible{outline:none;box-shadow:var(--ring)}
.lang-switcher__menu{
  position:absolute;inset-inline-end:0;top:calc(100% + var(--sp-2));z-index:50;
  min-width:9rem;padding:4px;border-radius:var(--r-md);box-shadow:var(--shadow-2);
}
.lang-switcher__option{
  display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);
  padding:8px 12px;border-radius:var(--r-sm);
  font-size:var(--text-sm);font-weight:500;color:var(--ink-700);text-decoration:none;
  transition:color var(--dur-1) var(--ease-out);
}
.lang-switcher__option:hover{color:var(--accent)}
.lang-switcher__option--current{color:var(--accent);font-weight:600}
.lang-switcher__code{font-family:var(--font-mono);font-size:var(--text-xs);text-transform:uppercase;color:var(--ink-400)}

/* Alpine ховає елементи до ініціалізації саме цим атрибутом. */
[x-cloak]{display:none !important}

/* Бургер: видимий там, де сховані пункти меню. */
.nav-toggle{
  display:inline-flex;align-items:center;justify-content:center;
  width:40px;height:40px;border:0;border-radius:var(--r-pill);
  background:none;color:var(--ink-700);cursor:pointer;
}
.nav-toggle:focus-visible{outline:none;box-shadow:var(--ring)}
@media (min-width:768px){.nav-toggle{display:none}}

/* Мобільна панель — у потоці під смугою, з лінією-роздільником, як на
   головному домені. Обмеження висоти — єдине доповнення: sticky-шапка, вища
   за екран, перестає триматися зверху, і її верх виїжджає за верхню грань. */
.header-mobile{
  border-top:1px solid var(--border-soft);
  max-height:calc(100dvh - 68px);overflow-y:auto;overscroll-behavior:contain;
}
/* Відкривається класом is-open на .site-header — його ставить .nav-toggle.
   Вище 768px панель не потрібна взагалі: пункти стоять у смузі, і якщо
   лишити її відкритою, вона висітиме під шапкою на десктопі. */
.header-mobile{display:none}
.site-header.is-open .header-mobile{display:block}
@media (min-width:768px){.site-header.is-open .header-mobile{display:none}}

.header-mobile__nav{display:flex;flex-direction:column;gap:4px;padding:var(--sp-4) var(--gutter)}
.header-mobile__link{
  padding:10px 12px;border-radius:var(--r-sm);
  font-size:var(--text-md);font-weight:500;color:var(--ink-700);text-decoration:none;
}
.header-mobile__link:hover{color:var(--accent)}

/* Головна дія шапки — на місці WhatsApp-кнопки головного домену й у тій самій
   формі: залита пігулка 126×40. Колір акцентний, бо це наша дія, а не чужий
   месенджер. Нижче 640px лишається кругла іконка: підпис з'їдає рядок, але
   зникати кнопка не має — на телефоні вона потрібна найбільше. */
.site-header__cta{
  width:126px;height:40px;padding-inline:0;
  background:var(--accent);border-color:transparent;color:var(--accent-ink);
  box-shadow:var(--shadow-accent);
}
.site-header__cta:hover{color:var(--accent-ink)}
@media (max-width:639px){
  .site-header__cta{width:40px;gap:0}
  .site-header__cta span{display:none}
}

/* Theme-swapped artwork. Ship BOTH images/icons and let CSS pick — no JS, no
   flash. Named for the theme they belong to, not for their own colour.
   Identical rule to fluviaweb.com; keep the class names in sync. */
.theme-dark-only{display:none}
:root[data-theme="dark"] .theme-light-only{display:none}
:root[data-theme="dark"] .theme-dark-only{display:inline-flex}

/* The Fluviaweb wordmark. The SAME artwork on every domain and subdomain:
   logo-light.svg (dark artwork) for the light theme, logo-dark.svg for dark.

   NEVER set `display` here. `.site-logo` and `.theme-dark-only` have identical
   specificity, so a `display:block` on this rule beats the theme-swap rules and
   BOTH logos show up at once. fluviaweb.com deliberately sets only width. */
.site-logo{width:auto}
.site-logo--header{height:30px}
.site-logo--footer{height:60px}

/* Vertical lockup for auth / splash / empty screens: brand wordmark on top,
   product name under it in the accent. */
.brand-vertical{display:inline-flex;flex-direction:column;align-items:center;gap:var(--sp-4);text-align:center}
.brand-vertical .site-logo{height:clamp(44px,7vw,64px)}
.brand-vertical--lg .site-logo{height:clamp(64px,10vw,96px)}
.brand-vertical .brand-product{font-size:var(--text-h3)}

/* Product name — the ONLY per-product mark. Accent colour, caps, mono. */
.brand-product{
  font-family:var(--font-mono);font-weight:600;text-transform:uppercase;
  letter-spacing:var(--ls-eyebrow);color:var(--accent);line-height:1;
}
.brand-lockup{display:inline-flex;align-items:center;gap:var(--sp-3);text-decoration:none}

/* Inside a product the lockup stacks — a sidebar column has no room for a row.
   The order does not change: Fluviaweb on top, the product under it, and the
   two stay close in size because this is a lockup, not a hierarchy.
   The product name takes the accent at full strength; see SKILL.md for what
   that costs in the light theme and when to use --accent-display instead. */
.brand-stack{display:inline-flex;flex-direction:column;gap:.35rem;text-decoration:none;color:inherit}
.brand-stack__mark{
  font-family:var(--font-display);font-weight:700;font-size:1.75rem;line-height:1;color:var(--ink);
}
.brand-stack__mark .text-flow{color:var(--accent)}
.brand-stack__product{
  font-family:var(--font-display);font-weight:700;text-transform:uppercase;
  font-size:1.5625rem;line-height:1;letter-spacing:-.02em;color:var(--accent);
}

/* Назві дається та сама висота коробки, що й логотипу, плюс оптична поправка
   в 1px. Геометричного центрування тут не досить: у вордмарку є нижні виносні
   елементи, а в назві — суцільна капітель без них, тому математично рівні
   коробки виглядають зміщеними. 1px — це виміряна на око різниця, а не смак. */
.brand-lockup .brand-product{
  display:inline-flex;align-items:center;min-height:30px;font-size:var(--text-sm);
  position:relative;top:1px;
}

/* Icon button — the theme toggle and its siblings in the header. */
/* 40×40 і скляна поверхня — точно як .header-icon-btn на головному домені.
   Видимої рамки немає: --glass-edge прозорий, форму тримає скло. */
.icon-btn{
  display:inline-flex;align-items:center;justify-content:center;
  width:40px;height:40px;border-radius:var(--r-pill);
  border:1px solid var(--glass-edge);
  background:var(--glass-bg-soft);
  -webkit-backdrop-filter:blur(var(--glass-blur-soft)) saturate(150%);
  backdrop-filter:blur(var(--glass-blur-soft)) saturate(150%);
  box-shadow:inset 0 1px 0 var(--glass-highlight);
  color:var(--ink-700);cursor:pointer;
  transition:transform var(--dur-1) var(--ease-out),color var(--dur-1) var(--ease-out);
}
.icon-btn:hover{transform:translateY(-1px);color:var(--text-strong)}
.icon-btn:focus-visible{outline:none;box-shadow:var(--ring)}
/* Кабінет підсвічується, коли ти всередині нього — інакше з шапки не видно,
   де ти: підписів у цієї іконки немає. */
.icon-btn.is-active{color:var(--accent)}

/* Той самий контрол, але з підписом: вхід в акаунт, «Написати», тощо.
   Живе праворуч у шапці, поряд із перемикачами, а НЕ серед пунктів меню:
   вхід — це дія, а не розділ сайту. */
.icon-btn--labelled{
  width:auto;padding-inline:var(--sp-3);gap:var(--sp-2);
  font-family:var(--font-body);font-size:var(--text-sm);font-weight:600;
  white-space:nowrap;text-decoration:none;
}

/* ==========================================================================
   App shell — sidebar + topbar + content. A minimal, brand-correct scaffold.
   ========================================================================== */
.app{min-height:100vh;display:grid;grid-template-columns:auto 1fr;background:var(--bg-page)}
.app__sidebar{width:248px;display:flex;flex-direction:column;gap:var(--sp-2);padding:var(--sp-4);border-right:1px solid var(--border-soft);background:var(--surface-card)}
.app__sidebar-brand{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-2) var(--sp-2) var(--sp-4)}
.app__sidebar-brand img{height:28px;width:auto}
.nav-group{font-family:var(--font-mono);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.08em;color:var(--text-faint);padding:var(--sp-4) var(--sp-2) var(--sp-1)}
.nav-item{display:flex;align-items:center;gap:var(--sp-3);padding:.55rem .7rem;border-radius:var(--r-sm);color:var(--text-soft);font-size:var(--text-sm);font-weight:500;transition:background var(--dur-1) var(--ease-out),color var(--dur-1) var(--ease-out)}
.nav-item:hover{background:var(--bg-tint);color:var(--text-strong)}
.nav-item.is-active{background:var(--accent-soft);color:var(--accent-hover)}
.nav-item svg{width:18px;height:18px;flex:0 0 auto}
.app__main{display:flex;flex-direction:column;min-width:0}
.app__topbar{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-4) var(--gutter);border-bottom:1px solid var(--border-soft);background:var(--surface-card)}
.app__content{padding:clamp(20px,3vw,40px) var(--gutter);flex:1}
@media (max-width:900px){.app{grid-template-columns:1fr}.app__sidebar{display:none}}

/* ==========================================================================
   Auth screen — 50/50: a picture that says what the product is for, and the
   form. This is the one screen where the product is alone with somebody who
   does not know it yet, so it gets the whole viewport and no page chrome.

   The left half carries the brand: the vertical wordmark down its edge, the
   product name at the top, one line of pitch in the middle, over a darkened
   photograph. The right half carries the form and nothing else.

   Below 1024px the picture is gone entirely — not shrunk. On a phone it would
   push the form below the fold, and the form is the reason the page exists.
   ========================================================================== */
.auth{min-height:100dvh;display:grid;grid-template-columns:1fr}
@media (min-width:1024px){.auth{grid-template-columns:1fr 1fr}}

/* ---- the picture half ---------------------------------------------------- */
/* The image is a product decision, so it comes in as a variable rather than
   being baked here: --auth-image on .auth, set by the product's stylesheet.
   With none set the panel falls back to an accent-tinted wash, which is dull
   but never broken. */
.auth__aside{display:none}
@media (min-width:1024px){
  .auth__aside{
    display:grid;grid-template-rows:auto 1fr auto;gap:var(--sp-6);
    position:relative;isolation:isolate;overflow:hidden;
    /* Left padding leaves the rail its lane; the wordmark sits in it. */
    padding:var(--sp-8) var(--sp-8) var(--sp-8) calc(var(--sp-8) + 9rem);
    background:var(--auth-image, linear-gradient(140deg,var(--ink-950),var(--azul-600))) center/cover no-repeat;
  }
}

/* Darkened, and darkened enough. Light text over an unmodified photograph is
   legible on the photograph you tested and illegible on the next one, so the
   veil is heavy and constant rather than tuned per image. The gradient is
   deeper at the bottom, where the pitch and the tagline sit. */
.auth__aside::after{
  content:'';position:absolute;inset:0;z-index:-1;
  background:linear-gradient(180deg,
    color-mix(in oklab,var(--ink-950) 62%,transparent) 0%,
    color-mix(in oklab,var(--ink-950) 74%,transparent) 100%);
}

/* The wordmark down the left edge of the picture — the same letters as the
   site rail, at picture scale. Fixed light ink: it sits on a photograph, not
   on a theme surface, so it must not follow the theme. */
.auth__rail{
  position:absolute;left:0;top:0;bottom:0;width:9rem;
  display:flex;align-items:center;justify-content:center;
  text-decoration:none;pointer-events:auto;
}
.auth__rail .fit-rail-word{
  /* Larger than the site rail: this page carries no header and no footer, so
     nothing competes with the mark for vertical room. */
  color:#fff;font-size:clamp(3.5rem,16.5vh,10rem);
  text-shadow:0 2px 24px color-mix(in oklab,var(--ink-950) 55%,transparent);
}

/* Product name, top of the picture. Caps, accent, mono — the same mark the
   header uses, just bigger. */
.auth__product{
  font-family:var(--font-mono);font-weight:700;text-transform:uppercase;
  letter-spacing:var(--ls-eyebrow);color:var(--accent);
  font-size:clamp(1.1rem,1.6vw,1.45rem);line-height:1;margin:0;
}

/* The pitch: why this product is worth an account. One sentence, centred in
   the half. Display face, because it is a headline and not body copy. */
.auth__pitch{
  align-self:center;max-width:26ch;margin:0;
  font-family:var(--font-display);font-weight:800;
  font-size:clamp(1.6rem,2.6vw,2.5rem);line-height:1.14;letter-spacing:-.02em;
  color:#fff;text-wrap:balance;
}
.auth__pitch em{font-style:normal;color:var(--accent)}

.auth__note{
  margin:0;color:color-mix(in oklab,#fff 72%,transparent);
  font-size:var(--text-sm);line-height:1.5;max-width:34ch;
}

/* ---- the form half ------------------------------------------------------- */
.auth__main{
  display:grid;place-items:center;
  padding:var(--sp-8) var(--gutter);background:var(--bg-page);
}

/* Обгортка над карткою — і вона обов'язкова. `place-items:center` робить
   колонку розміром за вмістом, тому `width:100%` картки всередині рахується
   від цього вмісту, а не від смуги. Ширина в такій верстці мовчки залежить від
   довжини найдовшого рядка тексту: картка виходить то 400px, то 490px, і
   причину видно тільки в інспекторі. Ширину тримає обгортка. */
.auth__panel{width:100%;max-width:30.625rem}
/* 490px — однакова ширина для входу, реєстрації й відновлення пароля. Одна
   ширина на всі три форми означає, що перехід між ними не смикає екран: картка
   лишається на місці, міняється тільки те, що в ній. */
.auth__card{width:100%;max-width:30.625rem;display:flex;flex-direction:column;gap:var(--sp-5)}

/* The form's own heading. The picture already said the product's name, so this
   says what this particular screen is for. */
.auth__title{margin:0;font-size:var(--text-h2);line-height:1.15;letter-spacing:-.02em}
.auth__lede{margin:var(--sp-2) 0 0;color:var(--text-soft)}

/* The primary action keeps its own shape. A button stretched to the width of
   the card stops looking like a button — it becomes a coloured bar with a word
   in it, and the shape is what makes these read as pressable. Room comes from
   padding, not from width. */
.auth__card .btn-accent{align-self:flex-start;width:fit-content;padding:14px 30px;font-size:1.02rem}

/* Below the rail's breakpoint the product still needs naming — the small
   wordmark + product name sit above the card. */
.auth__brand{display:flex;justify-content:center;margin-bottom:var(--sp-8)}
@media (min-width:1024px){.auth__brand{display:none}}

/* Розділювач «або» між входом через сервіс і формою. Лінія по боках, а не
   рамка навколо тексту: рамка додала б четверту межу на екрані, де їх і так
   три. */
.auth__divider{display:flex;align-items:center;gap:var(--sp-4);color:var(--text-faint);font-size:var(--text-xs)}
.auth__divider::before,.auth__divider::after{content:'';flex:1;height:1px;background:var(--border-soft)}

/* The row under the password field: "remember me" on one side, "forgot?" on
   the other. The recovery link belongs here, at the moment the password
   fails — not at the bottom of the page. */
.auth__row{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4)}

/* The way to the other form. One line, centred, under the card. */
.auth__alt{margin:var(--sp-6) 0 0;text-align:center;color:var(--text-soft);font-size:var(--text-sm)}

/* ==========================================================================
   Очікування — повноекранний лоадер
   ========================================================================== */
/* Дії, що йдуть довше за мить, мають виглядати як дія, а не як завислий
   інтерфейс. Затемнення тут не декорація: воно знімає питання «чи натиснулось»
   і водночас закриває доступ до кнопок, повторний клік на які нічого доброго
   не зробить. */
.loader{
  position:fixed;inset:0;z-index:200;
  display:none;align-items:center;justify-content:center;
  padding:var(--gutter);
  background:color-mix(in srgb, var(--canvas-50) 78%, transparent);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
}
.loader.is-on{display:flex}
/* Прокрутка під затемненням лишала б відчуття, що сторінка жива й нею можна
   користуватися. */
body.is-waiting{overflow:hidden}

.loader__box{
  display:flex;flex-direction:column;align-items:center;gap:var(--sp-4);
  max-width:32rem;text-align:center;
}

.loader__spin{
  width:56px;height:56px;border-radius:50%;
  border:3px solid var(--border-soft);
  border-top-color:var(--accent);
  animation:loader-spin 900ms linear infinite;
}

.loader__title{font-family:var(--font-display);font-size:var(--text-h3);color:var(--text-strong)}
.loader__note{color:var(--text-soft);font-size:var(--text-sm);line-height:var(--lh-body)}
.loader__step{
  font-family:var(--font-mono);font-size:var(--text-xs);text-transform:uppercase;
  letter-spacing:.08em;color:var(--accent);
}
/* Смужка прогресу. Ширина фіксована, а не за вмістом: шкала, що змінює
   довжину разом із текстом поруч, читається як стрибок прогресу. */
.loader__bar{
  width:min(22rem,70vw);height:6px;border-radius:var(--r-pill);
  background:var(--border-soft);overflow:hidden;
}
.loader__bar i{
  display:block;height:100%;width:0;border-radius:inherit;
  background:var(--accent);
  transition:width var(--dur-2) var(--ease-out);
}
.loader__percent{font-size:var(--text-sm);color:var(--text-soft)}

.loader__stats{display:flex;gap:var(--sp-6);font-family:var(--font-mono);color:var(--text-main)}
.loader__stats b{display:block;font-size:var(--text-h3);color:var(--text-strong)}
.loader__stats span{font-size:var(--text-xs);color:var(--text-faint)}

@keyframes loader-spin{to{transform:rotate(360deg)}}

/* Обертання — єдине, що тут рухається, і для тих, кому рух шкодить, воно
   замінюється пульсацією прозорості, а не зникає зовсім: індикатор має
   лишатися індикатором. */
@media (prefers-reduced-motion:reduce){
  .loader__spin{animation:loader-pulse 1.6s ease-in-out infinite}
}
@keyframes loader-pulse{0%,100%{opacity:1}50%{opacity:.35}}

/* ==========================================================================
   Блог: картка в списку і обстановка навколо статті.

   Витягнуто з двох місць, де це вже написано двічі — fluviaweb.com/blog і
   блог PUBLICA, — щоб третій раз його не відновлювали з HTML чужої сторінки.

   Форми тут спільні, палітра — та, що на поверхні: публічний сайт бере ці
   токени, продукт бере свої дашбордні. Дві палітри поруч читаються як дві
   різні компанії, а однакові пропорції в різних палітрах — як один бренд.
   ========================================================================== */

.blog-grid{
  display:grid;gap:var(--sp-5);
  grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));
}

.blog-card{display:flex;height:100%;flex-direction:column;overflow:hidden;transition:transform var(--dur-1) var(--ease-out)}
.blog-card:hover{transform:translateY(-2px)}

/* Фіксована пропорція — не охайність: три обкладинки трьох форм перетворюють
   ряд карток на сходи, а бокс має бути зарезервований до того, як картинка
   приїде, інакше сітка стрибає, поки вони вантажаться. */
.blog-card__cover{width:100%;aspect-ratio:16/9;object-fit:cover}

/* Стаття без обкладинки — не діра в ряду. Плейсхолдер тримає ту саму
   пропорцію й каже, що він плейсхолдер, замість вдавати картинку. */
.blog-card__placeholder{
  position:relative;display:flex;width:100%;aspect-ratio:16/9;
  align-items:center;justify-content:center;
  background:linear-gradient(135deg,var(--accent-soft),var(--bg-tint));
  color:var(--accent);
}

.blog-card__body{display:flex;flex:1;flex-direction:column;gap:var(--sp-2);padding:clamp(18px,3vw,24px)}
.blog-card__meta{display:flex;align-items:center;gap:var(--sp-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-faint)}
.blog-card__category{color:var(--accent)}
.blog-card__title{font-family:var(--font-display);font-size:1.15rem;font-weight:800;letter-spacing:-.02em;line-height:1.3;color:var(--text-strong)}
.blog-card__excerpt{font-size:var(--text-sm);line-height:1.6;color:var(--text-soft)}

/* ---- одна стаття ---- */

/* 3/2 і зарезервовано наперед: обкладинка — найбільше на сторінці й те, що
   браузер малює останнім, тож без пропорції вона зсуває всю статтю вниз,
   поки вантажиться. */
.post-cover{width:100%;aspect-ratio:3/2;object-fit:cover;border-radius:var(--r-lg)}

.post-layout{display:grid;grid-template-columns:1fr;gap:clamp(2rem,5vw,3.5rem)}
.post-main{min-width:0}
.post-sidebar{display:flex;flex-direction:column;gap:clamp(1.5rem,3vw,2.25rem)}

@media (min-width:992px){
  .post-layout{grid-template-columns:minmax(0,1fr) 300px;align-items:start}
  /* Липкий — і це причина, чому сайдбар збоку, а не знизу: читач, який
     дочитав дві тисячі слів, стоїть унизу довгої сторінки, і дорога далі має
     бути там, де вже його око. */
  .post-sidebar{position:sticky;top:92px}
}

.post-widget__title{
  font-family:var(--font-display);font-size:1rem;font-weight:800;letter-spacing:-.01em;
  margin-bottom:.85rem;padding-bottom:.5rem;border-bottom:1px solid var(--border-soft);
  color:var(--text-strong);
}

.post-cat-link{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-2);padding:6px 0;font-size:var(--text-sm);color:var(--text-soft);transition:color var(--dur-1)}
.post-cat-link:hover{color:var(--accent)}
.post-cat-link__count{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-faint)}

.post-teaser{display:flex;align-items:center;gap:var(--sp-3)}
.post-teaser__thumb{width:64px;height:48px;flex-shrink:0;border-radius:var(--r-sm);object-fit:cover}
.post-teaser__thumb--empty{display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--accent-soft),var(--bg-tint));color:var(--accent)}
.post-teaser__title{font-size:var(--text-sm);font-weight:600;line-height:1.35;color:var(--text-strong);transition:color var(--dur-1)}
.post-teaser:hover .post-teaser__title{color:var(--accent)}

/* Розмір каже, наскільки мітка вживана. Три кроки, а не безперервна шкала:
   розмір, порахований на кожен тег, дає записку викрадача, та й різницю в
   один піксель однаково ніхто не прочитає. */
.post-tag{
  display:inline-block;font-family:var(--font-mono);font-size:.72rem;line-height:1;
  padding:7px 11px;border-radius:var(--r-pill);
  background:var(--bg-tint);color:var(--text-soft);
  transition:background var(--dur-1),color var(--dur-1);
}
.post-tag:hover{background:var(--border-soft);color:var(--text-strong)}
.post-tag--w1{font-size:.68rem}
.post-tag--w2{font-size:.8rem}
.post-tag--w3{font-size:.92rem}

.post-more__title{font-family:var(--font-display);font-size:1.05rem;font-weight:700;line-height:1.35;color:var(--text-strong)}
.post-more__excerpt{font-size:var(--text-sm);line-height:1.55;color:var(--text-soft)}
