:root {
  --color-bg: #fcfcfc;
  --color-text-primary: #1a1a1a;
  --color-text-secondary: #767676;
  --color-border-divider: rgba(6, 12, 20, 0.1);

  --radius-xs: 4px;

  --space-100: 4px;
  --space-200: 8px;
  --space-300: 12px;

  --font-sans: 'Inter', sans-serif;
  --font-serif: 'Piazzolla', serif;
  --font-display: 'Fraunces', serif;

  --tilt-perspective: 1000px;
  --tilt-return: 1000ms;
  --tilt-return-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --tilt-follow: 400ms;
  --tilt-follow-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --tilt-glare-opacity: 0.32;
  --tilt-glare-fade: 300ms;
  --tilt-glare-ease: cubic-bezier(0.22, 1, 0.36, 1);

  --page-scale: 1;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* Место под скроллбар зарезервировано всегда. Нужно для блокировки
     скролла при открытом меню (overflow:hidden в script.js): без этого
     скроллбар пропадает, ширина вьюпорта скачет на ~15px и всё
     съезжает — в том числе плавающее меню, спозиционированное fixed
     по центру экрана. */
  scrollbar-gutter: stable;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--color-bg);
}

body {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  -webkit-font-smoothing: antialiased;
}

/* Обёртка только для удобного просмотра: масштабирует фрейм 1920x2745
   по ширине окна браузера (но не увеличивает сверх 100%), высота
   скроллится как обычная страница. Сам .page остаётся точной копией
   фрейма Figma в исходном размере — масштаб не меняет вёрстку. */
.stage {
  position: relative;
  flex-shrink: 0;
}

h1, h2, h3, p {
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

.underline-link {
  position: relative;
}

img {
  display: block;
  max-width: none;
}

.text-trim {
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
}

/* ---------- page / desktop frame ---------- */

.page {
  position: absolute;
  top: 0;
  left: 0;
  width: 1920px;
  height: 2738px;
  background: var(--color-bg);
  transform-origin: top left;
  /* clip (не hidden!): обрезает фон-глоу по границе фрейма так же
     визуально, но не создаёт скрытую внутреннюю прокрутку — hidden
     здесь ломал переходы по якорям меню (см. историю правок). */
  overflow: clip;
}

/* ---------- header ---------- */

.header {
  position: absolute;
  left: 40px;
  top: 40px;
  width: 1840px;
  height: 725px;
}

.hero-glow {
  position: absolute;
  left: 0;
  top: -299px;
  width: 1920px;
  height: 218px;
  /* Чисто декоративный слой — не должен перехватывать клики у контента
     под собой (см. тот же фикс у .footer-glow: там он реально ловил
     клики по тексту "Обо мне", потому что отрицательный inset у ::inner
     раздувает картинку далеко за пределы этого блока). */
  pointer-events: none;
}

.hero-glow__inner {
  position: absolute;
  inset: -192.66% -18.07% -349.54% -16.77%;
}

.hero-glow__img {
  display: block;
  width: 100%;
  height: 100%;
  animation: glow-pulse 18s ease-in-out infinite;
}

@keyframes glow-pulse {
  0%,
  100% {
    opacity: 0.6;
    transform: translate(0, 0);
  }
  25% {
    opacity: 0.85;
    transform: translate(2.5%, -2%);
  }
  50% {
    opacity: 1;
    transform: translate(-2%, 2.5%);
  }
  75% {
    opacity: 0.85;
    transform: translate(-2.5%, -2.5%);
  }
}

.hero-offer {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 598px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}

.hero-offer__title {
  width: 100%;
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 30px;
  line-height: normal;
  color: var(--color-text-primary);
  user-select: none;
  -webkit-user-select: none;
}

.caret {
  display: inline-block;
  width: 2px;
  height: 1.1em;
  background: var(--color-text-primary);
  animation: blink 1s steps(1) infinite;
  vertical-align: text-bottom;
}
@keyframes blink {
  50% {
    opacity: 0;
  }
}

.hero-offer__subtitle {
  width: 100%;
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 24px;
  line-height: normal;
  color: var(--color-text-secondary);
  user-select: none;
  -webkit-user-select: none;
}

.brand-identity {
  position: absolute;
  bottom: 118px;
  right: 0;
  width: 261px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-100);
}

.brand-identity__name {
  width: 100%;
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 20px;
  line-height: normal;
  color: var(--color-text-primary);
  user-select: none;
  -webkit-user-select: none;
}

