/* ══════════════════════════════════════════════════════════════════════════
   ДИЗАЙН-СИСТЕМА ПОРТФОЛИО — токены и компоненты

   Что здесь: всё, что повторяется от страницы к странице. Токены, типографика,
   панели, плитки, аватар, соцсети, состояния, тайминги, адаптивные переопределения.

   Чего здесь нет: раскладка конкретной страницы. Сетка сцены, портрет, веер Shorts,
   превью проектов — это главная, они живут в index.html.

   Разбор, почему значения именно такие, — в ../design-system.md.
   Разбор главной страницы целиком — в ../blueprint-main.md.

   Подключается двумя строками в <head>:

     <link href="https://fonts.googleapis.com/css2?family=Geologica:wght@200;300&display=swap" rel="stylesheet">
     <link rel="stylesheet" href="design-system.css">

   Шрифт подключается ссылкой, а не @import: @import в CSS браузер находит только
   после того, как скачает сам CSS — это лишний рейс до сервера, и текст успевает
   мигнуть системным шрифтом.
   ══════════════════════════════════════════════════════════════════════════ */


/* ── ТОКЕНЫ: десктоп ────────────────────────────────────────────────────────
   Значения сняты с макета Prototype v.2 (node 1440:2), холст 1600×900.
   Каждое переведено в резину: px из макета делится на 1600 → доля в vw.
   clamp(минимум, резина, максимум) не даёт съехать в крайностях.        */
