/* ═══════════════════════════════════════════════════════════════════════
   Говорит ИИ — слой страницы поверх дизайн-системы.

   Подключается ПОСЛЕ ds/styles.css:
     <link rel="stylesheet" href="/static/ds/styles.css">
     <link rel="stylesheet" href="/static/style.css">

   Четыре части:
     1. правки, без которых app.js перестаёт работать;
     2. оболочка и раскладка страницы;
     3. «мост»: классы состояний, которые ставит код → вид системы;
     4. оформление разметки, которую строит код.

   Ни одного внешнего адреса. Иконки внутри страницы — контуры Lucide
   инлайном в разметке, а там, где разметку строит код, — data:-маска.
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── 1. Обязательное ───────────────────────── */

/* Компоненты системы — display: flex и grid, канвасы — display: block.
   И то и другое сильнее встроенного браузерного [hidden]: правила браузера
   имеют самый низкий приоритет. Код прячет и показывает атрибутом два
   десятка элементов, включая оба канваса, карточку хода работы и тост. */
[hidden] { display: none !important; }

/* Код читает три переменные с корня документа: --accent (волна озвучки),
   --danger и --line (шкала записи). Двух последних в системе нет под
   этими именами. Объявлять нужно именно в :root — getComputedStyle
   вызывается на documentElement. */
:root {
  --danger: var(--status-danger);
  --line: var(--border-default);
}

/* ───────────────────────── 2. Оболочка ───────────────────────── */

.app {
  display: flex;
  min-height: 100vh;
  background: var(--surface-page);
}

.app-side {
  position: sticky;
  top: 0;
  height: 100vh;
  gap: var(--space-6);
  padding: var(--space-6) var(--space-5);
  overflow-y: auto;
}

.app-brand {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-3) var(--space-5);
}

/* Пункт меню в две строки: название и что раздел делает */
.gai-nav__item {
  min-height: 52px;
  padding-block: var(--space-3);
}
.app-nav-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.app-nav-text > span {
  font: var(--type-caption);
  color: var(--text-muted);
}
.gai-nav__item.is-active .app-nav-text > span { color: inherit; opacity: 0.7; }

.app-body {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}

/* Шапка нужна только на телефоне: там бокового меню нет, а знак нужен */
.app-bar { display: none; }

.app-main {
  flex: 1;
  width: 100%;
  max-width: 900px;
  margin-inline: auto;
  padding: var(--space-9) var(--space-8) var(--space-11);
}

.app p { margin: 0; }

.app-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Разделы существуют в разметке одновременно, код переключает .is-active.
   Анимация запускается при переходе из display: none в видимое состояние —
   то есть каждый раз при переключении, а не единожды при загрузке. */
.panel { display: none; }
.panel.is-active {
  display: block;
  animation: gai-slide-from-right var(--dur-4) var(--ease-out) both;
}
.panel > * + * { margin-top: var(--space-8); }

/* Вертикальный ритм внутри карточек и разделов */
.app-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
}
#video-form { gap: var(--space-8); }

.app-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.app-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-5);
  align-items: end;
}

.app-stack > .gai-card__head { margin-bottom: 0; }
.app-stack > .gai-segmented,
.gai-field > .gai-segmented { align-self: flex-start; max-width: 100%; }

.app-note-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

/* Переключатель темы. Кнопки две — в подвале бокового меню и в шапке
   телефона; какая видна, решает ширина экрана. Код цепляется за класс и
   работает с обеими сразу. */
.app-side-foot {
  margin-top: auto;
  padding-top: var(--space-5);
  border-top: var(--stroke-hairline) solid var(--border-subtle);
}
.app-side-foot .theme-toggle { width: 100%; justify-content: flex-start; }

/* Показываем то, на что кнопка переключит, а не то, что сейчас: в светлой
   теме луна, в тёмной солнце. До того как отработает скрипт в <head>,
   атрибута нет — тогда луна, как в светлой. */
.theme-icon-light { display: none; }
[data-theme="dark"] .theme-icon-light { display: block; }
[data-theme="dark"] .theme-icon-dark { display: none; }

/* Меню телефона наверху. На широком экране его нет — там боковое. */
.app-tabs { display: none; }

/* Кнопка ожидания: спиннер занимает место обычной иконки, ширина не прыгает.
   Спиннер обязан быть <span>, а не <svg>: busy() показывает его через
   `.hidden = false`, а свойство hidden есть только у HTMLElement — у SVG
   присваивание молча заводит обычное поле в объекте и на экране не меняет
   ничего. Проверено в браузере: с <svg> спиннер не появлялся никогда. */
.spinner { display: inline-flex; flex: none; }
.spinner:not([hidden]) + .app-icon-lead { display: none; }