.brand-identity__role {
  width: 100%;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: 22px;
  line-height: normal;
  color: var(--color-text-secondary);
}

/* Decoder-эффект: символы "расшифровываются" в текст роли и снова
   зашифровываются, по кругу. Цвета — из палитры сайта (никаких неоновых),
   моноширинный шрифт нужен, чтобы ширина строки не "прыгала" при скрэмбле. */
.brand-identity__role.fx-decoder {
  display: inline-block;
  color: var(--color-text-secondary);
  cursor: pointer;
  /* Символы для скрэмбла подобраны узкими (i,v,f,j,z...) так, чтобы каждое
     состояние анимации было не шире настоящего текста (255.8px из 261px
     блока) — это и есть исходный размер шрифта, без переносов и "прыжков". */
  white-space: nowrap;
}
.fx-decoder::after {
  /* Пока блок ещё скрыт/проявляется, анимация к элементу вообще не
     привязана — просто статичный расшифрованный текст (см. revealHero в
     js). Это важно: если привязать анимацию заранее и просто поставить её
     на паузу, застывшим кадром будет 0% кейфрейма (зашифрованное
     состояние) — получится, что шифрование как будто идёт с самого
     запуска сайта. */
  content: 'Product | UX/UI Designer';
}
.fx-decoder.is-ready::after {
  /* Автоцикл: decode → показ кликабельной Telegram-ссылки на 1.5s → долгая
     пауза → снова decode, по кругу (см. @keyframes fx-decoder). Наведение
     мыши перебивает цикл тем же пошаговым появлением "россыпью" в
     Telegram-ссылку, а при уходе курсора автоцикл перезапускается с нуля.
     Отрицательный delay = 30% от 8s цикла: анимация стартует сразу с
     чистого "Product | UX/UI Designer", а не с зашифрованного состояния на
     0% — шифрование включится только когда цикл дойдёт до конца. */
  animation: fx-decoder 8s steps(1) infinite;
  animation-delay: -2.4s;
}
.fx-decoder.is-hovering::after {
  animation: fx-decoder-scramble 0.6s steps(1) forwards;
}
/* Буквы проявляются не разом, а по частям (www → www.t.me → полностью),
   остальное пока остаётся зашифрованным — та же логика, что раньше
   расшифровывала "Product | UX/UI Designer". */
@keyframes fx-decoder-scramble {
  0% {
    content: 'ijf.z.kq/hxr_jfzx';
  }
  33% {
    content: 'www.z.kq/hxr_jfzx';
  }
  66% {
    content: 'www.t.me/hxr_jfzx';
  }
  100% {
    content: 'www.t.me/vkv_dsgn';
  }
}
@keyframes fx-decoder {
  0% {
    content: 'ijf.z.kq/hxr_jfzx';
  }
  2.5% {
    content: 'www.z.kq/hxr_jfzx';
  }
  5% {
    content: 'www.t.me/hxr_jfzx';
  }
  7.5%,
  26.25% {
    content: 'www.t.me/vkv_dsgn';
  }
  28.75% {
    content: 'www.t.me/hxr_jfzx';
  }
  30%,
  100% {
    content: 'Product | UX/UI Designer';
  }
}

.brand-avatar {
  position: absolute;
  right: 0;
  top: 0;
  width: 130px;
  height: 130px;
  border-radius: 2px;
  overflow: hidden;
}

.brand-avatar__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.brand-avatar__img--color {
  opacity: 0;
  /* Автоцикл: фото само периодически "моргает" в цвет на пару секунд, даже
     без наведения (см. @keyframes avatar-auto-reveal). Наведение мышью
     перебивает его и держит цвет постоянно, пока курсор на фото. */
  animation: avatar-auto-reveal 14s steps(1) infinite;
}
.brand-avatar:hover .brand-avatar__img--color {
  animation: none;
  opacity: 1;
}
@keyframes avatar-auto-reveal {
  0%,
  68% {
    opacity: 0;
  }
  72%,
  93% {
    opacity: 1;
  }
  97%,
  100% {
    opacity: 0;
  }
}

/* Глитч поверх цветного фото: два "рваных" слоя того же цветного снимка,
   обрезанные по горизонтали и дёргающиеся врозь — вместо гладкого перехода
   чёрно-белое/цветное дребезжит, как в эффекте. Играет и коротким рывком в
   момент автопоявления (avatar-glitch-auto-a/b), и при наведении — тогда
   быстрее и без остановки, пока наведено (avatar-glitch-a/b). */
