/* ===========================================================================
   Gnaizel — personal card
   ---------------------------------------------------------------------------
   Чёрная тема, одна на весь сайт: моноширинный шрифт, ни одной рамки,
   один зелёный акцент. Секции ничем не обведены и не разделены линиями:
   каждую открывает заголовок с зелёной иконкой, а заливка остаётся
   только у карточек, по которым реально кликают или скроллят.
   Зелёный живёт в интерактиве и маркерах, но не в основном тексте —
   иначе страница уходит в неон.
   Перекрасить акцент: --accent и --accent-rgb рядом с ним.
   =========================================================================== */

:root {
    color-scheme: dark;

    /* поверхности */
    --bg:          #000000;
    --panel:       #0c0c0c;
    --panel-soft:  #121212;
    --panel-hover: #1a1a1a;
    --line:        #262626;
    --line-soft:   #1b1b1b;

    /* текст */
    --text:   #e4e4e4;
    --strong: #ffffff;
    --muted:  #9c9c9c;
    --dim:    #858585;
    --faint:  #444444;

    /* Акцент. --accent-rgb нужен heatmap'у, полоскам ротации и подложкам:
       они красят прозрачностью того же цвета, чтобы шкала не разъезжалась
       с акцентом. --accent-mark — маркеры списков, самый тихий оттенок акцента. */
    --accent:      #46c07a;
    --accent-soft: rgba(70, 192, 122, .10);
    --accent-line: rgba(70, 192, 122, .40);
    --accent-mark: rgba(70, 192, 122, .55);
    --accent-rgb:  70, 192, 122;

    --font: "JetBrains Mono", ui-monospace, "SF Mono", "Consolas", "DejaVu Sans Mono", monospace;

    --radius: 4px;
    /* Три яруса скруглений: мелочь вроде кнопок и полей — --radius, плитки
       (игры, лента TikTok, поле гостевой) — --radius-tile, крупное фото — --radius-card. */
    --radius-tile: 14px;
    --radius-card: 24px;
    /* плашка «где послушать» и её тень; от этого числа считается TAB_FLUSH в time.js */
    --radius-sheet: 10px;
    --shell:  1000px;
    --side:   290px;
}

/* класс ставит сервер, пока не наступил halloween.until из application.yaml */
.halloween {
    --accent:      #ff8a2a;
    --accent-soft: rgba(255, 138, 42, .10);
    --accent-line: rgba(255, 138, 42, .40);
    --accent-mark: rgba(255, 138, 42, .55);
    --accent-rgb:  255, 138, 42;
}

/* --------------------------------------------------------------- reset -- */

*, *::before, *::after { box-sizing: border-box; }

html { background: var(--bg); }

body {
    margin: 0;
    /* viewport-fit=cover в myCard.html: в альбомной ориентации вырез экрана заходит на поля */
    padding: 0 max(20px, env(safe-area-inset-right)) 0 max(20px, env(safe-area-inset-left));
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    font-size: 14px;
    line-height: 1.65;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    overflow-x: hidden;
}

h1, h2, h3, h4, p, ul, figure { margin: 0; padding: 0; }
ul { list-style: none; }
img { display: block; }

a {
    color: var(--text);
    text-decoration: none;
    transition: color .15s ease;
}
a:hover { color: var(--accent); }

a,
button {
    touch-action: manipulation;
    -webkit-tap-highlight-color: rgba(var(--accent-rgb), .12);
}

::selection { background: var(--accent-line); color: #fff; }

::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--faint); }

/* --------------------------------------------------------------- shell -- */

.shell {
    position: relative;
    z-index: 1;
    max-width: var(--shell);
    margin-inline: auto;
    padding-block: 64px 0;
}

/* -------------------------------------------------------------- layout --
   Две колонки: слева узкая «визитка» (кто я), справа широкая лента (что я
   слушаю/на чём сижу/во что играю). Раньше всё шло одной стопкой одинаковых
   коробок — не за что зацепиться глазу. */

.layout {
    display: grid;
    grid-template-columns: var(--side) minmax(0, 1fr);
    gap: 34px;
    align-items: start;
}

.col-side,
.col-main {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* в узкой колонке ссылки в два столбца не помещаются */
.col-side .links { grid-template-columns: minmax(0, 1fr); }

/* Знак занимает только свободное место под левой колонкой: нет места
   (лента TikTok развёрнута) — знак прячется, а не удлиняет страницу. */
.col-side { align-self: stretch; }
.side-mark {
    flex: 1 1 0;
    min-height: 0;
    container-type: size;
    /* clip, а не hidden: hidden делает блок прокручиваемым, и sticky внутри перестаёт липнуть */
    overflow: clip;
}
.side-mark img {
    position: sticky;
    top: 40vh;
    width: 190px;
    margin: 80px auto 0;
    opacity: .07;
}
@container (max-height: 340px) {
    .side-mark img { display: none; }
}

/* ------------------------------------------------------- карточка-имя -- */

/* Левый флаг, как у всей остальной страницы: центрированный блок посреди
   левофлаговой вёрстки и был главной причиной, почему шапка выглядела
   приклеенной со стороны. */
.card-id {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Крупное фото — единственная большая точка на странице, всё остальное мельче.
   Поэтому и ярус скругления у него свой, самый крупный: с --radius,
   ярусом для кнопок и полей, самый большой элемент страницы выпадал из
   семейства и читался как наклейка поверх вёрстки.

   Аватарок несколько — это фото профиля в Telegram. Кадры лежат стопкой,
   виден один; листают нажатием по половине кадра, как в самом Telegram,
   свайпом, стрелками клавиатуры и кнопками по наведению. */
.card-id .avatar {
    position: relative;
    width: 100%;
    aspect-ratio: 1;
    overflow: hidden;
    /* иначе Safari на время перехода между кадрами теряет скругление */
    isolation: isolate;
    border-radius: var(--radius-card);
    border: 1px solid var(--line-soft);
    background: var(--panel);
    /* вертикальный свайп остаётся странице, горизонтальный листает */
    touch-action: pan-y;
    user-select: none;
    -webkit-user-select: none;
    -webkit-tap-highlight-color: transparent;
    outline: none;
}
.card-id .avatar.many { cursor: pointer; }
.card-id .avatar:focus-visible { box-shadow: 0 0 0 2px var(--accent-line); }

.avatar-frame {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity .28s ease;
}
.avatar-frame.on { opacity: 1; }

/* Фильтр — не про конкретный кадр, а про любой следующий: страница почти
   монохромная и очень тёмная, а в профиль ставят что угодно. Приглушение
   удерживает фото в палитре, оставляя его цветным и узнаваемым, и избавляет
   от необходимости подбирать аватарку «подходящую по цвету». */
.avatar-frame img,
.avatar-frame video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
    -webkit-user-drag: none;
    filter: saturate(.5) brightness(.82) contrast(1.06);
}

/* Видео ложится поверх своего же неподвижного кадра и проявляется, когда
   реально пошло: до первого кадра на его месте был бы чёрный квадрат. */
.avatar-frame video {
    opacity: 0;
    transition: opacity .2s ease;
}
.avatar-frame.rolling video { opacity: 1; }

/* Счётчик — только когда фото много: в двух десятках иначе не понять,
   где ты. Полосок, как в Telegram, нет — лишнее поверх фото. */
.avatar-count {
    position: absolute;
    right: 10px;
    bottom: 10px;
    z-index: 2;
    padding: 1px 8px;
    border-radius: 999px;
    background: rgba(0, 0, 0, .55);
    color: var(--text);
    font-size: 11.5px;
    line-height: 1.6;
    pointer-events: none;
}

.avatar-arrow {
    position: absolute;
    top: 50%;
    z-index: 2;
    transform: translateY(-50%);
    width: 30px;
    height: 30px;
    border: 0;
    border-radius: 50%;
    background: rgba(0, 0, 0, .5);
    color: var(--text);
    font: inherit;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
    transition: opacity .15s ease, color .15s ease;
}
.avatar-arrow.prev { left: 10px; }
.avatar-arrow.next { right: 10px; }
/* Стрелки — только мышью: пальцем листают нажатием и свайпом, а на
   маленькой аватарке телефона кнопки закрыли бы пол-лица. */
@media (hover: hover) and (pointer: fine) {
    .card-id .avatar:hover .avatar-arrow,
    .card-id .avatar:focus-visible .avatar-arrow { opacity: .8; }
    .card-id .avatar .avatar-arrow:hover { opacity: 1; color: var(--accent); }
}
@media not all and (hover: hover) and (pointer: fine) {
    .avatar-arrow { display: none; }
}

.card-id-text { min-width: 0; }

/* 28px — предел, в котором моноширинный шрифт ещё выглядит набранным, а не
   растянутым, и при этом остаётся крупнейшим текстом страницы.
   > повторяет приглашение из гостевой: имя говорит на языке остального сайта. */
/* Ник и алиас одной строкой: алиас перестаёт быть отдельной мелкой строкой
   в стопке и читается как приписка к имени. */
.wordmark {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    /* Расстояния тут точечные, потому что они разные: приглашение жмётся
       к имени, а алиас наоборот отодвинут. Первое число — перенос строки. */
    gap: 2px .3em;
    font-size: 24px;
    font-weight: 500;
    letter-spacing: -0.02em;
    color: var(--strong);
    line-height: 1.15;
}

/* Своего отступа у приглашения нет: как флекс-элемент оно и так получает
   зазор контейнера, а вместе с margin выходило 20px вместо 7. */
.wordmark::before {
    content: ">";
    color: var(--accent);
}

/* второй ник — тише всего на странице: он подпись, а не заголовок */
.wordmark-alias {
    margin-left: .25em;
    font-size: 11.5px;
    font-weight: 400;
    letter-spacing: .02em;
    line-height: 1.4;
    color: var(--dim);
}

/* под алиасом нужен воздух, иначе две мелкие строки читаются одним комком */
.role {
    margin-top: 6px;
    color: var(--muted);
    font-size: 13px;
}

/* --------------------------------------------------------------- panel -- */

/* Секция — не коробка: ни фона, ни рамки, ни полей по бокам, ни линии
   между соседями — их разделяет только отступ. */
.col-side > .panel + .panel,
.col-main > .panel + .panel {
    margin-top: 34px;
}
.col-side > .about + .contacts {
    margin-top: 20px;
}

.panel-title {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 14px;
    font-weight: 500;
    color: var(--text);
}
.panel-title svg {
    width: 1.05em;
    height: 1.05em;
    flex: 0 0 auto;
    color: var(--accent);
}

.panel-sub {
    font-size: 12.5px;
    color: var(--dim);
}

/* Подпись со значком репоста. flex с gap, а не отступ у svg: так значок
   встаёт ровно по центру строки при любом размере шрифта, без подгонки
   vertical-align. Размер — в em, чтобы он тянулся за текстом, а не
   расходился с ним, если шрифт подписи когда-нибудь поменяется. */
.repost-sub {
    display: flex;
    align-items: center;
    gap: 5px;
}
.repost-sub svg {
    width: 1em;
    height: 1em;
    flex: 0 0 auto;
    color: var(--accent-mark);
}

.panel-body { margin-top: 12px; }

/* ---------------------------------------------------------- hours note -- */

/* Значок «!» в правом верхнем углу блока игр: откуда часы и почему их
   меньше, чем на самом деле. С мышью подсказка открывается наведением,
   на телефоне — нажатием: наводить там нечем. Значок стоит по центру
   строки заголовка, а его край — по краю колонки. top подогнан под строку
   в 23px и кнопку в 24px. */
.game-section { position: relative; }
.hours-note {
    position: absolute;
    top: -1px;
    right: -4px;
    z-index: 20;
}
.hours-note-btn {
    display: flex;
    padding: 4px;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--dim);
    cursor: pointer;
    transition: color .15s ease;
}
.hours-note-btn svg { width: 16px; height: 16px; }
.hours-note-btn:focus-visible { outline: 1px solid var(--accent-line); }
.hours-note-btn[aria-expanded="true"] { color: var(--accent); }

.hours-note-pop {
    position: absolute;
    top: calc(100% + 8px);
    right: -6px;
    width: 300px;
    max-width: calc(100vw - 40px);
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-tile);
    background: var(--panel-soft);
    box-shadow: 0 14px 34px rgba(0, 0, 0, .55), 0 2px 6px rgba(0, 0, 0, .35);
    color: var(--muted);
    font-size: 12.5px;
    line-height: 1.6;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity .16s ease, transform .16s ease, visibility 0s linear .16s;
}
/* Уголок смотрит точно в центр значка. */
.hours-note-pop::before {
    content: "";
    position: absolute;
    top: -5px;
    right: 14px;
    width: 8px;
    height: 8px;
    border-left: 1px solid var(--line);
    border-top: 1px solid var(--line);
    background: var(--panel-soft);
    transform: rotate(45deg);
}
/* Мостик через зазор: без него мышь по пути от значка к тексту на миг
   теряет наведение, и подсказка захлопывается. */
