/* Оформление через CSS-переменные: темы добавляются набором значений,
   без инлайновых стилей — строгая CSP остаётся в силе (ТЗ §4.10). */

:root,
[data-theme="dark"] {
  --bg: #0d1117;
  --panel: #161b22;
  --line: #30363d;
  --fg: #e6edf3;
  --muted: #8b949e;
  --accent: #2f81f7;
  --ok: #3fb950;
  --bad: #f85149;
  --warn: #d29922;
}

[data-theme="light"] {
  --bg: #f6f8fa;
  --panel: #ffffff;
  --line: #d0d7de;
  --fg: #1f2328;
  --muted: #656d76;
  --accent: #0969da;
  --ok: #1a7f37;
  --bad: #cf222e;
  --warn: #9a6700;
}

* { box-sizing: border-box; }

/* Атрибут `hidden` браузер реализует правилом `[hidden] { display: none }` в
 * своей таблице стилей, а она слабее авторской. Любое наше правило с
 * `display` — например `button { display: inline-block }` — молча отменяет
 * его, и элемент остаётся на экране.
 *
 * Поймали на кнопках офлайна: «Удалить с устройства» висела рядом со
 * «Скачать», а «Отменить» оставалась после завершения загрузки. Сломано при
 * этом было везде, где скрипт прячет кнопку, — просто заметили здесь.
 *
 * `!important` тут не затычка, а единственный способ вернуть правилу
 * приоритет, который у него и подразумевается. */
[hidden] { display: none !important; }

/* Цвет ссылок — из темы, а не браузерный по умолчанию. Без этого правила
   обычная ссылка красилась в #0000ee: на тёмной теме почти нечитаемо, и ни с
   чем в оформлении не совпадало. Ссылки внутри списков и кнопок красят себя
   сами — там правила точнее этого. */
a { color: var(--accent); }

/* --- безопасные поля экрана ---
 *
 * `viewport-fit=cover` в шаблоне растягивает страницу под вырез и скруглённые
 * углы — без него по краям остаются полосы фона. Плата за это: содержимое
 * заезжает под строку состояния и «динамический остров», и шапку становится
 * не прочитать. В окне браузера дефекта не видно, он проявляется только на
 * добавленном на рабочий стол приложении.
 *
 * `env()` на платформах без выреза равен нулю, поэтому отдельной ветки для
 * них не нужно.
 */
body {
  margin: 0;
  padding: 0 0 calc(4rem + env(safe-area-inset-bottom));
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

main {
  max-width: 52rem; margin: 0 auto;
  padding: 1rem;
  padding-left: calc(1rem + env(safe-area-inset-left));
  padding-right: calc(1rem + env(safe-area-inset-right));
}

/* --- шапка --- */
.top {
  /* Переносится: на телефоне четыре пункта в строку не влезают, и «Выйти»
     уезжало за край. Ширина шапки — не то, чем можно управлять, а состав
     пунктов зависит от прав: у администратора их на один больше. */
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: .5rem 1rem;
  /* Фон шапки уходит под строку состояния, а текст — нет. */
  padding: calc(.75rem + env(safe-area-inset-top)) calc(1rem + env(safe-area-inset-right))
           .75rem calc(1rem + env(safe-area-inset-left));
  border-bottom: 1px solid var(--line); background: var(--panel);
}
.brand { font-weight: 600; text-decoration: none; color: var(--fg); }
.top nav { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem 1rem; }
.top a { color: var(--accent); text-decoration: none; }

/* --- блоки --- */
.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem;
}
.card.narrow { max-width: 26rem; margin-inline: auto; }
h1 { font-size: 1.4rem; margin: 0 0 .5rem; }
h2 { font-size: 1.1rem; margin: 1.25rem 0 .5rem; color: var(--accent); }
.row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.muted { color: var(--muted); }
.small { font-size: .875rem; }
.error { color: var(--bad); }
.ok { color: var(--ok); }

/* --- формы --- */
label { display: block; margin-bottom: .75rem; }
input[type="text"], input[type="password"] {
  display: block; width: 100%; margin-top: .25rem; padding: .6rem .7rem;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px; font-size: 1rem;
}
.fields { display: grid; gap: .25rem; margin: 1rem 0; }

