/* Вариант D · The Room — сцена под страницей.
 *
 * Фирменный приём: кадры романа висят в тёплой темноте на разной глубине,
 * скролл ведёт сквозь них камеру. Здесь только слой сцены и то, что
 * меняется, когда сцена запустилась; вся типографика и ритм — в
 * shared/base.css, общие для четырёх вариантов.
 *
 * Зерно и виньетка не дублируются: body::before и body::after из base.css
 * лежат на z-index: 1, то есть уже поверх canvas. Сцена получает ту же
 * плёнку, что и A/B/C, — варианты не расходятся по обработке.
 */

/* ── Слой сцены ─────────────────────────────────────────────────────────── */
/* Стопка слоёв целиком задана в base.css, здесь мы только встраиваемся в неё
   снизу:
     0  — #room, сцена;
     1  — body::before и body::after, зерно и виньетка;
     2  — .scene, содержимое секций;
     20 — .nav.
   Текст оказывается поверх сцены не из-за порядка в разметке, а потому что
   у .scene в base.css стоит явный z-index: 2. Порядок в разметке решал бы
   исход только между элементами одного уровня — и, не будь у секций своего
   z-index, canvas рисовался бы как раз ПОВЕРХ них: позиционированный элемент
   с z-index: 0 красится позже непозиционированного содержимого потока. */
#room {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* До запуска сцены canvas пустой и прозрачный: под ним фон body из
     base.css, страница выглядит как обычный вариант. */
  pointer-events: none;
}

/* ── Контраст строки на титуле ──────────────────────────────────────────── */
/* На узком экране (см. пересчёт взгляда для узкого aspect в scene.js) кадр
   «rain» сдвинут ближе к центру, и тёплый блик у лица героини оказывается
   прямо под строкой .line — а у неё тот же янтарный цвет (--amber), что и
   у блика. Геометрией камеры (см. scene.js) выше уже подвинули кадр
   настолько, насколько позволяет конус обзора без риска зацепить его
   правым краем видимую область, — дальше сдвигать некуда, а светлая зона
   блика шире, чем запас по углу. Тень возвращает контраст, не меняя ни
   цвет текста, ни его начертание; на других остановках у .line под ней
   тёмный или контрастный фон — трогать их незачем. */
#title .line { text-shadow: 0 1px 10px rgba(0, 0, 0, .55), 0 1px 3px rgba(0, 0, 0, .7); }

/* ── Skip-link ──────────────────────────────────────────────────────────── */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  /* Выше .nav (z-index: 20 в base.css): шапка фиксирована у верхнего края,
     непрозрачна и с размытием, а сфокусированная ссылка встаёт в левый
     верхний угол — с меньшим значением она оказывается прямо под шапкой и
     читателю не видна. Тот же приём и по той же причине в c-credits. */
  z-index: 30;
  background: var(--coal);
  color: var(--bone);
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 0.75rem 1rem;
}
.skip:focus {
  left: 0.5rem;
  top: 0.5rem;
}
#main:focus { outline: none; }

/* ── Слейты стали h2 ────────────────────────────────────────────────────── */
/* .slate перебивает h2 по специфичности во всём, что задаёт сама
   (гарнитура, кегль, трекинг, регистр), но line-height: 1.05 у h2 в
   base.css не перекрыт ничем — возвращаем служебной подписи её интерлиньяж. */
h2.slate { line-height: 1.4; }

/* ── Когда сцена запустилась ────────────────────────────────────────────── */
/* Кадры уезжают из вёрстки, но остаются в документе: они и есть источники
   текстур. Не display: none — скрытая так картинка в ряде браузеров не
   декодируется, а диктор теряет alt. Приём «визуально скрыто».

   Класс ставится на каждый кадр отдельно, когда его текстура уже в сцене.
   Общий класс на документе спрятал бы и те кадры, чьи текстуры не доехали
   на медленной сети, — содержание исчезло бы со страницы совсем. */