.brand-avatar::before,
.brand-avatar::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 2px;
  background-image: url('../assets/images/avatar_color.png');
  background-size: cover;
  background-position: center;
  opacity: 0;
  pointer-events: none;
}
.brand-avatar::before {
  clip-path: inset(0 0 55% 0);
  animation: avatar-glitch-auto-a 14s steps(1) infinite;
}
.brand-avatar::after {
  clip-path: inset(45% 0 0 0);
  animation: avatar-glitch-auto-b 14s steps(1) infinite;
}
.brand-avatar:hover::before {
  animation: avatar-glitch-a 2.6s steps(1) infinite;
}
.brand-avatar:hover::after {
  animation: avatar-glitch-b 2.6s steps(1) infinite;
}
@keyframes avatar-glitch-auto-a {
  0%,
  68%,
  73%,
  92%,
  97%,
  100% {
    transform: translate(0);
    opacity: 0;
  }
  69% {
    transform: translate(-5px, -2px);
    opacity: 0.9;
  }
  70.5% {
    transform: translate(4px, 1px);
    opacity: 0.9;
  }
  93.5% {
    transform: translate(-3px, 2px);
    opacity: 0.9;
  }
  95% {
    transform: translate(2px, -1px);
    opacity: 0.9;
  }
}
@keyframes avatar-glitch-auto-b {
  0%,
  68.5%,
  73.5%,
  92.5%,
  97.5%,
  100% {
    transform: translate(0);
    opacity: 0;
  }
  69.5% {
    transform: translate(5px, 2px);
    opacity: 0.9;
  }
  71% {
    transform: translate(-4px, -1px);
    opacity: 0.9;
  }
  94% {
    transform: translate(3px, -2px);
    opacity: 0.9;
  }
  95.5% {
    transform: translate(-2px, 1px);
    opacity: 0.9;
  }
}
@keyframes avatar-glitch-a {
  0%,
  12%,
  20%,
  55%,
  100% {
    transform: translate(0);
    opacity: 0;
  }
  13% {
    transform: translate(-5px, -2px);
    opacity: 0.9;
  }
  16% {
    transform: translate(4px, 1px);
    opacity: 0.9;
  }
  48% {
    transform: translate(-3px, 2px);
    opacity: 0.9;
  }
  51% {
    transform: translate(2px, -1px);
    opacity: 0.9;
  }
}
@keyframes avatar-glitch-b {
  0%,
  14%,
  22%,
  60%,
  100% {
    transform: translate(0);
    opacity: 0;
  }
  15% {
    transform: translate(5px, 2px);
    opacity: 0.9;
  }
  18% {
    transform: translate(-4px, -1px);
    opacity: 0.9;
  }
  52% {
    transform: translate(3px, -2px);
    opacity: 0.9;
  }
  56% {
    transform: translate(-2px, 1px);
    opacity: 0.9;
  }
}

.brand-letter-pos {
  position: absolute;
}

.brand-letter-pos--v-right {
  top: 187px;
  right: 177px;
  transform: translateX(100%);
}

.brand-letter-pos--k {
  bottom: 328px;
  left: calc(50% - 100px);
  transform: translateY(100%);
}

.brand-letter-pos--v-left {
  left: 0;
  top: 0;
}

.brand-letter {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 300px;
  line-height: normal;
  color: var(--color-text-primary);
  white-space: nowrap;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
  user-select: none;
  -webkit-user-select: none;
}

.magnet-btn {
  transition: transform 0.15s ease-out;
}

.header-nav {
  position: absolute;
  left: 795px;
  top: -1px;
  width: 277px;
  height: 148px;
}

.header-nav__item {
  position: absolute;
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 22px;
  line-height: normal;
  color: var(--color-text-primary);
  white-space: nowrap;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
}

.header-nav__item--projects {
  left: 0;
  right: 185px;
  top: 45px;
  user-select: none;
  -webkit-user-select: none;
}

.header-nav__item--active {
  left: calc(50% - 5px);
  top: 1px;
  transform: translateX(-50%);
}

.header-nav__item--about {
  left: 92px;
  right: 102px;
  top: 89px;
  user-select: none;
  -webkit-user-select: none;
}