button, .button {
  display: inline-block; appearance: none; cursor: pointer;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px;
  padding: .6rem 1rem; font-size: 1rem; text-decoration: none;
  min-height: 44px;  /* палец на телефоне */
}
button:disabled { opacity: .5; cursor: default; }
.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
button.link {
  background: none; border: none; color: var(--accent);
  padding: 0; min-height: 0; font-size: 1rem;
}
form.inline { display: inline; }

.filebtn {
  display: inline-block; background: var(--bg); border: 1px solid var(--line);
  border-radius: 8px; padding: .6rem 1rem; cursor: pointer; min-height: 44px;
}
.filebtn input { display: none; }

/* --- каталог --- */
.works { list-style: none; padding: 0; margin: 0; }
.works li {
  display: flex; align-items: center; gap: .75rem;
  padding: .7rem 0; border-bottom: 1px solid var(--line);
}
.works a { display: flex; flex-direction: column; text-decoration: none; color: var(--fg); }
.badge {
  font-size: .78rem; padding: .15rem .5rem; border-radius: 999px;
  border: 1px solid var(--accent); color: var(--accent);
}
.badge.warn { border-color: var(--warn); color: var(--warn); }

.narration {
  border: 1px solid var(--line); border-radius: 10px;
  padding: .9rem; margin-bottom: .75rem;
}
.chapters { margin: .6rem 0 0; padding-left: 1.2rem; }
.chapters li { padding: .2rem 0; }
.chapters li.current { color: var(--accent); font-weight: 600; }
.chapters button.chapter {
  display: flex; justify-content: space-between; width: 100%;
  background: none; border: none; color: inherit; text-align: left;
  padding: .3rem 0; min-height: 0; font: inherit;
}

/* --- плеер --- */
.player audio { display: none; }
.player .now { display: flex; justify-content: space-between; margin: 1rem 0 .5rem; }
.controls { display: flex; gap: .5rem; align-items: center; margin-bottom: .75rem; }
.controls button { flex: 1; }
.controls .primary { flex: 2; font-size: 1.25rem; }
#seek { width: 100%; }

/* --- загрузка --- */
.bar { height: 8px; background: var(--bg); border-radius: 4px; overflow: hidden; margin: .75rem 0; }
.bar > span { display: block; height: 100%; width: 0; background: var(--accent); transition: width .2s; }
.upload { margin-bottom: 1rem; }
.upload-head { display: flex; justify-content: space-between; }
.picked { margin: .75rem 0; padding-left: 1.2rem; }
.choice { border: 1px solid var(--warn); border-radius: 10px; padding: .9rem; margin-top: 1rem; }
.choice button { display: block; width: 100%; margin-bottom: .5rem; text-align: left; }

/* --- подвал --- */
/* Метка версии стоит в потоке, а не поверх страницы. Прибитая к низу экрана,
   она ложилась на кнопки: на карточке книги — прямо на «Скачать на устройство».
   Видимой она должна быть без нажатий (ТЗ §6.5), но не ценой чужого места. */
.version {
  text-align: right; padding: .5rem 1rem calc(.5rem + env(safe-area-inset-bottom));
  font-size: .75rem; color: var(--muted); pointer-events: none;
}

/* Журнал событий плеера — раскрывается кнопкой, нужен при проверке на
   устройстве, где консоли нет. */
.log {
  margin-top: 1rem; padding: .6rem; max-height: 14rem; overflow: auto;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); color: var(--fg);
  font: 11px/1.35 ui-monospace, Menlo, monospace; white-space: pre;
}

.diag { margin-top: 1rem; border-top: 1px solid var(--line); padding-top: .5rem; }

/* --- офлайн --- */
.download { margin-top: .75rem; }
.download .row { align-items: center; gap: .75rem; }
.download button { margin-left: .75rem; }
.download .bar { margin: .5rem 0 0; }
.warn-note {
  border: 1px solid var(--warn); border-radius: 10px;
  padding: .75rem; margin-top: 1rem;
  color: var(--warn); font-size: .9rem;
}
.offline-badge { border-color: var(--ok); color: var(--ok); }