/* ───────────────────────── 3. Мост ───────────────────────── */
/* Система держит состояние на aria-* и модификаторах --.
   Код ставит свои классы и не меняется — по правилу на состояние. */

/* .is-active на пункте меню = .gai-nav__item--active */
.gai-nav__item.is-active {
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: var(--weight-semibold);
}

/* .is-active на пункте нижней панели (aria-selected код ставит сам) */
.gai-tabbar__item.is-active { color: var(--accent-ink); }
.gai-tabbar__item.is-active .gai-icon { transform: translate3d(0, -2px, 0) scale(1.12); }

/* .is-active на .chip = .gai-segmented__item[aria-selected="true"].
   Бегунок .gai-segmented__thumb позиционируется скриптом, которого нет, —
   поэтому подложку берёт на себя сам выбранный пункт. */
.gai-segmented__item { white-space: nowrap; }
.gai-segmented__item.is-active {
  background: var(--surface-card);
  color: var(--text-primary);
  font-weight: var(--weight-semibold);
  box-shadow: var(--shadow-sm);
}

/* .is-over на зоне перетаскивания = .gai-drop--over */
.gai-drop.is-over {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-soft);
  transform: scale(1.015);
}
.gai-drop.is-over .gai-drop__mark {
  transform: scale(1.1);
  animation: gai-nudge-down 900ms var(--ease-inout) infinite;
}

/* .has-file — своё состояние, аналога в системе нет: рамка сплошная, тон успеха */
.gai-drop.has-file {
  border-style: solid;
  border-color: var(--status-success);
  background: var(--status-success-soft);
}
.gai-drop.has-file .gai-drop__mark {
  background: var(--status-success);
  color: #fff;
}
.gai-drop.has-file .gai-drop__hint { color: var(--status-success-ink); }

/* ─── правовые ссылки и окно принятия документов ─── */

.app-legal {
  margin: var(--space-3) 0 0;
  padding-inline: var(--space-3);
  color: var(--text-muted);
}
.app-legal a { color: inherit; }
.app-legal a:hover { color: var(--text-link-hover); }

/* Подвал нужен там, где меню слева спрятано, — иначе ссылки на документы
   дублируются на одном экране. Точка перелома та же, что у меню. */
.app-foot { display: none; }

.app-gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
  background: var(--surface-scrim);
  overflow-y: auto;
}

.app-gate__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: 100%;
  max-width: 34rem;
  margin: auto;
  padding: var(--space-7);
  box-shadow: var(--shadow-xl);
}
.app-gate__card h2 { margin: 0; }
.app-gate__card p { color: var(--text-secondary); }

.app-gate__list {
  margin: 0;
  padding-left: var(--space-5);
  color: var(--text-muted);
}
.app-gate__list li { margin-bottom: var(--space-2); }

/* Пока окно открыто, страница за ним не прокручивается: иначе на телефоне
   палец уводит фон, окно остаётся на месте, и выглядит это сломанным. */
.заперто { overflow: hidden; }

/* Остаток на сегодня — под зоной выбора, а не в ней: в зоне подсказку
   занимает выбранный файл, и число оттуда пропадает как раз тогда, когда
   оно интереснее всего, — сразу после перевода. */
.app-left {
  margin: 0;
  text-align: center;
}
.app-left.is-empty { color: var(--danger); }

/* .is-recording на кнопке записи = .gai-record--on */
.gai-record.is-recording {
  background: var(--status-danger);
  color: #fff;
}
.gai-record.is-recording .gai-record__core { animation: gai-breathe 1400ms var(--ease-inout) infinite; }
.gai-record.is-recording .gai-record__ring { animation: gai-pulse-ring 1900ms var(--ease-out) infinite; }
.gai-record.is-recording .gai-record__ring:nth-of-type(2) { animation-delay: 640ms; }
.app-icon-stop { display: none; }
.gai-record.is-recording .app-icon-mic { display: none; }
.gai-record.is-recording .app-icon-stop { display: block; }

/* .err на тосте = .gai-toast--danger. Текст код ставит через textContent,
   поэтому иконка — псевдоэлемент с маской, а не вложенный <svg>. */
.toast::before {
  content: "";
  width: var(--icon-md);
  height: var(--icon-md);
  flex: none;
  margin-top: 1px;
  background: var(--status-success);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m9 12 2 2 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m9 12 2 2 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
}
.toast.err {
  background: var(--status-danger-soft);
  border-color: transparent;
  color: var(--status-danger-ink);
}
.toast.err::before {
  background: var(--status-danger);
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cline x1='12' x2='12' y1='8' y2='12'/%3E%3Cline x1='12' x2='12.01' y1='16' y2='16'/%3E%3C/svg%3E");
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cline x1='12' x2='12' y1='8' y2='12'/%3E%3Cline x1='12' x2='12.01' y1='16' y2='16'/%3E%3C/svg%3E");
}