.header-nav__item--contacts {
  left: 175px;
  right: 0;
  top: 133px;
}

.underline-link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -4px;
  width: 100%;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.underline-link:hover::after {
  transform: scaleX(1);
}

/* ---------- projects ---------- */

.projects {
  position: absolute;
  left: 40px;
  top: 905px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 40px;
}

.projects__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 1840px;
  height: 31px;
}

.projects__title {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 40px;
  line-height: normal;
  color: var(--color-text-primary);
  white-space: nowrap;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
  user-select: none;
  -webkit-user-select: none;
}

.projects__list {
  display: flex;
  align-items: flex-start;
  gap: 30px;
  /* Full-bleed: список шире родителя (который сидит с отступом 40px от
     края страницы) и растянут во всю ширину фрейма (1920px), со своим
     внутренним паддингом 40px — первая карточка визуально стоит там же,
     где и заголовок, но при скролле список не упирается в старые поля,
     а идёт до самого края, без белых полос по бокам. */
  width: 1920px;
  margin-left: -40px;
  padding-left: 40px;
  padding-right: 40px;
  box-sizing: border-box;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  cursor: grab;
  /* Скроллбар скрыт визуально, скролл остаётся нативным (трекпад вбок,
     Shift+колесо, свайп, а теперь и перетаскивание мышью) — без перехвата
     обычного вертикального скролла. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.projects__list::-webkit-scrollbar {
  display: none;
}
.projects__list.is-dragging {
  cursor: grabbing;
  user-select: none;
}

.card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-200);
  width: 600px;
  /* У ссылок (SkoutAI/Бауцентр — <a>) браузер по умолчанию сам перетаскивает
     ссылку как объект (можно перенести в закладки и т.п.) — это перехватывает
     mousemove и ломает наш drag-to-scroll ленты (см. script.js), поэтому у
     карточки-статьи (Инвестколлекция, без этого поведения) перетаскивание
     работало, а у ссылочных — нет. draggable="false" в HTML — основной
     фикс, здесь дублируем для WebKit/Blink. */
  -webkit-user-drag: none;
  user-drag: none;
}

.card__visual {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-300);
  width: 100%;
}

.card__media {
  position: relative;
  width: 600px;
  border-radius: var(--radius-xs);
  overflow: hidden;
}

.card__media--1 { height: 533px; }
.card__media--2 { height: 462px; }
.card__media--3 { height: 386px; }

.card__media-img--cover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.card__media-img--crop {
  position: absolute;
  left: 0;
  top: -1.04%;
  width: 100%;
  height: 116.58%;
  max-width: none;
  pointer-events: none;
}

/* 3D-наклон + блик на изображениях карточек проектов при наведении.
   .t-tilt — плоская зона наведения (сама не крутится, тут JS считает
   позицию курсора), .t-tilt-card — то, что реально наклоняется,
   .t-tilt-glare — блик, следующий за курсором. */
.t-tilt {
  touch-action: none;
}
.t-tilt-card {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-xs);
  overflow: hidden;
  /* scale(1.12) даёт запас по краям, чтобы параллакс-сдвиг (--parallax-y)
     не оголял пустоту у границ карточки при скролле. */
  transform:
    translateY(var(--parallax-y, 0px))
    scale(1.12)
    perspective(var(--tilt-perspective))
    rotateX(var(--tilt-rx, 0deg))
    rotateY(var(--tilt-ry, 0deg));
  transform-style: preserve-3d;
  transition: transform var(--tilt-return) var(--tilt-return-ease);
  will-change: transform;
}
.t-tilt-card.is-tilting {
  transition: transform var(--tilt-follow) var(--tilt-follow-ease);
}
.t-tilt-glare {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  background:
    radial-gradient(circle 95px at var(--tilt-gx, 50%) var(--tilt-gy, 50%), rgba(255, 255, 255, 0.48), rgba(255, 255, 255, 0.06) 52%, rgba(255, 255, 255, 0) 84%),
    radial-gradient(circle 200px at var(--tilt-gx, 50%) var(--tilt-gy, 50%), rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.04) 58%, rgba(255, 255, 255, 0) 78%),
    radial-gradient(circle 360px at var(--tilt-gx, 50%) var(--tilt-gy, 50%), rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0) 88%);
  transition: opacity var(--tilt-glare-fade) var(--tilt-glare-ease);
}
.t-tilt.is-hover .t-tilt-glare {
  opacity: var(--tilt-glare-opacity);
}
@media (prefers-reduced-motion: reduce) {
  .t-tilt-card {
    transform: none !important;
    transition: none !important;
  }
}