:root {
  /* Ритм. Одна величина на отступ от края и на зазор между блоками —
     тогда к ней же можно привязать скругление вложенных элементов. */
  --edge:   clamp(6px, 0.625vw, 10px);   /* 10 из 1600 */
  --gap:    clamp(6px, 0.625vw, 10px);   /* 10 из 1600 */

  /* Рельса — четверть ширины экрана. В макете 410 из 1600 (25.6%),
     округлено до чистых 25%.

     Потолок 60vh: рельса задаёт сторону квадратного блока соцсетей, и без
     потолка на широком низком окне квадрат съедал бы всю рельсу. Порог
     срабатывает после отношения сторон 2.6:1 — на 16:9, 16:10 и 21:9
     рельса остаётся ровно 25%. */
  --rail:   min(25vw, 60vh);

  --radius: clamp(24px, 2.5vw, 40px);    /* 40 из 1600 */

  /* Аватар — собственный размер, а не следствие чужой пропорции.

     Раньше его величину задавал ряд, в котором он стоял: на главной одна
     пропорция ряда, на кейсе другая, и один и тот же элемент выходил
     97.6 против 94.9 на десктопе и 77 против 95 на планшете. Аватар —
     постоянный знак в левом верхнем углу всех страниц, он обязан быть
     одинаковым, а значит размер должен задаваться ему, а не выводиться
     из соседей.

     6.1vw — это ровно то, что даёт главная на десктопе: рельса 25vw,
     аватар 100 из 410 её ширины, 25 × 0.2439 = 6.098. */
  --ava:    clamp(64px, 6.1vw, 112px);

  /* Цвет */
  --panel:     rgba(233, 233, 233, 0.7); /* панели: полупрозрачные, портрет просвечивает */

  /* Фон страницы вынесен в токен, потому что нужен не только body.
     Липкий блок обязан быть непрозрачным — иначе содержимое едет СКВОЗЬ
     него, а не под ним. Подложить под такой блок этот же градиент
     с background-attachment: fixed значит сделать его непрозрачным,
     не сделав видимым: при fixed областью позиционирования становится
     ЭКРАН, а не сам элемент, и градиент совпадает с фоном страницы
     пиксель в пиксель. */
  --page-bg:   radial-gradient(140% 70% at 49% 39%, #fdfdfd 0%, #dddddd 100%);
  --circle:    #f2f2f2;                  /* подсветка внутри панели */
  --avatar-bg: #dedede;
  --ink:       #000;
  --accent:    #F64A00;                  /* только под курсором и в фокусе */
  --focus:     #1a1a1a;                  /* кольцо фокуса */

  /* Кегли без верхнего потолка: max(минимум, доля ширины).
     clamp с максимумом здесь не годится — выше 1600 текст переставал бы расти,
     и на широком мониторе страница выглядела бы мельче задуманного.
     Доли те же, что в макете: имя 50/1600, подписи 30/1600, роль 16/1600. */
  --fs-name:    max(32px, 3.125vw);
  --fs-section: max(20px, 1.875vw);
  --fs-role:    max(13px, 1vw);

  /* Заголовок страницы — крупная подпись раздела: «Projects» на витрине,
     «Shorts» в шапке ленты. Потолка нет намеренно, как и у имени: выше 1600
     он перестал бы расти, и на широком мониторе страница выглядела бы мельче
     задуманного. Страница, которой заголовок тесен, ставит свой предел
     сама — так делает Shorts, где он стоит в ряду ростом с аватар. */
  --fs-title:   max(32px, 4.4vw);

  /* Движение. Одна короткая длительность на все смены цвета и фона,
     одна длинная с мягким выходом — на перемещения. */
  --t-fast:  .18s;
  --t-slow:  .40s;
  --e-out:   cubic-bezier(.16, 1, .3, 1);

  /* Кривая «магнита»: трогается бодро и долго доезжает. Из той же семьи,
     что --e-out, но с чуть менее резким началом — на коротком пути --e-out
     читается рывком, а этот остаётся притяжением.

     Заведена для веера колоды на витрине, потом понадобилась обратному ходу
     подсказки про свайп там же и подсказке про прокрутку в Shorts. Три
     адресата на двух страницах — значит, место кривой в системе, а не
     в файле страницы. */
  --e-fan:   cubic-bezier(.22, 1, .30, 1);

  /* Смена контента — отдельная пара, длиннее и мягче остальных.

     Реакция на курсор и смена текста — разные вещи, и торопиться им надо
     по-разному. Реакция обязана быть мгновенной: она отвечает на действие.
     Смена текста ответом ни на что не является, читатель её не заказывал —
     поэтому она идёт спокойно, чтобы глаз успел заметить, что абзац сменился.

     Кривая с мягким входом, а не резким: --e-out стартует на полной скорости,
     и на длинном пути в целую панель это читается как рывок. Здесь движение
     трогается плавно и так же плавно останавливается. */
  --t-swap:  .62s;
  --e-soft:  cubic-bezier(.42, 0, .18, 1);
}

/* ── ТОКЕНЫ: портретная раскладка (телефон и вертикальный планшет) ──────────
   Пересчитано от ширины 320 — это ширина макета мобилки. */
@media (orientation: portrait) {
  :root {
    --edge:       clamp(8px, 3.125vw, 16px);   /* 10 из 320 */
    --gap:        clamp(8px, 3.125vw, 16px);
    --radius:     clamp(14px, 6.25vw, 28px);   /* 20 из 320 — относительно крупнее десктопного */
    /* Ровно та же величина, что даёт главная. Она эталон, и там аватар
       не имеет собственного размера: его задаёт верхний ряд сцены, а тот
       считается от ВЫСОТЫ экрана — треть за вычетом зазора, из неё 0.303.

       Раньше здесь стоял clamp(56px, 20.3vw, 84px) — доля от ШИРИНЫ.
       На 375 совпадало (76), а на вертикальном планшете 834 упиралось
       в потолок 84 против эталонных 112.5, и шапка кейса переставала
       быть похожей на шапку главной.

       Потолка нет намеренно: на главной его тоже нет, а любой предел
       здесь вернул бы то же расхождение на экране повыше. */
    --ava:        calc(((100svh - var(--edge) * 2) / 3 - var(--gap)) * 0.303);
    --fs-name:    clamp(24px, 9vw, 44px);      /* 28.84 из 320 */

    /* Потолки подняты под планшет. Формулы посчитаны от ширины 320, то есть
       от телефона, и на планшете доля от ширины улетает далеко за потолок:
       на 1024 это 4.375vw = 44.8 и 3.2vw = 32.8. Значит вертикальный планшет
       получал РОВНО потолок — 20 и 15, числа, назначенные телефону.

       Мало это было вдвойне. В портрете страница кейса раскладывается в одну
       колонку, и текст идёт не по рельсе в 300, а во всю ширину: на 1024
       карточка 992, строка 915, 119 знаков при норме 45–75. Кегль телефона
       на строке втрое длиннее телефонной.

       26 и 19 дают на 1024 текст 19.95 и подписи 26, строка садится до 79
       знаков. Оба потолка начинают действовать на одной ширине — 26/0.04375
       и 19/0.032 обе дают 594. Ниже её работает телефонная формула и ничего
       не меняется: на 375 по-прежнему 16.4 и 12, на 430 — 18.8 и 13.8. */
    --fs-title:   clamp(26px, 8vw, 44px);
    --fs-section: clamp(13px, 4.375vw, 26px);  /* 14 из 320 */
    --fs-role:    clamp(11px, 3.2vw, 19px);    /* в макете 9.2 — поднято до читаемого */
  }
}

/* ── ТОКЕНЫ: альбомная компактная (планшет и телефон боком) ────────────────
   Пересчитано от ширины 720. */
@media (orientation: landscape) and (max-width: 1100px) {
  :root {
    --edge:       clamp(8px, 2.08vw, 18px);    /* 15 из 720 */
    --gap:        clamp(8px, 2.08vw, 18px);
    --radius:     clamp(16px, 3.5vw, 34px);
    --ava:        clamp(60px, 7.5vw, 82px);    /* 77 на 1024 — как на главной */
    --fs-name:    clamp(22px, 5.4vw, 44px);    /* 39 из 720 */
    --fs-title:   clamp(26px, 4.1vw, 46px);
    --fs-section: clamp(13px, 2.5vw, 22px);
    --fs-role:    clamp(11px, 1.7vw, 15px);
  }
}


/* ── БАЗА ──────────────────────────────────────────────────────────────────
   Geologica плотнее Gilroy из макета: на одном и том же весе выглядит жирнее.
   Поэтому веса сдвинуты на две ступени легче — так возвращается воздух макета.
   Подписи ExtraLight 200, имя Light 300. Ниже 200 в семье ничего нет,
   поэтому 200 — это пол, а не выбор из нескольких. */
* { box-sizing: border-box; }

/* figure у браузера идёт с margin: 1em 40px. На странице кейса из-за этих
   сорока пикселей кадры выходили уже своей колонки: 272 вместо 352.
   Сбрасываем в системе, а не на каждой странице заново. */
figure { margin: 0; }

html, body {
  margin: 0;
  font-family: "Geologica", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-weight: 200;
  color: var(--ink);
}

/* Фон из макета: радиальный градиент, светлое пятно чуть выше центра.
   Эллипс сильно растянут по горизонтали — как в исходном градиенте Figma.
   fixed: пятно стоит на месте, даже если страница длиннее экрана. */
body {
  background: var(--page-bg);
  background-attachment: fixed;
}


/* ── ТИПОГРАФИКА ───────────────────────────────────────────────────────────
   Трекинг задан в долях кегля (em), а не в пикселях: кегль растёт вместе
   с экраном, и абсолютный трекинг на широком мониторе становится незаметным. */
.role { margin: 0; font-size: var(--fs-role); font-weight: 200; }

/* nowrap: имя — подпись бренда, переносить его нельзя ни при какой ширине.
   Ограничивать строку через max-width в ch тоже нельзя: ch считается
   от базового кегля, а не от кегля имени, и предел встаёт намертво. */
.name {
  margin: 0;
  font-size: var(--fs-name);
  font-weight: 300;
  line-height: 1.1;
  letter-spacing: -0.03em;
  white-space: nowrap;
}


/* ── ПАНЕЛЬ ────────────────────────────────────────────────────────────────
   Основа всех блоков. Полупрозрачная, а не размытая: в макете это обычная
   прозрачность 70%, сквозь неё приглушённо виден портрет. */
.panel {
  background: var(--panel);
  border-radius: var(--radius);
}


/* ── ПЛИТКА ────────────────────────────────────────────────────────────────
   Кликабельный блок с подписью. Ссылка, а не div с обработчиком:
   работает средняя кнопка мыши, работает клавиатура, видно адрес. */
.tile {
  display: block;
  position: relative;
  /* isolation создаёт у плитки свой слой наложения. Без него подсветка
     с z-index -1 провалилась бы под фон: отрицательный слой уходит
     под ближайший контекст наложения, а им была бы вся группа. */
  isolation: isolate;
  text-decoration: none;
  color: inherit;
  font-size: var(--fs-section);
  letter-spacing: -0.03em;
  text-align: center;
  transition: background-color var(--t-fast) ease, transform var(--t-fast) ease;
}

/* Подпись по центру блока — для маленьких плиток (CV, About). */
.tile--sm { display: grid; place-items: center; }

/* Подсветка под курсором: светлая плашка ВНУТРИ блока, с отступом и
   скруглением по тому же правилу, что у ячеек соцсетей. Раньше на hover
   перекрашивался фон блока целиком — реакция читалась грубее и по-разному
   в разных блоках.

   z-index -1 кладёт плашку между фоном плитки и подписью, поэтому текст
   остаётся сверху и не нуждается в обёртке. */
.tile::before {
  content: "";
  position: absolute;
  inset: var(--gap);
  border-radius: calc(var(--radius) - var(--gap));   /* концентрическое, см. ниже */
  background: var(--circle);
  opacity: 0;
  transition: opacity var(--t-fast) ease;
  z-index: -1;
}


/* ── ПЛИТКА С ИКОНКОЙ (переключатель языка) ────────────────────────────────
   Правило обязано стоять ПОСЛЕ .tile: специфичность одинаковая, и при
   обратном порядке display: block из .tile перебивает grid — иконка
   прижимается к верху блока.

   Размер иконки долей, а не в пикселях: на широком экране фиксированная
   иконка тонет в панели. Пропорцию 1:1 держит viewBox, поэтому height auto. */
.tile--lang {
  display: grid;
  place-items: center;
  /* Квадрат — часть определения компонента, а не забота страницы.

     Без aspect-ratio ширину блока задаёт содержимое, а содержимое —
     иконка шириной 42% от блока: круг замыкается, и в колонке auto
     блок разносит ряд шире контейнера. Ровно это и случилось на первой
     же странице, которая подключила систему. */
  aspect-ratio: 1;
  min-height: 0;
}
.tile--lang svg { width: 42%; height: auto; opacity: .8; }


/* ── АВАТАР ────────────────────────────────────────────────────────────────
   На внутренних страницах — ссылка на главную. На самой главной ведёт сюда же. */
.ava {
  display: block;
  text-decoration: none;
  border-radius: var(--radius);
  background: var(--avatar-bg);
  overflow: hidden;              /* фото обрезается по скруглению рамки */
  /* Размер задан явно и одинаков на всех страницах. aspect-ratio оставлен
     страховкой: если размер вдруг переопределят по одной стороне,
     квадрат всё равно уцелеет. */
  width: var(--ava);
  height: var(--ava);
  aspect-ratio: 1;
  flex: none;
  min-height: 0;
}
.ava img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .2s ease;
}