.hours-note-pop::after {
    content: "";
    position: absolute;
    right: 0;
    bottom: 100%;
    left: 0;
    height: 10px;
}
/* pretty — чтобы последнее слово абзаца не висело на строке в одиночку */
.hours-note-pop p { text-wrap: pretty; }
.hours-note-pop p + p { margin-top: 7px; }
.hours-note-pop strong { color: var(--text); font-weight: 500; }
.hours-note-pop .hours-note-lost { color: var(--dim); }

.hours-note-btn[aria-expanded="true"] + .hours-note-pop,
.hours-note-btn:focus-visible + .hours-note-pop {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 0s;
}
/* Только там, где наведение настоящее: на телефоне :hover залипает после
   касания, и подсказка не закрывалась бы. */
@media (hover: hover) {
    .hours-note-btn:hover { color: var(--accent); }
    .hours-note:hover .hours-note-pop {
        opacity: 1;
        visibility: visible;
        transform: none;
        transition-delay: 0s;
    }
}

/* ------------------------------------------------------------ metadata -- */

.meta-rows li {
    display: flex;
    flex-wrap: wrap;
    gap: 0 10px;
    padding: 2px 0;
}
/* ширина — как у иконок ссылок (.link-card img), зазор тот же 10px:
   ключи about и названия ссылок под ним начинаются с одной вертикали */
.meta-rows li::before {
    content: "—";
    width: 17px;
    color: var(--accent-mark);
    flex: 0 0 auto;
}
.meta-rows .k { color: var(--dim); }
.meta-rows .v { color: var(--text); }

.meta-rows .langs { display: flex; flex-direction: column; }
.lang-level { color: var(--muted); }

.lang-note {
    position: relative;
    display: inline-flex;
    vertical-align: middle;
    margin-left: 6px;
}
.lang-note-btn {
    display: flex;
    padding: 2px 3px;
    border: 0;
    border-radius: var(--radius);
    background: none;
    color: var(--accent);
    cursor: pointer;
}
.lang-note-btn:focus-visible { outline: 1px solid var(--accent-line); }
/* пальцу нужна зона больше значка: пустой ::after и есть эта зона */
@media not all and (hover: hover) and (pointer: fine) {
    .lang-note-btn { position: relative; }
    .lang-note-btn::after { content: ""; position: absolute; inset: -6px -5px; }
}
.lang-rise { width: 10px; height: 14px; }
/* Пути в svg идут снизу вверх, поэтому задержки гонят волну вверх. */
.lang-rise path { opacity: .3; animation: lang-rise 2.4s ease-in-out infinite; }
.lang-rise path:nth-child(2) { animation-delay: .3s; }
.lang-rise path:nth-child(3) { animation-delay: .6s; }
@keyframes lang-rise {
    0%, 40%, 100% { opacity: .3; }
    20%           { opacity: .85; }
}

.lang-note-pop {
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    z-index: 20;
    padding: 5px 10px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel-soft);
    box-shadow: 0 10px 26px rgba(0, 0, 0, .55), 0 2px 5px rgba(0, 0, 0, .35);
    color: var(--muted);
    font-size: 12.5px;
    white-space: nowrap;
    opacity: 0;
    visibility: hidden;
    transform: translate(-50%, 4px);
    transition: opacity .16s ease, transform .16s ease, visibility 0s linear .16s;
}
.lang-note-btn[aria-expanded="true"] + .lang-note-pop,
.lang-note-btn:focus-visible + .lang-note-pop {
    opacity: 1;
    visibility: visible;
    transform: translate(-50%, 0);
    transition-delay: 0s;
}
@media (hover: hover) {
    .lang-note:hover .lang-note-pop {
        opacity: 1;
        visibility: visible;
        transform: translate(-50%, 0);
        transition-delay: 0s;
    }
}

/* --------------------------------------------------------------- chips -- */

.chips {
    display: flex;
    flex-wrap: wrap;
    gap: 12px 20px;
}

.chip {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    color: var(--muted);
    font-size: 12.5px;
}

/* Логотипы приглушены до монохрома и оживают по наведению — ровно та же
   обработка, что у иконок в ссылках, чтобы блоки читались как родня. */
.chip-icon {
    width: 26px;
    height: 26px;
    flex: 0 0 auto;
    filter: grayscale(1) brightness(1.35);
    opacity: .7;
    transition: filter .15s ease, opacity .15s ease;
}
.chip:hover .chip-icon {
    filter: none;
    opacity: 1;
}

/* Название всплывает под чипом. Из разметки оно никуда не девается, только
   прячется прозрачностью, поэтому скринридер читает его всегда. Снизу, а не
   сверху: сверху подсказка налезала бы на заголовок [ stack ]. */
.chip-name {
    position: absolute;
    top: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 3;
    padding: 3px 8px;
    border-radius: 6px;
    background: var(--panel-hover);
    color: var(--text);
    font-size: 11.5px;
    line-height: 1.4;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity .15s ease;
}
.chip:hover .chip-name { opacity: 1; }

/* На тач-экранах наведения не существует, и подсказку было бы не достать —
   там названия просто стоят рядом с логотипом, как раньше. */
@media (hover: none) {
    /* здесь смысл несёт подпись, а не значок, поэтому логотип поменьше */
    .chip-icon { width: 18px; height: 18px; }
    .chip-name {
        position: static;
        transform: none;
        z-index: auto;
        padding: 0;
        background: none;
        color: inherit;
        font-size: 12.5px;
        opacity: 1;
    }
}
.chip:hover { color: var(--text); }

/* --------------------------------------------------------------- links -- */

.links {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 32px;
}

.link-card {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 3px 0;
    min-width: 0;
}
.link-card:hover .link-name { color: var(--accent); }

/* Логотипы приглушены до монохрома, цвет возвращается по наведению.
   brightness(0) invert(1) здесь не годится: у telegram/steam заливка и глиф
   схлопываются в один белый круг. */
.link-card img {
    width: 17px;
    height: 17px;
    flex: 0 0 auto;
    filter: grayscale(1) brightness(1.35);
    opacity: .7;
    transition: filter .15s ease, opacity .15s ease;
}
.link-card:hover img {
    filter: none;
    opacity: 1;
}

.link-name { color: var(--text); font-size: 13px; transition: color .15s ease; }

.link-handle {
    margin-left: auto;
    color: var(--dim);
    font-size: 12.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Steam: карточка не ссылка, а раскрывашка — аккаунтов несколько, и какой
   открыть, выбирают по аватарке. Нажатие растягивает саму карточку вниз;
   высота анимируется через grid-template-rows 0fr → 1fr, так её не нужно
   знать заранее. */
.steam-card {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0;
}
.steam-card.open .steam-head img { filter: none; opacity: 1; }
.steam-card.open .link-name { color: var(--accent); }

/* В сетке в две колонки открытая карточка иначе растянула бы соседку
   по строке, и та раздулась бы пустой. */
.links { align-items: start; }

.steam-head {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 3px 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}
.steam-head:focus-visible { outline: 1px solid var(--accent-line); outline-offset: -1px; }

.steam-handle { transition: color .15s ease; }
.steam-handle.is-name { color: var(--accent); }

.steam-chevron {
    width: 14px;
    height: 14px;
    flex: 0 0 auto;
    margin-left: -3px;
    color: var(--dim);
    transition: transform .3s ease, color .15s ease;
}
.steam-head:hover .steam-chevron,
.steam-card.open .steam-chevron { color: var(--accent); }
.steam-card.open .steam-chevron { transform: rotate(180deg); }
/* аккаунты не пришли — разворачивать нечего, карточка просто ссылка */
.steam-card.no-accounts .steam-chevron { display: none; }

.steam-drawer {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows .3s ease;
}
.steam-card.open .steam-drawer { grid-template-rows: 1fr; }

/* Шесть в ряд при любой ширине карточки: шесть аккаунтов ложатся ровно
   от края до края, а седьмой ушёл бы строкой ниже, не ломая размер. */
.steam-accounts {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 7px;
    min-height: 0;
    overflow: hidden;
    transition: padding .3s ease;
}
.steam-card.open .steam-accounts { padding: 6px 0 8px; }

/* Выезжают по одной, слева направо: --i ставит скрипт. backwards, а не
   both — иначе конечный кадр анимации перебивал бы transform при наведении. */
.steam-account {
    position: relative;
    display: block;
    aspect-ratio: 1;
    border-radius: 7px;
    overflow: hidden;
    background: var(--panel-hover);
}
.steam-card.open .steam-account {
    animation: steam-avatar-in .32s cubic-bezier(.2, .7, .3, 1) backwards;
    animation-delay: calc(var(--i, 0) * 35ms + 70ms);
}
@keyframes steam-avatar-in {
    from { opacity: 0; transform: translateY(6px) scale(.94); }
}

/* Селекторы через .steam-card, чтобы перебить .link-card img — иначе
   аватарка сжалась бы до размера логотипа и посерела. */
.steam-card .steam-account img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: brightness(.82) saturate(.9);
    opacity: 1;
    transition: filter .2s ease, opacity .2s ease, transform .3s ease;
}
.steam-card .steam-account:hover img,
.steam-card .steam-account:focus-visible img {
    filter: none;
    transform: scale(1.06);
}
/* Наведена одна — остальные отходят в тень, видно, какую выбираешь. */
.steam-accounts:has(.steam-account:hover) .steam-account:not(:hover) img { opacity: .4; }

/* Кольцо поверх картинки, а не снаружи: светлые аватарки иначе сливаются
   с краем, а внешнее кольцо обрезал бы overflow ряда. */
.steam-account::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06);
    pointer-events: none;
    transition: box-shadow .2s ease;
}
/* Выше :hover — иначе наведённая основная осталась бы с тонким кольцом. */
.steam-account.is-main::after { box-shadow: inset 0 0 0 1px var(--accent); }
.steam-account:hover::after,
.steam-account:focus-visible::after { box-shadow: inset 0 0 0 2px var(--accent); }
.steam-account:focus-visible { outline: none; }

.steam-crown {
    position: absolute;
    top: 3px;
    right: 3px;
    width: 11px;
    height: 11px;
    fill: var(--accent);
    filter: drop-shadow(0 0 1.5px rgba(0, 0, 0, .9));
    pointer-events: none;
    transition: opacity .2s ease;
}
.steam-accounts:has(.steam-account:hover) .steam-account:not(:hover) .steam-crown { opacity: .4; }

/* ------------------------------------------------------ h-scroll shell -- */

.scroller { position: relative; }

/* --fade-* выставляет initScrollers(): край растворяется только с той стороны,
   куда ленту ещё можно прокрутить. */