.card__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
}

.card__tags {
  display: flex;
  align-items: center;
  gap: var(--space-200);
}

.card__tag,
.card__year {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 18px;
  line-height: normal;
  color: var(--color-text-secondary);
  white-space: nowrap;
}

.card__tag-sep {
  width: 3px;
  height: 3px;
}

.card__content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-200);
  width: 100%;
}

.card__title {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 26px;
  line-height: normal;
  color: var(--color-text-primary);
  white-space: nowrap;
}

.card__desc {
  width: 100%;
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 20px;
  line-height: normal;
  color: var(--color-text-secondary);
}

/* ---------- about ---------- */

.about {
  position: absolute;
  left: 40px;
  top: 1784px;
  width: 1840px;
  display: flex;
  align-items: flex-start;
  gap: 483px;
  user-select: none;
  -webkit-user-select: none;
}

.about__title {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 40px;
  line-height: normal;
  color: var(--color-text-primary);
  white-space: nowrap;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
}

.about__body {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 40px;
}

.about__lead {
  width: 100%;
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 30px;
  line-height: normal;
  color: var(--color-text-secondary);
  /* .about целиком запрещает выделение (заголовок это оформление) — этот
     текст, наоборот, должен выделяться и копироваться. */
  user-select: text;
  -webkit-user-select: text;
}

.about__facts {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 30px;
}

.facts-row {
  width: 100%;
  display: flex;
  align-items: flex-start;
  gap: 139px;
}

.fact-item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  padding-top: 15px;
}
.fact-item::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 0;
  border-top: calc(1px / var(--page-scale, 1)) solid var(--color-border-divider);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.9s cubic-bezier(0.65, 0, 0.35, 1);
}
.fact-item.is-visible::before {
  transform: scaleX(1);
}

.fact-item--fixed {
  width: 461px;
  flex-shrink: 0;
}

.fact-item--flex {
  flex: 1 0 0;
  min-width: 0;
}

.fact-item__label {
  width: 100%;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: 20px;
  line-height: normal;
  color: var(--color-text-secondary);
  /* Подпись ("Опыт", "Инструменты"...) — оформление, не контент: остаётся
     невыделяемой, как и весь .about. А вот значение рядом (.fact-item__value
     ниже) — реальный текст, его выделение специально разрешено. */
}

.fact-item__value {
  width: 100%;
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 18px;
  line-height: normal;
  color: var(--color-text-primary);
  user-select: text;
  -webkit-user-select: text;
}

/* Текст факта проявляется только после того, как линия над ним дочертится. */
.fact-item__label,
.fact-item__value {
  opacity: 0;
  transition: opacity 0.5s ease-out;
}
.fact-item.is-visible .fact-item__label,
.fact-item.is-visible .fact-item__value {
  opacity: 1;
  transition-delay: 0.9s;
}

/* ---------- footer ---------- */

.footer {
  position: absolute;
  left: 0;
  top: 2404px;
  width: 1920px;
  padding: 40px;
  /* Как .about — оформление/навигация, не контент для копирования. */
  user-select: none;
  -webkit-user-select: none;
}
.footer::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 0;
  border-top: calc(1px / var(--page-scale, 1)) solid var(--color-border-divider);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 1.2s cubic-bezier(0.65, 0, 0.35, 1);
}
.footer.is-visible::before {
  transform: scaleX(1);
}

.footer-glow {
  position: absolute;
  left: 40px;
  top: 69px;
  width: 1920px;
  height: 218px;
  /* Без этого картинка (раздутая отрицательным inset у ::inner далеко за
     пределы этого блока) перехватывала клики по тексту "Обо мне" над
     футером — там не работало выделение текста. */
  pointer-events: none;
}

.footer-glow__inner {
  position: absolute;
  inset: -259.17% -20.16% -312.84% -24.06%;
}

.footer-glow__img {
  display: block;
  width: 100%;
  height: 100%;
  animation: glow-pulse 18s ease-in-out infinite;
  animation-delay: -3.5s;
}

.footer-main {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 100px;
  width: 100%;
}
/* Контент футера проявляется только после того, как дочертится линия
   над ним (.footer::before). */
.footer-main.is-visible {
  transition-delay: 1.2s;
}