/* ── СОЦСЕТИ ───────────────────────────────────────────────────────────────
   Десктоп: квадрат 2×2. Сторона считается от СОБСТВЕННОЙ ширины через
   aspect-ratio, а не от высоты окна. Два независимых числа совпадали ровно
   при 1600×900, на любом другом отношении сторон блок вытягивался и кружки
   вылезали за панель. */
.socials {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: var(--gap);
  padding: var(--gap);
  aspect-ratio: 1;
  width: 100%;
}
.socials a {
  display: grid;
  /* minmax(0, 1fr), а не auto. Тот же приём, что уровнем выше у самой полосы,
     и по той же причине — только здесь он спасает от круга в другую сторону.

     У ряда `auto` базовый размер берётся из содержимого, а содержимое — это
     картинка с собственным размером 40×40 из файла. Пока ячейка крупнее сорока,
     ряд растягивается по ней и проценты считаются от ячейки. Как только ячейка
     становится МЕЛЬЧЕ сорока, ряд остаётся сорока, и `height: 50%` начинает
     считаться от них: на альбомном телефоне ячейка 27, а иконка выходила 20 —
     73% ячейки вместо half. Конверт при этом слушался правила: у встроенного
     svg своего размера в пикселях нет, ряд он не раздувает. Из-за этого три
     иконки стояли по одному числу, а четвёртая по другому.

     Ноль в минимуме разрешает ряду сжаться до ячейки, и проценты снова
     считаются от неё. */
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  aspect-ratio: 1;
  /* Концентрическое скругление: радиус ребёнка = радиус родителя минус отступ.
     Тогда внутренняя и внешняя дуги идут параллельно и зазор не «толстеет» в углах. */
  border-radius: calc(var(--radius) - var(--gap));
  background: transparent;       /* плашка проявляется под курсором, как у плиток */
  color: inherit;
  text-decoration: none;
  font-size: var(--fs-role);
  letter-spacing: 0.04em;
  transition: background-color var(--t-fast) ease, color var(--t-fast) ease;
}

