/* Тема колод Марабу — «Люминофор».

   Слайд ведёт себя как экран лабораторного прибора: тёмное поле, свет идёт
   от содержимого. Все цвета — ТОЛЬКО через переменные; палитры лежат в
   electron.css / electron-day.css / turbulence.css и переопределяют :root.

   Ничего из вёрстки не завязано на конкретную лекцию. */

/* Шрифты подкладываются файлами в shared/fonts/. Пока файлов нет, работают
   системные фолбэки — вёрстка от этого не разъезжается. */
@font-face { font-family: 'Onest'; src: url('../fonts/Onest-Bold.woff2') format('woff2'); font-weight: 700; font-display: swap; }
@font-face { font-family: 'Onest'; src: url('../fonts/Onest-Medium.woff2') format('woff2'); font-weight: 500; font-display: swap; }
@font-face { font-family: 'Golos Text'; src: url('../fonts/GolosText-Regular.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Golos Text'; src: url('../fonts/GolosText-Medium.woff2') format('woff2'); font-weight: 500; font-display: swap; }
@font-face { font-family: 'JetBrains Mono'; src: url('../fonts/JetBrainsMono-Medium.woff2') format('woff2'); font-weight: 500; font-display: swap; }

:root {
  --bg: #0a0d1a;
  --ink: #eceef6;
  --muted: #8c93ac;
  --accent: #ffb454;
  --chip: #141a2b;
  --line: rgba(236, 238, 246, .12);
  --glow: rgba(255, 180, 84, .28);
  --screen: #06080f;
  --scan: .045;

  --font-head: 'Onest', 'Golos Text', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-body: 'Golos Text', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}

.reveal-viewport { background: var(--bg); }
.reveal { font-family: var(--font-body); color: var(--ink); }
.reveal .slides section {
  text-align: left;
  height: var(--slide-height, 100%);
  box-sizing: border-box;
}

/* Развёртка: тонкие строки поверх всего слайда. Статичные, без анимации —
   на слабой машине это ноль работы для GPU. */
.reveal-viewport::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 60;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, var(--scan)) 0 1px,
    transparent 1px 3px);
}

/* Нижние 46px — шина сквозного электрона (LANE_Y = 700 в electron-lane.js
   плюс радиус свечения ~18). Контент туда не заходит ни на одном
   типе слайда — иначе свечение ложится на кромку кадра. */
.slide-inner {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: 46px 64px 46px;
  box-sizing: border-box;
}

/* ---------- шапка слайда ---------- */

.kicker {
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: .2em;
  color: var(--muted);
  margin: 0 0 14px;
  display: flex;
  align-items: center;
  gap: 12px;
  text-transform: uppercase;
}

.kicker-step {
  color: var(--accent);
  border: 1px solid var(--glow);
  border-radius: 4px;
  padding: 2px 8px 1px;
  letter-spacing: .12em;
}

.kicker-phase { color: var(--muted); }

/* Хвост-линия уводит кикер в правый край: строка приборной шкалы,
   а не подпись. */
.kicker::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(to right, var(--line), transparent);
}

.heading {
  font-family: var(--font-head);
  font-size: 47px;
  font-weight: 700;
  line-height: 1.04;
  letter-spacing: -.015em;
  color: var(--ink);
  margin: 0 0 30px;
  text-wrap: balance;
  max-width: 22ch;
}

.blocks {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 22px;
  min-height: 0;
}

.blk { margin: 0; }

/* ---------- крупная мысль ---------- */

.blk-lead {
  font-family: var(--font-head);
  font-size: 35px;
  font-weight: 500;
  line-height: 1.22;
  letter-spacing: -.01em;
  color: var(--accent);
  max-width: 34ch;
  text-wrap: pretty;
}

/* ---------- список ---------- */

.blk-bullets {
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: 28px;
  line-height: 1.38;
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-width: 34ch;
  text-wrap: pretty;
}
.blk-bullets li { position: relative; padding-left: 38px; }
.blk-bullets li::before {
  content: '';
  position: absolute;
  left: 0;
  top: .62em;
  width: 20px;
  height: 2px;
  background: var(--accent);
}

/* ---------- формула ---------- */

.blk-formula {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  text-align: center;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: 30px 0;
}
/* Рядом с другими блоками формула берёт ровно свою высоту:
   иначе два flex:1-блока делят экран пополам и демка схлопывается. */