.shot.frame-on {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Портреты каста в сцене тоже показываются трёхмерно, но подписи под ними
   остаются на странице: прячем только картинку, не карточку целиком —
   и только на широких экранах, см. медиа-блок ниже. */
@media (min-width: 900px) {
  .cast-frame.frame-on {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  /* .cast-frame стоит в карточке первым — не между двумя соседями, как
     .shot в другой разметке, а в самом начале. Когда он уходит из потока,
     первым видимым элементом карточки становится figcaption, а
     .cast-card > * + * в base.css отмеряет отступ по разметке и не знает,
     что элемент перед figcaption пропал: без сброса все три карточки
     получили бы паразитный отступ сверху. */
  .cast-card > .cast-frame.frame-on + * { margin-top: 0; }
}

/* Ниже 900px .cast в shared/base.css становится свайп-каруселью
   (`@media (max-width: 899.98px)` там же) — карточка «фото плюс имя» едет
   по экрану как единое целое, как в вариантах A/B/C. Портрет в этом случае
   должен остаться в самой карточке: .cast-frame.frame-on тут НЕ скрываем
   (правило выше на этот диапазон не распространяется), а три кадра каста
   в самой 3D-сцене гасит cull() в scene.js (mesh.userData.cast) — иначе
   лицо было бы видно и в карточке, и неподвижным пятном в пространстве
   позади. Граница 900px — та же, что у карусели в base.css, до сотых
   умышленно не совпадает (899.98 vs 900): между ними зазора в 0.02px
   с реальным содержанием не бывает. */

/* ── Мягкие края у портретов каста ──────────────────────────────────────── */
/* В сцене у кадров нет рамки: они растворяются в темноте по всем четырём
   сторонам (FRAG в scene.js, FEATHER = 0.62). Но три портрета каста ниже
   900px в сцену не попадают — они остаются карточками на странице, и края
   у них выходили жёстким прямоугольником. Автор поймал разницу.
 
   Здесь та же растушёвка, пересчитанная в CSS. Пороги взяты из шейдера
   один в один: по вертикали кадр цел до 0.62 полустороны, по горизонтали
   порог пересчитан из пропорций 9:16 (1 - 0.38 / 0.5625 = 0.3244) — чтобы
   полоса растворения вышла одинаковой ширины со всех сторон, а не вчетверо
   уже по бокам. Остановки внутри полосы повторяют smoothstep: линейный
   градиент дал бы видимый залом на границе.
 
   Две маски на двух элементах, а не одна составная: mask-composite в
   Safari появился поздно, а вложенность тут и так есть. Внешний бокс
   гасит бока, само фото — верх и низ. */
.cast-frame {
  -webkit-mask-image: linear-gradient(to right,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.156) 8.44%, rgba(0,0,0,.5) 16.89%,
    rgba(0,0,0,.844) 25.33%, #000 33.78%, #000 66.22%,
    rgba(0,0,0,.844) 74.67%, rgba(0,0,0,.5) 83.11%,
    rgba(0,0,0,.156) 91.56%, rgba(0,0,0,0) 100%);
  mask-image: linear-gradient(to right,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.156) 8.44%, rgba(0,0,0,.5) 16.89%,
    rgba(0,0,0,.844) 25.33%, #000 33.78%, #000 66.22%,
    rgba(0,0,0,.844) 74.67%, rgba(0,0,0,.5) 83.11%,
    rgba(0,0,0,.156) 91.56%, rgba(0,0,0,0) 100%);
}
.cast-frame img {
  -webkit-mask-image: linear-gradient(to bottom,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.156) 4.75%, rgba(0,0,0,.5) 9.5%,
    rgba(0,0,0,.844) 14.25%, #000 19%, #000 81%,
    rgba(0,0,0,.844) 85.75%, rgba(0,0,0,.5) 90.5%,
    rgba(0,0,0,.156) 95.25%, rgba(0,0,0,0) 100%);
  mask-image: linear-gradient(to bottom,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.156) 4.75%, rgba(0,0,0,.5) 9.5%,
    rgba(0,0,0,.844) 14.25%, #000 19%, #000 81%,
    rgba(0,0,0,.844) 85.75%, rgba(0,0,0,.5) 90.5%,
    rgba(0,0,0,.156) 95.25%, rgba(0,0,0,0) 100%);
}

/* ── Чтение стихов на узком экране ──────────────────────────────────────── */
/* Листок со стихами, взятый на телефоне, выходит почти на весь экран и
   ложится на рассказ автора о себе: холст лежит под разметкой, и текст
   проступал бы сквозь бумагу. Пока листок держат, секция гаснет — читают
   сейчас стихи. Гаснет, а не прячется: подмена display сдвинула бы
   страницу и сбила бы прокрутку, а с ней и саму сцену.
   Класс ставит scene.js, и только ниже 900px. */
