/* Mivio Admin — "studio console" theme.
   Warm paper workspace · charcoal sidebar · single pine-teal accent · editorial
   type (Bricolage Grotesque / Hanken Grotesk / JetBrains Mono). Styled by domain
   block (tokens · shell · sidebar · topbar · cards · tables · forms · buttons ·
   utilities · alerts · login). No inline styles in templates. */

:root {
  /* Surfaces — warm, paper-like */
  --paper:    #f5f2ec;
  --surface:  #ffffff;
  --line:     #e8e1d4;
  --line-2:   #f0ebe1;
  /* Граница, которая ОБЯЗАНА быть видна (кнопка, контрол): 3:1 к бумаге и к белому. */
  --line-hard:#8d8474;

  /* Ink */
  --ink:      #211e19;
  --ink-soft: #4b463d;
  /* Muted text sits on every surface (paper, white, tinted cards) and is always small:
     dark enough to clear WCAG AA 4.5:1 on the lightest of them. */
  --mut:      #726b5e;

  /* Accent — pine teal */
  --acc:      #0f7a6e;
  --acc-d:    #0b6058;
  --acc-tint: #e4f0ed;
  --acc-ring: rgba(15, 122, 110, .22);

  /* Sidebar — warm charcoal */
  --side:     #1b1a17;
  --side-2:   #2a2822;
  --side-tx:  #b4ac9d;
  --side-mut: #8a8171; /* group headings on the charcoal rail - kept above 4.5:1 on --side */
  --side-line:#312e27;

  /* Feedback */
  --danger:   #c8401f;
  --danger-d: #a8330f;
  --warn:     #b8862f;  /* warm ochre — "waiting / queued" */
  --warn-d:   #866422;  /* darker ochre — economics / spend bars, and text on --warn-tint */
  --warn-tint:#f3ead4;
  --logout:   #f0897b;
  --logout-h: #f6b3a9;
  --ok-bg:    #e2f0e3;
  --ok-fg:    #1f6b3a;
  --err-bg:   #f7e3dd;
  --err-fg:   #a8330f;

  /* The dropdown chevron: a select draws it, and so does a filter pill over its hidden select. */
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23837b6c' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  /* The user's photo slot: a figure, drawn as a mask so it takes the colour of its tile. */
  --person: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='12' cy='8' r='4'/%3E%3Cpath d='M4.5 20.5c.8-4 3.8-6.5 7.5-6.5s6.7 2.5 7.5 6.5'/%3E%3C/svg%3E");

  /* Shape & depth */
  --r:    12px;
  --r-sm: 9px;
  --r-lg: 18px;
  --foot: 3.5rem; /* shared height of the bottom rail: logout + editor actions */
  /* A one-line input (Forms) and its height, for whatever must sit level with it. */
  --control-pad-y: .55rem;
  --control-fs: .92rem;
  --control-lh: 1.35;
  --control-h: calc(2 * var(--control-pad-y) + var(--control-fs) * var(--control-lh) + 2px);
  --sh-sm: 0 1px 2px rgba(48, 38, 22, .05), 0 1px 3px rgba(48, 38, 22, .06);
  --sh-md: 0 6px 18px rgba(48, 38, 22, .07), 0 2px 6px rgba(48, 38, 22, .05);
  --sh-lg: 0 18px 48px rgba(48, 38, 22, .14);

  /* Type */
  --f-disp: "Golos Text", system-ui, sans-serif;
  --f-body: "Golos Text", system-ui, -apple-system, sans-serif;
  --f-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
}

* { box-sizing: border-box; }

html, body { height: 100%; }
/* Phone browsers resize text on their own: a block whose content is wider than the screen gets
   ITS type scaled up while the block next to it keeps the original size, and a list turns into a
   mix of sizes. Every screen here is laid out for the width it gets, so nothing may be rescaled
   behind the layout's back. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  font-family: var(--f-body);
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  overflow: hidden; /* only the sidebar and content panes scroll, not the page */
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

::selection { background: var(--acc-tint); color: var(--acc-d); }

/* Текст только для скринридера: остаётся в дереве доступности, но не занимает места.
   display:none и visibility:hidden не годятся - они убирают узел и оттуда тоже. */