.blocks > .blk-formula:not(:only-child) {
  flex: 0 0 auto;
  gap: 14px;
  padding: 20px 0 18px;
}
.blocks > .blk-formula:not(:only-child) .formula-text { font-size: 54px; }
/* size:'small' обязан пережить правило выше: оно длиннее и без этой строки
   молча выигрывало у .formula-text.small — ровно на тех слайдах, где формула
   стоит рядом с демкой и мельче её просили именно потому, что она длинная. */
.blocks > .blk-formula:not(:only-child) .formula-text.small { font-size: 42px; }
.blocks > .blk-formula:not(:only-child) .formula-caption { font-size: 24px; max-width: 62ch; }
.formula-text {
  font-family: var(--font-head);
  font-size: 68px;
  font-weight: 500;
  letter-spacing: .01em;
  color: var(--accent);
  text-shadow: 0 0 34px var(--glow);
}
.formula-text.small { font-size: 44px; }
.formula-caption {
  font-size: 24px;
  color: var(--muted);
  max-width: 46ch;
  line-height: 1.45;
}

/* ---------- демка ---------- */

.blk-demo, .blk-live {
  border-radius: 14px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
}
.blk-demo {
  flex: 1 1 auto;
  min-height: 120px;
  padding: 18px 24px;
  text-align: center;
  background: var(--chip);
  border: 1px dashed var(--line);
}
/* Готовая демка — не карточка с полями, а сам прибор: рамка в один пиксель */
.blk-demo.has-frame {
  padding: 0;
  background: var(--screen);
  border: 1px solid var(--line);
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .4), 0 18px 60px rgba(0, 0, 0, .45);
}
/* Иллюстрация, а не прибор: ни рамки, ни фона, ни тени. Так нарисованное
   на канве читается частью слайда — например, маятник рядом со списком,
   пока зал называет, от чего зависит период. */
.blk-demo.is-bare {
  background: transparent;
  border: 0;
  box-shadow: none;
  padding: 0;
  overflow: visible;
}
.demo-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: transparent; }

.blk-live { background: var(--chip); padding: 18px 24px; }
.blk-label {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .2em;
  color: var(--muted);
  text-transform: uppercase;
}
.blk-desc {
  font-size: 24px;
  line-height: 1.4;
  color: var(--muted);
  max-width: 70ch;
  margin: 0 auto;
}

/* ---------- две карточки ---------- */

.blk-compare {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 26px;
  flex: 1 1 auto;
  align-items: stretch;
}
.compare-col {
  background: var(--chip);
  border-radius: 14px;
  border-top: 2px solid var(--line);
  padding: 30px 32px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
}
/* Правая карточка почти всегда несёт поворот мысли — ей луч */
.compare-col + .compare-col { border-top-color: var(--accent); }
.compare-title {
  font-family: var(--font-head);
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--ink);
}
.compare-col + .compare-col .compare-title { color: var(--accent); }
.compare-body { font-size: 24px; line-height: 1.42; color: var(--muted); text-wrap: pretty; }

/* ---------- медиа ---------- */

.slide-media .slide-inner { padding: 34px 44px 46px; }
.slide-media .kicker { margin-bottom: 8px; }
.slide-media .heading { font-size: 38px; margin-bottom: 0; max-width: none; }
.slide-media .blocks { gap: 16px; }
.slide-media .blk-lead { font-size: 27px; max-width: none; }

.blk-video, .blk-image {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-height: 0;
}
.video-frame, .image-frame {
  flex: 1;
  min-height: 0;
  width: 100%;
  border: 0;
  border-radius: 12px;
  background: #000;
  object-fit: contain;
}
.blk-image { text-align: center; }
.image-frame { background: transparent; }

/* [платформа] Инлайновые SVG-рисунки ядра (window.FIGS): ведут себя как
   кадр image-блока, но лежат в DOM и красятся токенами темы. */
.blk-image .fig-inline {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
}
.blk-image .fig-inline svg {
  max-width: 100%;
  max-height: 100%;
  height: auto;
}