.footer-top {
  width: 100%;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
}

.footer-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  border-radius: 150px;
  cursor: pointer;
}

.footer-brand__mark {
  width: 113px;
  height: 47px;
  /* Как .card__media-img на главной: клик проходит насквозь (на сам
     <a href="#hero">.footer-brand), логотип нельзя выделить/сохранить
     через контекстное меню. */
  pointer-events: none;
}

.footer-brand__suffix {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: 22px;
  line-height: normal;
  color: var(--color-text-primary);
  white-space: nowrap;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
}

.footer-links {
  position: relative;
  width: 133px;
  height: 132px;
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 22px;
  line-height: normal;
  color: var(--color-text-primary);
}

.footer-links__item {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
}

.footer-links__item--telegram { left: 36px; top: 0; }
.footer-links__item--linkedin { left: 1px; top: 56px; }
.footer-links__item--cv { left: 1px; top: 114px; }

.footer-links__cv-arrow {
  width: 14.75px;
  height: 15.06px;
  transform: rotate(135deg);
}

.footer-email {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 22px;
  line-height: normal;
  color: var(--color-text-primary);
  white-space: nowrap;
}

.tooltip-zone {
  position: relative;
  display: inline-block;
}

.tooltip-bubble {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 10px);
  padding: 6px 12px;
  border-radius: 6px;
  background: var(--color-text-primary);
  color: var(--color-bg);
  font-family: var(--font-sans);
  font-size: 14px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translateX(-50%) scale(0.85);
  transition: opacity 0.15s, transform 0.15s;
}
.tooltip-zone:hover .tooltip-bubble {
  opacity: 1;
  transform: translateX(-50%) scale(1);
  /* delay only on the way IN */
  transition: opacity 0.15s 0.4s, transform 0.15s 0.4s;
}

/* ---------- loader ---------- */

.loader {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  /* Круговой reveal сходится в левый верхний угол (не в логотип по центру),
     как переход темы на jib.design — та же кривая
     cubic-bezier(0.65, 0, 0.35, 1). */
  clip-path: circle(150% at 0% 0%);
  transition: clip-path 1.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.loader.is-hidden {
  clip-path: circle(0% at 0% 0%);
  pointer-events: none;
}

/* Последовательное появление контента hero после лоадера: элементы
   стартуют размытыми и прозрачными, JS проявляет их по очереди. */
.js-reveal {
  opacity: 0;
  filter: blur(8px);
  transition: opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1), filter 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}
.js-reveal.is-visible {
  opacity: 1;
  filter: blur(0);
}

/* Более плавное и медленное появление — для фото и имени/роли в hero,
   чтобы взгляд успевал зацепиться за каждый элемент. */
.js-reveal--slow {
  transition-duration: 1.1s;
}

/* Появление остальных секций при скролле (projects/about/footer):
   fade + сдвиг снизу вверх, срабатывает через IntersectionObserver один раз,
   когда блок входит во вьюпорт — как fadeInSlide на kei-inc.jp. */
.js-scroll-reveal {
  opacity: 0;
  transform: translateY(32px);
  transition: opacity 0.8s cubic-bezier(0.65, 0, 0.35, 1), transform 0.8s cubic-bezier(0.65, 0, 0.35, 1);
}
.js-scroll-reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.ld-echo {
  position: relative;
  width: 160px;
  height: 160px;
}
.ld-echo::before,
.ld-echo::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 2px solid var(--color-text-primary);
  border-radius: 50%;
  animation: echo 1.8s ease-out infinite;
  animation-fill-mode: backwards;
}
.ld-echo::after {
  animation-delay: 0.9s;
}
@keyframes echo {
  0% {
    transform: scale(0.15);
    opacity: 1;
  }
  100% {
    transform: scale(1);
    opacity: 0;
  }
}

.ld-echo__logo {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 80px;
  height: 80px;
  /* Как остальные картинки сайта — не должен выделяться/перетаскиваться. */
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  border-radius: 50%;
}

.footer-bottom {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  user-select: none;
  -webkit-user-select: none;
}

.copyright {
  display: flex;
  align-items: center;
  gap: var(--space-200);
}

.copyright__name-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  padding-bottom: 4px;
}

.copyright__name,
.copyright__year,
.footer-location {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 18px;
  line-height: normal;
  color: var(--color-text-secondary);
  white-space: nowrap;
  transition: opacity 0.2s ease;
}
.footer-location.is-swapping {
  opacity: 0;
}