.sr-only,
.character-row + .character-row label,
.filedrop > input[type="file"][hidden] {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Обход навигации: ссылка живёт за верхним краем и выезжает только под фокусом, поэтому
   клавиатура получает вход в контент первым же Tab, а вид страницы не меняется. */
.skip-link {
  position: fixed; top: 0; left: .6rem; z-index: 80;
  transform: translateY(-120%);
  padding: .55rem 1rem; border-radius: 0 0 var(--r-sm) var(--r-sm);
  background: var(--acc); color: #fff; font-size: .88rem; font-weight: 600;
  text-decoration: none; box-shadow: var(--sh-md);
  transition: transform .15s ease;
}
.skip-link:focus { transform: translateY(0); outline: none; box-shadow: 0 0 0 3px var(--acc-ring), var(--sh-md); }
/* Контейнер контента получает фокус программно (tabindex=-1) — рамка ему не нужна. */
.content:focus { outline: none; }

/* --- App shell (fixed sidebar + scrollable content) --------------------- */
.shell { display: grid; grid-template-columns: 232px 1fr; height: 100vh; height: 100dvh; }

aside {
  background: var(--side); color: var(--side-tx); padding: 1.1rem 0 0;
  height: 100vh; height: 100dvh; overscroll-behavior: contain;
  display: flex; flex-direction: column;
  border-right: 1px solid #000;
}
/* Прокручивается СПИСОК РАЗДЕЛОВ, а не панель целиком: раньше уезжало всё вместе, и на
   коротком экране «Выйти» вместе с именем оператора оставались за нижним краем, а
   последний раздел обрывался на полуслове - панель выглядела сломанной. */
aside > nav {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  /* Затухание у нижней кромки: без него раздел, срезанный границей прокрутки, читается
     как обрыв вёрстки, а не как «ниже есть ещё». */
  mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
}
aside .me, aside .logout { flex: none; }
aside .brand {
  color: #fff; font-family: var(--f-disp); font-weight: 700; letter-spacing: -.01em;
  padding: .15rem 1.4rem 1.1rem; font-size: 1.15rem;
  display: flex; align-items: center; gap: .55rem;
}
aside .brand-logo {
  width: 30px; height: 30px; border-radius: 50%; object-fit: contain; flex: none;
  /* The mark is a dark coin → give it a light plate so it reads on the charcoal sidebar,
     a teal ring for brand, and a soft drop for depth. */
  background: #fff; padding: 2px;
  box-shadow: 0 0 0 2px var(--acc-ring), 0 1px 3px rgba(0, 0, 0, .4);
}
aside .grp {
  color: var(--side-mut); font-size: .68rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .12em;
  padding: 1.15rem 1.4rem .35rem;
}
aside a {
  position: relative; display: block; color: var(--side-tx); text-decoration: none;
  padding: .5rem 1.4rem; font-size: .9rem; font-weight: 500;
  transition: color .15s ease, background .15s ease;
}
aside a::before {
  content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%) scaleY(0);
  width: 3px; height: 60%; background: var(--acc); border-radius: 0 3px 3px 0;
  transition: transform .18s ease;
}
aside a:hover { color: #fff; background: var(--side-2); }
aside a.active { color: #fff; background: var(--side-2); }
aside a.active::before { transform: translateY(-50%) scaleY(1); }

/* Operator badge: who is signed in + their role, pinned just above the logout rail.
   margin-top:auto shoves it (and the logout below) to the bottom of the sidebar. */
aside .me {
  margin-top: auto; padding: .9rem 1.4rem .6rem;
  color: var(--side-mut); font-size: .74rem; font-weight: 600; line-height: 1.3;
}
aside .me + .logout { margin-top: 0; }

/* Logout: bottom rail of the sidebar, in red — same height as the editor actions
   so the dark bottom band has one continuous top edge across the whole window. */
.logout { margin-top: auto; border-top: 1px solid var(--side-line); min-height: var(--foot); display: flex; }
.logout-btn {
  display: flex; align-items: center; width: 100%;
  background: none; border: 0; cursor: pointer; font-family: var(--f-body);
  color: var(--logout); font-size: .9rem; font-weight: 600; padding: 0 1.4rem;
  transition: color .15s ease, background .15s ease;
}
.logout-btn:hover { background: var(--side-2); color: var(--logout-h); }

.main { display: flex; flex-direction: column; min-width: 0; height: 100vh; height: 100dvh; }

/* --- Topbar ------------------------------------------------------------- */
.topbar {
  background: rgba(245, 242, 236, .82); backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line); padding: .7rem 2rem;
  display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;
  font-size: .85rem; color: var(--mut); font-weight: 500;
}
.topbar form { margin: 0; }
.topbar a, .topbar .linkbtn {
  color: var(--ink-soft); text-decoration: none; font-weight: 600;
  background: none; border: 0; cursor: pointer; font-family: inherit; font-size: inherit;
  transition: color .15s ease;
}
/* Поля по вертикали поднимают цель крошки с 16px до 24px; на высоту шапки они не влияют -
   это строчный элемент, высоту строки задаёт только его содержимое. Кнопка «Назад» рядом
   уже крупная и своих отступов не отдаёт, поэтому правило смотрит только внутрь крошки. */
.topbar .cluster > span a { padding-block: .25rem; }
.topbar a:hover, .topbar .linkbtn:hover { color: var(--acc); }
.backbtn {
  display: inline-flex; align-items: center; gap: .3rem; flex: none;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink-soft); font-family: inherit; font-size: .8rem; font-weight: 600;
  padding: .35rem .7rem; cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.backbtn:hover { background: var(--paper); border-color: #d8cfbe; color: var(--acc); }

/* Mobile nav: hamburger in the topbar + dimming scrim behind the drawer.
   Both are desktop-hidden; the off-canvas behaviour lives in the media query
   at the bottom of the file. The hidden checkbox (#nav-toggle) drives it with
   no JS — navigating to a link reloads the page, which resets it (drawer closes). */
.nav-burger, .nav-scrim { display: none; }
.nav-burger {
  /* margin: 0 - бургер это <label>, а глобальное правило вешает подписям отступ снизу:
     кнопка вставала на 3.7px выше центра шапки. Тот же перекос ловят любые label-контролы. */
  align-items: center; justify-content: center; flex: none; margin: 0;
  width: 38px; height: 38px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink-soft); cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.nav-burger:hover { background: var(--paper); border-color: #d8cfbe; color: var(--acc); }
.nav-burger svg { width: 20px; height: 20px; }

.content {
  /* Сверху меньше, чем по бокам: шапка уже отделяет контент линией, и одинаковый отступ
     со всех сторон читался как провал между тонкой полосой и первым блоком. */
  padding: 1.25rem 2rem 2rem; flex: 1; overflow-y: auto; overscroll-behavior: contain;
  scrollbar-gutter: stable; /* always reserve the scrollbar so pages don't shift */
  animation: fade .18s ease both;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

/* --- Cards & panels ----------------------------------------------------- */
.card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 1.4rem 1.6rem; box-shadow: var(--sh-sm);
}
/* Table cards scroll horizontally (instead of clipping) when a wide list table is wider than
   the content pane — paired with the nowrap below so columns stay readable, not crammed.
   The scroll has to ANNOUNCE itself: macOS hides overlay scrollbars until something moves, so a
   table with half its columns off-card (the audit log hides ~500px even at 1280) reads as one
   that simply ends there. Two shadows sit under the edges and are covered by an opaque patch
   that scrolls WITH the content (background-attachment: local), so each shadow shows up exactly
   while there is more table on that side. */
.card.flush {
  padding: 0; overflow-x: auto; overflow-y: hidden;
  background-image:
    linear-gradient(to right, var(--surface) 40%, rgba(255, 255, 255, 0)),
    linear-gradient(to left, var(--surface) 40%, rgba(255, 255, 255, 0)),
    radial-gradient(farthest-side at 0 50%, rgba(33, 30, 25, .13), rgba(33, 30, 25, 0)),
    radial-gradient(farthest-side at 100% 50%, rgba(33, 30, 25, .13), rgba(33, 30, 25, 0));
  background-position: 0 0, 100% 0, 0 0, 100% 0;
  background-size: 44px 100%, 44px 100%, 15px 100%, 15px 100%;
  background-repeat: no-repeat;
  background-attachment: local, local, scroll, scroll;
}
.card.flush::-webkit-scrollbar { height: 10px; }
.card.flush::-webkit-scrollbar-thumb {
  background: #ddd4c4; border: 3px solid var(--surface); border-radius: 10px;
}
.card.flush::-webkit-scrollbar-thumb:hover { background: #ccc1ad; }
.card.narrow { max-width: 680px; }
.card.wide { max-width: 760px; }
.panel { background: var(--acc-tint); border-color: #cfe6e0; }

/* Dashboard sections: set each group apart with a divider + extra air, so the page reads as
   distinct blocks (Генерации / Нагрузка / Экономика / Пул) instead of one flat even-spaced stack. */
.dash-sec + .dash-sec { margin-top: 2.4rem; padding-top: 2.4rem; border-top: 1px solid var(--line); }

/* Заголовок страницы и заголовок секции выглядят одинаково, но в разметке это разные
   уровни: h1 на странице один, секции под ним - h2 с тем же видом. Класс, а не тег:
   общее правило на h2 перекрасило бы и подзаголовки внутри карточек (очередь модерации). */
h1, .sec-title {
  font-family: var(--f-disp); font-size: 1.5rem; font-weight: 600;
  letter-spacing: -.02em; margin: 0 0 1.2rem; color: var(--ink);
}
h1 .hint, .sec-title .hint { font-family: var(--f-body); font-weight: 500; }

/* --- Tables ------------------------------------------------------------- */
table { width: 100%; border-collapse: collapse; font-size: .9rem; font-variant-numeric: tabular-nums; }
th, td { text-align: left; padding: .7rem .85rem; border-bottom: 1px solid var(--line-2); }
thead th {
  color: var(--mut); font-weight: 600; font-size: .7rem;
  text-transform: uppercase; letter-spacing: .07em;
  background: #fbfaf7; position: sticky; top: 0;
}
tbody tr { transition: background .12s ease; }
/* Both cell kinds: a key/value detail row leads with <th>, so dropping the rule on <td> alone
   left the last row with half a divider — a line under the label and nothing under the value. */
tbody tr:last-child > * { border-bottom: 0; }
td.right, th.right { text-align: right; }
/* Ссылка в ячейке - одна строка текста, то есть цель в 17px. Вертикальные поля у строчного
   элемента в высоту строки не входят, так что цель растёт, а таблица остаётся прежней. */
td a, .rec a:not(.rec-open) { color: var(--acc); font-weight: 600; text-decoration: none; padding-block: .25rem; }
td a:hover, .rec a:not(.rec-open):hover { color: var(--acc-d); text-decoration: underline; }
/* Vertical key/value tables (detail / user pages) */
.card.narrow > table th { width: 38%; color: var(--mut); font-weight: 600; }

code {
  font-family: var(--f-mono); font-size: .82em;
  background: var(--line-2); color: var(--ink-soft);
  padding: .12em .42em; border-radius: 6px;
}

/* --- Forms -------------------------------------------------------------- */
.field { margin-bottom: 1.05rem; }
label {
  display: block; font-size: .82rem; font-weight: 600; margin-bottom: .4rem;
  color: var(--ink-soft); letter-spacing: -.005em;
}
label small { color: var(--mut); font-weight: 400; }
input, select, textarea {
  /* line-height задан явно: без него высота поля зависит от метрик шрифта, и моноширинный
     инпут (slug) вставал на 2px выше соседей в той же строке таблицы. С ним же поле и кнопка
     рядом совпадают по высоте. */
  width: 100%; padding: var(--control-pad-y) .7rem; line-height: var(--control-lh);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font-size: var(--control-fs); font-family: inherit; color: var(--ink);
  background: var(--surface); transition: border-color .15s ease, box-shadow .15s ease;
}
textarea { line-height: 1.5; }
/* Плейсхолдер у поля поиска работает подписью - другой у него нет, - поэтому он держит
   ту же планку, что и остальной приглушённый текст (4.72:1 вместо прежних 2.53:1). */
input::placeholder, textarea::placeholder { color: var(--mut); opacity: 1; }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--acc); box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--acc);
}
textarea { resize: vertical; line-height: 1.5; }
input:disabled, select:disabled, textarea:disabled {
  background: var(--paper); color: var(--mut); cursor: not-allowed;
}
select {
  appearance: none; cursor: pointer; padding-right: 2rem;
  background-image: var(--chevron);
  background-repeat: no-repeat; background-position: right .7rem center;
}
input[type="checkbox"], input[type="radio"] {
  width: 1.15rem; height: 1.15rem; padding: 0; accent-color: var(--acc); cursor: pointer;
  vertical-align: -.2rem;
}
.field small.hint { display: block; margin-top: .35rem; }
.field label small.hint { display: inline; margin: 0; } /* keep label hints on the label line */
/* Подпись чекбокса и подсказка к нему делят строку, но подсказка стоит РЯДОМ с подписью,
   а не внутри неё: клик по «?» внутри <label> активирует подпись и переключает сам
   чекбокс, а от знака вопроса этого никто не ждёт. */
.check-line { display: flex; align-items: center; }
.check-line > label { margin-bottom: 0; }

/* Inline help: explanation is hidden, surfaced as a tooltip on a "?" chip
   next to the label. Tooltip is left-anchored to the chip so it never clips
   on the left edge of the form column. */
/* Кружок остаётся мелким - рядом с подписью он и должен читаться как сноска, - а нажимают
   по коробке 24×24 вокруг него. Отрицательные поля по вертикали не дают этой коробке
   растянуть строку подписи: у inline-grid высоту строки задаёт именно margin-box. */
.help-tip {
  display: inline-grid; place-items: center; vertical-align: .1rem;
  width: 24px; height: 24px; flex: none; margin: -5px 0 -5px .05rem;
  cursor: help; position: relative;
}
.help-tip-dot {
  display: grid; place-items: center; width: 15px; height: 15px;
  border-radius: 50%; border: 1px solid var(--line); background: var(--surface);
  color: var(--mut); font-size: .68rem; font-weight: 700; line-height: 1;
  /* Разрядка заголовка секции сдвигала бы «?» от центра кружка вправо. */
  font-family: var(--f-body); letter-spacing: normal;
  transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease;
}
.help-tip:hover .help-tip-dot, .help-tip:focus-visible .help-tip-dot {
  border-color: var(--acc); color: var(--acc); background: var(--acc-tint);
}
.help-tip:focus-visible { outline: none; }
.help-tip:focus-visible .help-tip-dot { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--acc); }
/* display:none в покое - не косметика: абсолютный тултип шириной до 230px входит в область
   прокрутки СВОЕГО предка, и каждая подсказка «?» в узкой колонке добавляла карточке
   редактора горизонтальную полосу прокрутки, хотя видимого контента за краем не было. */
.help-tip::after, .help-tip::before {
  display: none;
  position: absolute; bottom: calc(100% + 2px);
  opacity: 0; visibility: hidden; pointer-events: none; z-index: 50;
  transition: opacity .15s ease, transform .15s ease;
}
.help-tip:hover::after, .help-tip:hover::before,
.help-tip:focus-visible::after, .help-tip:focus-visible::before { display: block; }
.help-tip::after {
  content: attr(data-tip);
  left: 0; transform: translateY(4px);
  /* 300px, а не 230: пояснения к пакетам кредитов и предложениям доходят до 140 символов,
     и в узкой колонке они вставали лесенкой в семь строк. Больше не берём - тултип не должен
     перерастать колонку формы, самая узкая из которых 340px (сетка настроек модели). */
  width: max-content; max-width: 300px; padding: .5rem .65rem;
  background: var(--ink); color: var(--paper);
  font-size: .76rem; font-weight: 400; line-height: 1.45; text-align: left;
  /* Подсказка бывает и у заголовка секции: без сброса она печаталась бы капсом вразрядку. */
  text-transform: none; letter-spacing: normal;
  white-space: normal; border-radius: 8px; box-shadow: var(--sh-md);
}
.help-tip::before {
  content: ""; left: 7px; bottom: calc(100% - 3px); transform: translateY(4px);
  border: 5px solid transparent; border-top-color: var(--ink);
}
.help-tip:hover::after, .help-tip:focus-visible::after,
.help-tip:hover::before, .help-tip:focus-visible::before {
  opacity: 1; visibility: visible; transform: translateY(0);
}
/* A long tip in a narrow column at the screen edge: opened from the chip it runs past the
   window, so it opens from the start of the label line, without the pointer to the chip. */
label:has(> .tip-from-label) { position: relative; }
.help-tip.tip-from-label { position: static; }
.help-tip.tip-from-label::before { content: none; }

/* Spacing between a rendered preview (poster or clip) and the dropzone below it. */
#preview-slot:not(:empty), #video-slot:not(:empty), #ref-video-slot:not(:empty) { display: block; margin-bottom: .6rem; }
/* The «не загружено» line keeps its own small leading when it wraps, not the body's. */
#preview-slot > .hint, #video-slot > .hint, #ref-video-slot > .hint { display: block; line-height: 1.4; }
/* File upload: hidden native input + a clean dashed dropzone label. */
.filedrop {
  display: flex; align-items: center; gap: .7rem;
  padding: .7rem .9rem; border: 1.5px dashed #d8cfbe; border-radius: var(--r-sm);
  background: var(--paper); color: var(--mut); cursor: pointer;
  /* margin задан целиком: дропзона это <label>, и глобальный отступ подписи добавлял ей
     6px снизу - блоки с превью стояли с другим воздухом, чем соседние поля. */
  font-size: .85rem; font-weight: 500; margin: .15rem 0 0;
  transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.filedrop:hover { border-color: var(--acc); background: var(--acc-tint); color: var(--acc-d); }
/* The native input is hidden from sight, not from the keyboard (see .sr-only): with plain
   `hidden` Tab skipped every dropzone and :focus-within below never lit up. */
.filedrop { position: relative; }
.filedrop > input[type="file"][hidden] { display: block; }
.filedrop.is-disabled > input[type="file"] { visibility: hidden; }
.filedrop:focus-within { border-color: var(--acc); box-shadow: 0 0 0 3px var(--acc-ring); }
.filedrop small.hint { display: inline; margin: 0; }
.filedrop-plus {
  display: grid; place-items: center; flex: none; width: 30px; height: 30px;
  border-radius: 8px; background: var(--surface); border: 1px solid var(--line);
  color: var(--acc); font-size: 1.3rem; line-height: 1;
}
/* User header: avatar (custom photo or initials placeholder) + identity. */
.userhead { display: flex; align-items: center; gap: 1.1rem; margin-bottom: 1.3rem; }
.userhead-main { min-width: 0; flex: 1; }
.userhead-main h1 { margin: 0 0 .55rem; word-break: break-word; }
.avatar {
  display: grid; place-items: center; flex: none; overflow: hidden;
  width: 56px; height: 56px; border-radius: 50%;
  color: #fff; font-family: var(--f-disp); font-weight: 700; font-size: 1.2rem;
  user-select: none;
}
.avatar.lg { width: 76px; height: 76px; font-size: 1.7rem; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-c0 { background: #0f7a6e; }
.avatar-c1 { background: #b4632a; }
.avatar-c2 { background: #5566b8; }
.avatar-c3 { background: #2f8a52; }
.avatar-c4 { background: #a8497a; }
.avatar-c5 { background: #b08a1f; }
.avatar-c6 { background: #4a8ab0; }
.avatar-c7 { background: #7a52c0; }

/* User page: profile (wide) + grant (narrow) on top; history full-width below. */
.user-top { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 1.4rem; align-items: start; }
@media (max-width: 900px) { .user-top { grid-template-columns: minmax(0, 1fr); } }

/* Read-only meta line (created / id) under the user edit form. */
.meta-strip {
  display: flex; flex-wrap: wrap; gap: .35rem 1.4rem;
  margin-top: 1.3rem; padding-top: 1.1rem; border-top: 1px solid var(--line-2);
  color: var(--mut); font-size: .82rem;
}

/* Inline "пополнить баланс" form (stacked, fits the narrow grant column). */
.grantbar {
  display: flex; flex-direction: column; gap: .8rem;
  margin-top: 1.2rem; padding-top: 1.2rem; border-top: 1px solid var(--line-2);
}
.grantbar .field { margin: 0; }
.grantbar .btn { align-self: start; }

/* Inline warning inside a meta strip (a frozen author, a blocked account). */
.danger-text { color: var(--danger); font-weight: 600; }

/* What people actually wrote, under the counters on a complaint card. */
.complaint-notes { margin: .9rem 0 0; padding-left: 1.1rem; color: var(--mut); font-size: .9rem; }
.complaint-notes li { margin: .2rem 0; }

/* First element of a card: no separator above it. */
.meta-strip.flush-top { margin-top: 0; padding-top: 0; border-top: 0; }

/* Author payout rates: a row across a full-width card — unlike .grantbar, which is
   stacked because it lives in the narrow side column. */
.ratebar {
  display: flex; flex-wrap: wrap; align-items: end; gap: .8rem 1rem;
  margin-top: 1.2rem; padding-top: 1.2rem; border-top: 1px solid var(--line-2);
}
.ratebar .field { margin: 0; }
.ratebar .rate { width: 9rem; }
.ratebar .grow { flex: 1 1 18rem; }

/* Section heading inside a form card (Основные · Настройки · Промпт). */
.form-section {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .09em;
  color: var(--mut); margin: 1.7rem 0 .95rem; padding-top: 1.3rem;
  border-top: 1px solid var(--line-2);
}
.editor-main > .form-section:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* Standalone cards that open with a section heading (moderation / test blocks): same
   trim as editor-main, otherwise the heading drags its separator spacing to the top. */
.card > .form-section:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* Model parameters in two columns (prompt & references stay full-width). */
/* minmax(0) обязателен: колонка `1fr` не уже своего контента, и длинная подпись параметра
   распирала карточку редактора - в узком окне у страницы появлялась горизонтальная прокрутка. */
.param-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.05rem 1.2rem; margin-bottom: 1.05rem; }
.param-grid .field { margin-bottom: 0; min-width: 0; }
.param-grid .field > label { overflow-wrap: anywhere; }
.param-grid select, .param-grid input { max-width: 100%; }
/* A setting the model disregards under the current value of another one (param-ignored.js). */
.param-grid .field.is-ignored > label,
.param-grid .field.is-ignored > select,
.param-grid .field.is-ignored > input { opacity: .45; }
.param-grid .param-ignored[hidden] { display: none; }
/* A switch sits level with the control of its neighbour, not with its caption. */
/* A grid, not the flex row of .check-line: a long caption wraps in its own column beside the
   box, and only the «не действует» note takes a line of its own. */
.param-grid .check-line {
  display: grid; grid-template-columns: auto minmax(0, max-content) auto;
  justify-content: start; align-content: center; align-self: end; min-height: var(--control-h);
}
.param-grid .check-line > .param-ignored,
.param-grid .check-line > .param-open { grid-column: 1 / -1; }
.check-line > input + label { margin-left: .5rem; }
.check-line > input + label + .help-tip { margin-left: .2rem; }
@media (max-width: 680px) { .param-grid { grid-template-columns: minmax(0, 1fr); } }

/* "Порядок": level select + always-visible custom number input, side by side. */
.order-field { display: flex; gap: .6rem; align-items: center; }
.order-field .order-level { flex: 2; }
.order-field .order-custom { flex: 1; }

/* --- Effect editor: adaptive two columns + sticky action rail ----------- */
.editor { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 1.4rem; align-items: start; }
/* Both columns are normal flow and scroll together — the side column is not pinned. */
.editor-side { display: flex; flex-direction: column; gap: 1.2rem; }
.editor-side .panel { display: flex; flex-direction: column; gap: .15rem; }
/* Footer rail: a flex child of .main, sibling of the scroll container (.content),
   so it stays pinned to the bottom of the workspace and never scrolls or overlaps. */
.editor-actions {
  flex-shrink: 0; min-height: var(--foot);
  display: flex; gap: .6rem; align-items: center;
  padding: 0 2rem;
  background: var(--side);
}
.editor-actions .spacer { margin-left: auto; } /* push destructive (delete) to the right */
.card .field:last-child { margin-bottom: 0; }
@media (max-width: 1040px) {
  .editor { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 680px) {
  /* Stacked, and stretched: a row that keeps `align-items: flex-end` (the marketplace form)
     would size each field to its own content and pin them to the right edge once the axis
     turns vertical. */
  .row { flex-direction: column; gap: .9rem; align-items: stretch; }
  /* The gap sets the rhythm here; leaving the field margins on top of it spaces fields
     unevenly, because the last field of each row has none (`.card .field:last-child`). */
  .row > .field { margin-bottom: 0; }
  .row:not(:last-child) { margin-bottom: 1.05rem; }
}

/* --- Buttons ------------------------------------------------------------ */
.btn {
  /* .6rem, а не .55rem: с полем рядом (padding .6rem) кнопка была на 1px ниже - в каждой
     строке фильтров и в каждой форме сразу. */
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .6rem 1.1rem; border: 1px solid transparent; border-radius: var(--r-sm);
  background: var(--acc); color: #fff; font-weight: 600; cursor: pointer;
  text-decoration: none; font-size: .88rem; font-family: inherit; line-height: 1.3;
  box-shadow: var(--sh-sm); transition: background .15s ease, transform .12s ease, box-shadow .15s ease;
}
.btn:hover { background: var(--acc-d); transform: translateY(-1px); box-shadow: var(--sh-md); }
.btn:active { transform: translateY(0); box-shadow: var(--sh-sm); }
/* Двойное кольцо: внутреннее совпадает с поверхностью, внешнее - акцент. Одно полупрозрачное
   кольцо тонуло и на папере, и на белом (1.34:1 при норме 3:1), а фокус должен быть виден на
   любой подложке, включая тёмную панель действий. */
.btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--acc);
}
/* Вторичная кнопка раньше заливалась цветом поверхности: на белой карточке её не было видно
   в покое, а на папере она ИСЧЕЗАЛА при наведении - hover-заливка совпадала с фоном страницы
   пиксель в пиксель. Теперь заливка и граница не повторяют ни одну из поверхностей, а наведение
   уводит кнопку в акцент, а не в фон. */
.btn.sec {
  /* Светлая кнопка на светлом фоне различима только своей границей, поэтому граница держит
     3:1 к обеим поверхностям (3.31:1 на папере, 3.69:1 на белом) - прежние #d8cfbe давали 2. */
  background: var(--surface); color: var(--ink-soft); border-color: var(--line-hard);
  box-shadow: none;
}
.btn.sec:hover {
  background: var(--acc-tint); border-color: var(--acc); color: var(--acc-d);
  box-shadow: var(--sh-sm);
}
.btn.sec:focus-visible { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--acc); }
.btn.danger { background: var(--danger); }
.btn.danger:hover { background: var(--danger-d); }
/* На тёмной панели действий «темнее на наведение» работает против себя: кнопка сливалась с
   панелью (3.34:1 → 2.34:1). Там наведение осветляет: 4.49:1 и 4.37:1. */
.editor-actions .btn:hover { background: #149184; }
.editor-actions .btn.danger:hover { background: #d85536; }
.editor-actions .btn.sec:hover { background: var(--acc-tint); }

/* List rows open the record on click (whole row, see row-link.js). The trailing
   chevron is the keyboard/screen-reader anchor + the "open" affordance — quiet by
   default, accented when the row is hovered. Replaces the old pencil edit button. */
.list-table tbody tr[data-href] { cursor: pointer; }
/* Keep cells on one line so a many-column table (effects: the long model id) grows to its
   content width and scrolls horizontally inside .card.flush, rather than cramming/wrapping its
   columns when the pane is narrow. The phone card view (responsive block below) restores wrap. */
.list-table th, .list-table td { white-space: nowrap; }
/* Заголовку одна строка не нужна: он из двух-трёх слов и читается в две. Запрет переноса
   стоил ширины ровно там, где под ним стоит галочка: «Только по предложению» занимало 207px
   при содержимом в один символ и уносило за край три последние колонки пакетов кредитов.
   Переносим только по пробелам - слово целиком не рвём. */
.list-table thead th { white-space: normal; overflow-wrap: normal; }
/* ...but ONE long value must not set the width of its column. Unbounded, a 42-character slug or
   a vendor model id stretched the effects table to 1489px inside a 968px pane: every other column
   was pushed apart to match, and half the table sat behind a horizontal scroll at ordinary laptop
   widths. Capped and ellipsized, the same table is 1164px and reads as a table again. The chevron
   and the thumbnail are exempt: the first would clip its focus ring, the second is 40px anyway. */
.list-table td { max-width: 12rem; overflow: hidden; text-overflow: ellipsis; }
/* Идентификатор модели - самая длинная строка в списке, и целиком он всё равно не читается
   (различает модели хвост, который виден на карточке). Своя, более жёсткая граница вместо
   общих 12rem: столько ширины таблица отдавала колонке, которую сама же и обрезала. */
.list-table td:has(code) { max-width: 8rem; }
/* The chevron would clip its own focus ring, and the thumbnail is 40px wide to begin with. */
.list-table td.row-go, .list-table td.thumb-cell { max-width: none; overflow: visible; }
/* Frozen edges and the cell-painted hover belong to the TABLE layout only. Below 761px a row is a
   card (responsive block at the end), and these rules, left on, painted the card's heading and
   corner their own hover rectangles over the card's - instantly, while the card faded in. */
@media (min-width: 761px) {
  /* Frozen to the right edge, the way the record's name is frozen to the left. A wide table
     (effects, payment products) pushed exactly two things out of sight: the «Активен» flag -
     the one column an operator scans for - and the chevron, which is how the row is opened
     from the keyboard. Scrolling sideways to reach the open control is not an option. */
  .list-table th.pin-right, .list-table td.pin-right { position: sticky; right: 44px; z-index: 1; }
  .list-table th.row-go, .list-table td.row-go { position: sticky; right: 0; z-index: 1; }
  /* The column that names the record stays put while the rest of a wide table scrolls under it.
     A nine-column list hides its right half at ordinary laptop widths, and a number scrolled into
     view belongs to a row the operator can no longer name - the anchor is what makes the sideways
     scroll usable instead of merely possible. No divider marks it off: this table draws horizontal
     rules only, and a vertical one on a list that needs no scrolling at all reads as a stray line. */
  .list-table th:first-child, .list-table td:first-child { position: sticky; left: 0; z-index: 1; }
  /* Frozen cells carry the card's own background, so the rest slides under them - and so the row
     hover is painted on the cells rather than the row, or a frozen cell would stand out of it. */
  .list-table td:first-child, .list-table td.pin-right, .list-table td.row-go { background: var(--surface); }
  .list-table tbody tr[data-href]:hover td { background: #faf8f3; }
}
/* A value that carries a status tag next to it needs the whole width: clipped, the tag reads as
   if the record simply did not have one. */
.list-table td:has(.tag) { max-width: none; }
th.row-go, td.row-go { width: 1%; padding-left: 0; text-align: right; }
td.row-go a {
  display: inline-grid; place-items: center; width: 30px; height: 30px;
  border-radius: 8px; color: var(--mut); text-decoration: none;
  transition: color .15s ease, background .15s ease;
}
td.row-go a svg { width: 17px; height: 17px; }
.list-table tbody tr:hover td.row-go a { color: var(--acc-d); }
td.row-go a:focus-visible {
  outline: none; background: var(--acc-tint); color: var(--acc-d);
  box-shadow: 0 0 0 3px var(--acc-ring);
}
/* A field's caption: under a table row turned card (phone) and in a record card alike. */
.list-table td::before, .rec-fields dt {
  color: var(--mut); font-weight: 600; font-size: .66rem;
  text-transform: uppercase; letter-spacing: .05em;
}

/* --- Record cards (_list.html: record_card) --------------------------------
   Lists whose records are pictures (effects, generations) are cards on every width: the name
   across the top, the pictures in 9:16 frames on the left, the fields beside them. The frame
   sets the height and the fields are laid into it, four to a column, one line each, so every
   card of a list is as tall as the next. The same card a table row becomes on a phone. A card
   is at least as wide as its frames (--rec-slots, set by the list) plus two columns of fields;
   narrower than that the fields move under the frames. */
.rec-grid {
  --rec-frame: 10.5rem;
  display: grid; gap: 1rem;
  /* Each frame takes 7rem and each arrow between two of them 1.5rem, on top of two field columns. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, calc(13rem + var(--rec-slots, 1) * 8.5rem)), 1fr));
}
.rec {
  position: relative; padding: .75rem .9rem .9rem;
  border: 1px solid var(--line); border-radius: var(--r); background: var(--surface);
  box-shadow: var(--sh-sm); transition: background .12s ease;
}
.rec:hover { background: #faf8f3; }
.rec-open { position: absolute; inset: 0; border-radius: inherit; }
.rec-open:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--acc-ring); }
/* Above the card's own link: a picture opens the lightbox, a link goes where it points. */
.rec .thumb, .rec a:not(.rec-open) { position: relative; z-index: 1; }
.rec-title {
  margin: 0 0 .75rem; padding: .1rem .1rem .55rem; border-bottom: 1px solid var(--line-2);
  font-size: .9rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rec-title .cell-link { max-width: none; padding: 0; }
.rec-body { display: flex; flex-wrap: wrap; gap: .9rem; }
.rec-media { position: relative; display: flex; align-items: flex-start; gap: .4rem; flex: none; }
.rec-media .thumb { height: var(--rec-frame); }
.rec-media .vid-badge { position: absolute; top: .35rem; left: .35rem; margin: 0; z-index: 2; }
.rec-slot { position: relative; display: flex; flex-direction: column; align-items: center; gap: .3rem; margin: 0; }
.rec-arrow { display: flex; align-items: center; height: var(--rec-frame); color: var(--mut); font-size: 1.05rem; }
.rec-fields {
  flex: 1 1 12rem; min-width: 0; height: var(--rec-frame); margin: 0;
  display: grid; grid-auto-flow: column; grid-template-rows: repeat(4, auto);
  grid-auto-columns: minmax(0, 1fr); align-content: space-between; column-gap: .9rem;
}
.rec-fields > div { min-width: 0; }
.rec-fields dt { margin-bottom: .1rem; }
.rec-fields dd {
  margin: 0; font-size: .88rem; line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --- Utilities ---------------------------------------------------------- */
.hint { color: var(--mut); font-size: .82rem; font-weight: 400; }
/* A caution on a form the panel still lets you save: allowed, but what it produces will not
   be what the operator is expecting. */
.hint.warn { color: var(--warn-d); font-weight: 500; }
.muted { color: var(--mut); }
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.3rem; gap: 1rem; }
.cluster { display: flex; gap: .5rem; align-items: center; }
.actions { display: flex; gap: .6rem; align-items: center; margin-top: 1.4rem; }
.actions .spacer { margin-left: auto; }
.row { display: flex; gap: 1rem; }
.row > * { flex: 1; }
.price { font-family: var(--f-disp); font-size: 1.6rem; font-weight: 700; color: var(--acc-d); letter-spacing: -.02em; }
/* "No price yet" inside the price slot: as display type it renders as a fat green dash that
   reads like a rendering artefact, so the placeholder drops back to ordinary muted text. */
.price .price-none { font-family: var(--f-body); font-size: .92rem; font-weight: 500; color: var(--mut); letter-spacing: 0; }
/* How the price is made up goes on its own line: run on after the large figure, it wrapped
   mid-bracket in the side column and sat a display line-height below. */
.price .price-detail { display: block; margin-top: .25rem; font-family: var(--f-body); line-height: 1.4; letter-spacing: 0; }
/* A tile never crops: whatever does not fill it shows whole, on the paper colour. */
.thumb { height: 40px; border-radius: 8px; vertical-align: middle; object-fit: contain; background: var(--paper); border: 1px solid var(--line); cursor: zoom-in; }
/* Safari draws a video's frames past its border-radius; a clip-path it does honour. */
video.thumb { clip-path: inset(0 round 8px); }
/* The app's frame for an effect preview. Drawn at 9:16 whatever the file is, so a poster of
   another shape shows with bars here before it shows cut in a catalog tile. */
.thumb.frame { aspect-ratio: 9 / 16; width: auto; }
.thumb.md { height: 64px; }
/* A detail card has room the list column never had: the generation's own result is the reason
   the operator opened the record, so it is not a 64px icon there. */
.card.narrow .thumb.md { height: 132px; }
.thumb.lg { height: 96px; display: block; }
.thumb.xl { height: auto; max-height: 260px; max-width: 100%; display: block; }
/* Placeholder for effects without a (valid) preview — keeps the cell sized like a thumb. */
.thumb-empty { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 8px; border: 1px dashed var(--line); color: var(--ink-soft); cursor: default; }
.thumb-cell { position: relative; white-space: nowrap; }
/* The name that sits next to a preview in the same cell: the pair is the row's identity. The
   cap lives on the text, not on the cell - clipping the cell would cut the picture and the
   video badge next to it, and a 42-character slug would otherwise stretch the column again. */
.cell-title {
  display: inline-block; max-width: 11rem; margin-left: .55rem; vertical-align: middle;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vid-badge { margin-left: 6px; font-size: .7rem; color: var(--acc-d); vertical-align: middle; }

/* Fullscreen media preview (lightbox.js) — shared by every screen that shows a .thumb. */
.lightbox {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  padding: 4vmin; background: rgba(10, 12, 16, .84); cursor: zoom-out;
}
.lightbox[hidden] { display: none; }
.lightbox-media {
  max-width: 92vw; max-height: 92vh; border-radius: 10px;
  box-shadow: 0 16px 56px rgba(0, 0, 0, .55); cursor: default;
}
.lightbox-close {
  position: absolute; top: 1rem; right: 1.25rem;
  width: 2.4rem; height: 2.4rem; border-radius: 50%; border: 0;
  background: rgba(255, 255, 255, .14); color: #fff;
  font-size: 1.7rem; line-height: 1; cursor: pointer;
  transition: background .15s ease;
}
.lightbox-close:hover { background: rgba(255, 255, 255, .26); }
/* Кольцо белое: акцентное на почти чёрной подложке не видно, а закрыть с клавиатуры нужно. */
.lightbox-close:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
body.lightbox-open { overflow: hidden; }
/* Превью, до которого достаёт клавиатура (lightbox.js делает его кнопкой), должно
   показывать фокус - иначе Enter нажимают вслепую. */
.thumb[tabindex]:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }

/* Effect editor: test-generation block (admin runs the effect for free, polls the result). */
.test-block { margin-top: 1rem; }
.test-form { display: flex; align-items: stretch; gap: .75rem; flex-wrap: wrap; }
/* One dropzone per photo the effect takes: they share the row and wrap together, so the launch
   button stays put no matter how many photos an effect wants. */
.test-slots { display: flex; flex-wrap: wrap; gap: .6rem; flex: 1 1 260px; min-width: 0; }
.test-drop { flex: 1 1 220px; min-width: 0; margin: 0; }
.test-drop.is-filled { border-style: solid; border-color: var(--acc); background: var(--acc-tint); }
.test-drop-file { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.test-drop-text { display: flex; flex-direction: column; gap: .05rem; min-width: 0; line-height: 1.3; }
.test-drop-title {
  font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.test-drop small.hint { display: block; margin: 0; }
.test-go-btn { flex: 0 0 auto; align-self: stretch; white-space: nowrap; }
#test-error:not(:empty) { margin-top: .9rem; }
.test-state {
  padding: .7rem .85rem; border-radius: var(--r-sm); background: var(--paper);
  animation: fade .2s ease;
}
.test-pending { display: flex; align-items: center; gap: .55rem; color: var(--ink-soft); font-size: .92rem; }
.test-msg { font-size: .9rem; }
.test-msg.ok { display: block; margin-top: .55rem; color: var(--acc-d); font-weight: 600; }
.test-msg.err { color: var(--danger); font-weight: 500; }
/* Provider's own words under the failure code: its own line, quieter than the code itself. */
.test-msg .hint { display: block; margin-top: .2rem; font-weight: 400; }
.test-media {
  max-width: 100%; max-height: 320px; height: auto; display: block;
  border-radius: var(--r-sm); border: 1px solid var(--line);
}
/* Effect editor: test history — one row per run, prompt beside the picture it produced. */
.test-history { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .85rem; }
/* One column for the tiles, so the text of every row starts at the same edge whatever the
   tile's shape. */
.test-run {
  --run-tile: 171px;
  display: grid; grid-template-columns: var(--run-tile) minmax(0, 1fr); gap: .85rem;
  align-items: start;
}
.test-run + .test-run { border-top: 1px solid var(--line); padding-top: .85rem; }
/* The slot holds its size from the first paint and shows the state (queued, failure) under the
   picture, so lazy media never moves the row. A measured result (.sized) fits the column's
   square by its long side; the floor keeps a panorama strip big enough to click. */
.test-run-slot {
  position: relative; justify-self: center; width: 96px; aspect-ratio: 1; overflow: hidden;
  display: flex; align-items: center; justify-content: center; text-align: center;
  border-radius: 8px; border: 1px solid var(--line); background: var(--paper);
}
.test-run-slot.sized {
  width: calc(var(--run-tile) * min(1, var(--result-w) / var(--result-h)));
  aspect-ratio: var(--result-w) / var(--result-h);
  min-width: 54px; min-height: 54px; max-height: var(--run-tile);
}
.test-run-slot.err { border-color: #e8c7bd; }
.test-run-slot.err .test-run-caption { color: var(--err-fg); }
.test-run-caption { padding: .3rem; color: var(--ink-soft); font-size: .72rem; line-height: 1.25; }
.test-run-media {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  border: 0; border-radius: 0; display: block; background: var(--paper);
}
.test-run-body { min-width: 0; }
/* Spend: the run's own provider bill on its row, the effect's total in the block header. */
.test-run-cost { font-variant-numeric: tabular-nums; }
/* A vendor's reply is often raw JSON with URLs in it, one long word that must still break. */
.test-run-detail { margin: 0 0 .3rem; color: var(--err-fg); font-size: .8rem; line-height: 1.35; overflow-wrap: anywhere; }
.test-run-note { margin: 0 0 .35rem; color: var(--ink); font-size: .8rem; line-height: 1.4; }
.test-run-settings {
  margin: .3rem 0 0; color: var(--ink-soft); font-size: .74rem; line-height: 1.4;
  font-family: var(--f-mono); word-break: break-word;
}
.test-run-model { color: var(--ink); font-weight: 600; }
/* An edit on the same timeline as the runs: its slot sits in the tile column like a picture,
   but reads as a label rather than a missing image. */
.edit-slot { border-style: dashed; background: transparent; }
.effect-edit .test-run-caption { text-transform: uppercase; letter-spacing: .06em; font-size: .64rem; font-weight: 600; }
.edit-changes { margin: 0 0 .2rem; font-size: .85rem; }
/* The edit's own diff: short pairs on one line, a rewritten prompt on two — the whole point of
   showing it is comparing the wording, so it is never truncated, only scrolled. */
.edit-diff { list-style: none; margin: .25rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .3rem; }
.edit-diff-row { font-size: .78rem; line-height: 1.4; color: var(--ink-soft); }
.edit-diff-row.stacked { display: flex; flex-direction: column; }
.edit-field { color: var(--ink); font-weight: 600; margin-right: .35rem; }
.edit-side {
  font-family: var(--f-mono); font-size: .74rem; white-space: pre-wrap; word-break: break-word;
  max-height: 5em; overflow-y: auto; border-radius: 6px; padding: .12rem .35rem;
}
.edit-side em { font-style: normal; color: inherit; opacity: .75; margin-right: .25rem; }
/* Old value red, new value green — the same reading as a diff, so which way the change went
   is visible before the text is read. */
.edit-before, .edit-side.was { background: var(--err-bg); color: var(--err-fg); }
.edit-after, .edit-side.became { background: var(--ok-bg); color: var(--ok-fg); }
.edit-before, .edit-after { border-radius: 6px; padding: .05rem .3rem; }
.test-spend { float: right; text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--ink-soft); }
.test-run-caption .spinner { width: 13px; height: 13px; display: block; margin: 0 auto .25rem; }

.test-run-head { color: var(--ink-soft); font-size: .82rem; margin-bottom: .3rem; }
/* Monospace and scrollable: prompts run long, and a page of twelve runs must stay readable
   without collapsing the text an operator came here to compare. */
.test-run-prompt {
  margin: 0; font-family: var(--f-mono);
  font-size: .8rem; line-height: 1.45; white-space: pre-wrap; word-break: break-word;
  max-height: 7.5em; overflow-y: auto;
}
@media (max-width: 640px) {
  .test-run { grid-template-columns: minmax(0, 1fr); }
  .test-run-slot { justify-self: start; }
}
.spinner {
  width: 16px; height: 16px; flex: none; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--acc);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

@media (max-width: 640px) {
  .test-form { flex-direction: column; align-items: stretch; }
  /* In a column the 260px basis of the row turns into height: the dropzones stood that tall. */
  .test-slots { flex-basis: auto; }
  .test-go-btn { width: 100%; }
}

/* --- Dashboard monitoring (throughput chart + KPI tiles) ---------------- */
/* Throughput chart: server-rendered SVG, scales to the card width. */
.chart-card { margin-bottom: 1.1rem; }
.chart-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: .5rem 1rem; }
.chart-title .eyebrow { font-family: var(--f-disp); font-size: 1.02rem; font-weight: 600; letter-spacing: -.01em; color: var(--ink); }
.chart-sub { color: var(--mut); font-size: .76rem; margin-left: .5rem; }
.legend { display: flex; gap: 1.1rem; color: var(--mut); font-size: .74rem; white-space: nowrap; }
.legend span { display: inline-flex; align-items: center; }
.legend .dot { width: 9px; height: 9px; border-radius: 3px; margin-right: .4rem; }
.dot.ok { background: var(--acc); }
.dot.err { background: var(--danger); }
.dot.coin { background: var(--warn); }

/* The SVG scales with its pane, so an unbounded chart swings from a 59px sliver on a phone to
   a 291px wall at 1920. Cap the drawing and centre it: the plot keeps one size across desktop
   widths. */
.chart-plot { position: relative; max-width: 900px; height: 168px; margin: 1rem auto 0; }
.chart { width: 100%; height: 100%; display: block; overflow: visible; }
.chart .grid { stroke: var(--line-2); stroke-width: 1; }
.chart .axis { stroke: var(--line); stroke-width: 1; }
/* Axis labels are HTML laid over the drawing, NOT text inside it. Text in the SVG is scaled by
   the viewBox together with the bars, so one and the same label rendered ~4px on a phone and
   ~21px on a tablet, and every width needed its own correction to stay legible. Here the label
   is ordinary text at an ordinary size, and only its POSITION follows the drawing (percentages
   of the same box the bars are placed in). Nothing here may take the mouse: the bars underneath
   own the hover. */
.chart-labels { position: absolute; inset: 0; pointer-events: none; }
.chart-labels .ytick,
.chart-labels .tick {
  position: absolute; color: var(--mut); font-family: var(--f-mono); font-size: .68rem;
  line-height: 1; white-space: nowrap;
}
.chart-labels .ytick { left: 0; text-align: right; padding-right: .4rem; transform: translateY(-50%); }
.chart-labels .tick { transform: translate(-50%, .5rem); }
.chart-empty {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--mut); font-size: .8rem;
}
.chart #gOk .s0 { stop-color: var(--acc); }
.chart #gOk .s1 { stop-color: var(--acc-d); }
.chart #gErr .s0 { stop-color: #d2542f; }
.chart #gErr .s1 { stop-color: var(--danger); }
.chart #gCoin .s0 { stop-color: var(--warn); }
.chart #gCoin .s1 { stop-color: var(--warn-d); }
.chart .bar.ok { fill: url(#gOk); }
.chart .bar.err { fill: url(#gErr); }
.chart .bar.coin { fill: url(#gCoin); }
.chart .col { transition: opacity .12s ease; }
.chart:hover .col:not(:hover) { opacity: .45; }
/* One orchestrated reveal: bars grow from the baseline, staggered left→right. */
.chart .col .bar { transform-box: fill-box; transform-origin: bottom; animation: barGrow .55s cubic-bezier(.2,.85,.25,1) both; animation-delay: var(--d, 0ms); }
@keyframes barGrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }

/* Error breakdown — tied to the chart as its footer. */
.chart-foot { display: flex; align-items: center; gap: .9rem; margin-top: 1.05rem; padding-top: .9rem; border-top: 1px solid var(--line-2); flex-wrap: wrap; }
.chart-foot .eyebrow.sm { font-size: .68rem; text-transform: uppercase; letter-spacing: .07em; color: var(--mut); font-weight: 600; }
.err-chips { display: flex; flex-wrap: wrap; gap: .45rem; }
.err-chip { display: inline-flex; align-items: center; gap: .45rem; background: var(--paper); border: 1px solid var(--line); border-radius: 999px; padding: .16rem .3rem .16rem .6rem; }
.err-chip code, .err-chip .what { background: transparent; color: var(--ink-soft); padding: 0; font-size: .74rem; }
.err-chip b { background: var(--err-bg); color: var(--err-fg); font-size: .68rem; font-weight: 700; min-width: 1.15rem; text-align: center; border-radius: 999px; padding: .08rem .35rem; font-variant-numeric: tabular-nums; }
.err-chip .cnt { color: var(--mut); font-size: .68rem; font-variant-numeric: tabular-nums; }
.err-chip.coin b { background: var(--warn-tint); color: var(--warn-d); }

/* KPI tiles: a left accent stripe encodes the metric's domain. */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); gap: .8rem; margin-bottom: 1.4rem; }
/* Pool-load bar on the dashboard saturation table. */
.meterline { display: flex; align-items: center; gap: .5rem; }
.meterline b { font-variant-numeric: tabular-nums; font-size: .8rem; color: var(--mut); min-width: 2.6rem; text-align: right; }
.meter { flex: 1; height: 7px; min-width: 80px; background: var(--warn-tint); border-radius: 999px; overflow: hidden; }
.meter > span { display: block; height: 100%; background: var(--acc); border-radius: 999px; transition: width .4s ease; }
.meter.hot > span { background: var(--danger); }
.stat {
  position: relative; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  padding: .85rem 1rem .9rem 1.15rem; box-shadow: var(--sh-sm);
  display: flex; flex-direction: column; gap: .5rem; min-height: 80px; justify-content: center;
  animation: tileIn .42s ease both;
}
.stat::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--mut); opacity: .3; }
.stat.live::before { background: var(--warn); opacity: 1; }
.stat.live.accent::before, .stat.ok::before { background: var(--acc); opacity: 1; }
.stat.err::before { background: var(--danger); opacity: 1; }
.stat.coin::before { background: var(--warn); opacity: 1; }
.stat-k { display: flex; align-items: center; color: var(--mut); font-size: .69rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
.stat-v { font-family: var(--f-disp); font-size: 1.75rem; font-weight: 600; letter-spacing: -.02em; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
.stat.ok .stat-v, .stat.accent .stat-v { color: var(--acc-d); }
.stat.err .stat-v { color: var(--danger-d); }
.stat.coin .stat-v { color: var(--warn-d); }
.stats .stat:nth-child(1) { animation-delay: .03s; }
.stats .stat:nth-child(2) { animation-delay: .09s; }
.stats .stat:nth-child(3) { animation-delay: .15s; }
.stats .stat:nth-child(4) { animation-delay: .21s; }
@keyframes tileIn { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }
/* Live pulse on the "processing" tile. */
.pulse { display: inline-block; width: 6px; height: 6px; margin-left: .4rem; border-radius: 50%; background: var(--acc); animation: livePulse 1.8s ease-out infinite; }
@keyframes livePulse { 0% { box-shadow: 0 0 0 0 var(--acc-ring); } 70% { box-shadow: 0 0 0 6px rgba(15,122,110,0); } 100% { box-shadow: 0 0 0 0 rgba(15,122,110,0); } }
@media (prefers-reduced-motion: reduce) { .stat, .chart .col .bar, .pulse { animation: none; } }

/* Clickable user reference (generations + ledger), one look everywhere. The cell is
   nowrap, so cap the width and ellipsize — a long email can't stretch the column. */
/* Поля вверх-вниз с равным отрицательным полем: строка текста в ячейке даёт цель в 17px,
   а нажимают по ней мышью и пальцем. Коробка вырастает до 25px, вклад в высоту строки
   остаётся прежним - плотность списка не меняется. */
.user-link { display: inline-block; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; text-decoration: none; padding: .25rem 0; margin: -.25rem 0; }
.user-link code { color: var(--acc-d); transition: background .12s ease; }
.user-link:hover code { background: var(--acc-tint); }
/* Email fallback (no display name): smaller mono, lighter weight — reads as an address,
   not a name, and packs more in. Full value stays on hover (title) / one click away. */
.user-link.email { max-width: 150px; font-family: var(--f-mono); font-size: .78rem; font-weight: 500; }

/* Caption link under a media thumbnail (e.g. generation → its effect's editor). Block so it
   sits below the thumb; truncates long names so the column never widens. */
/* Отступ от миниатюры даёт padding, а не margin: он же поднимает цель с 15px до 24px. */
.cell-link { display: block; padding: 4px 0 5px; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--acc-d); text-decoration: none; }
.cell-link:hover { text-decoration: underline; }

/* Long free text in a column (e.g. why a generation failed): one clipped line, so a column
   that is usually empty can never push the table sideways. The detail card has room, and that
   is where the whole sentence matters - there it wraps instead. Hover shows it either way. */
.cell-note { display: block; max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Хвост заметки (подробности поставщика) в списке свёрнут до многоточия: целиком он всё равно
   не помещался, а обрывок дампа на месте причины читался как сама причина. */
.cell-note .note-tail { display: none; }
.cell-note:has(.note-tail)::after { content: " …"; color: var(--mut); }
.card.narrow .cell-note { max-width: none; white-space: normal; }
.card.narrow .cell-note .note-tail { display: inline; }
.card.narrow .cell-note:has(.note-tail)::after { content: none; }
/* Same deal for the truncating cell widgets: the caps above exist to keep a table column from
   stretching, and a detail card has no columns to protect — an email ellipsized to 150px in a
   680px card just hides the one value the operator opened the record for. */
.card.narrow .user-link,
.card.narrow .user-link.email,
.card.narrow .cell-link { max-width: none; }

/* Credit ledger (economics page): kind tag + signed amount. */
.tx-head { margin-top: 2rem; }
.tag { display: inline-block; padding: .12rem .55rem; border-radius: 999px; font-size: .73rem; font-weight: 600; background: var(--line-2); color: var(--ink-soft); }
/* UGC moderation states (effects list + editor). */
.tag.mod-pending { background: var(--warn-tint); color: var(--warn-d); }
.tag.mod-approved { background: var(--ok-bg); color: var(--ok-fg); }
.tag.mod-rejected { background: var(--err-bg); color: var(--err-fg); }
/* Template visibility (effects editor). Private keeps the neutral base tag: it is the
   resting state of anything a user makes, not a warning. */
.tag.vis-moderation { background: var(--warn-tint); color: var(--warn-d); }
.tag.vis-published { background: var(--ok-bg); color: var(--ok-fg); }
/* Moderation card sits right after the editor form — same breathing room as the
   test block below it. */
.mod-block { margin-top: 1rem; }
.history-block { margin-top: 1rem; }
.tag.mod-pending { background: var(--warn-tint); color: var(--warn-d); }

/* Marketplace settings — a narrow singleton form; inputs align to the bottom so a
   label wrapping to two lines doesn't stagger its row. Fields inside a row drop their
   own bottom margin (`.card .field:last-child` would zero only the last one and skew
   the flex-end baseline by the difference) — the row carries the spacing instead. */
/* Ширина растёт вместе с окном: в 640px строка из четырёх полей давала по 105px на поле,
   и подпись в два слова занимала в ней три строки. Потолок - чтобы длинная форма не
   расползалась по всей ширине монитора. */
.mp-form { max-width: min(880px, 100%); }
.mp-form .row { align-items: flex-end; margin-bottom: 1.05rem; }
.mp-form .row .field { margin-bottom: 0; }
/* Once the row turns into a column, aligning on the "end" of the cross axis lines every field
   up against the right edge at its own content width. Stretch is what a stacked form wants. */
@media (max-width: 680px) { .mp-form .row { align-items: stretch; } }
.mp-face { width: 96px; height: 96px; object-fit: cover; border-radius: 8px; }
.mp-save { margin-top: 1rem; }
/* Moderation form: the note gets a full-width textarea row (rejection reasons are
   rarely short), the action row below holds reason + buttons pushed apart. */
.mod-form { margin-top: .8rem; }
/* Wraps rather than overflows: on a phone the reason select plus two buttons are wider than
   the card, and a takedown button that hangs past its edge cannot be reached at all. */
.mod-actions { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-top: .6rem; }
/* The global select width:100% would swallow the flex row — pin it to content. */
.mod-actions select { flex: 0 1 auto; width: auto; min-width: 220px; }
.mod-spacer { flex: 1; }
.mod-actions .btn { white-space: nowrap; }
/* Moderation queue: one card per waiting submission, media on the left. */
.queue { display: flex; flex-direction: column; gap: 1rem; }
.queue-item { display: grid; grid-template-columns: auto auto 1fr; gap: 1rem; align-items: start; }
.queue-item.overdue { border-color: var(--danger); }
.queue-pick { display: flex; flex-direction: column; align-items: center; gap: .3rem; font-size: .75rem; color: var(--mut); }
.queue-pick input { width: auto; }
.queue-media { display: flex; gap: .6rem; }
.queue-media figure { margin: 0; text-align: center; }
.queue-body h2 { margin: 0 0 .3rem; font-size: 1.05rem; }
.queue-body p { margin: .3rem 0; }
/* Sticky action bar so a long queue never scrolls the buttons out of reach. */
.bulk-bar { position: sticky; bottom: 1rem; display: flex; gap: .8rem; align-items: center; margin-top: 1rem; }
.bulk-bar select { width: auto; min-width: 200px; }
.bulk-bar label { display: flex; align-items: center; gap: .4rem; margin: 0; }
.bulk-bar input[type="checkbox"] { width: auto; }
.bulk-bar .btn { margin-left: auto; }
.bulk-bar .btn + .btn { margin-left: 0; }
@media (max-width: 720px) {
  .queue-item { grid-template-columns: 1fr; }
  .bulk-bar { flex-wrap: wrap; }
  /* Stack the moderation/complaint actions: the spacer that pushes buttons apart on a wide
     card only strands them once the row wraps. */
  .mod-actions select { flex: 1 1 100%; min-width: 0; }
  .mod-actions .btn { flex: 1 1 auto; }
  .mod-spacer { display: none; }
}

.dup-warn { background: var(--warn-tint); color: var(--warn-d); border-radius: var(--r-sm); padding: .5rem .7rem; }
.dup-warn a { color: inherit; font-weight: 600; }
.mod-media { display: flex; gap: 1rem; flex-wrap: wrap; margin: .6rem 0; }
.mod-media figure { margin: 0; text-align: center; }
td.amount { font-weight: 600; font-variant-numeric: tabular-nums; }
td.amount.pos { color: var(--acc-d); }
td.amount.neg { color: var(--danger-d); }

/* Removable media thumbnail (preview / clip / icon / reference) — one look across
   all of them: a red corner badge that clears the value on click. */
/* Запас под угловую кнопку: она вынесена за рамку миниатюры на 7px, и без запаса каждый
   такой чип добавлял 7px к ширине прокрутки - именно из этого складывалась горизонтальная
   полоса внизу редактора. Заодно кнопка перестала наезжать на соседний блок. */
.media-chip { position: relative; display: inline-block; }
.media-chip .rm {
  position: absolute; top: 4px; right: 4px; width: 24px; height: 24px;
  display: grid; place-items: center; border-radius: 50%;
  border: 2px solid var(--surface); background: var(--danger); color: #fff;
  cursor: pointer; font-size: .85rem; line-height: 1; box-shadow: var(--sh-sm);
  transition: background .15s ease, transform .12s ease;
}
.media-chip .rm:hover { background: var(--danger-d); transform: scale(1.1); }
/* Filter bar above a list (_list.html: filterbar), one GET form in a line that wraps: the search
   stretches, each filter is a pill as wide as its content with its name inside, and a filter
   that is set wears the accent, so a narrowed list says so at a glance. The pill draws its name,
   value and chevron itself, and the native select lies over the whole of it, transparent: a tap
   anywhere on the pill opens the system list (keyboard, iOS picker), not only a tap on the value.
   The pill shape is the one the moderation tabs already use. */
/* Сводка disclosure'а нужна только там, где бар схлопывается (узкий экран, filters.js);
   на широком его открытое состояние и есть обычный вид фильтров. */
.filter-fold > summary { display: none; }
.filters { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: 1.2rem; }
.f-search { flex: 1 1 10rem; width: auto; min-width: 0; height: 2.25rem; padding: 0 .95rem; border-radius: 999px; }
.f-pill {
  position: relative; display: inline-flex; align-items: center; gap: .35rem;
  height: 2.25rem; margin: 0; padding: 0 .7rem 0 .75rem;
  border: 1px solid var(--line-hard); border-radius: 999px;
  background: var(--surface); color: var(--mut); font-size: .8rem; font-weight: 600;
  white-space: nowrap; cursor: pointer; transition: border-color .15s ease, background .15s ease;
}
.f-pill:hover { border-color: var(--acc); }
.f-pill:focus-within { border-color: var(--acc); box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--acc); }
.f-pill.on { border-color: var(--acc); background: var(--acc-tint); color: var(--acc-d); }
/* The chosen value, capped: a model id would otherwise make one pill a third of the bar. */
.f-pill-v {
  max-width: 14rem; overflow: hidden; text-overflow: ellipsis;
  padding-right: 1.1rem; background: var(--chevron) no-repeat right center;
  font-size: .85rem; color: var(--ink);
}
.f-pill select {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0;
  border: 0; border-radius: inherit; opacity: 0; appearance: none; cursor: pointer;
}
.f-dates { padding-right: .25rem; }
.f-dates input {
  width: auto; height: 100%; padding: 0 .3rem 0 .15rem; border: 0; border-radius: 0;
  background-color: transparent; font-size: .85rem; font-weight: 600; color: var(--ink);
}
.f-dates input:focus { box-shadow: none; }
.f-pill.on .f-pill-v, .f-pill.on input { color: var(--acc-d); }
.f-pill-sep { color: var(--mut); }
.filters.is-live .f-submit {
  position: absolute; width: 1px; height: 1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.f-reset { padding: 0 .3rem; font-size: .8rem; font-weight: 600; color: var(--mut); text-decoration: none; white-space: nowrap; }
.f-reset:hover { color: var(--acc-d); }

/* Moderation queue lenses (new / revisions / post-moderation). The count lives inside the
   tab so an empty one is visibly empty — otherwise the operator clicks through all of
   them to find the work. */
.tabs { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: 1rem; }
.tabs .tab { display: inline-flex; align-items: baseline; gap: .4rem; padding: .35rem .8rem;
  border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft);
  text-decoration: none; font-size: .85rem; font-weight: 600; }
.tabs .tab:hover { border-color: var(--line-2); color: var(--ink); }
.tabs .tab.on { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.tabs .tab.on .hint { color: inherit; opacity: .7; }
.mt { margin-top: 1.8rem; }
.pager { margin-top: 1.3rem; display: flex; justify-content: center; align-items: center; gap: .9rem; }
.pager a { color: var(--acc); text-decoration: none; font-weight: 600; padding: .3rem; }
.pager a:hover { color: var(--acc-d); }

/* --- Alerts ------------------------------------------------------------- */
.alert {
  padding: .7rem .95rem; border-radius: var(--r-sm); margin-bottom: 1.2rem;
  font-size: .85rem; font-weight: 500; border: 1px solid transparent;
}
.alert.ok { background: var(--ok-bg); color: var(--ok-fg); border-color: #c2dfc8; }
.alert.err { background: var(--err-bg); color: var(--err-fg); border-color: #e8c7bd; }

/* Toast — unified one-shot flash, top-right, auto-dismiss (see toast.js). */
.toast {
  /* Ниже шапки, а не поверх неё: всплывая в самый верх, уведомление накрывало хлебные крошки
     и кнопку «Назад» - ровно те элементы, к которым тянутся сразу после сохранения. */
  position: fixed; top: 4rem; right: 1rem; z-index: 200;
  display: flex; align-items: center; gap: .85rem;
  max-width: min(92vw, 420px); padding: .8rem .9rem .8rem 1rem;
  border: 1px solid transparent; border-radius: var(--r); box-shadow: var(--sh-lg);
  font-size: .88rem; font-weight: 500;
  animation: toastIn .26s cubic-bezier(.2,.8,.2,1) both;
}
.toast.ok { background: var(--ok-bg); color: var(--ok-fg); border-color: #c2dfc8; }
.toast.err { background: var(--err-bg); color: var(--err-fg); border-color: #e8c7bd; }
.toast-msg { flex: 1; }
.toast-x {
  flex: none; background: none; border: 0; color: inherit; cursor: pointer;
  font-size: 1.25rem; line-height: 1; padding: 0 .1rem; opacity: .55;
}
.toast-x:hover { opacity: 1; }
.toast.toast-out { animation: toastOut .2s ease forwards; }
@keyframes toastIn { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateY(-10px); } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }

.filedrop.is-disabled { opacity: .45; pointer-events: none; }
.slots-limit { margin: .4rem 0 0; }

/* One row per character: its name, its two counts and the button that removes it. The rows
   share fixed columns so they line up as a table, the counts stay as narrow as a number needs,
   and the column captions show once, over the first row (the rest stay for screen readers,
   see .sr-only). The layout follows the width
   of the block itself: the editor column is narrow on a wide screen too. */
#characters { container-type: inline-size; margin-bottom: 1.05rem; }
.character-row {
  display: grid; grid-template-columns: 6.5rem 6.5rem 6.5rem 2rem;
  grid-template-areas: "title min max rm"; align-items: end; gap: .75rem;
}
.character-row + .character-row { margin-top: .6rem; }
.character-row > .field { margin: 0; min-width: 0; }
.character-row > .field:nth-of-type(1) { grid-area: min; }
.character-row > .field:nth-of-type(2) { grid-area: max; }
.character-row label { white-space: nowrap; }
/* Title and × stand level with the inputs, not with their captions. */
.character-title {
  grid-area: title; display: flex; align-items: center; height: var(--control-h);
  font-size: .92rem; font-weight: 600; color: var(--ink); white-space: nowrap;
}
.character-row .rm {
  grid-area: rm; align-self: center; display: grid; place-items: center;
  width: 2rem; height: 2rem; padding: 0;
  border: 0; border-radius: 50%; background: none; cursor: pointer; color: var(--ink-soft);
  font-size: 1.15rem; line-height: 1; transition: color .15s ease, background .15s ease;
}
.character-row .rm:hover { color: var(--danger); background: var(--err-bg); }
.character-row .rm:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--acc); }
.character-row .rm[hidden] { display: grid; visibility: hidden; }
#add-character { margin: .75rem 0 0; }
#add-character[hidden] { display: none; }
@container (max-width: 25rem) {
  .character-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 2rem; grid-template-areas: "title title title" "min max rm"; row-gap: .35rem; }
  .character-title { height: auto; }
  .character-row + .character-row { margin-top: 1rem; }
}

/* --- Input slots (chips reuse the .media-chip component above) ------------
   Every picture the model receives, left to right in the order it receives it: the
   user's photo among the effect's references. Each chip carries the marker its prompt
   points at it by, so naming and ordering happen in the same place. */
#slots { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .6rem; }
/* 132px, not the 116 of a plain thumbnail: the caption under a slot names the picture the way
   the app names it («фото пользователя 1»), and at a narrower tile it wrapped — pushing the tag
   of that slot onto a different line from its neighbours'. */
.slot { display: flex; flex-direction: column; gap: .3rem; width: 132px; cursor: pointer; }
/* The reference clip is one more input picture and takes the same tile: at its own 96px height a
   portrait clip came out 54px wide, mostly covered by its × badge. */
.slot .thumb.md, .clip-chip .thumb { width: 132px; height: 132px; border-radius: var(--r-sm); }
.clip-chip, .audio-chip { display: inline-flex; flex-direction: column; gap: .3rem; }
#audio-slots { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .6rem; }
/* Room for the × badge, which sits over the player's top right corner. */
.audio-chip audio { width: 240px; padding-right: 1rem; }
/* The user's photo has no thumbnail to show, so its slot is a tile of the same size. */
.photo-tile {
  display: flex; align-items: center; justify-content: center;
  width: 132px; height: 132px;
  border: 1px dashed var(--line); border-radius: var(--r-sm); background: var(--acc-tint);
}
/* A drawn figure rather than an emoji: an emoji depends on the system font and showed up as an
   empty box where there is none. */
.photo-tile::before {
  content: ""; width: 44px; height: 44px; background: var(--acc);
  -webkit-mask: var(--person) no-repeat center / contain; mask: var(--person) no-repeat center / contain;
}
.slot-kind { text-align: center; font-size: .72rem; line-height: 1.2; color: var(--mut); }
/* An effect's preview in the editor: the poster, or the clip and the loop cut from it, on one
   surface with a caption under each — they are a single upload, and two loose thumbnails read as
   two separate videos the operator has to keep in sync. */
.preview-set {
  display: inline-flex; flex-wrap: wrap; align-items: flex-start; gap: .8rem;
  padding: .7rem .9rem; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--paper);
}
/* Fixed width per piece: the caption is what decides it otherwise. */
.preview-piece { display: flex; flex-direction: column; align-items: center; gap: .3rem; width: 120px; }
/* A poster or a clip with its focus point (focus-picker.js): the picture whole in a 9:16 stage
   with the point on it, and the app's two crops in a column beside it. The editor shows it in
   its narrow side column, so the block is two tiles wide and about one stage high. */
.focus-picker {
  display: inline-grid; grid-template-columns: auto auto; gap: .6rem .8rem; align-items: start;
  padding-right: 2.4rem; /* clear of the × in the corner */
}
.focus-picker .preview-piece { width: auto; }
.focus-stage {
  position: relative; display: block; width: 132px; aspect-ratio: 9 / 16;
  border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--paper);
  cursor: crosshair;
}
.focus-stage:focus-visible { outline: 2px solid var(--acc); outline-offset: -2px; }
.focus-stage img, .focus-stage video { display: block; width: 100%; height: 100%; object-fit: contain; }
/* Two rings, light inside dark, so the point reads on any picture. */
.focus-dot {
  position: absolute; left: 50%; top: 50%; width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid #fff; box-shadow: 0 0 0 2px rgb(0 0 0 / .65), inset 0 0 0 2px rgb(0 0 0 / .65);
  transform: translate(-50%, -50%); pointer-events: none;
}
.focus-picker.unset .focus-dot { border-style: dashed; opacity: .6; }
.focus-crops { display: flex; flex-direction: column; gap: .45rem; }
.focus-crop { display: block; width: 92px; border-radius: 8px; overflow: hidden; }
.focus-crop img.thumb, .focus-crop video.thumb {
  display: block; width: 100%; height: 100%; border-radius: 0; object-fit: cover; clip-path: none;
}
/* The loop list under the tiles spans the block without widening it. */
.focus-picker .loop-side { grid-column: 1 / -1; width: 0; min-width: 100%; }
/* The loops the catalog grid can play for a clip, under its focus picker: rows no taller than a
   line, the stage above plays the one that is ticked. */
.loop-options { display: grid; gap: 1px; min-width: 0; margin: 0; padding: 0; border: 0; }
.loop-options legend {
  padding: 0 0 .4rem .55rem; font-size: .82rem; font-weight: 600; color: var(--ink-soft);
}
.loop-option {
  display: grid; grid-template-columns: auto minmax(0, 1fr) 2.4rem 3.6rem;
  align-items: center; gap: .55rem; padding: .32rem .55rem; border-radius: var(--r-sm);
  font-size: .86rem; font-weight: 500; color: var(--ink); cursor: pointer;
  transition: background .12s ease;
}
.loop-option:hover { background: var(--line-2); }
.loop-option:has(input:checked) { background: var(--acc-tint); font-weight: 600; }
.loop-option:has(input:focus-visible) { box-shadow: 0 0 0 3px var(--acc-ring); }
.loop-option:has(input:disabled) { color: var(--mut); background: none; cursor: default; }
.loop-option input { margin: 0; accent-color: var(--acc); }
/* The block is two tiles wide, so a long name takes a second line instead of an ellipsis. */
.loop-name { line-height: 1.25; }
/* Weight against the heaviest choice: the one thing to compare, so it is drawn, not just told. */
.loop-bar { position: relative; height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; }
.loop-bar::after {
  content: ""; position: absolute; inset: 0; transform-origin: left;
  transform: scaleX(var(--share)); background: var(--line-hard);
}
.loop-option:has(input:checked) .loop-bar::after { background: var(--acc); }
.loop-size {
  grid-column: 4; text-align: right; font-size: .8rem; color: var(--mut);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.loop-option:has(input:checked) .loop-size { color: var(--acc-d); font-weight: 600; }
.loop-missing { grid-column: 3 / span 2; }
.loop-note { margin: .45rem 0 0 .55rem; }
@media (prefers-reduced-motion: reduce) { .loop-option { transition: none; } }
/* Screens where a preview is the thing being judged draw it large enough to see the frame. */
.preview-set .thumb, .queue-media .thumb, .mod-media .thumb { height: 160px; }
/* Prompt editor: a mirror div under a transparent textarea, so «Image 2» can be highlighted
   in text the browser otherwise refuses to style. Both layers MUST share every metric that
   affects wrapping — font, size, line-height, padding, border — or the highlight drifts off
   the words it marks. */
.prompt-editor { position: relative; }
.prompt-editor textarea,
.prompt-mirror {
  font-size: .92rem; font-family: inherit; line-height: 1.5; letter-spacing: normal;
  padding: .58rem .7rem; border: 1px solid transparent; border-radius: var(--r-sm);
  white-space: pre-wrap; overflow-wrap: break-word;
}
.prompt-mirror {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
  color: var(--ink); background: var(--surface); border-color: var(--line);
}
/* The tint is grown sideways by a shadow, NOT by padding: padding widens the mirror's text,
   the textarea above it stays as wide as it was, and from the first «Image 2» onwards the two
   layers drift apart: words double and the line breaks land in different places. Nothing here
   may add width. */
.prompt-mirror mark {
  border-radius: 3px; color: var(--acc-d); background: var(--acc-tint);
  box-shadow: .1rem 0 0 var(--acc-tint), -.1rem 0 0 var(--acc-tint);
}
/* block, not the inline-block a textarea is by default: as an inline box it leaves the baseline
   gap under itself, the mirror stretches to it and its border shows up as a second line under
   the field. */
.prompt-editor textarea {
  display: block; position: relative; background: transparent; color: transparent;
  caret-color: var(--ink); border-color: var(--line);
  /* Where the system draws the scrollbar inside the box it takes width off the text, and only
     this layer loses it. «stable» keeps that loss constant instead of letting it appear the
     moment the prompt outgrows the field; the mirror repeats the same width as padding from JS,
     because CSS reserves a gutter only for a box that may show a scrollbar, never for one with
     «overflow: hidden». */
  scrollbar-gutter: stable;
}
/* The mirror is the only layer that paints text, and that has to hold while the operator selects:
   a browser repaints selected text with a visible colour of its own, and the textarea's own copy
   shows up over the mirror's. */
.prompt-editor textarea::selection { background: var(--acc-ring); color: transparent; }

/* The vendor's prompting advice under the field: set off from the form as reading matter, quieter
   than the inputs around it, so it never passes for one more setting to fill in. */
.prompt-guide {
  margin-top: .7rem; padding: .6rem .85rem;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--paper);
  font-size: .85rem; line-height: 1.45; color: var(--ink-soft);
}
.prompt-guide summary { cursor: pointer; font-weight: 600; color: var(--ink); }
.prompt-guide ul { margin: .5rem 0 .55rem; padding-left: 1.15rem; }
.prompt-guide li + li { margin-top: .3rem; }
.prompt-guide a { color: var(--acc-d); }

/* The number sits under every slot — it is the only thing the prompt can address. */
.slot-no {
  display: block; text-align: center; font-family: var(--f-mono); font-size: .78rem;
  font-weight: 600; color: var(--acc);
}
/* A model whose prompt points at no picture (Veo) leaves the badge empty — drop it entirely so
   the tile doesn't carry the gap of a caption that isn't there. */
.slot-no:empty { display: none; }
/* The reference clip's tags: each one types itself into the prompt on click. */
.clip-tag { cursor: pointer; }

/* --- Studio: the task-form builder -------------------------------------- */
/* One question per block, every control under its own caption: a content manager fills this in,
   and a row of bare inputs held together by placeholders read as a puzzle. The layout follows
   the width of the builder itself, not the window: the editor column is narrow on a wide screen. */
.form-rows { container-type: inline-size; }
.form-row {
  position: relative;
  display: grid; gap: .6rem .7rem; align-items: end;
  margin-bottom: .8rem; padding: .85rem .9rem;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--paper);
  /* The minimums fit each caption with its «?» on one line; a wrapped caption pushes its field
     out of line with the neighbours. */
  grid-template-columns: minmax(0, 1fr) minmax(9.5rem, 12rem) minmax(9.5rem, 10rem);
  grid-template-areas:
    "label type required"
    "help help key"
    "choices choices choices";
}
@container (max-width: 30rem) {
  .form-row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "label label" "type required" "help help" "key key" "choices choices";
  }
}
@container (max-width: 22rem) {
  .form-row {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "label" "type" "required" "help" "key" "choices";
  }
}
.form-row .q-label { grid-area: label; }
.form-row .q-type { grid-area: type; }
.form-row .q-required { grid-area: required; }
.form-row .q-help { grid-area: help; }
.form-row .q-key { grid-area: key; }
.form-row .q-choices { grid-area: choices; }
.form-row .q-choices[hidden] { display: none; }
.form-row label { font-size: .76rem; margin-bottom: .3rem; white-space: nowrap; }
.form-row .row-key { font-family: var(--f-mono); }
/* In the corner, on the caption line: as a grid column it cut the first row short of the rest. */
.form-row .rm {
  position: absolute; top: .3rem; right: .45rem;
  border: 0; background: none; cursor: pointer; color: var(--ink-soft);
  font-size: 1.1rem; line-height: 1; padding: .25rem .4rem;
}
.form-row .rm:hover { color: var(--danger); }
/* The form's keys under the prompt: a click drops «{key}» at the caret, a filled one is
   already in the text. */
.prompt-slots { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .55rem; }
.slot-chip {
  padding: .25rem .65rem; border: 1px dashed var(--line); border-radius: 999px;
  background: var(--surface); color: var(--ink-soft); cursor: pointer;
  font-family: var(--f-mono); font-size: .8rem; line-height: 1.25;
}
.slot-chip:hover { border-color: var(--acc); color: var(--acc-d); }
.slot-chip.used { border-style: solid; border-color: var(--acc); color: var(--acc-d); }
.slot-chip:focus-visible { outline: 2px solid var(--acc-ring); outline-offset: 2px; }
/* The box that opens a setting to the user sits under the value it opens, quieter than the
   field's own label: the default is the decision, this is the permission. */
/* Раскладку задаёт .check-line (подсказка рядом с подписью, а не в ней) - здесь только вид. */
.param-open { margin-top: .35rem; font-size: .82rem; color: var(--ink-soft); }
.param-open input { margin-right: .35rem; }
.param-open > label { font-size: inherit; color: inherit; font-weight: 400; }

/* --- Tag picker (effect editor) ----------------------------------------- */
/* Chips, not a text field: the vocabulary is the offer, and what isn't offered isn't a tag.
   The box itself scrolls rather than pushing the rest of the editor down — a catalog with
   sixty words would otherwise own the whole column. */
.tag-picker .tag-count { margin-left: .4rem; white-space: nowrap; }
.tag-chips {
  display: flex; flex-wrap: wrap; gap: .4rem;
  max-height: 11rem; overflow-y: auto;
  padding: .55rem; border: 1px solid var(--line); border-radius: 10px; background: var(--paper);
}
/* A chip IS the checkbox: the native box next to a pill made every tag twice as tall as its
   own word and left the two vertically out of step. The input stays in the DOM (the form
   posts it, the keyboard reaches it), it just isn't what the operator looks at. */
.tag-chips .chip {
  /* Positioned so the hidden input stays INSIDE the chip: left static, the absolutely
     positioned box lands wherever the nearest positioned ancestor is, and clicking the
     label scrolls the page to that spot instead of to the tag. Margin zeroed: a chip is a
     <label>, and the global label rule hung 6px of empty space under the whole row. */
  position: relative; margin: 0;
  display: inline-flex; align-items: center; cursor: pointer;
  padding: .3rem .75rem; border: 1px solid var(--line); border-radius: 999px;
  font-size: .82rem; line-height: 1.25; background: var(--surface); color: var(--ink-soft);
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.tag-chips .chip input {
  position: absolute; width: 1px; height: 1px; margin: 0; padding: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.tag-chips .chip:hover { border-color: var(--acc); color: var(--acc-d); }
.tag-chips .chip.on, .tag-chips .chip:has(input:checked) {
  border-color: var(--acc); background: var(--acc); color: #fff; font-weight: 600;
}
/* Keyboard focus has to stay visible once the box itself is gone. */
.tag-chips .chip:focus-within { outline: 2px solid var(--acc-ring); outline-offset: 2px; }
/* Already worn, but no longer in the vocabulary — kept, and visibly not one of the offers. */
.tag-chips .chip.stray { border-style: dashed; }
/* The ceiling is reached: still readable, just not addable until something is dropped. */
.tag-chips .chip.off-limit { opacity: .45; cursor: not-allowed; }
/* «Завести тег» is a side door, not the main control: one line, and its explanation sits
   under it in the small type rather than stretching the block. */
.tag-add { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; margin-top: .5rem; }
.tag-add input { flex: 1 1 10rem; min-width: 0; }
.tag-add .hint { flex: 1 1 100%; margin: 0; }

/* --- Tag vocabulary: the whole dictionary, edited in place --------------- */
/* A tag is one word and four settings, so a page per tag would cost more clicks than the
   edit itself: the cells hold live inputs and the operator fixes a name where they read it.
   The forms sit outside the table (a <form> cannot be a child of <tr>) and the controls
   reach them by the `form` attribute — hence no extra layout here, only sizing. */
/* The list-table ellipsis cap (12rem) is meant for long text; here every cell holds a
   control, and clipping one hides the button instead of shortening a word. */
.tags-table td { max-width: none; overflow: visible; }
.tags-table input:not([type="checkbox"]) { width: 100%; min-width: 6rem; }
.tags-table input[name="slug"] { font-family: var(--f-mono); }
.tags-table input[name="sort_order"] { width: 4.5rem; min-width: 0; }
/* Цель нажатия вокруг чекбокса: сам он 18px и стоит вплотную к соседним полям ввода. */
.check-hit { display: inline-grid; place-items: center; width: 28px; height: 28px; margin: 0; }
.tags-table .tag-actions { white-space: nowrap; }
.tags-table .tag-actions .btn + .btn { margin-left: .4rem; }
.tag-new { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .6rem; }
/* Second form in the same card: a divider instead of another card, because creating a tag
   and merging two are the same kind of thing — vocabulary upkeep, not per-row editing. */
.tag-merge { margin-top: .9rem; padding-top: .9rem; border-top: 1px solid var(--line-2); }
.tag-merge select { min-width: 12rem; }
/* The label's own bottom margin (the global rule for stacked forms) is what put the button
   6px below the field beside it: the row aligns to the bottom of a box taller than the
   control inside it. Here the row IS the layout, so the margin goes. */
.tag-new label { display: grid; gap: .25rem; margin: 0; font-size: .85rem; color: var(--ink-soft); }
/* The vocabulary card sits right above the search bar, which brings no top margin of its own. */
.tag-forms { margin-bottom: 1rem; }
/* Slugs worn by effects with no vocabulary row: a drift the operator should see at once. */
.tag-orphans { border-left: 3px solid var(--warn); }
.tag-orphans code { margin-right: .2rem; }

/* --- Login (standalone, no shell) --------------------------------------- */
.login {
  display: flex; min-height: 100vh; align-items: center; justify-content: center;
  background:
    radial-gradient(60% 80% at 50% 0%, #2a2822 0%, transparent 70%),
    var(--side);
  overflow: auto;
}
.login .card { width: 340px; box-shadow: var(--sh-lg); border: 0; }
.login-logo { display: block; width: 76px; height: 76px; border-radius: 50%; object-fit: contain; background: #fff; padding: 4px; margin: 0 auto .9rem; box-shadow: 0 0 0 1px var(--line), 0 6px 18px rgba(33, 30, 25, .12); }
.login h1 { text-align: center; margin-bottom: 1.5rem; }
.login button { width: 100%; justify-content: center; margin-top: .3rem; }

/* --- Scrollbars --------------------------------------------------------- */
.content::-webkit-scrollbar, aside::-webkit-scrollbar { width: 10px; }
.content::-webkit-scrollbar-thumb { background: #ddd4c4; border: 3px solid var(--paper); border-radius: 10px; }
.content::-webkit-scrollbar-thumb:hover { background: #ccc1ad; }
aside::-webkit-scrollbar-thumb { background: var(--side-2); border: 3px solid var(--side); border-radius: 10px; }

/* --- Responsive: phones & narrow viewports ------------------------------ */
@media (max-width: 760px) {
  /* Sidebar becomes an off-canvas drawer; content takes the full width. */
  .shell { grid-template-columns: 1fr; }
  aside {
    position: fixed; top: 0; bottom: 0; left: 0; z-index: 60;
    width: min(82vw, 280px); transform: translateX(-100%);
    transition: transform .22s ease, box-shadow .22s ease;
  }
  /* Shadow only while the drawer is out: parked off-screen it still casts along the left edge,
     which reads as a dark stripe down the side of the page. */
  .nav-toggle:checked ~ .shell aside { transform: translateX(0); box-shadow: var(--sh-lg); }

  .nav-scrim {
    display: block; position: fixed; inset: 0; z-index: 55;
    background: rgba(20, 18, 15, .45);
    opacity: 0; visibility: hidden;
    transition: opacity .22s ease, visibility .22s ease;
  }
  .nav-toggle:checked ~ .shell .nav-scrim { opacity: 1; visibility: visible; }

  .nav-burger { display: inline-flex; width: 34px; height: 34px; }

  /* Topbar: burger first, then the breadcrumb cluster — left-aligned. */
  /* Та же высота, что и на широком экране: бургер 34px в полосе 48px, а не 38px в 66px.
     Шапка - это ориентир, а не блок контента, и на телефоне она особенно дорогая. */
  .topbar { padding: .35rem .9rem; justify-content: flex-start; gap: .7rem; }

  .content { padding: 1.1rem .9rem; }
  .card { padding: 1.1rem 1.05rem; }
  h1, .sec-title { font-size: 1.3rem; margin-bottom: 1rem; }

  /* A list row becomes a card. The first cell is what the record is called, so it stands as the
     card's heading - picture and name - and the remaining fields tile into as many columns as
     the width allows, each under its own label. Stacked one field per line instead, a nine-column
     list turned every record into a 300px column of labels that took a full screen per row.
     Vertical key/value tables (.card.narrow on detail / user pages) are untouched: these rules
     only target .list-table. */
  .list-table thead { display: none; }
  .list-table, .list-table tbody { display: block; width: auto; }
  /* The table's own card steps aside: kept, it framed every row card a second time, a border and
     a background a few pixels off the row's own. One frame per record. */
  .card.flush:has(> .list-table) {
    padding: 0; border: 0; border-radius: 0; box-shadow: none; background: none; overflow: visible;
  }
  .list-table tbody tr {
    position: relative; display: grid; width: auto;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));  /* a timestamp fits on one line */
    gap: .55rem 0; margin: 0 0 .7rem; padding: .75rem .9rem .8rem;
    border: 1px solid var(--line); border-radius: var(--r); background: var(--surface);
    box-shadow: var(--sh-sm);
  }
  /* Hover belongs to the whole card, not to its cells (they paint nothing below 761px): a cell
     tint covered the text and left the card's padding, gaps and heading row untouched. */
  .list-table tbody tr[data-href]:hover { background: #faf8f3; }
  /* Fields are separated by space, not by rules. The hairline-per-row this used to draw only
     joined into a clean line when a row happened to be full: the last row of a card left a
     stub hanging where the grid ran out of cells. The label above each value already says where
     one field ends. */
  .list-table td {
    display: block; width: auto; min-width: 0; text-align: left;
    padding: .1rem .7rem .05rem 0; border: 0;
    max-width: none; overflow: visible; white-space: normal; /* restore wrap in cards */
  }
  .list-table td::before { content: attr(data-label); display: block; margin-bottom: .1rem; }
  .list-table td:not([data-label])::before, .list-table td[data-label=""]::before { display: none; }
  /* The heading spans the card and carries no label: a record's own name needs no caption. */
  /* The record's own name heads the card, underlined the way a table head is: one rule per
     card, spanning it fully, instead of a rule per row that never lined up. */
  .list-table tbody tr > td:first-child {
    grid-column: 1 / -1; display: flex; align-items: center; gap: .6rem;
    padding: .1rem .1rem .55rem; border-bottom: 1px solid var(--line-2);
    font-weight: 600;
  }
  .list-table tbody tr > td:first-child::before { display: none; }
  .list-table tbody tr > td:first-child .cell-link { max-width: none; padding: 0; font-size: .86rem; }
  /* A card shows WHICH record this is; the record's own page shows the picture properly. */
  .list-table td .thumb, .list-table td .thumb.md { height: 44px; }
  /* An id wider than the card wrapped into a two-line block that read as a paragraph rather than
     a value. One line with an ellipsis keeps the card scannable; the full id is on the record. */
  .list-table td code {
    display: inline-block; max-width: 100%; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
  }
  /* A timestamp is a hair wider than half a phone card, so it broke over two lines in a grid it
     could not fit. Two columns is exactly what it needs: on a phone that is the whole row, on a
     wider card it merely borrows the neighbour's half instead of emptying the line. */
  .list-table td:has(.ts) { grid-column: span 2; }
  .list-table td.hint { grid-column: 1 / -1; text-align: center; } /* empty-state row */
  /* Кнопки строки справочника тегов - не поле карточки, а её подвал: в колонке сетки по 9rem
     пара «Сохранить/Удалить» не помещается и вторая уходила за правый край карточки. */
  .tags-table td.tag-actions {
    grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: .45rem;
    white-space: normal; padding-top: .55rem;
  }
  .tags-table td.tag-actions .btn + .btn { margin-left: 0; }
  /* No chevron on a card: the row's open link spreads over the whole card instead, so a tap
     anywhere opens the record, the keyboard still reaches it and a long press / middle click
     still opens a new tab. Links inside the card (the effect a generation ran, its user) sit
     above it and keep working. */
  .list-table td.row-go { position: absolute; inset: 0; padding: 0; }
  .list-table td.row-go a { display: block; width: 100%; height: 100%; border-radius: var(--r); }
  .list-table td.row-go a svg { display: none; }
  .list-table td.row-go a:focus-visible { background: transparent; }
  .list-table tbody tr[data-href] td:not(.row-go) a { position: relative; z-index: 1; }

  /* Toolbar stacks: title on its own row, actions wrap below it. */
  .toolbar { flex-wrap: wrap; gap: .6rem .5rem; }
  .toolbar h1, .toolbar .sec-title { width: 100%; margin-bottom: 0; }
  .toolbar > .cluster { width: 100%; flex-wrap: wrap; }

  /* Свёрнутый бар: строка-переключатель вместо полуэкрана контролов. */
  .filter-fold > summary {
    display: flex; align-items: center; gap: .4rem; list-style: none; cursor: pointer;
    margin-bottom: 1.2rem; padding: .6rem 1.1rem;
    border: 1px solid var(--line-hard); border-radius: var(--r-sm);
    background: var(--surface); color: var(--ink-soft);
    font-size: .88rem; font-weight: 600;
  }
  .filter-fold > summary::-webkit-details-marker { display: none; }
  .filter-fold > summary::after {
    content: "⌄"; margin-left: auto; font-size: 1rem; line-height: 0; transform: translateY(-.15em);
  }
  .filter-fold[open] > summary { margin-bottom: .7rem; }
  .filter-fold[open] > summary::after { transform: translateY(.15em) rotate(180deg); }
  .filter-fold > summary:hover { border-color: var(--acc); color: var(--acc-d); background: var(--acc-tint); }
  .filter-fold > summary:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--acc); }

  /* Filter bar: the search takes its own row, the pills wrap under it. */
  .f-search { flex-basis: 100%; }

  /* Editor action rail keeps the same look, just tighter side padding. */
  .editor-actions { padding: 0 .9rem; }

  /* Login card never wider than the viewport. */
  .login .card { width: 100%; max-width: 340px; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}