/* ---------- измерение поверх снимка ----------

   Разметка живёт в координатах исходного файла, поэтому картинка и слой
   обязаны лежать в одном прямоугольнике. object-fit тут не годится: он сам
   решает, где внутри блока показать кадр, а слой об этом не знает и уезжает.
   Поэтому обёртка держит пропорции снимка, а картинка занимает её целиком. */

.image-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  align-self: center;
  max-width: 100%;
}
.image-stage .image-frame { width: 100%; height: 100%; display: block; }
.image-marks { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }

/* Шаги измерения — обычные фрагменты reveal, но своё правило видимости
   нужно: у reveal показанный фрагмент получает visibility:inherit, и на
   SVG-группе это разрешалось в hidden. Отрезки появлялись прозрачными,
   то есть не появлялись вовсе. */
.reveal .slides section .image-marks > .fragment {
  opacity: 0;
  visibility: hidden;
  transition: opacity .25s ease;
}
.reveal .slides section .image-marks > .fragment.visible {
  opacity: 1;
  visibility: visible;
}

.mark-line { stroke: var(--accent); stroke-width: 8; stroke-linecap: round; }
.mark-arc {
  fill: none;
  stroke: var(--accent);
  stroke-width: 7;
  stroke-dasharray: 30 26;
  opacity: .9;
}
/* Подпись ложится на пересвеченный кадр, где белое на белом пропадает:
   тёмная обводка под заливкой держит её читаемой на любом месте снимка. */
.mark-text {
  fill: #fff;
  font-family: var(--font-head);
  font-size: 96px;
  font-weight: 600;
  paint-order: stroke;
  stroke: rgba(0, 0, 0, .8);
  stroke-width: 12px;
  stroke-linejoin: round;
}

/* ---------- ряд ---------- */

.blk-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 24px;
  flex: 1 1 auto;
  min-height: 0;
}
.row-cell { display: flex; flex-direction: column; min-height: 0; }
.row-cell > .blk { flex: 1; min-height: 0; }

/* ---------- титул, беат-развороты, финал ---------- */

.slide-accent .slide-inner { justify-content: center; padding: 46px 88px; }.accent-title {
  font-family: var(--font-head);
  font-weight: 700;
  letter-spacing: -.02em;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
}
.slide-title .accent-title { font-size: 92px; line-height: 1; }
.slide-closing .accent-title { font-size: 54px; line-height: 1.1; max-width: 18ch; }

/* Луч: горизонтальная линия развёртки под заголовком */
.accent-title::after {
  content: '';
  display: block;
  height: 2px;
  margin-top: 32px;
  background: linear-gradient(to right, var(--accent), transparent);
  box-shadow: 0 0 24px var(--glow);
  transform-origin: left center;
  width: 100%;
  max-width: 620px;
}
.reveal .present .accent-title::after { animation: beam-sweep .9s cubic-bezier(.2, .7, .2, 1) both; }

@keyframes beam-sweep {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: scaleX(1); opacity: 1; }
}

.accent-subtitle {
  color: var(--muted);
  margin: 28px 0 0;
  max-width: 40ch;
  line-height: 1.45;
  text-wrap: pretty;
}
.slide-title .accent-subtitle { font-size: 28px; color: var(--ink); }
.slide-closing .accent-subtitle { font-size: 23px; }
.accent-tag {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: .2em;
  color: var(--muted);
  text-transform: uppercase;
  margin: 52px 0 0;
}

/* Первый слайд беата — класс ставит deck.js, оформление живёт ниже */

/* ---------- непрерывность: кадровая развёртка и подача содержимого ----------

   Слайд не «сменяется», а перерисовывается: сверху вниз пробегает луч
   кадровой развёртки, и за ним проявляется содержимое. Внутри одного беата
   шапка стоит на месте и едет только начинка — именно это убирает
   ощущение перещёлкивания карточек. */

/* Сквозной электрон: шина под контентом, поверх слайда, под развёрткой */
.electron-lane {
  position: absolute;
  left: 0;
  top: 0;
  width: 1280px;
  height: 720px;
  z-index: 58;
  pointer-events: none;
}

/* Струйка «Турбулентности»: поверх фона, но под текстом слайда.

   z-index обязан быть меньше единицы. .reveal позиционирован с z-index:auto
   и своего контекста наложения не создаёт, поэтому слой и .reveal .slides
   (z-index: 1) меряются в одном контексте — на двойке дым шёл ПОВЕРХ
   заголовка и буллетов. Пока источник стоял справа от текста, это не было
   заметно, но клубы гуляют вширь, а на слайде акта 0 текста стало больше. */