/* Полоса прогресса. Код двигает заливку через style.width — inline-ширина
   перебивает эту, а calc оставлен на случай перехода на --gai-value
   (см. список правок к app.js). Блик лежит внутри заливки, поэтому бежит
   ровно по заполненной части и не требует своей переменной. */
.gai-progress__fill.bar-fill {
  right: auto;
  width: calc(var(--gai-value, 0) * 100%);
  transform: none;
  transition: width var(--dur-4) var(--ease-out);
}
.gai-progress__fill.bar-fill > .gai-progress__sheen { transform: none; }

/* .is-waiting код ставит на саму заливку (в системе модификатор стоит на
   контейнере). Значение: работа идёт, а проценты замерли. */
@media (prefers-reduced-motion: no-preference) {
  .gai-progress__fill.is-waiting {
    width: 100% !important;
    animation: gai-indeterminate 1700ms var(--ease-inout) infinite;
  }
  .gai-progress__fill.is-waiting > .gai-progress__sheen { display: none; }
}

/* ─────────────── 4. Разметка, которую строит код ─────────────── */

/* Волна озвучки и шкала записи — канвасы, их рисует код через 2D-контекст.
   Высота задаётся ТОЛЬКО здесь: атрибут height у <canvas> — размер буфера,
   и при плотности пикселей больше единицы получается петля роста. */
#tts-wave {
  width: 100%;
  height: 72px;
}
#stt-level {
  flex: 1 1 160px;
  min-width: 0;
  height: 44px;
}

.app-record {
  display: flex;
  align-items: center;
  gap: var(--space-6);
}

.app-rec-panel {
  flex-wrap: wrap;
  padding: var(--space-5) var(--space-6);
  border-radius: var(--radius-lg);
}
.app-rec-hint { flex: 1 1 100%; }

/* Плееры: своего компонента у нас нет, играет встроенный в браузер */
#tts-audio,
#stt-source,
#video-audio {
  display: block;
  width: 100%;
}

/* Сводки: код кладёт в контейнер голые <span> подряд — вид значка системы */
.stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.stats > span {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding-inline: var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--size-mono-s);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Сплошной текст расшифровки: код кладёт его одним textContent */
.app-plain {
  font: var(--type-body-l);
  line-height: var(--leading-loose);
  max-width: var(--measure);
  white-space: pre-wrap;
}

/* По фразам: код строит <div class="seg"><time>…</time><span>…</span></div>.
   Вид — как у .gai-transcript__line / __tc / __text. */
#stt-segments .seg {
  display: grid;
  grid-template-columns: 78px 1fr;
  gap: var(--space-5);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  transition: background-color var(--dur-2) var(--ease-out);
}
#stt-segments .seg:hover { background: var(--surface-hover); }
#stt-segments .seg > time {
  font-size: var(--size-mono);
  color: var(--text-muted);
  text-align: right;
  padding-top: 2px;
  cursor: pointer;
  transition: color var(--dur-2) var(--ease-out);
}
#stt-segments .seg > time:hover { color: var(--accent-strong); }
#stt-segments .seg > span {
  font: var(--type-body-l);
  line-height: var(--leading-loose);
  max-width: var(--measure);
}

/* Кнопки скачивания результата перевода: код создаёт <a class="ghost"> и
   <a class="ghost accent"> у главной. Вид — .gai-btn--secondary и --primary,
   иконка download маской: вложить <svg> в созданную кодом ссылку нельзя. */
#video-downloads a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--control-md);
  padding-inline: var(--space-6);
  border: var(--stroke-hairline) solid var(--border-default);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--text-primary);
  font: var(--weight-semibold) var(--size-body) / 1 var(--font-text);
  letter-spacing: var(--tracking-snug);
  text-decoration: none;
  white-space: nowrap;
  box-shadow: var(--shadow-xs);
  will-change: transform;
  transition:
    transform var(--dur-2) var(--ease-spring-soft),
    box-shadow var(--dur-2) var(--ease-out),
    background-color var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out);
}
#video-downloads a::before {
  content: "";
  width: var(--icon-md);
  height: var(--icon-md);
  flex: none;
  background: currentColor;
  mask: 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.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15V3'/%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: 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.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15V3'/%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E") center / contain no-repeat;
}
#video-downloads a:hover {
  transform: translate3d(0, var(--lift-hover), 0);
  border-color: var(--border-strong);
  background: var(--surface-hover);
  box-shadow: var(--shadow-sm);
}
#video-downloads a:active { transform: scale(var(--press-scale)); box-shadow: none; }
#video-downloads a:focus-visible { box-shadow: var(--focus-ring); }
/* Видео с переводом — то, за чем пришли */
#video-downloads a.accent {
  background: var(--action-primary-bg);
  border-color: transparent;
  color: var(--action-primary-fg);
  box-shadow: var(--shadow-sm);
}
#video-downloads a.accent:hover { background: var(--action-primary-bg-hover); box-shadow: var(--shadow-md); }
#video-downloads a.accent:active { background: var(--action-primary-bg-active); }