.scroll-rail {
    --fade-l: 0px;
    --fade-r: 0px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
    scroll-behavior: smooth;
    mask-image: linear-gradient(to right,
        transparent 0, #000 var(--fade-l),
        #000 calc(100% - var(--fade-r)), transparent 100%);
    -webkit-mask-image: linear-gradient(to right,
        transparent 0, #000 var(--fade-l),
        #000 calc(100% - var(--fade-r)), transparent 100%);
}
.scroll-rail::-webkit-scrollbar { display: none; }
.scroll-rail:focus-visible,
.tiktok-photos:focus-visible { outline: 1px solid var(--accent-line); outline-offset: -1px; }
/* у открытого трека-вкладки край ленты не растворяется (syncListenTab в time.js) */
.scroll-rail.tab-at-left { --fade-l: 0px !important; }
.scroll-rail.tab-at-right { --fade-r: 0px !important; }

/* стрелки — тихие, появляются при наведении на блок */
.scroll-btn {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    width: 24px;
    height: 46px;
    border: 0;
    border-radius: var(--radius);
    background: var(--panel-soft);
    color: var(--muted);
    font-family: var(--font);
    font-size: 11.5px;
    line-height: 1;
    cursor: pointer;
    padding: 0;
    opacity: 0;
    transition: opacity .15s ease, color .15s ease, background .15s ease;
}
.scroller:hover .scroll-btn { opacity: 1; }
.scroll-btn:hover { color: var(--text); background: var(--panel-hover); }
.scroll-btn:focus-visible { opacity: 1; outline: 1px solid var(--accent-line); }
.scroll-btn[data-scroll="-1"] { left: 0; }
.scroll-btn[data-scroll="1"]  { right: 0; }

/* прятать стрелку, если в эту сторону листать уже некуда */
.scroller:not(.can-left)  .scroll-btn[data-scroll="-1"],
.scroller:not(.can-right) .scroll-btn[data-scroll="1"] { display: none; }

/* -------------------------------------------------------------- tracks -- */

/* --listen-fill — общий цвет открытого трека и плашки под ним: вместе они
   одна фигура. --tab-gap — зазор между лентой и плашкой, его перекрывает
   перемычка от открытого трека. */
.music {
    --listen-fill: var(--panel-hover);
    --tab-gap: 8px;
}

.track-column {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    gap: 8px;
    width: max-content;
    padding: 1px;
}

/* Плитка — кнопка: нажатие открывает превью и ссылки. От кнопки ей нужна
   только суть, оформление браузера снимаем. */
.track {
    position: relative;
    flex: 0 0 auto;
    width: 232px;
    display: flex;
    align-items: center;
    gap: 10px;
    --track-pad: 9px;
    --cover-radius: 6px;
    padding: var(--track-pad);
    border: 0;
    /* концентричные углы: радиус плитки = радиус обложки + отступ до неё */
    border-radius: calc(var(--cover-radius) + var(--track-pad));
    background: var(--panel-soft);
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    overflow: hidden;
    transition: background .2s ease, box-shadow .2s ease, border-radius .2s ease;
}
.track:hover {
    background: var(--panel-hover);
}
.track:focus-visible { outline: 1px solid var(--accent-line); outline-offset: -1px; }

/* Открытый трек — вкладка, из которой вытекает плашка, как вкладка Chrome
   или статус гостевой книги: цвет тот же, что у плашки, и низ прямой —
   дальше его продолжает перемычка плашки (.listen-sheet::before).
   Класс ставит скрипт, а не aria-expanded: при закрытии вкладка держится,
   пока плашка не втянется обратно под трек. Засечка слева у «сейчас
   играет» остаётся: это другой знак. */
.track.tab,
.track.playing.tab {
    background: var(--listen-fill);
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.track-cover {
    position: relative;
    flex: 0 0 auto;
    border-radius: var(--cover-radius);
    overflow: hidden;
}
.track img {
    width: 40px;
    height: 40px;
    object-fit: cover;
}

/* На обложке — «play»: нажатие на неё сразу играет превью. Значок проступает,
   только когда наводят на саму обложку: нажатие на остальную плитку
   открывает плашку без звука. Пока превью играет, на обложке открытого
   трека «pause». Наведение — только где оно настоящее: на телефоне :hover
   залипает после касания, и значок оставался бы висеть. */
.track-cover::after {
    content: "";
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, .55) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M8 5.5v13l10.5-6.5z'/%3E%3C/svg%3E") center / 18px no-repeat;
    opacity: 0;
    transition: opacity .15s ease;
}
.track.previewing .track-cover::after {
    opacity: 1;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='7' y='5.5' width='3.4' height='13' rx='1.2' fill='%23fff'/%3E%3Crect x='13.6' y='5.5' width='3.4' height='13' rx='1.2' fill='%23fff'/%3E%3C/svg%3E");
}
@media (hover: hover) {
    .track-cover:hover::after { opacity: 1; }
}

.track-info {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1 1 auto;
    line-height: 1.4;
}

.track-name {
    color: var(--text);
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.track-artist {
    color: var(--dim);
    font-size: 12.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.track-time-later {
    flex: 0 0 auto;
    align-self: flex-start;
    color: var(--dim);
    font-size: 11.5px;
    white-space: nowrap;
}

/* «сейчас играет» помечаем засечкой и подложкой, а не обводкой */
.track.playing {
    background: var(--accent-soft);
    box-shadow: inset 2px 0 0 var(--accent);
}

/* Эквалайзер вместо статичной точки: три полоски, каждая со своей фазой.
   Базовые высоты разные, поэтому при prefers-reduced-motion анимация гаснет,
   а индикатор всё равно читается — просто замирает. */
.eq { display: none; }
.track.playing .eq {
    display: flex;
    flex: 0 0 auto;
    align-items: flex-end;
    gap: 2px;
    height: 11px;
}
.eq i {
    display: block;
    width: 2px;
    border-radius: 1px;
    background: var(--accent);
    transform-origin: bottom;
    animation: eq 1.1s ease-in-out infinite;
}
.eq i:nth-child(1) { height: 5px;  animation-delay: -.65s; }
.eq i:nth-child(2) { height: 11px; animation-delay: -.2s; }
.eq i:nth-child(3) { height: 7px;  animation-delay: -.95s; }

@keyframes eq {
    0%, 100% { transform: scaleY(.3); }
    50%      { transform: scaleY(1); }
}

/* у активного трека место занято эквалайзером, а не «5 минут назад» */
.track.playing .track-time-later { display: none; }

/* Шеврон «сложить» у открытого трека — на месте «5 минут назад»: крестика
   у плашки нет, закрывается она тем же нажатием на трек, и шеврон об этом
   говорит, как у раскрытого списка. Открыли — «5m» гаснет, а шеврон
   разворачивается из «вниз» в «вверх»; закрыли — так же обратно, пока лист
   втягивается. Поверх подписи, а не вместо неё: иначе место под ней
   схлопывалось бы рывком. У играющего трека подписи нет, а эквалайзер
   по центру — шеврон встаёт над ним. */
.track-fold {
    position: absolute;
    top: 10px;
    right: 9px;
    width: 14px;
    height: 14px;
    color: var(--dim);
    opacity: 0;
    transform: rotate(180deg) scale(.7);
    transition: opacity .2s ease, transform .4s cubic-bezier(.2, 0, 0, 1), color .15s ease;
    pointer-events: none;
}
.track[aria-expanded="true"] .track-fold {
    opacity: 1;
    transform: none;
}
.track[aria-expanded="true"]:hover .track-fold { color: var(--accent); }
.track-time-later { transition: opacity .2s ease; }
.track[aria-expanded="true"] .track-time-later { opacity: 0; }

/* -------------------------------------------------------------- listen --
   Превью и ссылки на трек — лист, который вытекает из самого трека.
   Движение по образцу «container transform» из Material: фигура перетекает
   из маленькой в большую, содержимое проявляется уже по ходу. Кривые оттуда
   же: на появление — emphasized decelerate, на уход — emphasized accelerate,
   и уходит быстрее, чем появляется.

   Лист лежит поверх страницы, а не раздвигает её — как статус
   в гостевой книге. Раздвигать место пришлось бы анимацией высоты, то есть
   пересчётом всей страницы на каждом кадре: на телефоне анимация от этого
   рвалась. Теперь анимируется только форма листа, а висит он над страницей
   с тенью — отсюда и глубина. «on rotation» на это время уходит под него. */

/* Якорь нулевой высоты сразу под лентой. На 1px выше ленты: у неё снизу
   поле в 1px, а перемычка должна начинаться от самого низа трека. */
.listen {
    position: relative;
    z-index: 5;
    height: 0;
    margin-top: -1px;
}

/* Закрытый лист прячется целиком, когда втянется: иначе от него оставалась
   бы полоска перемычки под треком и ловила бы нажатия. */
.listen-inner {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    visibility: hidden;
    transition: visibility 0s linear .3s;
}
.listen.open .listen-inner {
    visibility: visible;
    transition: visibility 0s;
}

/* Тень — отдельным слоем размером с плашку, а не фильтром на листе: фильтр
   пересчитывался бы на каждом кадре вместе с формой. Слой проступает,
   когда лист почти растёкся, и гаснет первым при закрытии — так тень не
   отстаёт от формы и не торчит из-под неё. */
.listen-inner::before {
    content: "";
    position: absolute;
    top: calc(var(--tab-gap) + 1px);
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: var(--radius-sheet);
    box-shadow: 0 26px 48px -10px rgba(0, 0, 0, .85), 0 6px 14px rgba(0, 0, 0, .45);
    opacity: 0;
    transition: opacity .12s ease;
    will-change: opacity;
    pointer-events: none;
}
.listen.open .listen-inner::before {
    opacity: 1;
    transition: opacity .3s ease .22s;
}

/* Пока плашка открыта, то, что под ней, отступает в тень: на почти чёрном
   фоне глубину делает не столько тень, сколько контраст ближнего и дальнего. */
.music .rotation { transition: opacity .3s ease; }
.music.listening .rotation { opacity: .3; }

/* Лист — перемычка от трека и сама плашка, одна фигура. Закрытый, он сжат
   clip-path'ом до полоски под треком (--tab-l/--tab-r ставит скрипт):
   ширина — как у трека, высота — одна перемычка. Раскрываясь, эта фигура
   со скруглённым низом вырастает до полной плашки; пока она не шире трека,
   выгибы перемычки спрятаны и проступают, когда лист начинает расползаться.
   Закрываясь — стягивается обратно в полоску и уходит под трек. */
.listen-sheet {
    /* Верхние углы фигуры: у трека они нулевые — иначе срезали бы
       перемычку, а к полному раскрытию скругляются, чтобы и на ходу у листа
       не было острых углов. Под этим скруглением уже скругление самой
       плашки, так что в раскрытом виде оно ничего не меняет. У края,
       где угол плашки прямой, и маска прямая. */
    --clip-tl: 20px;
    --clip-tr: 20px;
    position: relative;
    padding-top: calc(var(--tab-gap) + 1px);
    clip-path: inset(0 var(--tab-r, 50%) calc(100% - var(--tab-gap) - 1px) var(--tab-l, 50%)
                     round 0 0 var(--radius-sheet) var(--radius-sheet));
    transition: clip-path .28s cubic-bezier(.3, 0, .8, .15);
}
.listen.open .listen-sheet {
    clip-path: inset(0 0 0 0 round var(--clip-tl) var(--clip-tr) var(--radius-sheet) var(--radius-sheet));
    transition: clip-path .5s cubic-bezier(.2, 0, 0, 1);
}
.listen-sheet.flush-left { --clip-tl: 0px; }
.listen-sheet.flush-right { --clip-tr: 0px; }
/* стартовая форма — полоса под треком — ставится без анимации: иначе лист
   полз бы к ней от прошлого трека */
.listen-sheet.instant,
.listen-sheet.instant::before { transition: none; }

/* Перемычка и выгибы у её основания — квадраты, из которых вырезана
   четверть круга, как у статуса гостевой книги. Стоит ровно под треком
   и едет за ним, когда ленту листают; при смене трека переезжает плавно. */
.listen-sheet::before {
    content: "";
    position: absolute;
    top: 0;
    left: calc(var(--tab-l, 0px) - 8px);
    right: calc(var(--tab-r, 0px) - 8px);
    height: calc(var(--tab-gap) + 1px);
    background:
        radial-gradient(circle at 0 0, transparent 7.5px, var(--listen-fill) 8px) left bottom / 8px 9px no-repeat,
        radial-gradient(circle at 100% 0, transparent 7.5px, var(--listen-fill) 8px) right bottom / 8px 9px no-repeat,
        linear-gradient(var(--listen-fill), var(--listen-fill)) center / calc(100% - 16px) 100% no-repeat;
    pointer-events: none;
}
.listen-sheet.sliding::before {
    transition: left .35s cubic-bezier(.2, 0, 0, 1), right .35s cubic-bezier(.2, 0, 0, 1);
}
/* трек уехал из видимой части ленты — и перемычке держаться не за что */
.listen-sheet.tab-off::before { display: none; }

/* Плашка того же цвета, что открытый трек и перемычка: втроём они одна
   фигура. Верх — ровно цвет вкладки, иначе на стыке был бы шов, а книзу
   чуть темнеет: у плоской заливки нет объёма. */
.listen-card {
    position: relative;
    display: grid;
    grid-template-columns: 76px minmax(0, 1fr);
    gap: 12px 14px;
    padding: 12px;
    border-radius: var(--radius-sheet);
    background: linear-gradient(180deg, var(--listen-fill) 40%, #141414);
    transition: border-radius .2s ease;
}
/* трек-вкладка у самого края — угол с той стороны прямой (TAB_FLUSH в time.js) */
.listen-sheet.flush-left .listen-card { border-top-left-radius: 0; }
.listen-sheet.flush-right .listen-card { border-top-right-radius: 0; }

/* Содержимое проступает, когда лист уже потёк, а при закрытии и смене
   трека гаснет первым: иначе обложку и кнопки резало бы краем листа,
   а новые название и обложка выскакивали бы рывком. */
.listen-card > * {
    opacity: 0;
    transition: opacity .1s ease;
}
.listen.open .listen-card > * {
    opacity: 1;
    transition: opacity .2s ease;
}
.listen.open.opening .listen-card > * {
    transition: opacity .24s ease .22s;
}
.listen.open .listen-card.swap > * {
    opacity: 0;
    transition: opacity .12s ease;
}

.listen-cover {
    width: 76px;
    height: 76px;
    object-fit: cover;
    border-radius: 8px;
    /* обложка приподнята над плашкой — тот же приём глубины, что у листа */
    box-shadow: 0 8px 18px rgba(0, 0, 0, .45);
}

.listen-main {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.4;
}

.listen-name {
    color: var(--strong);
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.listen-artist {
    color: var(--dim);
    font-size: 12.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.listen-player {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 8px;
}

.listen-play {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
    cursor: pointer;
    transition: background .15s ease, color .15s ease;
}
.listen-play:hover { background: var(--accent); color: var(--bg); }
.listen-play:focus-visible { outline: 1px solid var(--accent-line); outline-offset: 2px; }
.listen-play svg { width: 15px; height: 15px; }
.listen-play .ico-pause,
.listen.playing .listen-play .ico-play { display: none; }
.listen.playing .listen-play .ico-pause { display: block; }

/* ищем превью или догружаем звук — кнопка неглубоко мерцает, как точка эфира */
.listen.looking .listen-play,
.listen.buffering .listen-play { animation: live-pulse 1.2s ease-in-out infinite; }

/* Высота — под палец и мышь, сама полоска внутри тонкая, как у ротации. */
.listen-bar {
    position: relative;
    flex: 1 1 auto;
    height: 16px;
    cursor: pointer;
}
.listen-bar::before,
.listen-bar i {
    content: "";
    position: absolute;
    top: 50%;
    left: 0;
    height: 3px;
    border-radius: 2px;
    transform: translateY(-50%);
}
.listen-bar::before {
    right: 0;
    background: var(--line);
}
.listen-bar i {
    display: block;
    width: 0;
    background: var(--accent);
    /* время приходит рывками по четверти секунды — сглаживаем */
    transition: width .25s linear;
}

.listen-time {
    flex: 0 0 auto;
    color: var(--dim);
    font-size: 11.5px;
    font-variant-numeric: tabular-nums;
}

.listen-note {
    margin-top: 4px;
    color: var(--dim);
    font-size: 11.5px;
}

/* превью нет — плеер не нужен вовсе, остаются подпись и ссылки */
.listen.no-preview .listen-player { display: none; }


.listen-links {
    grid-column: 1 / -1;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

/* Ссылки — одни логотипы, приглушённые до монохрома, как в «links»: цвет
   возвращается по наведению. Название площадки — в подсказке и для
   скринридера. */
.listen-link {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: 8px;
    background: var(--panel-soft);
    transition: background .15s ease;
}
.listen-link img {
    width: 18px;
    height: 18px;
    filter: grayscale(1) brightness(1.35);
    opacity: .7;
    transition: filter .15s ease, opacity .15s ease;
}
.listen-link:hover { background: var(--panel); }
.listen-link:hover img,
.listen-link:focus-visible img {
    filter: none;
    opacity: 1;
}
.listen-link:focus-visible { outline: 1px solid var(--accent-line); }

/* ------------------------------------------------------------- volume --
   Регулятор громкости (volume.js) — по образцу Apple: без шарика, дорожка
   с заливкой, тянуть можно за любое место. На наведении дорожка плавно
   толстеет, нажатие — плавный прыжок к точке, перетаскивание идёт 1:1,
   за краем дорожка тянется резиной и пружинит назад. Заливка не зелёная,
   а светлая: в плашке рядом зелёная полоска времени превью, их не спутать. */

.vol {
    --vol: .65;
    position: relative;
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    width: 84px;
    height: 22px;
    cursor: pointer;
    touch-action: none;
    outline: none;
}
.vol-track {
    position: relative;
    flex: 1 1 auto;
    height: 4px;
    border-radius: 999px;
    background: rgba(228, 228, 228, .16);
    overflow: hidden;
    transition: height .22s cubic-bezier(.2, 0, 0, 1), background .2s ease;
}
.vol:hover .vol-track,
.vol.dragging .vol-track,
.vol:focus-visible .vol-track { height: 8px; }
.vol:focus-visible .vol-track { box-shadow: 0 0 0 2px var(--accent-line); }
.vol.by-pointer:focus-visible .vol-track { box-shadow: none; }
.vol-fill {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: calc(var(--vol) * 100%);
    background: rgba(228, 228, 228, .78);
    transition: background .2s ease;
}
.vol:hover .vol-fill,
.vol.dragging .vol-fill { background: #fff; }
/* нажатие, колесо и стрелки — плавный прыжок; перетаскивание — без задержки */
.vol.jumping .vol-fill {
    transition: width .22s cubic-bezier(.2, 0, 0, 1), background .2s ease;
}
/* отпустили за краем — резина пружинит назад с лёгким перелётом */
.vol.springing .vol-track {
    transition: height .22s cubic-bezier(.2, 0, 0, 1), transform .55s cubic-bezier(.34, 1.56, .64, 1);
}

/* кнопка-динамик: «без звука» и обратно, к прежнему уровню */
.vol-mute {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--dim);
    cursor: pointer;
    transition: color .15s ease, background .15s ease;
}
.vol-mute:hover { color: var(--text); background: rgba(255, 255, 255, .05); }
.vol-mute:focus-visible { outline: 1px solid var(--accent-line); }
.vol-ico { width: 16px; height: 16px; }

/* Волны динамика гаснут по громкости: тихо — одна, ноль — перечёркнут.
   Уровень volume.js пишет на корень страницы, поэтому так же ведёт себя
   и значок на кнопке звука TikTok. */
.vol-ico .w1,
.vol-ico .w2,
.tiktok-sound-ico .w1,
.tiktok-sound-ico .w2 { transition: opacity .2s ease; }
.vol-ico .x { opacity: 0; transition: opacity .2s ease; }
html[data-vol-level="low"] .vol-ico .w2,
html[data-vol-level="low"] .tiktok-sound-ico .w2,
html[data-vol-level="off"] .vol-ico .w1,
html[data-vol-level="off"] .vol-ico .w2 { opacity: 0; }
html[data-vol-level="off"] .vol-ico .x { opacity: 1; }
html[data-vol-level="off"] .tiktok-sound .waves { display: none; }
html[data-vol-level="off"] .tiktok-sound .slash { display: block; }

.listen-volume {
    display: flex;
    align-items: center;
    gap: 2px;
    flex: 0 0 auto;
    margin-left: 4px;
}
.listen-volume .vol { width: 76px; }

/* Только где есть мышь или тачпад: на телефоне громкость кнопками,
   а iOS сайту её менять и не даёт. */
@media not all and (hover: hover) and (pointer: fine) {
    .vol,
    .listen-volume { display: none; }
}

/* «В игре прямо сейчас». Намеренно НЕ эквалайзер: он остаётся языком музыки,
   и если тем же знаком отмечать игры, два раздела перестают различаться
   боковым зрением. Здесь точка и слово — знак прямого эфира, понятный без
   пояснений. */
/* Вид у индикатора один на все места, а вот показывается он по-разному:
   на карточке игры — когда та играет, в строке под подзаголовком — всегда,
   потому что сама строка появляется только тогда, когда есть что сказать. */
.live {
    display: none;
    /* inline-flex с gap, а не отступ у точки: так точка встаёт по центру
       строки при любом размере шрифта, без подгонки vertical-align. */
    align-items: center;
    gap: 6px;
    flex: 0 0 auto;
    font-size: 11.5px;
    letter-spacing: .04em;
    color: var(--accent);
}
.game.playing .live,
.last-game.playing .live {
    display: inline-flex;
    /* появляется из display: none — без проявления он выскакивал рывком */
    animation: fade-in .35s ease-out;
}

@keyframes fade-in {
    from { opacity: 0; }
}

.live::before {
    content: "";
    width: 6px;
    height: 6px;
    flex: 0 0 auto;
    border-radius: 50%;
    background: var(--accent);
    animation: live-pulse 2.4s ease-in-out infinite;
}

/* Пульсация нарочно неглубокая и медленная: точка должна ловиться боковым
   зрением, а не тянуть внимание на себя с середины страницы. */
@keyframes live-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: .4; }
}

/* Ссылка на лобби. Её рисует скрипт и только когда Steam её дал, поэтому
   прятать нечего — но подчёркивание нужно: это единственное место в блоке,
   по которому уходят со страницы. */
.join {
    flex: 0 0 auto;
    /* inline-block: у строчного подчёркивание вылезает за строку, и
       обёртка с overflow: hidden, которая раскрывает строку, его срезала */
    display: inline-block;
    font-size: 11.5px;
    letter-spacing: .04em;
    color: var(--accent);
    border-bottom: 1px solid var(--accent-line);
}
.join:hover { color: var(--strong); border-bottom-color: var(--strong); }
.join::before { content: "[ "; border: 0; }
.join::after  { content: " ]"; border: 0; }

/* В списке часы уступают место надписи — как «5 минут назад» у трека. */
.game.playing .game-info .playtime-forever { display: none; }
.game.playing .game-title { color: var(--strong); }

/* трек сменился — карточка мигает появлением, чтобы это было заметно
   боковым зрением, но не дёргало страницу; так же входит новая игра в список */
.track.is-new,
.game.is-new { animation: track-in .28s ease-out; }

/* Обновившееся значение коротко вспыхивает акцентом и гаснет до своего
   цвета. Анимируется только цвет: цифры не прыгают и соседей не двигают. */
.bump { animation: value-in .9s ease-out; }

@keyframes value-in {
    from { color: var(--accent); }
}

@keyframes track-in {
    from { opacity: 0; transform: translateY(-5px); }
    to   { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------- games -- */

.game-bar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    border-radius: var(--radius-tile);
    background: var(--panel-soft);
    overflow: hidden;
}

.game-left-section {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.last-game {
    position: relative;
    flex: 1 1 auto;
    min-height: 158px;
    padding: 13px 15px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.banner-image {
    position: absolute;
    inset: 0 0 auto 0;
    height: 70%;
    background-size: cover;
    background-position: center;
    opacity: 0;
    transition: opacity .8s ease;
    z-index: 0;
    filter: saturate(.4) brightness(.55);
    mask-image: linear-gradient(to bottom, rgba(0,0,0,.7), rgba(0,0,0,0));
    -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,.7), rgba(0,0,0,0));
}

.game-head,
.game-views { position: relative; z-index: 1; }

/* Ссылка на лобби: прижата к правому краю и висит в пустой полосе под
   баннером, над часами. Она вынесена из потока — раньше она была своей
   строкой, и когда приходила, карточка подрастала и текст съезжал вниз.
   Теперь её появление не двигает ничего: она выезжает и проявляется
   поверх полосы, которая и так пустая. */
.game-join {
    position: absolute;
    right: 0;
    bottom: calc(100% + 6px);
    opacity: 0;
    visibility: hidden;
    transform: translateY(6px);
    transition: opacity .45s ease, transform .45s cubic-bezier(.2, .7, .2, 1),
                visibility 0s linear .45s;
}
.game-join.shown {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity .45s ease, transform .45s cubic-bezier(.2, .7, .2, 1),
                visibility 0s;
}
/* Пришла — один раз мягко вспыхивает, чтобы её заметили боковым зрением. */
.game-join.shown .join { animation: join-glow 1.6s ease-out .2s; }

@keyframes join-glow {
    0%   { text-shadow: 0 0 0 transparent; }
    30%  { text-shadow: 0 0 10px rgba(var(--accent-rgb), .8); }
    100% { text-shadow: 0 0 0 transparent; }
}

/* Выбрали другую игру — её шапка и цифры проявляются, а не подменяются
   в тот же кадр. */
.last-game.is-switching > .game-head,
.last-game.is-switching > .game-views { animation: card-in .35s ease-out; }

@keyframes card-in {
    from { opacity: 0; transform: translateY(3px); }
}

.game-head {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

.game-head img {
    width: 36px;
    height: 36px;
    flex: 0 0 auto;
    object-fit: cover;
    border-radius: 2px;
}

.last-game-title {
    font-size: 13px;
    font-weight: 500;
    color: var(--strong);
    line-height: 1.4;
    min-width: 0;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

/* Логотипы серверов — как логотипы в блоке ссылок: бесцветные и
   приглушённые, цвет проекта появляется по наведению. Надпись белая под
   той же прозрачностью, что и значки, чтобы звезда, «GTA5 RP» и вордмарк
   Majestic читались одним тоном. */
.rp-link {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    color: #fff;
    opacity: .6;
    transition: opacity .15s ease;
}
.rp-link:hover { color: #fff; opacity: 1; }

.rp-link img {
    display: block;
    flex: 0 0 auto;
    filter: grayscale(1) brightness(1.35);
    transition: filter .15s ease;
}
.rp-link:hover img { filter: none; }

/* Звезда с дугой целиком, а не квадратная фавиконка проекта: у той дуга
   и нижние лучи срезаны краем картинки. Поэтому значок шире, чем выше. */
.rp-star { width: auto; height: 13px; }

/* Шрифт проекта, а не моноширинный: «GTA5» жирным, «RP» тонким,
   как в их логотипе. */
.rp-name {
    font-family: "Montserrat", var(--font);
    font-size: 12px;
    font-weight: 400;
    letter-spacing: .02em;
    line-height: 1;
}
.rp-name b { font-weight: 700; }

/* Высота подобрана так, чтобы заглавная M совпала по росту с «GTA5»:
   M занимает почти три четверти высоты вордмарка. */
.rp-wordmark { height: 12px; width: auto; }

.game-information {
    margin-top: auto;
    display: flex;
    flex-direction: column;
}

.game-information p {
    font-size: 12.5px;
    font-weight: 400;
    color: var(--dim);
    display: flex;
    gap: 9px;
}
.game-information p::before { content: "—"; color: var(--accent-mark); }

/* Строка с RP-серверами (у GTA): та же строка с «—», что и часы, только
   вместо числа — логотипы. По центру, а не по базовой линии: картинки
   иначе садятся ниже текста. */
.game-information .game-note { align-items: center; }
.game-information .game-note[hidden] { display: none; }

/* Оба вида в одной ячейке друг на друге: при смене высота карточки не меняется. */
.game-views {
    margin-top: auto;
    display: grid;
}
.game-views > .game-information,
.game-views > .game-stats {
    grid-area: 1 / 1;
    align-self: end;
    min-width: 0;
}

.last-game[data-view] .game-information,
.last-game[data-view] .game-stats {
    transition: opacity .35s ease .12s, transform .35s ease-out .12s;
}
.last-game[data-view="stats"] .game-information,
.last-game[data-view="time"] .game-stats {
    opacity: 0;
    transform: translateY(4px);
    visibility: hidden;
    pointer-events: none;
    transition: opacity .2s ease, transform .2s ease, visibility 0s linear .2s;
}

.view-switch {
    flex: 0 0 auto;
    align-self: flex-start;
    display: flex;
    gap: 10px;
    margin-left: auto;
}
.view-switch[hidden] { display: none; }

.view-btn {
    position: relative;
    padding: 0 0 4px;
    border: 0;
    background: none;
    color: var(--dim);
    font: inherit;
    font-size: 11.5px;
    line-height: 1.4;
    cursor: pointer;
    transition: color .35s ease;
}
.view-btn::after { content: ""; position: absolute; inset: -8px -5px; }
.view-btn:hover { color: var(--text); }
.view-btn:focus-visible { outline: 1px solid var(--accent-line); outline-offset: 2px; }
.view-btn[aria-pressed="true"] { color: var(--accent); }

.view-timer {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 1px;
    background: var(--line);
    overflow: hidden;
}
/* Край размыт на 10px: линия длиной в слово ползёт около 3 px в секунду,
   и резкий край заметно перескакивал по пикселям. */
.view-fill {
    display: block;
    width: calc(100% + 10px);
    height: 100%;
    background: linear-gradient(to right, var(--accent) calc(100% - 10px), transparent);
    transform: translateX(-100%);
    will-change: transform;
}
.view-btn[aria-pressed="true"] .view-fill.is-running {
    animation: view-fill var(--view-ms, 10s) linear forwards;
}
.last-game.is-paused .view-fill { animation-play-state: paused; }

@keyframes view-fill {
    to { transform: translateX(0); }
}

.game-stats {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.game-stats[hidden] { display: none; }

.game-stats-grid {
    display: grid;
    grid-template-columns: repeat(3, auto);
    justify-content: space-between;
    align-items: end;
    gap: 8px 12px;
}
.stat-cell {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.3;
}
.faceit-rank {
    flex-direction: row;
    align-items: center;
    gap: 8px;
}
.faceit-pair { display: flex; flex-direction: column; }
.stat-v { font-size: 15px; font-weight: 500; color: var(--text); }
.stat-k { font-size: 11.5px; color: var(--dim); }

.faceit-icon {
    position: relative;
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
}
.faceit-icon svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.faceit-plate { fill: var(--panel); }
.faceit-track,
.faceit-arc {
    fill: none;
    stroke-width: 2.4;
    transform: rotate(130.6deg);
    transform-origin: 12px 12px;
}
.faceit-track { stroke: rgba(205, 205, 205, .1); stroke-dasharray: 40.88 100; }
.faceit-arc { stroke: var(--faceit-color, var(--muted)); stroke-dasharray: 0 100; }
.faceit-unknown path {
    fill: none;
    stroke: var(--muted);
    stroke-width: 1.3;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.faceit-unknown circle { fill: var(--muted); }
.faceit-icon.is-ranked .faceit-unknown { display: none; }
.faceit-level {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    font-weight: 500;
    line-height: 1;
    color: var(--faceit-color, var(--muted));
}

.game-stats-foot {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 11.5px;
    color: var(--dim);
}
.game-stats-foot > span:first-child { margin-right: 6px; }
.faceit-results { display: flex; gap: 4px; }
.faceit-results i {
    width: 16px;
    height: 16px;
    border-radius: 3px;
    font-size: 10px;
    font-style: normal;
    font-weight: 500;
    line-height: 16px;
    text-align: center;
}
.faceit-results .win { background: rgba(var(--accent-rgb), .16); color: var(--accent); }
.faceit-results .loss { background: #1f1f1f; color: var(--muted); }
.game-stats-src { margin-left: auto; }

.last-game.is-match .game-views > .game-information,
.last-game.is-match .game-views > .game-stats,
.last-game.is-match .game-join { display: none; }
/* visibility, а не opacity: opacity у баннера ставит скрипт прямо в style. */
.last-game.is-match .banner-image { visibility: hidden; }
.last-game.is-match .game-head { text-shadow: 0 1px 6px rgba(0, 0, 0, .8); }

.match-bg {
    position: absolute;
    inset: 0;
    z-index: 0;
    background-size: cover;
    background-position: center;
    opacity: 0;
    transition: opacity .8s ease;
    filter: saturate(.95) brightness(.85);
}
.match-bg::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to bottom, rgba(12, 12, 12, .6) 0%, rgba(12, 12, 12, .1) 30%,
                rgba(12, 12, 12, .3) 50%, rgba(12, 12, 12, .95) 100%);
}
.last-game.is-match .match-bg.is-loaded { opacity: 1; }

.match-view {
    grid-area: 1 / 1;
    align-self: end;
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
}
.match-view[hidden] { display: none; }

.match-plate {
    --result-rgb: 0, 0, 0;
    position: relative;
    isolation: isolate;
    display: grid;
    grid-template-columns: auto auto 1fr auto auto;
    align-items: center;
    gap: 0 10px;
    padding: 8px 12px;
    border: 1px solid rgba(255, 255, 255, .08);
    border-radius: 10px;
    background: rgba(10, 10, 10, .62);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    transition: border-color .8s ease;
}

.match-elo { display: flex; flex-direction: column; line-height: 1.2; }
.match-elo.enemy { text-align: right; }
.match-elo b { font-size: 12px; font-weight: 500; color: var(--muted); }
.match-elo small { font-size: 10.5px; color: var(--dim); }

.match-score {
    font-size: 24px;
    font-weight: 600;
    line-height: 1;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.match-score.is-scored { animation: match-scored 1.2s ease-out; }
@keyframes match-scored {
    15% { transform: scale(1.25); text-shadow: 0 0 14px currentColor; }
}

.match-mid { position: relative; min-width: 0; }
.match-mid-main,
.match-hint {
    display: flex;
    flex-direction: column;
    align-items: center;
    line-height: 1.3;
    transition: opacity .2s ease;
}
.match-map { font-size: 13px; font-weight: 500; color: var(--strong); text-transform: lowercase; }
.match-sub { font-size: 11px; color: var(--muted); white-space: nowrap; }
.match-res {
    margin-right: 4px;
    padding: 0 6px;
    border-radius: 3px;
    font-size: 10.5px;
    font-style: normal;
    font-weight: 500;
    line-height: 16px;
}
.match-res[hidden] { display: none; }
.match-res.is-win { background: rgba(70, 192, 122, .18); color: #46c07a; }
.match-res.is-loss { background: rgba(229, 83, 83, .18); color: #e55353; }

.match-hint {
    position: absolute;
    inset: 0;
    justify-content: center;
    opacity: 0;
    pointer-events: none;
}
.match-hint b {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 13px;
    font-weight: 500;
    color: var(--strong);
}
.match-hint small { font-size: 11px; color: var(--muted); }
.match-plate.is-hinting .match-hint { opacity: 1; }
.match-plate.is-hinting .match-mid-main { opacity: 0; }

.match-chips-row {
    grid-column: 1 / -1;
    display: flex;
    justify-content: space-between;
    margin-top: 7px;
    padding-top: 7px;
    border-top: 1px solid rgba(255, 255, 255, .07);
}
.match-chips { display: flex; gap: 4px; }
.match-chip {
    position: relative;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    line-height: 0;
    cursor: pointer;
    opacity: .8;
    transition: opacity .15s ease, transform .15s ease;
}
.match-chip::after { content: ""; position: absolute; inset: -4px -2px; }
.match-chip:hover,
.match-chip.is-on { opacity: 1; transform: translateY(-1px); }
.match-chip.is-me { opacity: 1; box-shadow: 0 0 0 1.5px var(--accent); }
.match-chip:focus-visible { outline: 1px solid var(--accent-line); outline-offset: 2px; }
.match-lvl { display: inline-block; width: 18px; height: 18px; }
.match-lvl .faceit-level { font-size: 8px; }

.match-me {
    display: grid;
    grid-template-columns: repeat(4, auto);
    justify-content: space-between;
    gap: 8px 12px;
}
.match-me[hidden] { display: none; }

.match-plate.is-win { --result-rgb: 70, 192, 122; }
.match-plate.is-loss { --result-rgb: 229, 83, 83; }
/* z-index: -1 держит тон под содержимым плашки только вместе с isolation у .match-plate. */
.match-plate::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: rgb(var(--result-rgb));
    opacity: 0;
    pointer-events: none;
}
.match-plate.is-revealed:is(.is-win, .is-loss) { border-color: rgba(var(--result-rgb), .4); }
.match-plate.is-revealed:is(.is-win, .is-loss)::before { animation: match-result-in 1.8s ease-out forwards; }
.match-plate.is-settled:is(.is-win, .is-loss)::before { animation: none; opacity: .13; }
@keyframes match-result-in {
    30%  { opacity: .3; }
    100% { opacity: .13; }
}
@media (prefers-reduced-motion: reduce) {
    .match-plate.is-revealed:is(.is-win, .is-loss)::before { animation: none; opacity: .13; }
    .match-score.is-scored { animation: none; }
}

.info-box {
    flex: 0 0 auto;
    border-top: 1px solid var(--line-soft);
    padding: 10px 15px;
}

.playtime-in-total {
    display: flex;
    align-items: center;
    gap: 9px;
}
.playtime-in-total img {
    width: 13px;
    height: 13px;
    opacity: .34;
    filter: brightness(0) invert(1);
}
.playtime {
    font-size: 12.5px;
    font-weight: 400;
    color: var(--dim);
}

/* Общие часы — одометром, но тихим: в покое это просто цифры в неглубоких
   ячейках на тон темнее фона блока, того же цвета, что и остальной текст.
   Механика видна в движении: у каждой цифры свой барабан-десятигранник,
   и он вращается в 3D — цифры заваливаются на гранях, как на настоящем
   цилиндре. Корпус со стеклом и светящиеся цифры были ярче всего блока
   и перетягивали внимание на себя. Число прижато вправо. */
.odo {
    --odo-slot: #0d0d0d;   /* ячейка: фон блока (--panel-soft), притемнённый на тон */
    flex: 0 0 auto;
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 2px;
    color: var(--text);
    font-size: 15px;
    font-weight: 500;
}
.odo-wait {
    color: var(--dim);
    font-size: 12.5px;
    font-weight: 400;
}

/* Окно барабана чуть выше грани, радиус десятигранника — h / (2·tg 18°),
   чтобы грани сходились. */
.odo-drum {
    --odo-h: 1.3em;
    --odo-r: calc(var(--odo-h) * 1.539);
    position: relative;
    display: block;
    width: .9em;
    height: 1.6em;
    overflow: hidden;
    border-radius: 3px;
    background: var(--odo-slot);
    perspective: 12em;
}
/* Барабан отодвинут назад на свой радиус: передняя грань лежит ровно
   в плоскости окна, и цифра в покое остаётся чёткой, а не раздутой. */
.odo-wheel {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    transform: translateZ(calc(var(--odo-r) * -1)) rotateX(calc(var(--turn, 0) * 36deg));
    transition: transform var(--dur, .8s) var(--ease, ease);
}
.odo-face {
    position: absolute;
    top: 50%;
    right: 0;
    left: 0;
    height: var(--odo-h);
    margin-top: calc(var(--odo-h) / -2);
    line-height: var(--odo-h);
    text-align: center;
    backface-visibility: hidden;
    transform: rotateX(calc(var(--i) * -36deg)) translateZ(var(--odo-r));
}
/* К краям окна цифры растворяются в цвете ячейки, а не в чёрной тени:
   в покое не видно ни полос, ни обрывков соседних цифр. */
.odo-drum::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to bottom,
        var(--odo-slot), transparent 32%, transparent 68%, var(--odo-slot));
    pointer-events: none;
}
/* Минуты тише часов — как копейки у цены, но без подсветки. */
.odo-drum.is-minor { color: var(--muted); }

.odo-unit {
    margin: 0 4px 0 2px;
    color: var(--dim);
    font-size: 12.5px;
    font-weight: 400;
}
.odo-unit:last-child { margin-right: 0; }

.game-right-section {
    border-left: 1px solid var(--line-soft);
    min-width: 0;
    overflow: hidden;
}

/* Вид списка — просто словами, как фильтры ленты: под рукой, но нажать
   не зовёт. Слова начинаются по одной линии с номерами строк. */
.game-sort {
    display: flex;
    gap: 12px;
    padding: 9px 14px 2px;
}
.game-sort-btn {
    padding: 0;
    border: 0;
    background: none;
    color: var(--dim);
    font: inherit;
    font-size: 11.5px;
    cursor: pointer;
    transition: color .15s ease;
}
.game-sort-btn:hover { color: var(--text); }
.game-sort-btn:focus-visible { outline: 1px solid var(--accent-line); outline-offset: 2px; }
.game-sort-btn[aria-pressed="true"] { color: var(--accent); }
/* пальцу нужна зона больше самого слова: пустой ::after и есть эта зона */
@media not all and (hover: hover) and (pointer: fine) {
    .game-sort-btn { position: relative; }
    .game-sort-btn::after { content: ""; position: absolute; inset: -8px -6px -4px; }
}

/* Высота меньше на строку переключателя: вся колонка остаётся прежней,
   и крупная карточка слева из-за неё не растягивается. */
.game-lib {
    counter-reset: gamerank;
    display: flex;
    flex-direction: column;
    max-height: 218px;
    overflow-y: auto;
    padding: 4px 8px 8px;
    scrollbar-width: thin;
    scrollbar-color: var(--line) transparent;
    transition: opacity .2s ease;
}
/* Второй вид впервые грузится — старый список притухает, чтобы нажатие
   не казалось пропавшим. */
.game-lib.is-loading { opacity: .45; }

/* Плитка — кнопка: по клику её статистика уезжает в крупный блок слева.
   Собственные рамку, фон и шрифт браузера приходится снимать, иначе список
   перестаёт выглядеть списком. */
.game {
    counter-increment: gamerank;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 4px 6px;
    border-radius: 2px;
    min-width: 0;
    transition: background .15s ease;

    width: 100%;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}
.game:hover { background: var(--panel-hover); }
.game:focus-visible {
    outline: 1px solid var(--accent-line);
    outline-offset: 1px;
}

/* Выбранная — та, что сейчас показана крупно. Без этой пары подсветок
   непонятно, чью статистику видишь: номер держит акцент, фон — место. */
.game.is-active { background: var(--panel-hover); }
.game.is-active::before { color: var(--accent); }
.game.is-active .game-info .playtime-forever { color: var(--muted); }

.game::before {
    content: counter(gamerank, decimal-leading-zero);
    flex: 0 0 auto;
    color: var(--faint);
    font-size: 11.5px;
}

.game img {
    width: 22px;
    height: 22px;
    flex: 0 0 auto;
    object-fit: cover;
    border-radius: 2px;
}

.game-info {
    display: flex;
    align-items: baseline;
    gap: 12px;
    min-width: 0;
    flex: 1 1 auto;
}

.game-info .game-title {
    font-size: 12.5px;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1 1 auto;
    min-width: 0;
}

.game-info .playtime-forever {
    flex: 0 0 auto;
    font-size: 11.5px;
    color: var(--dim);
}
.game:hover .game-info .playtime-forever { color: var(--muted); }

/* -------------------------------------------------------------- tiktok --
   Лента как в самом TikTok: один пост на кадр, следующий — прокруткой вниз.
   Свёрнута по умолчанию: наружу торчит верхушка первого поста, чтобы было
   видно, что блок разворачивается и там что-то есть. */

.tiktok-wrap {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius-tile);
    /* max-height, а не height: только её можно плавно анимировать, а высота
       развёрнутого состояния зависит от ширины колонки и заранее неизвестна. */
    max-height: 96px;
    transition: max-height .35s ease;
}
.tiktok-wrap.expanded { max-height: 900px; }

/* Пока свёрнуто, ленту нельзя ни прокрутить, ни нажать: торчащий кадр —
   это приглашение развернуть, а не рабочий элемент. Без этого в щель
   в сто пикселей можно было листать видео вслепую.
   Кнопка-шеврон лежит снаружи обёртки, её это не касается. */
.tiktok-wrap:not(.expanded) {
    cursor: pointer;
}
.tiktok-wrap:not(.expanded) .tiktok-feed {
    overflow: hidden;
    pointer-events: none;
    /* Свёрнутое превью размыто: видно, что там ролик, но не видно какой —
       это приглашение открыть, а не сам просмотр украдкой через щель.
       Увеличение идёт в паре с размытием: blur подмешивает по краям кадра
       прозрачность, и без запаса по бокам вдоль обрезки шла бы светлая
       кайма. */
    filter: blur(9px);
    transform: scale(1.12);
}

/* Расплывается и собирается за то же время, что раскрывается сама обёртка,
   иначе разворот распадается на два отдельных движения. */
.tiktok-feed {
    transition: filter .35s ease, transform .35s ease;
}

/* Затемнение по нижнему краю: показывает, что кадр обрезан, а не кончился.
   В развёрнутом виде оно не нужно и мешало бы подписи. */
.tiktok-wrap::after {
    content: "";
    position: absolute;
    inset: auto 0 0 0;
    height: 48px;
    background: linear-gradient(to top, var(--bg), transparent);
    pointer-events: none;
    transition: opacity .35s ease;
}
.tiktok-wrap.expanded::after { opacity: 0; }

/* Шеврон вместо слова: надпись в узкой колонке занимала целую строку
   и звучала как команда, а уголок читается сам. Развернули — он
   переворачивается. */
.tiktok-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    margin-top: 4px;
    padding: 4px;
    border: 0;
    background: none;
    color: var(--dim);
    cursor: pointer;
    transition: color .15s ease;
}
.tiktok-toggle:hover { color: var(--accent); }
.tiktok-toggle svg {
    width: 18px;
    height: 18px;
    transition: transform .35s ease;
}
.tiktok-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }

.tiktok-feed {
    /* Полный формат вертикального видео вернулся: кадр не обрезается,
       а блок не занимает полколонки, потому что свёрнут. */
    aspect-ratio: 9 / 16;
    overflow-y: auto;
    scroll-snap-type: y mandatory;
    background: var(--panel-soft);
    /* полоса прокрутки тут только мешает: перелистывание и так очевидно */
    scrollbar-width: none;
}
.tiktok-feed::-webkit-scrollbar { display: none; }

.tiktok-item {
    position: relative;
    height: 100%;
    scroll-snap-align: start;
    scroll-snap-stop: always;
    overflow: hidden;
}

/* Размытая подложка под кадром. Из репостов вертикальные — меньше половины:
   есть квадратные, 4:3 и даже горизонтальные. С object-fit: cover квадрат
   резался по бокам до узкой полоски, а горизонтальное видео превращалось
   в кашу. Поэтому кадр показывается целиком (contain), а пустота вокруг
   закрывается размытой копией — ровно как в самом TikTok. */
.tiktok-item .backdrop {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: blur(22px) saturate(.7) brightness(.45);
    /* Небольшое увеличение прячет размытые края, которые иначе светятся
       по периметру. */
    transform: scale(1.15);
    z-index: 0;
}

.tiktok-item video,
.tiktok-item .poster {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

/* Пока видео не загрузилось, на его месте обложка — иначе при перелистывании
   зияет пустой тёмный прямоугольник. */
.tiktok-item .poster {
    position: absolute;
    inset: 0;
    transition: opacity .25s ease;
}
.tiktok-item.ready .poster { opacity: 0; }

/* Фото-пост: кадры листаются вбок. Нативная прокрутка тут оставлена как
   запасной путь, но полагаться на неё нельзя — вложенный горизонтальный
   скролл внутри вертикального scroll-snap перехватывается родителем,
   а мышью вбок вообще не покрутить. Поэтому листают стрелки и тап по краю. */
.tiktok-photos {
    position: relative;
    z-index: 1;
    display: flex;
    height: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
}
.tiktok-photos::-webkit-scrollbar { display: none; }

.tiktok-photos img {
    flex: 0 0 100%;
    width: 100%;
    height: 100%;
    /* кадры фото-постов тоже бывают не 9:16 — показываем целиком */
    object-fit: contain;
    scroll-snap-align: center;
    scroll-snap-stop: always;
}

.tiktok-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    border: 0;
    border-radius: 50%;
    width: 26px;
    height: 26px;
    background: rgba(0, 0, 0, .55);
    color: var(--text);
    font: inherit;
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    opacity: .75;
    transition: opacity .15s ease, color .15s ease;
}
.tiktok-arrow:hover { opacity: 1; color: var(--accent); }
.tiktok-arrow[disabled] { opacity: .2; cursor: default; }
.tiktok-arrow.prev { left: 6px; }
.tiktok-arrow.next { right: 6px; }

/* Точки: сколько кадров и какой сейчас. Без них непонятно, что пост вообще
   листается вбок — в ленте это единственная подсказка. */
.tiktok-dots {
    position: absolute;
    top: 8px;
    left: 0;
    right: 0;
    display: flex;
    justify-content: center;
    gap: 4px;
    pointer-events: none;
    z-index: 2;
}
.tiktok-dots i {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: rgba(255, 255, 255, .35);
    transition: background .2s ease;
}
.tiktok-dots i.on { background: #fff; }

/* Подпись поверх нижнего края: описание, автор и ссылка на оригинал.
   Затемнение под ней осталось ровно таким, каким было до появления
   описания, — усиливать его не стали. */
.tiktok-meta {
    position: absolute;
    inset: auto 0 0 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 24px 10px 8px;
    background: linear-gradient(to top, rgba(0,0,0,.8), transparent);
    font-size: 12.5px;
    z-index: 2;
}
.tiktok-meta a { color: var(--text); }
.tiktok-meta a:hover { color: var(--accent); }

/* Автор, время и счётчик — своей строкой под описанием. */
.tiktok-meta-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* Описание. По умолчанию две строки: этого хватает понять, о чём пост,
   и кадр остаётся кадром. */
.tiktok-desc {
    line-height: 1.45;
    color: var(--text);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* В подписях попадаются сплошные простыни хештегов без пробелов —
       без этого они распирали бы кадр вбок. */
    overflow-wrap: anywhere;
}

/* Развёрнутое всё равно с потолком: подпись бывает длиннее самого экрана,
   и без предела она закрыла бы видео целиком. Дальше прокруткой, но своей —
   contain не пускает её наружу, в ленту постов. */
.tiktok-desc.open {
    -webkit-line-clamp: unset;
    max-height: 7.3em;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
}
.tiktok-desc.open::-webkit-scrollbar { display: none; }

/* «ещё» — своей строкой, а не в конце текста: втиснуть его в последнюю
   строку можно только заливкой под ним, а заливке тут не за что зацепиться,
   под ней произвольный кадр. */
.tiktok-more {
    align-self: flex-start;
    border: 0;
    padding: 0;
    background: none;
    font: inherit;
    color: var(--muted);
    cursor: pointer;
}
.tiktok-more:hover { color: var(--accent); }
/* пальцу нужна зона больше самого слова: пустой ::after и есть эта зона */
@media not all and (hover: hover) and (pointer: fine) {
    .tiktok-more { position: relative; }
    .tiktok-more::after { content: ""; position: absolute; inset: -8px; }
}

/* Возраст поста рядом с автором. Отдельным приглушённым цветом: это
   пояснение, а не ссылка, и на него не должно тянуть нажать. */
.tiktok-age {
    color: var(--dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}
.tiktok-count {
    margin-left: auto;
    color: var(--dim);
    font-variant-numeric: tabular-nums;
}

/* Звук выключен по умолчанию — иначе страница заговорит сама. Кнопка —
   одна на всю ленту и висит в углу обёртки, а не на каждом ролике: так она
   не уезжает вместе с роликом при пролистывании. Значок динамика в круглой
   полупрозрачной пилюле, как кнопки звука в Reels и в самом TikTok; надписи
   нет, «unmute / mute» — в подсказке. С мышью при наведении на кнопку из неё
   влево выезжает регулятор громкости (volume.js), и пилюля растягивается
   вместе с ним, как в плеере YouTube. Пока лента свёрнута, кнопки нет. */
.tiktok-sound-wrap {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 3;
    display: flex;
    align-items: center;
    height: 34px;
    border-radius: 999px;
    background: rgba(0, 0, 0, .45);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    transition: background .2s ease, opacity .25s ease, transform .35s cubic-bezier(.2, 0, 0, 1), visibility 0s;
}
.tiktok-wrap:not(.expanded) .tiktok-sound-wrap {
    opacity: 0;
    visibility: hidden;
    transform: scale(.8);
    transition: opacity .15s ease, transform .15s ease, visibility 0s linear .15s;
}
.tiktok-sound-wrap:hover { background: rgba(0, 0, 0, .62); }
.tiktok-sound-wrap .vol {
    width: 0;
    margin-left: 0;
    opacity: 0;
    transition: width .32s cubic-bezier(.2, 0, 0, 1), margin .32s cubic-bezier(.2, 0, 0, 1), opacity .2s ease;
}
.tiktok-sound-wrap .vol-track { background: rgba(255, 255, 255, .22); }
@media (hover: hover) and (pointer: fine) {
    .tiktok-sound-wrap:hover .vol,
    .tiktok-sound-wrap:has(:focus-visible) .vol {
        width: 72px;
        margin-left: 14px;
        opacity: 1;
    }
}
.tiktok-sound {
    position: relative;
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: #fff;
    cursor: pointer;
    transition: color .15s ease, transform .15s ease;
}
.tiktok-sound:hover { color: var(--accent); }
.tiktok-sound:active { transform: scale(.9); }
.tiktok-sound:focus-visible { outline: 1px solid var(--accent-line); outline-offset: 2px; }

.tiktok-sound-ico {
    width: 18px;
    height: 18px;
}

/* Ленту развернули — кнопка один раз чуть вспыхивает и расходится кольцом:
   вот где звук. Ждёт, пока обёртка раскроется. */
.tiktok-sound-wrap.blink { animation: sound-blink .8s ease-out .35s; }
@keyframes sound-blink {
    0%   { box-shadow: 0 0 0 0 rgba(255, 255, 255, .45); }
    25%  { background: rgba(255, 255, 255, .3); transform: scale(1.12); }
    100% { box-shadow: 0 0 0 14px rgba(255, 255, 255, 0); }
}

/* Динамик один на оба состояния: меняются только волны и перечёркивание.
   Класс muted означает «звука нет» — то есть кнопка предлагает включить. */
.tiktok-sound .slash { display: none; }
.tiktok-sound.muted .waves { display: none; }
.tiktok-sound.muted .slash { display: block; }

/* Значок паузы поверх кадра. Полупрозрачный и мелкий: он подтверждает, что
   видео остановлено, а не зовёт нажать — нажимают по самому кадру, поэтому
   значок событий не ловит вовсе. */
.tiktok-pause {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    opacity: 0;
    transition: opacity .18s ease;
}
.tiktok-pause svg {
    width: 34px;
    height: 34px;
    color: rgba(255, 255, 255, .5);
    /* Тень, а не сплошная подложка: кадр под значком бывает и белым,
       и чёрным, и без неё значок пропадал бы на светлом. */
    filter: drop-shadow(0 1px 4px rgba(0, 0, 0, .55));
}

/* Только в развёрнутой ленте: в свёрнутой всё стоит на паузе по определению,
   и значок висел бы на превью не переставая. */
.tiktok-wrap.expanded .tiktok-item.paused .tiktok-pause { opacity: 1; }

/* -------------------------------------------------------------- github --
   Наружу приходят одни числа: ни имён репозиториев, ни сообщений коммитов.
   Поэтому вся секция — это счётчики и календарь, а не лента изменений. */

.gh-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 10px 26px;
    margin-bottom: 16px;
}

.gh-stat {
    display: flex;
    flex-direction: column;
    line-height: 1.3;
}

.gh-stat .num {
    font-size: 19px;
    font-weight: 500;
    color: var(--strong);
    font-variant-numeric: tabular-nums;
}

.gh-stat .lab {
    font-size: 11.5px;
    color: var(--muted);
    letter-spacing: .03em;
}

.gh-stat .add { color: var(--accent); }
.gh-stat .del { color: var(--muted); margin-left: 8px; }

/* календарь шире колонки на узких экранах — кладём его в ту же ленту,
   что треки и железо, чтобы работали те же затухания края */
.gh-calendar {
    display: flex;
    flex-direction: column;
    gap: 5px;
    width: 100%;
    /* клетки по 9px с зазором 2px — уже календарь не сжимается, и лента листается */
    min-width: calc(var(--weeks, 53) * 11px);
    padding: 1px;
}

/* --weeks ставит time.js: подписи месяцев и клетки делят одни и те же
   колонки, иначе резиновые колонки двух сеток разъехались бы */
.gh-months {
    display: grid;
    grid-auto-flow: column;
    grid-template-columns: repeat(var(--weeks, 53), minmax(9px, 1fr));
    gap: 2px;
    font-size: 10.5px;
    color: var(--muted);
    line-height: 1;
    height: 12px;
}

.gh-months span {
    grid-row: 1;
    white-space: nowrap;
}

.gh-heatmap {
    display: grid;
    grid-auto-flow: column;
    grid-template-rows: repeat(7, auto);
    grid-template-columns: repeat(var(--weeks, 53), minmax(9px, 1fr));
    gap: 2px;
}
.gh-heatmap .gh-cell {
    width: auto;
    height: auto;
    aspect-ratio: 1;
}

.gh-cell {
    width: 9px;
    height: 9px;
    border-radius: 2px;
    background: var(--panel-soft);
}
.gh-cell[data-level="1"] { background: rgba(var(--accent-rgb), .22); }
.gh-cell[data-level="2"] { background: rgba(var(--accent-rgb), .45); }
.gh-cell[data-level="3"] { background: rgba(var(--accent-rgb), .70); }
.gh-cell[data-level="4"] { background: rgb(var(--accent-rgb)); }

.gh-legend {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 12px;
    font-size: 11.5px;
    color: var(--muted);
}
.gh-legend span:first-child { margin-right: 4px; }
.gh-legend span:last-child { margin-left: 4px; }

.gh-empty {
    color: var(--muted);
    font-size: 12.5px;
}

/* ------------------------------------------------------------ rotation --
   Доля считается внутри топа, а не от всех прослушиваний за неделю: общее
   число last.fm по периоду не отдаёт, поэтому и подпись говорит «across N». */

.rotation { margin-top: 20px; }

.rotation-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px 10px;
    margin-bottom: 10px;
}

.rotation-title {
    font-size: 12.5px;
    color: var(--muted);
    letter-spacing: .04em;
    white-space: nowrap;
}

.rotation-sub {
    font-size: 11.5px;
    color: var(--dim);
    margin-left: auto;
}

.rotation-list {
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: rot;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.rotation-list li {
    counter-increment: rot;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 3px 6px;
    border-radius: 2px;
    min-width: 0;
    transition: background .15s ease;
}
.rotation-list li:hover { background: var(--panel-hover); }

.rotation-list li::before {
    content: counter(rot, decimal-leading-zero);
    flex: 0 0 auto;
    color: var(--faint);
    font-size: 11.5px;
}

.rot-name {
    font-size: 12.5px;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1 1 auto;
    min-width: 0;
}

/* полоска длиной в долю артиста — единственный график на странице,
   поэтому она тонкая и без подписей */
.rot-bar {
    flex: 0 0 120px;
    height: 3px;
    border-radius: 2px;
    background: var(--panel-soft);
    overflow: hidden;
}
.rot-bar i {
    display: block;
    height: 100%;
    border-radius: 2px;
    background: rgba(var(--accent-rgb), .55);
    transition: width .7s cubic-bezier(.2, .7, .2, 1);
}
.rotation-list li:first-child .rot-bar i { background: rgb(var(--accent-rgb)); }

.rot-plays {
    flex: 0 0 auto;
    font-size: 11.5px;
    color: var(--dim);
    font-variant-numeric: tabular-nums;
}
.rotation-list li:hover .rot-plays { color: var(--muted); }

.rotation-note {
    margin-top: 10px;
    font-size: 11.5px;
    color: var(--dim);
}
.rotation-note b {
    font-weight: 400;
    color: var(--accent);
}

/* ----------------------------------------------------------- guestbook -- */

.gb-composer {
    position: relative;
    display: flex;
    flex-direction: column;
}

.gb-line {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
    padding-block: 10px;
    border-bottom: 1px solid var(--line-soft);
    transition: border-color .15s ease;
}
.gb-line:focus-within { border-bottom-color: var(--accent-line); }

.gb-prompt {
    flex: 0 0 auto;
    color: var(--accent);
    font-size: 13px;
    line-height: 1;
}

.gb-nickfield,
.gb-bodyfield {
    font: inherit;
    font-size: 13px;
    color: var(--text);
    background: none;
    border: 0;
    outline: 0;
    padding: 0;
    min-width: 0;
}
.gb-nickfield { flex: 1 1 auto; }
.gb-nickfield::placeholder { color: var(--dim); }
@media not all and (hover: hover) and (pointer: fine) {
    .gb-nickfield { padding-block: 6px; }
    /* мельче 16px — и Safari на iPhone приближает всю страницу при фокусе на поле */
    .gb-nickfield,
    .gb-bodyfield { font-size: 16px; }
}

/* Высоту растит скрипт, поэтому ни ручки ресайза, ни скроллбара. Линия под
   полем — тенью: высоту ставят по scrollHeight, а рамка в него не входит.
   Отступ слева — ширина «>» и зазор .gb-line, текст встаёт под ником. */
.gb-bodyfield {
    line-height: 1.55;
    resize: none;
    overflow: hidden;
    min-height: 42px;
    padding: 12px 0 12px 16px;
    box-shadow: inset 0 -1px 0 var(--line-soft);
    transition: box-shadow .15s ease;
}
.gb-bodyfield:focus { box-shadow: inset 0 -1px 0 var(--accent-line); }
.gb-bodyfield::placeholder { color: var(--faint); }

/* ловушка: убрана с экрана, но остаётся в разметке — её и заполняют боты */
.gb-trap {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* Рожицы живут в одной строке со счётчиком и отправкой: слева набор,
   справа действие. Сжимаются и переносятся внутри себя, если не влезают. */
.gb-faces {
    display: flex;
    flex-wrap: wrap;
    flex: 0 1 auto;
    gap: 4px;
    min-width: 0;
}

.gb-face {
    font: inherit;
    font-size: 12.5px;
    line-height: 1.2;
    color: var(--muted);
    background: none;
    border: 0;
    border-radius: var(--radius);
    padding: 4px 7px;
    cursor: pointer;
    transition: background .15s ease, color .15s ease;
}
.gb-face:hover {
    background: var(--panel-hover);
    color: var(--text);
}
.gb-face:focus-visible { outline: 1px solid var(--accent-line); }

.gb-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 12px;
    min-width: 0;
    padding-top: 10px;
}

/* Статус — строка над правым краем композера, вне потока, поэтому ни поля,
   ни кнопку не двигает (в строке с рожицами статус выталкивал send вниз).
   Пустая прозрачна, но не display: none — live-регион остаётся для
   скринридера. */
.gb-status {
    position: absolute;
    right: 0;
    bottom: 100%;
    max-width: calc(100% - 44px);
    padding-bottom: 4px;
    font-size: 11.5px;
    line-height: 16px;
    color: var(--muted);
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transform: translateY(6px);
    transition: opacity .18s ease, transform .18s ease;
}
.gb-status.shown { opacity: 1; transform: none; }
/* «sent, thanks» и лимиты гаснут сами: GB_NOTICE_MS и GB_FADE_MS в time.js */
.gb-status.fading { opacity: 0; transform: translateY(6px); transition-duration: .4s; }
.gb-status.bad { color: var(--accent); }

.gb-status-text {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* «sending» — точки проявляются по одной: . .. ... При reduced motion
   анимации нет, и стоят все три. */
.gb-dots > :nth-child(2) { animation: gb-dot-2 1.2s steps(1, end) infinite; }
.gb-dots > :nth-child(3) { animation: gb-dot-3 1.2s steps(1, end) infinite; }
@keyframes gb-dot-2 { from { opacity: 0; } 33.333% { opacity: 1; } to { opacity: 1; } }
@keyframes gb-dot-3 { from { opacity: 0; } 66.667% { opacity: 1; } to { opacity: 1; } }

/* счётчик идёт на убывание: важно, сколько осталось, а не сколько набрано */
.gb-count {
    margin-left: auto;
    flex: 0 0 auto;
    font-size: 11.5px;
    color: var(--dim);
    font-variant-numeric: tabular-nums;
}
.gb-count.low { color: var(--accent); }

.gb-submit {
    flex: 0 0 auto;
    font: inherit;
    font-size: 12.5px;
    color: var(--accent);
    background: none;
    border: 0;
    border-radius: var(--radius);
    padding: 6px 8px;
    cursor: pointer;
    transition: background .15s ease, color .15s ease;
}
.gb-submit::before { content: "[ "; }
.gb-submit::after { content: " ]"; }
.gb-submit:hover:not(:disabled) { background: var(--accent-soft); }
.gb-submit:disabled {
    color: var(--dim);
    cursor: default;
}
.gb-submit:focus-visible { outline: 1px solid var(--accent-line); }

/* -------------------------------------------------------------- footer -- */

footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    margin-top: 40px;
    padding: 18px 0 48px;
    border-top: 1px solid var(--line-soft);
    color: var(--faint);
    font-size: 12.5px;
}

/* Футер — одна живая строка: тихий // и фраза, которая меняется по кругу.
   Это единственное акцентное пятно внизу страницы. */
footer .quote {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-width: 0;
}

.quote-mark {
    flex: 0 0 auto;
    color: var(--faint);
}

.quote-text {
    color: var(--accent);
    transition: opacity .25s ease;
}
.quote-text.swap { opacity: 0; }

.dev-splash {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: grid;
    place-items: center;
    background: rgba(0, 0, 0, .8);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    pointer-events: none;
    animation: dev-splash 3.6s ease forwards;
}

.dev-glyph {
    position: relative;
    width: min(84vmin, 640px);
    aspect-ratio: 1;
    color: var(--accent);
    animation: dev-breathe 1.4s ease-in-out 2;
}
.dev-glyph svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    filter: drop-shadow(0 0 22px rgba(var(--accent-rgb), .55));
}
.dev-glyph path,
.dev-glyph circle {
    stroke-width: 3px;
    vector-effect: non-scaling-stroke;
}
.dev-gear { animation: dev-turn 3.6s linear forwards; }
.dev-gear circle { opacity: .4; }

@keyframes dev-splash {
    0%   { opacity: 0; }
    8%   { opacity: 1; }
    78%  { opacity: 1; }
    100% { opacity: 0; visibility: hidden; }
}
@keyframes dev-breathe {
    0%, 100% { opacity: 1; transform: scale(1); }
    50%      { opacity: .3; transform: scale(.96); }
}
@keyframes dev-turn {
    to { transform: rotate(45deg); }
}

.dev-mode footer { padding-bottom: 96px; }

.dev-tape {
    position: fixed;
    left: 50%;
    bottom: max(16px, env(safe-area-inset-bottom));
    z-index: 50;
    max-width: calc(100vw - 32px);
    padding: 2px;
    border-radius: var(--radius-tile);
    transform: translateX(-50%);
    background-color: rgba(var(--accent-rgb), .14);
    background-image: linear-gradient(45deg,
        var(--accent) 25%, transparent 25%, transparent 50%,
        var(--accent) 50%, var(--accent) 75%, transparent 75%, transparent);
    background-size: 16px 16px;
    box-shadow: 0 8px 28px rgba(0, 0, 0, .7);
    animation: dev-tape-crawl .8s linear infinite;
}

.dev-tape-inner {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 7px 15px 7px 13px;
    border-radius: calc(var(--radius-tile) - 2px);
    background: var(--panel);
    color: var(--muted);
    font-size: 12.5px;
    line-height: 1.4;
    white-space: nowrap;
}
.dev-tape-inner b {
    color: var(--accent);
    font-weight: 500;
}

.dev-tape-dot {
    flex: 0 0 auto;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 8px rgba(var(--accent-rgb), .8);
    animation: dev-dot 1.6s ease-in-out infinite;
}

.dev-tape-cursor {
    color: var(--accent);
    animation: dev-cursor 1.1s steps(1) infinite;
}

@keyframes dev-tape-crawl {
    from { background-position: 0 0; }
    to   { background-position: 16px 0; }
}
@keyframes dev-dot {
    50% { opacity: .35; box-shadow: 0 0 2px rgba(var(--accent-rgb), .4); }
}
@keyframes dev-cursor {
    50% { opacity: 0; }
}

.halloween body::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 84px;
    background: url("/image/halloween/garland-047e50278771b9a4bd6bebfb769af1da.svg") top left / auto 64px round no-repeat;
    pointer-events: none;
}

.spider {
    position: absolute;
    top: 0;
    right: 96px;
    z-index: 2;
    width: 32px;
    height: 200px;
    background:
        url("/image/halloween/spider-6be53846a5a0180164828753b881ea76.svg") bottom center / 32px 32px no-repeat,
        linear-gradient(var(--line), var(--line)) top center / 1px calc(100% - 22px) no-repeat;
    transform-origin: top center;
    animation: spider-sway 5s ease-in-out infinite alternate;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: translate 2.2s cubic-bezier(.3, .6, .4, 1);
}
.spider.climbing {
    translate: 0 calc(30px - 100%);
    transition-duration: .45s;
    transition-timing-function: cubic-bezier(.5, 0, .3, 1);
}
@keyframes spider-sway {
    from { transform: rotate(3deg); }
    to   { transform: rotate(-3deg); }
}

.bat {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 40;
    width: 30px;
    height: 12px;
    pointer-events: none;
}
.bat::before {
    content: "";
    position: absolute;
    inset: 0;
    background: url("/image/halloween/bat-948651e2a7e35ae285efb66bb9a44c8b.svg") center / contain no-repeat;
    animation: bat-flap .16s ease-in-out infinite alternate;
}
@keyframes bat-flap {
    to { transform: scaleY(.35); }
}

@media (max-width: 720px) {
    .halloween body::before { height: 60px; background-size: auto 44px; }
    .spider { right: 28px; height: 130px; }
}

/* ---------------------------------------------------------- responsive -- */

/* колонки схлопываются раньше остального: при 900px правой уже тесно */
@media (max-width: 900px) {
    .layout {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 0;
    }
    /* в общей стопке github идёт следом за ссылками, и отступ нужен и ему */
    .col-main > .panel:first-child {
        margin-top: 34px;
    }

    /* Обе колонки растворяются, панели встают в общую стопку. Порядок после
       этого задаётся разметкой, а она устроена под две колонки: блок tiktok
       живёт в левой и оказался бы сразу под ссылками, в самом верху. Поэтому
       на узком экране порядок задаётся явно — и tiktok уходит под игры. */
    .col-side,
    .col-main { display: contents; }
    .side-mark { display: none; }
    footer::after {
        content: "";
        width: 100%;
        height: 40px;
        margin-top: 26px;
        background-color: var(--faint);
        -webkit-mask: url("/image/moon-cat-e34e9506566e6e0c24013ed7670da077.svg") center / contain no-repeat;
        mask: url("/image/moon-cat-e34e9506566e6e0c24013ed7670da077.svg") center / contain no-repeat;
    }

    /* Лента — единственное место на странице с прилипанием. Остальные секции
       ничем не помечены, поэтому обычная прокрутка везде сохраняется.
       proximity, а не mandatory: mandatory тянул бы страницу к ленте даже
       когда просто проходишь мимо. */
    /* Притяжение появляется только вместе с развёрнутой лентой — и на самой
       странице тоже. Постоянный scroll-snap-type на html притягивал её к
       ленте уже при открытии, хотя открывать никто ничего не просил.
       Пока свёрнута, наружу торчит полоска в 96px: листать там нечего,
       и прилипание к ней только мешало бы уйти. */
    html:has(.tiktok-wrap.expanded) { scroll-snap-type: y proximity; }
    #tiktok-section:has(.tiktok-wrap.expanded) { scroll-snap-align: center; }

    /* Пост по высоте экрана, а не по ширине колонки. На телефоне колонка
       во всю ширину, и кадр 9:16 оказывался выше экрана — приходилось
       прокручивать страницу внутри поста, то есть ровно та беда, от которой
       уходим: тянешься листать пост и уезжаешь страницей.
       Оставшейся трети экрана хватает, чтобы взяться за страницу и прокрутить
       её мимо ленты, не задев видео. */
    .tiktok-feed {
        aspect-ratio: auto;
        height: 68vh;
        height: 68svh;
    }
    .tiktok-wrap.expanded {
        max-height: 68vh;
        max-height: 68svh;
    }

    .card-id        { order: 1; }
    .about          { order: 2; }
    .contacts       { order: 3; }
    .github         { order: 4; }
    .stack          { order: 5; }
    .music          { order: 6; }
    .game-section   { order: 7; }
    #tiktok-section { order: 8; }
    .blackmail      { order: 9; }

    /* в одну колонку огромный квадрат фото не нужен — кладём карточку в строку */
    .card-id {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 14px;
    }
    /* 108, а не 88 как раньше: на телефоне фото единственная картинка в шапке,
       и в прежнем размере оно выглядело обрезком.
       Скругление на ярус ниже: 24px от 290px — это 8%, а от 108px уже 22%,
       и тот же радиус читался бы здесь заметно круглее, чем на широком экране. */
    .card-id .avatar { width: 108px; aspect-ratio: 1; border-radius: var(--radius-tile); }
    /* на 108px счётчик мельче и ближе к краю */
    .avatar-count { right: 5px; bottom: 5px; padding: 0 6px; font-size: 10.5px; }
    .card-id-text { flex: 1 1 auto; }

    .col-side .links { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 720px) {
    body { padding-inline: max(16px, env(safe-area-inset-left)) max(16px, env(safe-area-inset-right)); }

    .gh-stats { gap: 10px 20px; }
    .gh-stat .num { font-size: 17px; }

    /* в одну колонку подпись не помещается рядом с заголовком */
    .rotation-sub { margin-left: 0; flex-basis: 100%; }
    .rot-bar { flex-basis: 70px; }

    .gb-submit { min-height: 44px; padding-inline: 22px; }
    /* пальцем в кнопку 22px не попасть */
    .gb-face { min-height: 40px; padding: 4px 12px; flex: 0 0 auto; }
    /* крупные кнопки в строку с отправкой не влезут, а тремя рядами занимают
       пол-экрана — даём им одну строку с прокруткой, как у треков */
    .gb-faces {
        flex-wrap: nowrap;
        flex-basis: 100%;
        overflow-x: auto;
        scrollbar-width: none;
    }
    .gb-faces::-webkit-scrollbar { display: none; }
    /* подзаголовок тут во всю ширину, и свободного угла справа для статуса
       нет — даём ему свою полосу над композером */
    .blackmail .panel-body { margin-top: 27px; }

    .shell { padding-block: 40px 0; }

    .col-side .links,
    .links { grid-template-columns: minmax(0, 1fr); }

    .game-bar { grid-template-columns: minmax(0, 1fr); }
    .game-right-section { border-left: 0; border-top: 1px solid var(--line-soft); }
    .game-lib { max-height: 220px; }

    .scroll-btn { display: none; }
    .scroll-rail { scroll-snap-type: x mandatory; }
    .track { scroll-snap-align: start; }

    /* на телефоне всё, во что тычут пальцем, крупнее */
    .listen-card { grid-template-columns: 60px minmax(0, 1fr); gap: 12px; }
    .listen-cover { width: 60px; height: 60px; }
    .listen-play { width: 40px; height: 40px; }
    /* логотипы делят строку поровну, но не шире 44px: девять по 40 в узкую
       плашку не влезали, и пара уезжала одна на вторую строку */
    .listen-links { flex-wrap: nowrap; gap: 5px; }
    .listen-link { flex: 1 1 0; width: auto; max-width: 44px; height: 40px; }

    footer { flex-direction: column; align-items: flex-start; gap: 6px; }
}

@media (max-width: 420px) {
    .track { width: 205px; }
}

@media (max-width: 560px) {
    .dev-tape-more { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation: none !important; transition: none !important; }
    /* гаснет только анимацией: без неё закрыла бы страницу насовсем */
    .dev-splash { display: none; }
}