.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; margin-top: 1rem;
}

/* --- режим приложения ---
   Надпись о состоянии, а не об ошибке: спокойный тон здесь содержателен, а не
   косметичен (ТЗ §4.8). Красный цвет означал бы «что-то сломалось». */
.mode-note {
  margin: 0 0 1rem; padding: .6rem .9rem;
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: 8px; background: var(--panel);
  color: var(--muted); font-size: .9rem;
}

.filter { display: flex; gap: .5rem; margin: .25rem 0 1rem; }
/* Рамку и фон задаёт само правило, а не `button`: метка бывает и неактивным
   `span` — так показаны права, которые даёт роль и которые поэтому не
   переключаются. Без этого они выглядели голым текстом среди кнопок, то есть
   чем-то другим, хотя означают ровно то же. */
.chip {
  display: inline-block; min-height: 0; padding: .35rem .8rem; font-size: .9rem;
  background: var(--bg); border: 1px solid var(--line);
  border-radius: 999px; color: var(--muted);
}
.chip.active { border-color: var(--accent); color: var(--accent); }
.chip:disabled { opacity: .45; }

/* Строка каталога: ссылка растягивается, размер и удаление уходят вправо. */
.works li > a { flex: 1; min-width: 0; }

.clear-all { margin-top: .75rem; border-top: 1px solid var(--line); padding-top: .75rem; }
.clear-all .confirm { display: flex; align-items: center; flex-wrap: wrap; gap: .75rem; }
button.danger { color: var(--bad); }
.fail { margin: .5rem 0 0; flex-basis: 100%; }

/* --- плашка «сейчас играет» ---
   Закреплена внизу: звук продолжается при переходах между экранами, и человек
   должен видеть, что играет, и иметь возможность остановить это из любого
   места. На экране плеера скрыта — там свои кнопки. */
.nowplaying {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 10;
  display: flex; align-items: center; gap: .75rem;
  padding: .6rem calc(1rem + env(safe-area-inset-right))
           calc(.6rem + env(safe-area-inset-bottom)) calc(1rem + env(safe-area-inset-left));
  background: var(--panel); border-top: 1px solid var(--line);
}
.np-info {
  display: flex; flex-direction: column; min-width: 0; flex: 1;
  text-decoration: none; color: var(--fg);
}
.np-info strong, .np-info span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nowplaying button { min-width: 3.5rem; }

/* Версия не должна лежать под плашкой. */
.nowplaying ~ .version { bottom: calc(4rem + env(safe-area-inset-bottom)); }

/* Подменяемая область: во время запроса слегка гасим, чтобы переход был виден
   на медленной сети и не казался зависанием. */
#page[aria-busy="true"] { opacity: .6; transition: opacity .15s; }

/* Ошибка скрипта — видимая. На устройстве консоли нет, а невидимая ошибка
   выглядит как «ничего не работает» и стоит круга проверки. */
.boot-error {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 100;
  margin: 0; padding: .7rem 1rem calc(.7rem + env(safe-area-inset-bottom));
  max-height: 45vh; overflow: auto;
  background: var(--panel); color: var(--bad);
  border-top: 2px solid var(--bad);
  font: 11px/1.4 ui-monospace, Menlo, monospace; white-space: pre-wrap;
}

/* --- поиск --- */
.search { display: flex; gap: .5rem; margin: .25rem 0 .75rem; }
.search input { flex: 1; min-width: 0; margin-top: 0; }

/* --- правка метаданных --- */
.edit { margin: 1rem 0; border-top: 1px solid var(--line); padding-top: 1rem; }
.edit textarea {
  display: block; width: 100%; margin-top: .25rem; padding: .6rem .7rem;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px;
  font: inherit; resize: vertical;
}

/* --- обложки ---
   Заглушка рисуется разметкой: так она не стоит ни файла, ни запроса и
   подстраивается под тему. Буква даёт разным книгам разный вид — пустые
   одинаковые квадраты в списке сливаются. */
.cover {
  flex: none; border-radius: 6px; object-fit: cover;
  background: var(--bg); border: 1px solid var(--line);
}
.cover-small { width: 48px; height: 48px; }
.cover-large { width: 140px; height: 140px; }