#author.reading .wrap > * {
  opacity: 0.08;
  transition: opacity .45s ease;
}
#author .wrap > * { transition: opacity .45s ease; }

/* ── Заголовок «Следующей книги» ────────────────────────────────────────── */
/* Правило заведено под прежнее длинное название («Ridgewood, You're Not
   My Weakness»). Нынешнее, «What Became of Our Souls», короче, но ширина
   оставлена: название может смениться снова. На 1440×900 длинное
   название в одну строку упирается в кадр кровати — камера смотрит на него по центру
   экрана, а заголовок без ограничения ширины (обычный h2, не .slate)
   дотягивается почти до его середины и ложится прямо на лицо героя.
   На мобильном и так уже 2-3 строки — ширины меньше `measure`. Сужаем
   только этот заголовок, не трогая h2 в других секциях. */
#next h2:not(.slate) { max-width: 24rem; }

/* ── Пролог поверх титульного кадра ─────────────────────────────────────── */
/* «Первые строки» — единственная секция, где кадр стоит СЛЕВА, под самим
   текстом, и не только на узком экране: титульный кадр переезжает туда на
   первом перегоне и остаётся там всю секцию (см. остановку 2 в beats.js).
   Раньше под прологом была темнота и тень была не нужна. Тень та же, что у
   #title .line, и по той же причине: снимок в нижней половине светлый
   (платье, блик лампы), и проза на нём гаснет. */
#first-lines .prose p,
#first-lines .slate,
#first-lines .meta {
  text-shadow: 0 1px 10px rgba(0, 0, 0, .55), 0 1px 3px rgba(0, 0, 0, .7);
}

/* ── Текст поверх полноэкранных кадров на узком экране ──────────────────── */
/* На широком экране кадр стоит правее (aside в beats.js), под текстом
   темнота — тень там лишняя. На узком сдвига нет (lookOf() в scene.js
   возвращает центр кадра), кадр по центру за текстом, и без тени проза на
   светлых местах снимка гаснет. Та же тень, что у #title .line, и по той же
   причине. */
@media (max-width: 899.98px) {
  #story .prose p,
  #episodes .episode,
  /* «Читатели»: на узком экране колесо стоит вертикальной колонкой ровно
     под цифрами (см. RING.narrow в beats.js), на широком оно высоко над
     коридором и текста не касается. */
  #readers .stat-num,
  #readers .line,
  #readers .meta,
  #next h2:not(.slate),
  #next .line,
  #follow .meta {
    text-shadow: 0 1px 10px rgba(0, 0, 0, .55), 0 1px 3px rgba(0, 0, 0, .7);
  }

}

/* ── Лента эпизодов уже кадра ───────────────────────────────────────────── */
/* .episode в base.css — непрозрачные плашки на всю ширину .wrap (72rem): на
   широком экране они закрывали кадр kiss, ради которого камера к нему
   подъезжает. Сужаем ленту только здесь и только на широком экране — текст
   слева, кадр справа, как на титуле; в A/B/C кадр стоит под лентой, там
   ширина не мешает. Ниже 900px кадр по центру за текстом, лента во всю
   ширину как была. */
@media (min-width: 900px) {
  #episodes .episodes { max-width: 40rem; }
}

/* ── Видео на финале ────────────────────────────────────────────────────────── */
/* Пока сцена не взяла видео, это обычная фигура с плеером — фолбэк без JS
   и без WebGL. Кнопка звука в фолбэке не нужна: у плеера свои controls. */
.sound-row { display: none; }

/* Фолбэк: .shot в base.css — широкий бокс 448×270 под горизонтальную обрезку
   фото, а ролик вертикальный 9:16 и с плеером — в широком боксе от него
   осталась бы полоска без кнопок. Бокс видео — портретный; само видео
   растянуто на бокс, как .shot img. */
.shot.shot-video {
  width: min(18rem, 60vw);
  height: auto;
  aspect-ratio: 9 / 16;
}
/* Тонирующая подложка .shot::after (base.css) лежит поверх содержимого и
   перехватывает клики по кнопкам плеера — у видео её делаем сквозной. */