.copyright__sep {
  width: 3px;
  height: 3px;
}

/* ---------- float-menu ---------- */
/* Плавающий бургер + Telegram-кнопка: живёт вне .page, но масштабируется
   тем же коэффициентом --page-scale, что и весь макет (см. fit() в
   script.js) — иначе на узких окнах эта кнопка визуально крупнее всего
   остального, хотя в Фигме она такого же 59px размера, как и остальные
   элементы 1920px-канвы. Появляется, когда шапочное меню (.header-nav)
   уходит за пределы экрана при скролле — см. IntersectionObserver в
   script.js. */
/* Подложка под раскрытым меню: слегка размывает страницу и перехватывает
   клик мимо меню (закрытие обрабатывает document-хендлер в script.js).
   Отдельным слоем, а не фильтром на .stage — иначе блюр пришлось бы
   накладывать на всю прокручиваемую страницу целиком. */
.float-menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 99;
  opacity: 0;
  pointer-events: none;
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  transition: opacity 0.3s cubic-bezier(0.65, 0, 0.35, 1);
}
.float-menu-backdrop.is-open {
  opacity: 1;
  pointer-events: auto;
}

.float-menu {
  position: fixed;
  /* 40px — как отступы хедера (top/left:40px) внутри .page: там это
     значение того же 1920px-макета, которое сжимается вместе со всей
     страницей через --page-scale. Тут элемент лежит вне .page, поэтому
     тот же эффект приходится повторять вручную через calc(). */
  bottom: calc(40px * var(--page-scale, 1));
  left: 50%;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 6px;
  opacity: 0;
  pointer-events: none;
  translate: -50% 12px;
  scale: var(--page-scale, 1);
  transform-origin: bottom center;
  transition: opacity 0.3s cubic-bezier(0.65, 0, 0.35, 1), translate 0.3s cubic-bezier(0.65, 0, 0.35, 1);
  /* Бургер + выпадающее меню + кнопка Telegram — навигация, не контент. */
  user-select: none;
  -webkit-user-select: none;
}
.float-menu.is-visible {
  opacity: 1;
  pointer-events: auto;
  translate: -50% 0;
}

.float-menu__toggle-wrap {
  position: relative;
}

.float-menu__toggle {
  width: 59px;
  height: 59px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  /* Обводка нарисована внутренней тенью, а не border: border-width
     браузер округляет до целых физических пикселей ДО применения scale()
     на .float-menu, поэтому "1px" превращается в 0.66 реального пикселя —
     тоньше и бледнее полосок бургера. box-shadow не округляется и берёт
     ту же компенсированную переменную, что и полоски (см. fit() в
     script.js), поэтому обводка выходит ровно в 1 реальный пиксель. */
  box-shadow: inset 0 0 0 var(--fm-hairline, 1px) var(--color-border-divider);
  /* Без этого у <button> остаётся дефолтный бордер браузера в 2px. */
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  flex-shrink: 0;
  position: relative;
  z-index: 1;
}
.float-menu__icon {
  position: relative;
  display: block;
  width: 25px;
  height: calc(var(--fm-bar-gap, 9.25px) * 2 + var(--fm-hairline, 1px));
}
/* Гамбургер трансформируется в крестик: полоски — обычные span'ы, позиция
   (top) и поворот (transform) анимируются независимо, поэтому не нужно
   вычислять общую точку вращения в одной transform-матрице (на SVG-линиях
   это было хрупко и разъезжалось в шеврон). Крайние полоски сначала
   съезжаются на место средней (top), затем крутятся на ±45° каждая вокруг
   своего же центра — получается ровный крестик. Тайминг 0.2s — как у
   .js-modal__iconLine на kei-inc.jp. */