/* Иконки приходят в фирменных цветах: LinkedIn синий, WhatsApp зелёный,
   Instagram с градиентом. На сплошь серой странице четыре цветных пятна
   перетягивают внимание с портрета, поэтому в покое они гасятся в чёрный.

   brightness(0) умножает все каналы на ноль: любой цвет становится чёрным,
   прозрачность цела. Работает и с градиентом, и с обводкой — править svg не нужно. */
.socials a img {
  /* Размер задаёт ВЫСОТА, ширина выводится из пропорции самой иконки.
     Так все четыре стоят на одной высоте независимо от своих пропорций —
     см. «Иконка меряется по высоте» ниже. В макете вектор 40 в круге 190. */
  width: auto;
  height: 20%;
  object-fit: contain;
  filter: brightness(0);
  opacity: .78;
  transition: filter var(--t-fast) ease, opacity var(--t-fast) ease;
}

/* Почта нарисована встроенным svg, а не картинкой: картинку нельзя
   перекрасить средствами CSS, а конверту нужен акцентный цвет.
   Цвет задан прямо на svg — у иконки своё правило color, и оно
   перебивает то, что пришло бы по наследству от ссылки.

   Высота ТА ЖЕ, что у соседей, ширина выводится из viewBox 43×35.
   Конверт выходит шире — он прямоугольный, и это нормально. А вот высота
   обязана совпадать: раньше здесь стояло width: 23%; height: auto, и высота
   получалась 23% × 35/43 = 18.7% против 20% у соседей. Конверт был ниже
   на 2.4 пикселя при ячейке 185. */