/* На телефоне большая обложка забирала больше половины карточки: текстовой
   колонке оставалось 145 пикселей из 301, и в них переносилось всё — автор,
   год, серия, жанры. Обложка здесь подпись к книге, а не сама книга. */
@media (max-width: 420px) {
  .cover-large { width: 104px; height: 104px; }
  .cover-large.cover-empty { font-size: 2.2rem; }
}
.cover-empty {
  display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-weight: 600; user-select: none;
}
.cover-small.cover-empty { font-size: 1.2rem; }
.cover-large.cover-empty { font-size: 3rem; }

.works li > a { flex-direction: row; align-items: center; gap: .75rem; }
.works-text { display: flex; flex-direction: column; min-width: 0; }
.works-text strong, .works-text .muted { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Третья строка карточки скачанного — чтец и размер. Ей разрешено переноситься:
   на ширине телефона она не влезала и обрезалась многоточием, а теряли при этом
   имя чтеца — ровно то, ради чего эта карточка отличается от каталожной.
   Лишняя строка дешевле потерянного смысла. */
.works-text .works-sub { white-space: normal; overflow: visible; }

.work-head { display: flex; gap: 1rem; align-items: flex-start; margin-bottom: .5rem; }
.work-head > div { min-width: 0; }
.work-head p { margin: 0 0 .35rem; }

.cover-edit { justify-content: flex-start; gap: .75rem; flex-wrap: wrap; }

/* --- предпросмотр данных извне ---
   Отдельная область: полученное ничего не затирает, пока человек не применит
   его по полю (ТЗ §4.5). */
.preview {
  margin: .75rem 0; padding: .9rem;
  border: 1px dashed var(--line); border-radius: 10px;
}
.preview h3 { margin: 0 0 .5rem; font-size: 1rem; }
.preview-field {
  display: flex; align-items: flex-start; gap: .75rem;
  padding: .4rem 0; border-top: 1px solid var(--line);
}
.preview-field:first-of-type { border-top: none; }
.preview-name { flex: none; width: 7rem; color: var(--muted); font-size: .875rem; }
.preview-value { flex: 1; min-width: 0; word-break: break-word; }
.preview-value.same { color: var(--muted); }
.external-row { justify-content: flex-start; gap: .75rem; }

/* --- правка чтеца на месте --- */
.narrator { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.narrator-input {
  margin-top: 0; width: auto; min-width: 12rem;
  padding: .35rem .5rem; font-size: 1rem;
}
.narrator-input.error { border-color: var(--bad); }

/* --- расширенный поиск --- */
.filters .fields { gap: .75rem; }
.filters-actions { justify-content: flex-start; gap: 1rem; margin-top: 1rem; }
.genre-box { border: 1px solid var(--line); border-radius: 10px; padding: .75rem 1rem; margin: 1rem 0; }
.genre-box legend { padding: 0 .4rem; color: var(--muted); font-size: .875rem; }
.chips { display: flex; flex-wrap: wrap; gap: .5rem; }
.chip-box {
  display: inline-flex; align-items: center; gap: .4rem; margin: 0;
  padding: .35rem .7rem; border: 1px solid var(--line); border-radius: 999px;
  font-size: .9rem; cursor: pointer;
}
.chip-box input { margin: 0; }
.chip-box:has(input:checked) { border-color: var(--accent); color: var(--accent); }
select[disabled] { opacity: .5; }
select {
  display: block; width: 100%; margin-top: .25rem; padding: .6rem .7rem;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px; font-size: 1rem;
}

/* --- пользователи (ТЗ §4.3) --- */
.person {
  border: 1px solid var(--line); border-radius: 10px;
  padding: .9rem; margin-bottom: .75rem;
}
/* Отключённая запись приглушена, но читаема: её данные на месте, и вернуть
   человека можно обратной операцией — это состояние, а не руина. */
.person-off { opacity: .6; }

/* Права — переключателями в ряд: видно сразу весь набор, а не по одному. */
.perms { display: flex; flex-wrap: wrap; gap: .4rem; margin: .6rem 0 .3rem; }
.perms .perm { display: contents; }

.perms-bulk { display: flex; gap: 1rem; margin: .1rem 0 .4rem; }

.person-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1rem;
  margin-top: .6rem; padding-top: .6rem; border-top: 1px solid var(--line);
}
.newpass { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.newpass input { width: 12rem; margin-top: 0; }

/* Удаление уносит чужие данные безвозвратно, и выглядеть оно обязано иначе,
   чем соседние обратимые действия. */
.danger { color: var(--bad); }

/* --- удаление (ТЗ §4.3) --- */
.removal { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem;
  margin-top: .6rem; padding-top: .6rem; border-top: 1px solid var(--line); }
.removal-work { margin: 1rem 0; }
/* Перечень теряемого обведён предупреждающим цветом: он появляется там же, где
   стояла кнопка, и не должен сливаться с обычным текстом карточки. */
.removal-confirm {
  flex-basis: 100%;
  border: 1px solid var(--warn); border-radius: 10px;
  padding: .75rem; margin-top: .25rem;
}
.removal-confirm p { margin: 0 0 .5rem; }
.removal-actions { display: flex; gap: 1rem; }

/* --- отметки (ТЗ §4.6) --- */
.marks { display: flex; flex-wrap: wrap; gap: .4rem; margin: .75rem 0 .4rem; }
.marks-others { margin: 0 0 .75rem; }
/* Метка на карточке каталога: короткая форма той же отметки. Приглушена —
   она сообщает о своём, а не о книге, и спорить с названием не должна. */
.works-mark { font-size: .78rem; color: var(--accent); white-space: nowrap; }

/* Предложение следующей книги в серии: заметно, но не настойчиво — решение
   за слушателем (ТЗ §4.7), и кнопка здесь была бы шагом к автозапуску. */
.next-up {
  border: 1px solid var(--accent); border-radius: 10px;
  padding: .6rem .75rem; margin: .75rem 0;
}

/* --- журнал действий (ТЗ §4.3) --- */
.journal-days { display: flex; flex-wrap: wrap; gap: .4rem; margin: .75rem 0; }
.journal { list-style: none; padding: 0; margin: .5rem 0 0; }
.journal > li {
  border-top: 1px solid var(--line); padding: .6rem 0;
}
.journal-head { display: flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: baseline; }
.journal-object { color: var(--muted); }
.journal-changes { margin: .3rem 0 0; padding-left: 1.1rem; }
/* Старое значение зачёркнуто, новое выделено: пара читается взглядом, без
   чтения подписей «было» и «стало». */
.journal-changes s { color: var(--muted); }

/* --- таймер сна (ТЗ §4.7) --- */
.sleep { margin: 1rem 0; padding-top: .75rem; border-top: 1px solid var(--line); }
.sleep-state { margin-bottom: .4rem; }
.sleep-choices { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }

/* --- порядок глав при загрузке (ТЗ §4.1) --- */
.order { margin-top: 1rem; }
.order-list { list-style: decimal; padding-left: 1.6rem; margin: .5rem 0; }
.order-list li { padding: .35rem 0; border-bottom: 1px solid var(--line); }
.order-row { display: flex; align-items: center; gap: .5rem; }
.order-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Стрелки крупные: порядок правят пальцем на телефоне, и мелкая стрелка рядом
   с соседней — это промах через раз. */
.order-move { min-width: 44px; min-height: 44px; }

/* --- правка состава глав (ТЗ §4.1) --- */
.chapter-tools { margin-top: .5rem; }
.chapter-ops { display: inline-flex; gap: .4rem; margin-left: .5rem; }
/* Стрелки крупные: по ним попадают пальцем, а промах меняет ревизию озвучки. */
.chapter-move { min-width: 40px; min-height: 40px; padding: 0; }
.chapter-warn { margin-top: .5rem; }
.chapter-pick { cursor: pointer; }

/* Предложение обновиться (ТЗ §6.5). Заметно, но не перекрывает управление:
   решение за человеком, и торопить его нечем. */
.update-note { border-left-color: var(--accent); }
.update-note button { margin-left: .5rem; }