.shot.shot-video::after { pointer-events: none; }
.shot-video video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Сцена взяла видео. Обычный .frame-on сжимает кадр до 1×1 — для видео это
   опасно: Chrome на Android ставит на паузу беззвучное видео, которое
   считает не попавшим в экран, и текстура замирает. Поэтому фигура уходит
   из потока, растягивается на всю секцию (.scene — position: relative) и
   прячется прозрачностью: пока финал на экране, элемент тоже «в экране».
   Специфичность три класса против двух у .shot.frame-on. */
.shot.shot-video.frame-on {
  position: absolute;
  inset: 0;
  width: auto;
  height: auto;
  aspect-ratio: auto;
  margin: 0;
  overflow: hidden;
  clip-path: none;
  white-space: normal;
  opacity: 0;
  pointer-events: none;
}
/* Фигуры лежат в обёртке .reels, соседский селектор до .sound-row больше не
   дотягивается — спрашиваем у общего предка. */
.wrap:has(.shot-video.frame-on) .sound-row { display: block; }

/* Фолбэк: три вертикальных плеера в ряд на широком экране, столбиком на
   узком. Ширина колонки ограничена — иначе на 1440 каждый плеер был бы
   780 px высотой и финал уехал бы на три экрана. */
.reels { display: grid; gap: var(--gap); }
@media (min-width: 900px) {
  .reels { grid-template-columns: repeat(3, 1fr); align-items: start; justify-items: center; }
}
.reels .shot.shot-video { width: 100%; max-width: 14rem; }
.reel-dots { display: none; }

/* Телефон: ролики всегда в вёрстке (scene.js не берёт их в сцену ниже
   900px) — лента с листанием вбок, по одному, соседи выглядывают из-за
   краёв, чтобы было видно: роликов больше. Лента выходит за поля .wrap до
   краёв экрана, первый и последний ролик встают по центру за счёт полей
   самой ленты. Точки под лентой ставит reels.js. */
@media (max-width: 899.98px) {
  #follow .reels {
    --slide: min(72vw, 20rem);
    display: flex;
    gap: .9rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    margin-inline: calc(-1 * var(--gap));
    padding-inline: calc((100vw - var(--slide)) / 2);
    scroll-padding-inline: calc((100vw - var(--slide)) / 2);
  }
  #follow .reels::-webkit-scrollbar { display: none; }
  #follow .reels .shot.shot-video {
    flex: 0 0 var(--slide);
    width: var(--slide);
    max-width: none;
    margin: 0;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    box-shadow: 0 10px 40px rgba(0, 0, 0, .55);
  }
  #follow .reel-dots {
    display: flex;
    justify-content: center;
    gap: .35rem;
    margin-top: .9rem;
  }
  .reel-dots button {
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    display: grid;
    place-items: center;
  }
  .reel-dots button::before {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--bone) 35%, transparent);
    transition: background .3s, transform .3s;
  }
  .reel-dots button[aria-current="true"]::before {
    background: var(--amber);
    transform: scale(1.25);
  }
}

/* .btn-ghost в base.css написан для ссылок: у <button> свой шрифт и кегль
   от браузера, наследование включаем здесь. */
button.btn-ghost {
  font: inherit;
  font-family: var(--mono);
  cursor: pointer;
}

/* ── Стихи на шёлке ─────────────────────────────────────────────────────── */
/* #poem — по экрану прокрутки на строфу. Со сценой строфы пишутся светом по
   шёлку (author/stage.js), а разметка остаётся в документе для диктора, но
   не видна — приём «визуально скрыто». Без сцены это обычный текст. */
.poem-run { position: relative; z-index: 2; outline: none; }
.poem-run .passage {
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding: clamp(4rem, 10vh, 8rem) var(--gap);
}
.poem-run .stanza { margin: 0; }
.poem-run .stanza p { font-style: italic; font-size: clamp(1.1rem, 1.8vw, 1.4rem); line-height: 1.65; }
.poem-run .poem-title { font-family: var(--display); font-weight: 400; }
.silk-on .poem-run .wrap {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.poem-run:focus-visible::after {
  content: ""; position: fixed; inset: 1rem; pointer-events: none;
  border: 1px solid color-mix(in srgb, var(--amber) 55%, transparent);
  border-radius: .25rem;
}

/* Слой шёлка: над сценой, под зерном и текстом. Пока его нет в кадре,
   он скрыт и не рисует. */
#author-silk {
  position: fixed; inset: 0; z-index: 0;
  width: 100%; height: 100%; display: block;
  pointer-events: none; visibility: hidden;
}