.smoke-lane {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

.frame-scan {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 180px;
  z-index: 59;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(to bottom,
    transparent,
    rgba(255, 255, 255, .015) 55%,
    var(--glow) 96%,
    var(--accent) 100%);
}
.frame-scan.on { animation: frame-scan 460ms cubic-bezier(.35, 0, .25, 1); }

/* Титул: развёртка не останавливается. Трубка держит кадр ровно потому,
   что луч рисует его заново десятки раз в секунду, а люминофор между проходами
   медленно гаснет. На титуле это замедлено до видимого: зал рассаживается,
   а экран живёт. Текст при этом не исчезает — только дышит яркостью вслед за лучом. */
.frame-scan.loop { animation: frame-scan-loop 2600ms linear infinite; }

.slide-title.scan-loop.present .accent-title,
.slide-title.scan-loop.present .accent-subtitle,
.slide-title.scan-loop.present .accent-tag {
  animation: phosphor 2600ms linear infinite;
}
/* Фаза каждой строки считается в deck.js по её высоте на экране:
   строка вспыхивает в тот момент, когда через неё проходит луч. */

@keyframes phosphor {
  0%   { opacity: 1; }
  100% { opacity: .72; }
}

@media (prefers-reduced-motion: reduce) {
  .frame-scan.loop { animation: none; }
  .slide-title.scan-loop.present .accent-title,
  .slide-title.scan-loop.present .accent-subtitle,
  .slide-title.scan-loop.present .accent-tag { animation: none; }
}

@keyframes frame-scan {
  from { transform: translateY(-180px); opacity: .95; }
  to   { transform: translateY(540px); opacity: .35; }
}

/* Петля на титуле тише одиночного прохода: её смотрят минутами,
   а не полсекунды, и она не должна перетягивать внимание с названия. */
@keyframes frame-scan-loop {
  0%   { transform: translateY(-180px); opacity: 0; }
  12%  { opacity: .5; }
  88%  { opacity: .5; }
  100% { transform: translateY(540px); opacity: 0; }
}

/* Кадр рисуется самим лучом: маска раскрывается сверху вниз синхронно
   с нижней кромкой развёртки — та же длительность, тот же easing, тот же путь
   0→720. Луч не украшение перехода, а его причина: содержимое появляется ровно
   там, где он уже прошёл. */
.anim:not(.same-beat) .slide-inner {
  animation: paint-down 460ms cubic-bezier(.35, 0, .25, 1) both;
}

@keyframes paint-down {
  from { clip-path: inset(0 0 100% 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* Внутри беата: шапка неподвижна, блоки сдвигаются вбок — как кадр
   в одной и той же сцене */
.anim.same-beat .blocks > .blk {
  animation: shift-in 380ms cubic-bezier(.2, .7, .2, 1) both;
}
.anim.same-beat[data-nav='back'] .blocks > .blk { animation-name: shift-in-back; }
.anim.same-beat .blocks > .blk:nth-child(2) { animation-delay: 60ms; }
.anim.same-beat .blocks > .blk:nth-child(3) { animation-delay: 110ms; }

/* Новый беат или разворот: кадр рисует луч (paint-down выше).
   Листание назад — тот же ход, чтобы не ломать метафору: трубка рисует
   кадр только сверху вниз. */

@keyframes shift-in {
  from { opacity: 0; transform: translateX(34px); }
  to   { opacity: 1; transform: none; }
}
@keyframes shift-in-back {
  from { opacity: 0; transform: translateX(-34px); }
  to   { opacity: 1; transform: none; }
}

/* Первый слайд беата: к подъёму добавляется раскрытие линии шкалы */
.anim.beat-start .kicker::after { animation: scale-line 620ms cubic-bezier(.2, .7, .2, 1) both; }
@keyframes scale-line {
  from { transform: scaleX(0); transform-origin: left center; }
  to   { transform: scaleX(1); transform-origin: left center; }
}

@media print {
  .frame-scan { display: none; }
  .anim .slide-inner,
  .anim .slide-inner > *,
  .anim .blocks > .blk { animation: none !important; clip-path: none !important; }
}

/* ---------- буллеты по одному ---------- */

.reveal .slides section .fragment.visible { opacity: 1; }

/* ---------- обвязка reveal ---------- */

.reveal .progress { color: var(--accent); height: 2px; }
.reveal .slide-number {
  background: transparent;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: .1em;
  right: 22px;
  bottom: 14px;
}

/* Печать в PDF: развёртка и свечение только мешают */
@media print {
  .reveal-viewport::after { display: none; }
  .formula-text { text-shadow: none; }
}

/* ══════════════════ простая колода ══════════════════

   Метафора электронно-лучевой трубки — сканлайны, свечение люминофора,
   луч развёртки, прогрев экрана — придумана под «Электрон». В лекции про
   течения она не значит ничего, поэтому колода с meta.plain отключает её
   целиком и остаётся с чистой типографикой на тёмном поле.

   Отменяем эффекты, а не переопределяем вёрстку: раскладка, кегли и ритм
   у обеих колод общие. */

.deck-plain .reveal-viewport::after { display: none; }     /* сканлайны */
.deck-plain .frame-scan { display: none; }                 /* луч развёртки */

.deck-plain .formula-text,
.deck-plain .accent-title { text-shadow: none; }

.deck-plain .blk-formula { border-color: var(--line); }

/* никакого прогрева и выезда: слайд просто появляется */
.deck-plain .slide-inner,
.deck-plain .slide-inner > *,
.deck-plain .blocks > .blk,
.deck-plain .accent-title,
.deck-plain .accent-subtitle,
.deck-plain .accent-tag {
  animation: none !important;
  clip-path: none !important;
  opacity: 1 !important;
  transform: none !important;
}

/* ---------- разбор по шагам (blk-derive) ---------- */

.blk-derive {
  display: flex;
  flex-direction: column;
  gap: 18px;
  justify-content: center;
  flex: 1 1 auto;
}
.derive-row {
  display: grid;
  grid-template-columns: minmax(0, auto) 1fr;
  align-items: baseline;
  gap: 26px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--line);
}
.derive-row:last-child { border-bottom: 0; }
.derive-tex { font-size: 30px; color: var(--ink); white-space: nowrap; }
.derive-note { font-size: 22px; color: var(--muted); line-height: 1.35; }

/* Разбор в соседстве с формулой ужимается: слайд «Энергия из одной
   фотографии» несёт и плашку, и три шага. В одиночку разбор дышит как
   прежде — на слайде про размерности маятника ему отдан весь экран. */
.blocks > .blk-derive:not(:only-child) { gap: 12px; }
.blocks > .blk-derive:not(:only-child) .derive-row { padding-bottom: 12px; }
.blocks > .blk-derive:not(:only-child) .derive-tex { font-size: 27px; }
.blocks > .blk-derive:not(:only-child) .derive-note { font-size: 21px; }

/* KaTeX должен слушаться палитры слайда, а не тащить свой чёрный */
.katex { color: inherit; }
.blk-formula .katex { font-size: 1em; }

/* Своя вёрстка отступов. KaTeX в display-режиме ставит на формулу поля
   в 1em сверху и снизу, а em здесь — кегль формулы: при 54 px это 108 px
   пустоты вокруг каждой плашки. Именно из-за них слайд «Энергия из одной
   фотографии» не влезал в 720 px и терял строку с ответом. Расстояния
   между блоками задаёт раскладка слайда, формуле их держать не за чем. */
.formula-text .katex-display { margin: 0; }

/* [платформа] Служебный хром колоды: ссылка «← назад к неделе» в углу.
   Рендерит deck.js из meta.back спеки; живёт вне слайдов, полупрозрачная,
   в печать и аварийный PDF (?print-pdf) не попадает. */
.deck-back {
  position: fixed;
  top: 12px;
  left: 14px;
  z-index: 40;
  font-size: 14px;
  line-height: 1;
  padding: 6px 10px;
  border-radius: 8px;
  color: var(--muted);
  background: rgba(0, 0, 0, .25);
  text-decoration: none;
  white-space: nowrap;
  opacity: .45;
  transition: opacity .15s ease;
}
.deck-back:hover, .deck-back:focus-visible { opacity: 1; color: var(--ink); }
@media print { .deck-back { display: none; } }