.float-menu__bar {
  position: absolute;
  left: 0;
  width: 25px;
  /* Толщина и шаг приходят из fit() в script.js: там они округлены так,
     чтобы после scale() на .float-menu лечь ровно на целые физические
     пиксели. Без этого у каждой полоски своя дробная фаза пиксельной
     сетки и они выглядят разной толщины (то же семейство багов, что было
     с линией в футере, см. .footer::before). */
  height: var(--fm-hairline, 1px);
  border-radius: calc(var(--fm-hairline, 1px) / 2);
  background: var(--color-text-primary);
  transform: translateZ(0);
  transition: top 0.2s, transform 0.2s, opacity 0.2s;
}
.float-menu__bar--1 {
  top: 0;
}
.float-menu__bar--2 {
  top: var(--fm-bar-gap, 9.25px);
}
.float-menu__bar--3 {
  top: calc(var(--fm-bar-gap, 9.25px) * 2);
}
.float-menu.is-open .float-menu__bar--1 {
  top: var(--fm-bar-gap, 9.25px);
  transform: rotate(45deg) translateZ(0);
}
.float-menu.is-open .float-menu__bar--2 {
  opacity: 0;
}
.float-menu.is-open .float-menu__bar--3 {
  top: var(--fm-bar-gap, 9.25px);
  transform: rotate(-45deg) translateZ(0);
}

.float-menu__telegram {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Высота задана явно, чтобы совпадать с квадратной кнопкой бургера:
     раньше её добирал 2px-бордер, а обводка теперь рисуется тенью и в
     размер не входит. */
  height: 59px;
  padding: 0 30px;
  background: var(--color-bg);
  /* Обводка тенью, а не border — см. комментарий в .float-menu__toggle. */
  box-shadow: inset 0 0 0 var(--fm-hairline, 1px) var(--color-border-divider);
  border-radius: 10px;
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 22px;
  color: var(--color-text-primary);
  white-space: nowrap;
}
/* Подчёркивание — только под текстом и стрелкой, не во всю кнопку (как
   у Telegram/CV в футере): кликабельная зона с паддингом на .float-menu__
   telegram, а .underline-link — на внутренней обёртке впритык к контенту. */
.float-menu__telegram-inner {
  display: flex;
  align-items: center;
  gap: 10px;
}
.float-menu__arrow {
  width: 14.75px;
  height: 15.06px;
  transform: rotate(135deg);
}

/* Панель раскрывается вверх от кнопки и растёт из неё — тот же приём, что
   и .js-modal__animeTrigger на kei-inc.jp: scale 0→1 из точки bottom
   center, 0.3s cubic-bezier(.16,1,.3,1) (их --default-transition-timing-
   function). */
.float-menu__dropdown {
  position: absolute;
  /* Тот же зазор, что между бургером и Telegram (gap у .float-menu). */
  bottom: calc(100% + 6px);
  left: 50%;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 200px;
  padding: 10px;
  background: var(--color-bg);
  /* Обводка тенью, а не border — см. комментарий в .float-menu__toggle. */
  box-shadow: inset 0 0 0 var(--fm-hairline, 1px) var(--color-border-divider);
  border-radius: 10px;
  translate: -50% 0;
  scale: 0;
  transform-origin: bottom center;
  pointer-events: none;
  transition: scale 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.float-menu__dropdown.is-open {
  scale: 1;
  pointer-events: auto;
}
/* Пункты появляются по очереди — тот же keyframe fadeInSlide и та же
   кривая --slide-ease: cubic-bezier(.33,1,.68,1), что и .js-modal__item на
   kei-inc.jp; задержка на каждый пункт (80ms * индекс) выставляется
   инлайново в script.js. */
@keyframes float-menu-item-in {
  0% {
    transform: translateY(100%);
    opacity: 0;
  }
  30% {
    opacity: 0;
  }
  90% {
    opacity: 1;
  }
  100% {
    transform: translateY(0);
    opacity: 1;
  }
}
.float-menu__dropdown-item {
  display: flex;
  justify-content: center;
  padding: 12px 14px;
  border-radius: 4px;
  opacity: 0;
}
/* Подчёркивание впритык к слову, а не во всю ширину пункта — та же
   логика, что и у .float-menu__telegram-inner выше. */
.float-menu__dropdown-item-inner {
  font-family: var(--font-serif);
  font-size: 22px;
  color: var(--color-text-primary);
}
/* Пункт "CV" — со стрелочкой, как в футере (.footer-links__item--cv):
   тот же .float-menu__arrow (см. кнопку Telegram выше) и тот же зазор
   6px, что у футера, а не 10px, как у Telegram здесь же в меню. */
.float-menu__dropdown-item-inner--cv {
  display: flex;
  align-items: center;
  gap: 6px;
}
.float-menu__dropdown.is-open .float-menu__dropdown-item {
  animation: float-menu-item-in 1.2s cubic-bezier(0.33, 1, 0.68, 1) forwards;
  animation-delay: var(--item-delay, 0s);
}