.ico-mail {
  width: auto;
  height: 20%;
  color: var(--ink);
  opacity: .78;
  transition: color var(--t-fast) ease, opacity var(--t-fast) ease;
}


/* ── СОСТОЯНИЯ ─────────────────────────────────────────────────────────────
   Всё на :hover заперто в @media (hover: hover). На тач-экране :hover
   срабатывает по касанию и залипает до тапа в стороне — блок остаётся
   подсвеченным. :focus-visible снаружи: клавиатура есть и на планшете. */
@media (hover: hover) {
  .tile:hover::before        { opacity: 1; }
  .tile:hover                { color: var(--accent); }
  .tile--lang:hover svg      { opacity: 1; }
  .ava:hover img             { transform: scale(1.05); }
  .socials a:hover           { background: var(--circle); }
  .socials a:hover img       { filter: none; opacity: 1; }
  /* Акцентом красится только почта: у остальных под курсором
     проявляется их собственный фирменный цвет. */
  .socials a:hover .ico-mail { color: var(--accent); opacity: 1; }
}

.tile:focus-visible::before        { opacity: 1; }
.tile:focus-visible                { color: var(--accent); }
.socials a:focus-visible           { background: var(--circle); }
.socials a:focus-visible img       { filter: none; opacity: 1; }
.socials a:focus-visible .ico-mail { color: var(--accent); opacity: 1; }

.tile:active { transform: scale(0.995); }

:where(a):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}


/* ── СОЦСЕТИ В КОМПАКТНЫХ РАСКЛАДКАХ ───────────────────────────────────────
   Портретная: квадрат 2×2 разворачивается в полосу 4×1. Размеры ячейки
   считать не нужно — панель держит пропорцию 300/63, отступы и зазоры
   в процентах, ячейка сама выходит 63×43.

   Альбомная компактная сюда не входит: там по ширине места хватает,
   и блок остаётся квадратным. */
@media (orientation: portrait) {
  .socials {
    aspect-ratio: 300 / 63;
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: 1fr;
  }
  .socials a { aspect-ratio: auto; }
}