/* Имя выбранного файла код подставляет как <strong> в заголовок зоны */
#stt-title strong,
#video-title strong {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* ───────────────────────── Телефон ───────────────────────── */

@media (max-width: 760px) {
  .app-bar {
    display: flex;
    justify-content: flex-start;
    padding-inline: var(--space-5);
  }

  /* Меню слева спрятано, а вместе с ним и ссылка на документы */
  .app-foot {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-2) var(--space-3);
    padding: var(--space-6) var(--space-5) var(--space-8);
    color: var(--text-muted);
  }
  .app-foot a { color: inherit; }

  /* На телефоне окно принятия во весь экран: текста в нём на полстраницы,
     и вписывать его в карточку с полями по краям некуда. */
  .app-gate { padding: 0; }
  .app-gate__card {
    min-height: 100dvh;
    max-width: none;
    justify-content: center;
    border-radius: 0;
    padding: var(--space-6) var(--space-5);
  }

  /* Меню сразу под шапкой. Не приклеено: прокручивается и уходит под шапку,
     освобождая экран. Раздел всё равно открывается с начала страницы, так
     что меню оказывается на виду в тот момент, когда оно нужно. */
  .app-tabs {
    display: flex;
    gap: var(--space-2);
    padding: var(--space-4) var(--space-4) var(--space-2);
    background: var(--surface-page);
  }
  /* Три пункта делят ширину поровну: иначе «Расшифровка» вытесняет «Видео»
     за край, и получается ровно та беда, из-за которой меню и переезжало.
     На треть от 320 точек приходится 96, и значок со словом в одну строку
     туда не влезают: на «Расшифровку» остаётся около 64 точек при нужных 78.
     Поэтому значок над словом — тогда слову достаётся вся ширина пункта. */
  .app-tabs .gai-tabs__item {
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    min-height: 58px;
    padding: var(--space-3) var(--space-2);
    overflow: hidden;
    font-size: var(--size-caption);
    line-height: 1.2;
  }
  .app-tabs .gai-icon { width: var(--icon-md); height: var(--icon-md); }

  /* Последняя защита. Ширину букв заранее не знать: Onest мог не догрузиться,
     в системе бывает укрупнение шрифта, у браузера свои подстановки. Если
     слово всё-таки шире пункта — обрежется многоточием, но не наползёт на
     соседа и не съедет из своей рамки. Это уже случалось на живом телефоне. */
  .app-tab-text {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .app-main { padding: var(--space-5) var(--space-5) var(--space-9); }
  .panel > * + * { margin-top: var(--space-7); }
  .app-fields { grid-template-columns: minmax(0, 1fr); }

  /* Сегменты на всю ширину: два длинных названия в строку не помещаются.
     justify-content обязателен: у системы пункт по содержимому, и там
     выравнивать нечего, а растянутый на треть экрана прижимает подпись
     к левому краю. */
  .gai-segmented { display: flex; width: 100%; }
  .gai-segmented__item {
    flex: 1 1 0;
    min-width: 0;
    justify-content: center;
    text-align: center;
    padding-inline: var(--space-3);
    white-space: normal;
  }

  .app-record { gap: var(--space-5); }
  /* На телефоне запись — первое действие: кнопка должна быть видна без
     прокрутки. Порядок в карточке меняем оформлением, разметка та же. */
  #panel-stt .app-record { order: 1; }
  #panel-stt #stt-rec-panel { order: 2; }
  #panel-stt .gai-divider { order: 3; }
  #panel-stt #stt-drop { order: 4; }
  #panel-stt #stt-go { order: 5; }
  /* Зона перетаскивания ниже: знак меньше, заголовок в одну строку */
  .gai-drop { padding: var(--space-6) var(--space-5); }
  .gai-drop__mark { width: 44px; height: 44px; }
  .gai-drop__mark .gai-icon { width: 22px; height: 22px; }
  .gai-drop__title { font: var(--type-body); font-weight: var(--weight-semibold); }
  .app-rec-panel { padding: var(--space-4) var(--space-5); }
  /* Таймкод над фразой, а не слева от неё.
     В колонку 62 точки он не влезает: «00:00:00.530» моноширинным — около
     78 точек, и лишнее наезжало прямо на текст расшифровки. Расширять
     колонку некуда, на узком экране она съела бы половину строки. */
  #stt-segments .seg {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }
  #stt-segments .seg > time { text-align: left; padding-top: 0; }
  #stt-segments .seg > span,
  .app-plain { font-size: var(--size-body); }
  #video-downloads a { flex: 1 1 100%; justify-content: center; }
}