/* Пока читают стихи на шёлке, текст соседних секций, заехавший в кадр,
   не спорит со строфой. */
.silk-reading #author .wrap, .silk-reading #next .wrap { opacity: .0; transition: opacity .6s ease; }
#author .wrap, #next .wrap { transition: opacity .6s ease; }

.poem-count {
  position: fixed; left: 0; right: 0; bottom: 1.6rem; z-index: 3;
  margin: 0; text-align: center; pointer-events: none; opacity: 0;
  font: 400 .68rem/1 var(--mono); letter-spacing: .12em;
  color: color-mix(in srgb, var(--bone) 34%, transparent);
}

/* ── Ответ на стихи ─────────────────────────────────────────────────────── */
/* «Write to Diana» под последней строфой. Со сценой строфа нарисована на
   холсте, а ссылка — живая разметка под подписью: проявляется, когда
   строфа дописана (класс poem-reply-on ставит author/stage.js). Скрытая —
   ещё и visibility, чтобы до неё не доходил Tab. */
.poem-run .passage { position: relative; }
.poem-reply { margin-top: 1.6rem; }
.poem-reply a {
  font-family: var(--prose); font-style: italic;
  font-size: clamp(1.05rem, 1.5vw, 1.25rem);
  color: var(--amber); text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--amber) 40%, transparent);
  padding-bottom: .15em;
  transition: border-color .3s ease, color .3s ease;
}
.poem-reply a:hover, .poem-reply a:focus-visible {
  color: color-mix(in srgb, var(--amber) 80%, var(--bone));
  border-bottom-color: currentColor;
}
.silk-on .poem-reply {
  position: absolute; left: 0; right: 0; bottom: 17svh; margin: 0;
  text-align: center;
  opacity: 0; visibility: hidden;
  transition: opacity 1.4s ease, visibility 0s linear 1.4s;
}
.silk-on.poem-reply-on .poem-reply {
  opacity: 1; visibility: visible;
  transition: opacity 1.4s ease .4s, visibility 0s linear 0s;
}

/* ── Кнопки финала ──────────────────────────────────────────────────────── */
/* Третья кнопка, «Write to Diana», на узком экране уходит на вторую строку —
   без зазора по вертикали строки кнопок слипаются. */
.follow-row { display: flex; flex-wrap: wrap; gap: .75rem .5rem; }

/* ── Эпизоды: светлая дымка вместо плашек ───────────────────────────────── */
/* Номера и даты убраны (25.09). Вместо тёмных прямоугольных плашек с
   волосяными линиями каждая фраза лежит в мягком тёплом пятне света: в
   центре светлее фона, к краям растворяется без границы. Это обратный ход
   к кадрам на сайте — у них края темнеют и уходят во тьму, у фраз светлеют.
   На узком экране под светом ещё мягкая тёмная подложка (см. выше), потому
   что там лента лежит поверх кадра kiss. */
#episodes .episodes {
  background: none;
  gap: clamp(0.5rem, 1.6vh, 1.1rem);
}
#episodes .episode {
  grid-template-columns: 1fr auto;
  padding: clamp(1.1rem, 2.8vw, 2rem) clamp(1.25rem, 3.5vw, 2.5rem);
  background: radial-gradient(ellipse 68% 62% at 50% 50%,
    color-mix(in srgb, var(--amber) 20%, transparent),
    color-mix(in srgb, var(--bone) 8%, transparent) 52%,
    transparent 76%);
}
@media (max-width: 599.98px) {
  #episodes .episode { grid-template-columns: 1fr; }
}
/* Узкий экран: лента лежит поверх кадра kiss — под светом мягкая тёмная
   подложка, тоже без границ, иначе фраза на светлых местах снимка гаснет.
   Стоит после общего правила, иначе оно перебило бы это. */
@media (max-width: 899.98px) {
  #episodes .episode {
    background:
      radial-gradient(ellipse 72% 62% at 50% 50%,
        color-mix(in srgb, var(--amber) 18%, transparent),
        color-mix(in srgb, var(--bone) 7%, transparent) 52%,
        transparent 80%),
      radial-gradient(ellipse 80% 75% at 50% 50%,
        color-mix(in srgb, var(--coal) 62%, transparent) 40%,
        transparent 82%);
  }
}