/* Общее для обеих компактных раскладок: курсора здесь нет.
   Плашка под иконкой постоянная, а не по наведению — без неё иконки
   висели бы на панели без опоры. Иконки сразу в фирменных цветах:
   гасить их незачем, вернуть цвет всё равно нечем. */
@media (orientation: portrait), (orientation: landscape) and (max-width: 1100px) {
  .socials a     { background: var(--circle); }

  /* 34% высоты, и одинаково всем четырём.

     Раньше стояло `width: 34%; height: 50%` у картинок и `width: 38%;
     height: auto` у конверта — то есть три иконки мерились по ширине,
     а четвёртая по своей пропорции. Высота 50% при этом была мёртвой
     строкой: с object-fit: contain размер связывала ширина, и иконка
     рисовалась 34% высоты, а не 50%. Замер на 1024×768: ячейка 96,
     соседи 32.7 в высоту, конверт 29.7 — ниже на 9%.

     Число 34 не подобрано заново: это ровно то, что рисовалось и раньше.
     Меняется только конверт, который встаёт вровень. */
  .socials a img,
  .ico-mail      { width: auto; height: 34%; }
  .socials a img { filter: none; opacity: 1; }
  .ico-mail      { color: var(--accent); opacity: 1; }
}


/* ── СОЦСЕТИ: ФОРМА «ПОЛОСА» ───────────────────────────────────────────────
   Вторая форма блока, наравне с квадратом. Размеры ячейки считать не нужно:
   панель держит пропорцию 300/63, отступы и зазоры заданы в процентах,
   ячейка сама выходит 63×43.

   Отдельный класс, а не только медиазапрос: форму выбирает МЕСТО, куда блок
   встал, а не ширина экрана. В рельсе кейса полоса нужна и на десктопе —
   квадрат 2×2 при рельсе в 400 занял бы 400 по высоте и выдавил текст.

   Стоит ПОСЛЕ компактных медиазапросов намеренно. У `.socials--row a img`
   и `.socials a img` специфичность одинаковая, (0,1,2) у обоих, и спор решает
   порядок в файле. Стой этот блок выше — портретный запрос перебивал бы
   размер иконок на телефоне. */
.socials--row {
  aspect-ratio: 300 / 63;
  grid-template-columns: repeat(4, 1fr);
  /* minmax(0, 1fr), а не 1fr. У доли автоминимум равен min-content, и ряд
     отказывается становиться ниже содержимого. Пока высота полосы выходила
     из пропорции, это не всплывало. Как только странице понадобилось задать
     полосе ВЫСОТУ (полоса контактов ростом с шапку), ряд перестал ужиматься
     и ячейки полезли за нижний край: на 812×375 ячейка была 59 при доступных
     27 — переполнение на 31.8. Ноль в минимуме разрешает ряду сжаться. */
  grid-template-rows: minmax(0, 1fr);
}
.socials--row a { aspect-ratio: auto; min-height: 0; }

/* В полосе иконка меряется по ВЫСОТЕ, а не по ширине — в отличие от квадрата.

   Полосу ограничивает высота: ширина ячейки берётся из четырёх колонок и
   ширины экрана и на широком экране становится огромной. Доля от такой ширины
   давала иконку выше самой полосы, и та переполнялась. Доля от высоты этого
   не может по определению.

   50% — ровно столько же, сколько у главной в компактных раскладках, поэтому
   соцсети на главной и на странице кейса совпадают без отдельной подгонки.

   Конверту та же высота: у него viewBox шире, чем выше (43×35), и при равной
   высоте он выходит шире соседей — как и на главной, где ему дано 38% против
   34% у остальных. */
.socials--row a img,
.socials--row .ico-mail { width: auto; height: 50%; }




/* ── ПЛАШКА-УВЕДОМЛЕНИЕ .toast ─────────────────────────────────────────────
   Короткое сообщение о том, что действие случилось. Пока адресат один —
   копирование почты, — но компонент общий: он про «система ответила»,
   а не про почту.

   Разметки в HTML нет, элемент заводит socials.js. Стиль всё равно живёт
   здесь, а не в скрипте: внешний вид — дело системы, скрипт отвечает
   только за момент появления.

   Всплывает снизу вверх: в покое сдвинута на 0.6em вниз и прозрачна,
   в показе встаёт на место. Сдвиг в долях кегля, а не в пикселях, — как
   и всё остальное движение в системе.

   translate записан одним свойством вместе с центрирующим -50%: если
   развести их на два transform, второй перебьёт первый и плашка уедет
   левым краем в середину экрана. */
.toast {
  position: fixed;
  left: 50%;
  /* Значение по умолчанию — у нижнего края. Если плашка при этом попадёт
     на полосу контактов, socials.js поднимет её над полосой, подставив
     bottom замером. Угадывать подъём в CSS не выходит: высота полосы
     приходит из разных источников на разных страницах, и общий компонент
     их не видит. Подробности в socials.js, функция поставить(). */
  bottom: var(--edge);
  /* Выше шапки и контактов: те стоят на 5, а сообщение о действии
     обязано быть видно поверх всего. */
  z-index: 10;
  max-width: calc(100vw - var(--edge) * 4);

  /* Ростом с полосу контактов — из того же --ava, а не подобранным числом.
     Плашка появляется рядом с контактами и обязана читаться как элемент
     того же ряда, а не как всплывашка со стороны.

     Отступ по бокам --gap * 2, а не --gap. В полосе контактов --gap отделяет
     от края КВАДРАТНУЮ ячейку, у которой своя плашка и свои поля внутри;
     буквам того же отступа мало — строка вплотную подходит к дуге скругления,
     которое здесь крупное (28 в портрете, 40 на десктопе). Двойной отступ
     возвращает букве тот же зрительный воздух, что есть у иконки. */
  height: var(--ava);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 calc(var(--gap) * 2);
  border-radius: var(--radius);
  /* Непрозрачная: под плашкой проезжает содержимое, а сообщение должно
     читаться. Приём тот же, что у шапки кейса, — слой цвета панели поверх
     фона страницы. Цвет обязан идти картинкой: в шорткоде background он
     допустим только в последнем слое. */
  background-image: linear-gradient(var(--panel), var(--panel)), var(--page-bg);
  background-attachment: scroll, fixed;
  color: var(--ink);
  font-size: var(--fs-role);
  font-weight: 200;
  letter-spacing: 0.02em;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, 0.6em);
  /* Кликов не ловит даже в показе: это сообщение, а не кнопка. */
  pointer-events: none;
  transition: opacity var(--t-fast) ease, transform var(--t-fast) var(--e-out);
}

.toast.is-on {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ── ДОСТУПНОСТЬ ───────────────────────────────────────────────────────────
   Пользователь попросил меньше движения — отдаём страницу без переходов. */
@media (prefers-reduced-motion: reduce) {
  .tile, .tile::before, .ava img, .socials a, .socials a img, .ico-mail {
    transition: none;
  }
  /* Плашка появляется и исчезает мгновенно, но НЕ пропадает совсем:
     это единственное подтверждение, что клик сработал. Убирается движение,
     а не сообщение. */
  .toast { transition: none; transform: translate(-50%, 0); }
}


/* ── ПЕРЕХОД МЕЖДУ СТРАНИЦАМИ ──────────────────────────────────────────────
   View Transitions: браузер сам снимает уходящую страницу, показывает
   приходящую и сводит их кроссфейдом. От нас — одна строка на страницу;
   ни оверлеев, ни перехвата кликов, ни JS.

   navigation: auto включает переход для обычных переходов внутри сайта.
   Работает только между страницами ОДНОГО источника — это наш случай.

   Где не поддерживается (Firefox на сегодня), переход просто не случится:
   страница сменится мгновенно, как раньше. Ломаться нечему.

   Важно: по file:// это не работает — там у каждой страницы свой
   непрозрачный источник, и браузер считает переход внешним. Чтобы увидеть
   анимацию, страницы надо открывать с локального сервера. */
@view-transition { navigation: auto; }

/* Своя длительность вместо браузерных ~250 мс: на них смена читается
   как подмена кадра, а не как переход. Кривая та же, что у медленных
   движений в системе, — мягкий выход без рывка на старте. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .45s;
  animation-timing-function: var(--e-out);
}

/* Попросили меньше движения — отдаём мгновенную смену страницы. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}
