Позиционирование элементов. Часть 2: sticky и z-index

Чтение — около 7 минут· без учёта практики

В предыдущем уроке мы разобрали относительное, абсолютное и фиксированное позиционирование и повесили значок на карточку альбома. Осталось последнее значение position — sticky, а вместе с ним вопрос, который неизбежно возникает, когда элементы накладываются: какой из них окажется сверху.

Липкое позиционирование

Элемент с липким позиционированием (position: sticky) ведёт себя как обычный элемент потока, пока страница не прокручена до заданного порога. Затем он «прилипает» к краю окна и остаётся на месте, пока не дойдёт до конца своего родителя, — после этого уходит вместе с ним. Порог задают свойствами смещения: top: 0 означает «прилипнуть, когда верх элемента дойдёт до верхнего края окна».

position: sticky: попробуйте сами

Прокрутите окно результата: заголовок «Утро» прилипает к верху, а когда раздел заканчивается, уходит вместе с ним, и его место занимает «День». Замените top: 0 на top: 2rem — заголовки будут прилипать ниже края окна.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Липкие заголовки режима дня</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; margin: 0 1rem; }
      section { margin-bottom: 1rem; border: 1px solid tan; }
      h2 {
        position: sticky; /* прилипает при прокрутке */
        top: 0; /* порог: верхний край окна */
        margin: 0;
        padding: 0.5rem 1rem;
        background-color: saddlebrown; /* без фона под заголовком просвечивал бы текст */
        color: white;
      }
      p { margin: 0; padding: 0.5rem 1rem; }
    </style>
  </head>
  <body>
    <p>Прокрутите окно вниз ↓</p>
    <section>
      <h2>Утро</h2>
      <p>7:00 — бужу хозяина.</p>
      <p>7:30 — первая прогулка у пруда.</p>
      <p>8:00 — завтрак.</p>
      <p>8:30 — проверяю, не осталось ли чего в миске.</p>
      <p>9:00 — провожаю хозяина на работу.</p>
      <p>10:00 — сплю на лежанке.</p>
    </section>
    <section>
      <h2>День</h2>
      <p>13:00 — встречаю хозяина на обед.</p>
      <p>13:30 — короткая прогулка.</p>
      <p>14:00 — играю с канатом.</p>
      <p>15:00 — сторожу окно.</p>
      <p>16:00 — снова сплю.</p>
      <p>17:00 — прячу ёжика в лежанке.</p>
    </section>
    <section>
      <h2>Вечер</h2>
      <p>19:00 — большая вечерняя прогулка.</p>
      <p>20:00 — ужин.</p>
      <p>20:30 — учу новую команду.</p>
      <p>21:00 — играю с резиновой косточкой.</p>
      <p>22:00 — сплю у хозяина в ногах.</p>
      <p>23:00 — сплю дальше.</p>
    </section>
    <p style="height: 60vh">Конец режима дня.</p>
  </body>
</html>

Чем липкий элемент отличается от фиксированного:

  • Он остаётся в потоке. Исходное место элемента сохраняется, поэтому резервировать его отступом, как для fixed, не нужно. Но при прилипании элемент сдвигается относительно этого места и может перекрывать соседей.
  • Он прилипает в пределах родителя. Элемент не выходит за границы своего содержащего блока: в примере заголовок уходит, когда заканчивается его раздел.
  • Он прилипает к ближайшему прокручиваемому предку. Обычно это окно, но если у предка задано overflow со значением hidden, auto или scroll, элемент прилипает к краю этого предка.

Почему sticky не прилипает

position: sticky чаще других значений «не работает». Причин обычно три, и все они видны в инструментах разработчика:

  1. Не задан порог. Без top (или bottom, left, right) элемент ведёт себя как relative: браузер не знает, к какому краю его прилеплять.
  2. У предка задан overflow. Значения hidden, auto и scroll делают предка прокручиваемой областью, и элемент прилипает к нему. Если предок сам не прокручивается, прилипания не видно. Если нужно лишь обрезать содержимое, используйте overflow: clip: оно не создаёт прокручиваемую область.
  3. Родитель не выше элемента. Элемент прилипает только в пределах родителя. Если родитель обёрнут вокруг одного элемента, двигаться внутри некуда. Так бывает и в сетке или во Flexbox: элементы ряда по умолчанию растягиваются на его высоту. Если липкой сделать саму колонку, она уже высотой с родителя и прилипать ей негде. Задайте ей align-self: start, чтобы её высота стала по содержимому.

Найдите ошибки: попробуйте сами

Ни один из трёх заголовков не прилипает. Добавьте первому top: 0, у второго раздела удалите overflow: hidden, у третьего уберите height: auto, чтобы раздел снова стал выше заголовка. Прокрутите окно и проверьте каждый случай.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Почему sticky не прилипает</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; margin: 0 1rem; }
      section { height: 260px; margin-bottom: 1rem; border: 1px solid tan; }
      .label {
        position: sticky;
        margin: 0;
        padding: 0.5rem 1rem;
        background-color: saddlebrown;
        color: white;
      }
      /* 1. Порог не задан: top, bottom, left и right — auto */

      /* 2. У предка overflow: hidden — прилипание идёт к нему, а он сам не прокручивается */
      .clip {
        top: 0;
      }
      .case-2 {
        overflow: hidden;
      }

      /* 3. Родитель не выше элемента: прилипать негде */
      .tight {
        top: 0;
      }
      .wrap {
        height: auto;
      }
    </style>
  </head>
  <body>
    <p>Прокрутите окно: ни один заголовок не прилипает. Исправьте каждый случай.</p>
    <section>
      <h2 class="label">1. Нет порога</h2>
      <p>Заголовку не задан top.</p>
    </section>
    <section class="case-2">
      <h2 class="label clip">2. Предок обрезает переполнение</h2>
      <p>У раздела overflow: hidden.</p>
    </section>
    <section class="wrap">
      <h2 class="label tight">3. Родитель по размеру элемента</h2>
    </section>
    <p style="height: 80vh">Конец страницы.</p>
  </body>
</html>

Липкая шапка и ссылки-якоря

Липкая шапка с меню — частый приём: навигация всегда под рукой. Но у неё есть побочный эффект. Ссылка-якорь (href="#toy") прокручивает страницу так, чтобы заголовок с этим id оказался у самого верхнего края окна, — прямо под шапкой, которая его закроет.

Свойство scroll-margin-top задаёт отступ, который браузер оставит над элементом при прокрутке к нему. Отступ должен быть не меньше высоты шапки; удобно задавать его в rem, чтобы при увеличении шрифта он рос вместе с шапкой:

CSS
/* Любой элемент с id, к которому ведёт якорь, останавливается в 4.5rem от верха окна */
[id] {
  scroll-margin-top: 4.5rem;
}

Отступ рассчитан на шапку в одну строку. Если меню на узком экране переносится, шапка становится выше и снова закрывает заголовки — а заодно отнимает у телефона заметную часть экрана. Поэтому липкую шапку делают низкой: пункты меню оставляют в одной строке, а при нехватке места меню прокручивается внутри шапки.

Тот же результат даёт scroll-padding-top у прокручиваемой области — для окна это элемент html. Подробнее о свойствах прокрутки поговорим в отдельном материале.

Порядок наложения и z-index

Когда элементы перекрываются, браузер рисует их слоями по оси z — от дальнего к ближнему. Упрощённо порядок такой:

  1. фон и рамки элементов потока;
  2. плавающие элементы;
  3. строчное содержимое потока — текст и изображения;
  4. позиционированные элементы — в порядке HTML-кода: записанный позже оказывается выше.

Поэтому в прошлом уроке жёлтый блок перекрыл зелёный: оба позиционированы, а жёлтый записан позже. Изменить порядок позволяет свойство z-index. Его значение — целое число: элемент с бо́льшим числом рисуется выше. Отрицательные значения опускают элемент ниже элементов потока (в пределах контекста наложения, о нём ниже), 0 оставляет его на уровне позиционированных элементов без z-index.

z-index: попробуйте сами

Косточка выше каната, канат выше мячика. Задайте мячику z-index: 1 — ничего не изменится: у static z-index не действует. Добавьте мячику position: relative — он поднимется над обоими. Затем задайте канату z-index: 2.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Порядок наложения и z-index</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
      .scene { position: relative; height: 220px; }
      .card {
        width: 160px;
        height: 110px;
        padding: 0.5rem;
        border: 2px solid #2b211b;
      }
      .ball {
        /* static: рисуется под позиционированными */
        background-color: mediumseagreen;
      }
      .rope {
        position: relative;
        top: -60px;
        left: 60px;
        background-color: gold;
      }
      .bone {
        position: relative;
        top: -130px;
        left: 120px;
        background-color: lightsteelblue;
      }
    </style>
  </head>
  <body>
    <div class="scene">
      <div class="card ball">Мячик — static</div>
      <div class="card rope">Канат — relative</div>
      <div class="card bone">Косточка — relative</div>
    </div>
  </body>
</html>

Контекст наложения

Значения z-index сравниваются не на всей странице, а внутри контекста наложения (англ. stacking context) — группы элементов, которая рисуется как единое целое. Корневой контекст создаёт элемент html. Новый контекст создают, например:

  • элемент с position: relative или absolute и z-index, отличным от auto (даже 0);
  • элемент с position: fixed или sticky — всегда;
  • флекс- или грид-элемент с z-index, отличным от auto;
  • элемент с opacity меньше 1, а также с transform или filter;
  • элемент с isolation: isolate.

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

Отсюда классическая ошибка. Модальное окно с z-index: 999 лежит в родителе с position: relative; z-index: 1, а рядом — оверлей с z-index: 2. Окно оказывается под оверлеем: в корне сравниваются родитель (1) и оверлей (2), а число 999 действует только внутри родителя. Поможет не ещё большее число, а поднятие родителя или удаление контекста, который он создаёт.

Свойство isolation

Иногда контекст наложения, наоборот, нужно создать — чтобы отрицательный z-index не «провалился» слишком глубоко. Декоративную подложку часто рисуют псевдоэлементом с z-index: -1, чтобы она легла под текст карточки. Но если карточка не создаёт контекст, подложка уходит под фон всех элементов потока — в том числе под фон раздела, где лежит карточка, — и становится невидимой.

Свойство isolation: isolate создаёт контекст наложения без смещения элемента и изменения его прозрачности. Его потомки теперь рисуются как одна группа, поэтому их наложение с соседями может измениться. В этом примере подложка остаётся внутри карточки — под её текстом, но над фоном раздела.

isolation: isolate: попробуйте сами

Подложки за карточкой не видно: z-index: -1 опустил её под светлый фон раздела. Раскомментируйте isolation: isolate у карточки — подложка появится под текстом. В этом примере тот же эффект даст z-index: 0: карточка уже позиционирована. isolation создаёт контекст и без position.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Изоляция наложения: isolation</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
      section {
        padding: 1.5rem;
        border-radius: 16px;
        background-color: #fffaf3;
      }
      .card {
        position: relative;
        max-width: 260px;
        padding: 1rem;
        /* isolation: isolate; */
      }
      /* Декоративная подложка позади текста карточки */
      .card::before {
        content: "";
        position: absolute;
        inset: -0.5rem 0.5rem; /* выступает сверху и снизу карточки */
        z-index: -1; /* под текстом карточки */
        border-radius: 12px;
        background-color: tan;
      }
    </style>
  </head>
  <body>
    <section>
      <div class="card">Канат с мячиком «Удав» — моя любимая игрушка.</div>
    </section>
  </body>
</html>

Потренируйтесь

В песочнице — тот самый случай с модальным окном. У окна z-index: 999, но оно под оверлеем. Поднимите родителя выше оверлея, затем верните родителю auto и выберите свойство, которое снова создаёт контекст: transform, opacity или isolation. В дереве контекстов видно, где сравниваются значения.

Дерево контекстов

Выберите узел, чтобы выделить его в примере и узнать, как он участвует в наложении.

Порядок наложения

Окно вложено в родителя. Оверлей идёт после родителя в HTML.

HTML и CSS текущего примера
CSS
.parent {
  position: relative;
  z-index: 1;
  transform: none;
  opacity: 1;
  isolation: auto;
}
.modal {
  position: absolute;
  z-index: 999;
}
.overlay {
  position: absolute;
  z-index: 2;
}

/* Оформление сцены */
* { box-sizing: border-box; }
html { color-scheme: light dark; }
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; background: light-dark(#f8fafc, #182026); color: light-dark(#243640, #e5edf2); }
.scene { position: relative; height: 300px; margin: 12px; }
.parent { height: 260px; padding: 8px; border: 2px dashed light-dark(#607d8b, #a8c8d6); }
.modal, .overlay { display: grid; place-items: center; border: 2px solid currentColor; }
.modal { top: 60px; left: 8%; width: 65%; height: 150px; background: light-dark(#c0e9cf, #24533c); }
.overlay { top: 110px; left: 40%; width: 55%; height: 150px; background: light-dark(#f4dcb0, #574527); }
[data-selected] { outline: 3px dotted light-dark(#243640, #fff); outline-offset: -6px; }
HTML
<main class="scene">
  <section class="parent" aria-label="Родитель">
    <span>Родитель</span>
    <div class="modal">Окно</div>
  </section>
  <div class="overlay">Оверлей</div>
</main>

Задания на контексты наложения — в тренажёре

Проверьте себя

Выберите ответ — объяснение появится сразу. Ответ можно менять.

1. Чем position: sticky отличается от position: fixed?
Показать ответ

Липкий элемент остаётся в потоке и прилипает только в пределах родителя — Место sticky-элемента сохраняется, а у конца содержащего блока он уходит вместе с ним. Fixed исключён из потока и обычно привязан к окну; transform или filter у предка могут изменить его точку отсчёта.

2. Заголовку задали только position: sticky. Что произойдёт при прокрутке?
Показать ответ

Он не прилипнет: без порога sticky ведёт себя как relative — Нужно указать край и расстояние, например top: 0.

3. У заголовка position: sticky; top: 0, но он не прилипает. У его раздела задано overflow: hidden. В чём дело?
Показать ответ

Раздел стал прокручиваемой областью, и заголовок прилипает к нему, а не к окну — Hidden, auto и scroll делают предка прокручиваемым. Сам раздел не прокручивается, поэтому прилипания не видно. Для обрезки подходит overflow: clip.

4. Зачем липкой шапке правило [id] { scroll-margin-top: 4.5rem; }?
Показать ответ

Чтобы после перехода по якорю заголовок не оказался под шапкой — Браузер останавливает прокрутку в 4.5rem от верха окна, и заголовок виден под шапкой.

5. Два блока с position: relative без z-index перекрываются. Какой окажется сверху?
Показать ответ

Тот, что записан в HTML позже — Позиционированные элементы без z-index рисуются в порядке кода: следующий поверх предыдущего.

6. Обычному абзацу без position задали z-index: 5, но он остался под позиционированным блоком. Почему?
Показать ответ

У элемента потока z-index не действует — Z-index работает у позиционированных элементов, а также у флекс- и грид-элементов. Абзацу нужно, например, position: relative.

7. Окно с z-index: 999 вложено в родителя с position: relative; z-index: 1. Рядом с родителем — оверлей с z-index: 2. Что сверху?
Показать ответ

Оверлей: в корне сравниваются родитель (1) и оверлей (2) — Родитель создаёт контекст наложения; 999 действует только внутри него. Нужно поднять родителя или убрать его контекст.

8. Что из перечисленного НЕ создаёт контекст наложения?
Показать ответ

position: relative; z-index: auto — Relative и absolute создают контекст только с числовым z-index, даже нулевым.

9. Подложка карточки — псевдоэлемент с z-index: -1 — пропала под фоном раздела. Как вернуть её под текст карточки, без смещения карточки и изменения её прозрачности?
Показать ответ

Задать карточке isolation: isolate — Карточка создаёт контекст, и подложка с -1 остаётся внутри него — над фоном раздела, под текстом карточки.

Задание

Ожидаемый результат: на всех страницах меню остаётся у верхнего края окна при прокрутке, а фото альбома и значок «Любимая» уходят под шапку, а не поверх неё. Без z-index шапка ниже карточек: и шапка, и карточки с position: relative позиционированы, а карточки записаны в HTML позже. На узком экране меню в одну строку и прокручивается вбок. После перехода по оглавлению заголовок раздела стоит под шапкой, а не скрыт за ней.

Выполните задание

css/style.css
CSS
/* Общие стили сайта Тони */

/* Ширина и высота включают внутренние отступы и рамку — у всех элементов и псевдоэлементов */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Шрифт заголовков Nunito (лицензия SIL Open Font License): кириллица и латиница в разных файлах */
@font-face {
  font-family: "Nunito";
  src: url("../assets/nunito-cyrillic-800-normal.woff2") format("woff2");
  font-weight: 800;
  font-display: swap; /* пока шрифт загружается, текст виден запасным шрифтом */
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: "Nunito";
  src: url("../assets/nunito-latin-800-normal.woff2") format("woff2");
  font-weight: 800;
  font-display: swap;
  unicode-range: U+0000-00FF, U+2000-206F, U+20AC, U+2122;
}

body {
  background-color: linen;
  color: #2b211b; /* цвет для потомков без собственного значения */
  font-size: 1.0625rem; /* 17px при настройке браузера по умолчанию */
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; /* системный шрифт устройства */
  line-height: 1.6; /* множитель: у каждого элемента интервал от его собственного размера шрифта */
  margin: 0; /* убираем отступ браузера: шапка и подвал во всю ширину окна */
}

/* Поля основного содержимого: текст не прижимается к краям окна */
main {
  padding: 1.5rem 1rem;
  max-width: 68rem; /* на широком экране строки не растягиваются на всё окно */
  margin: 0 auto; /* колонка по центру */
}

/* Изображения и видео не шире колонки, пропорции сохраняются */
img,
video {
  max-width: 100%;
  height: auto;
}

/* Фреймы, аудиоплеер, индикаторы и поля формы не выходят за колонку */
progress,
meter,
iframe,
audio,
input,
select,
textarea,
button {
  max-width: 100%;
}

/* Разрешаем группе полей сужаться вместе с колонкой. */
fieldset {
  min-width: 0;
}

/* Общий цвет заголовков — групповой селектор */
h1,
h2 {
  color: saddlebrown;
  font-family: "Nunito", "Trebuchet MS", system-ui, sans-serif;
  font-weight: 800;
  line-height: 1.2; /* крупному тексту хватает меньшего интервала */
}

/* Размеры заголовков в rem — от размера шрифта корня, а не родителя */
h1 {
  text-align: center;
  font-size: 2.5rem;
}

h2 {
  font-size: 1.75rem;
}

/* Вводный блок главной страницы */
.intro {
  background-color: hsl(30, 80%, 95%);
  padding: 1.5rem; /* фон вокруг текста; заодно не выпускает margin заголовка за пределы блока */
  border-radius: 16px; /* скругляется и фон, хотя рамки нет */
  /* Лапка в правом нижнем углу; цвет фона выше остаётся запасным, если картинка не загрузится */
  background-image: url("../assets/paw.svg");
  background-repeat: no-repeat;
  background-position: right 1rem bottom 1rem;
  background-size: 4rem;
}

/* Разделы главной — светлые карточки на фоне страницы */
main > section {
  background-color: #fffaf3;
  border: 1px solid hsl(28, 30%, 82%);
  border-radius: 16px;
  padding: 1.5rem;
  margin-top: 1.5rem;
}

/* Ширина вводных абзацев ограничена 60ch; количество букв в строке зависит от шрифта и текста */
.intro p {
  max-width: 60ch;
}

/* Важное предупреждение во вводном блоке — темнее основного акцента */
.intro strong {
  color: #6e3510;
}

/* Оглавление главной страницы — переносимый ряд «таблеток» на Flexbox */
.toc {
  background-color: white;
  border: 1px solid tan;
  display: flex;
  flex-wrap: wrap; /* «таблетки», которым не хватает места, переходят на новую строку */
  gap: 0.5rem; /* промежутки между «таблетками» и между строками */
  list-style: none;
  padding: 0.75rem;
}

/* Подчёркивание ссылок чуть ниже букв: не задевает выносные элементы */
a {
  text-underline-offset: 3px;
}

/* Тонкая линия отделяет шапку от страницы */
header {
  border-bottom: 1px solid tan;
}

/* Меню на Flexbox: пункты могут переноситься, промежутки задаёт gap */
nav ul {
  margin: 0;
  padding: 0.5rem 1rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  list-style: none;
}

/* Ссылка на запись стоит отдельно у правого края: автоматический отступ забирает свободное место */
nav li:last-child {
  margin-left: auto;
}

/* Ссылки меню в шапке — селектор потомков */
nav a {
  color: saddlebrown;
  text-decoration: none;
  display: inline-block; /* у строчной ссылки вертикальный padding не раздвигает строку */
  padding: 0.4rem 0.9rem;
  border-radius: 999px; /* «таблетка»: радиус больше половины высоты */
}

/* Ссылки оглавления главной страницы */
.toc a {
  color: #6b5d52; /* тёплый серый: контраст 6.34:1 на белом фоне оглавления */
  display: block; /* ссылка заполняет «таблетку» целиком */
  padding: 0.25rem 0.75rem;
  border: 1px solid tan;
  border-radius: 999px;
  text-decoration: none;
}

/* Фотографии со скруглёнными углами */
figure img {
  border-radius: 8px;
}

figcaption {
  color: #6b5d52; /* тёплый серый: контраст 5.64:1 на фоне linen */
  font-size: 0.95rem;
}

footer {
  background-color: hsl(30, 50%, 90%);
  text-align: center;
  padding: 1rem;
}

/* Ссылка в подвале берёт цвет текста подвала */
footer a {
  color: inherit;
}

/* Поля формы получают шрифт страницы вместо шрифта браузера */
input,
select,
textarea,
button {
  font: inherit;
}

/* Ссылки темнеют при наведении указателя */
a:hover {
  color: #5c2d0c;
}

/* Ссылки меню подчёркиваются при наведении и фокусе с клавиатуры */
nav a:hover,
nav a:focus-visible {
  text-decoration: underline;
}

/* Заметная рамка фокуса у ссылок, полей и кнопок при работе с клавиатуры */
:focus-visible {
  outline: 3px solid #1f6feb;
  outline-offset: 3px;
}

/* Цитата из книги — шрифтом с засечками и курсивом */
blockquote {
  font-family: Georgia, "Times New Roman", serif;
  font-style: italic;
  border-left: 4px solid tan; /* черта слева выделяет цитату */
  padding-left: 1rem;
}

/* Подпись под цитатой — у правого края */
blockquote + figcaption {
  text-align: right;
}

/* Строки формы на Flexbox: подпись — колонка постоянной ширины, поле занимает остаток строки,
   а на узком экране переносится под подпись */
fieldset p {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
}

fieldset p label {
  flex: 0 1 12rem; /* не растёт, но может сжаться в узкой строке */
  min-width: 0;
}

fieldset p input,
fieldset p select,
fieldset p textarea {
  flex: 1 1 12rem; /* растёт и сжимается от исходной ширины 12rem */
  min-width: 0; /* поле может стать уже своего автоматического минимума */
}

/* Фотоальбом на Grid: столько колонок, сколько помещается карточек шириной не меньше 15rem */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: 1rem;
  list-style: none;
  padding: 0;
}

.gallery figure {
  margin: 0;
}

.gallery img {
  width: 100%; /* фото заполняет ширину своей ячейки */
}

/* Список команд: первая колонка подстраивается под текст, оставляя место для перевода. */
dl {
  display: grid;
  grid-template-columns: fit-content(calc(50% - 0.5rem)) minmax(0, 1fr);
  gap: 0.5rem 1rem;
}

dt {
  overflow-wrap: anywhere;
  grid-column: 1;
  font-weight: bold;
}

dd {
  grid-column: 2;
  overflow-wrap: anywhere; /* длинный перевод переносится внутри своей колонки */
  margin: 0; /* браузерный отступ слева больше не нужен: колонку задаёт сетка */
}

/* Длинные слова и адреса переносятся, а не выходят за край узкого экрана */
body {
  overflow-wrap: break-word;
}

/* Таблица режима дня на узком экране прокручивается в своей рамке, а не сжимается до нечитаемых колонок */
.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  min-width: 28rem;
}

/* Подпись таблицы прокручивается вместе с ней: прижимаем её к началу, чтобы она была видна сразу */
caption {
  text-align: left;
}

/* Широкий экран: в разделе «Моя любимая игрушка» фото справа, текст слева.
   На узком экране правило не действует, и раздел остаётся одной колонкой. */
@media (width >= 48rem) {
  .toy {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    column-gap: 1.5rem;
  }

  .toy figure {
    grid-column: 2;
    grid-row: 1 / span 6; /* фото рядом со всеми абзацами раздела */
    margin: 0;
  }

  .toy hr {
    width: 100%;
  }
}

/* Карточка альбома — точка отсчёта для значка: position: relative не сдвигает её и не меняет поток */
.gallery li {
  position: relative;
}

/* Значок «Любимая» в правом верхнем углу карточки; из потока исключён, поэтому фото не сдвигается */
.badge {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  padding: 0.2rem 0.7rem;
  border-radius: 999px;
  background-color: saddlebrown;
  color: white;
  font-size: 0.85rem;
  font-weight: bold;
}
index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="Сайт карликового пинчера Тони: игрушки, режим дня и запись на прогулку.">
    <title>Сайт Тони</title>
    <link rel="stylesheet" href="css/style.css">
  </head>
  <body>
    <header>
      <!-- Меню сайта -->
      <nav>
        <ul>
          <li><a href="index.html">Главная</a></li>
          <li><a href="pages/toys.html">Мои игрушки</a></li>
          <li><a href="pages/signup.html">Запись на прогулку</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <div class="intro">
        <h1 id="top">Привет! Меня зовут Тони</h1>
        <p>Я карликовый пинчер. Мне три года, я вешу 8&nbsp;кг и люблю гулять в любую погоду. Хозяин говорит: <q>Тони — лучший сторож в доме</q>.</p>
        <p>Мой день рождения — <time datetime="2023-09-22">22 сентября</time>. <strong>Пожалуйста, не кормите меня со стола!</strong></p>
        <p>Моя родословная записана в Российской кинологической федерации (<abbr title="Российская кинологическая федерация">РКФ</abbr>).</p>
      </div>
      <ul class="toc">
        <li><a href="#toy">Моя любимая игрушка</a></li>
        <li><a href="#commands">Команды, которые я знаю</a></li>
        <li><a href="#schedule">Мой режим дня</a></li>
        <li><a href="#walks">Где я гуляю</a></li>
        <li><a href="#song">Песня для прогулок</a></li>
      </ul>
      <section class="toy">
        <h2 id="toy">Моя любимая игрушка</h2>
        <p data-toy-id="17">Это канат с мячиком. Хозяин бросает его, а я приношу обратно — <em>почти</em> всегда. <a href="pages/toys.html">Посмотреть все игрушки Тони</a>.</p>
        <figure>
          <img src="assets/tony-toy.png" alt="Тони сидит и держит в зубах канат с красным мячиком" width="300" height="400" title="Мой канат &quot;Удав&quot;">
          <figcaption>Я и мой канат «Удав»</figcaption>
        </figure>
        <p><a href="assets/tony-toy.png" download>Скачать фотографию</a></p>
        <p>Подробнее о моей породе — в статье <a href="https://ru.wikipedia.org/wiki/Карликовый_пинчер" target="_blank">«Карликовый пинчер» в Википедии</a>.</p>
        <hr>
        <p>Вечером игра закончилась. Канат остался на полу, а я свернулся калачиком в кресле.</p>
      </section>
      <section>
        <h2>Мой стишок</h2>
        <p>
          Я маленький, но смелый,<br>
          Гуляю целый день,<br>
          А вечером на кресле<br>
          Сплю — и мне не лень.
        </p>
      </section>
      <section>
        <h2 id="commands">Команды, которые я знаю</h2>
        <p>Хозяин научил меня командам на немецком:</p>
        <dl>
          <dt lang="de">Sitz!</dt>
          <dd>сидеть</dd>
          <dt lang="de">Platz!</dt>
          <dd>лежать</dd>
          <dt lang="de">Fuß!</dt>
          <dd>рядом</dd>
        </dl>
        <details>
          <summary>Почему команды на немецком?</summary>
          <p>Карликовый пинчер — немецкая порода, и хозяину понравилась эта традиция.</p>
        </details>
      </section>
      <section>
        <h2 id="schedule">Мой режим дня</h2>
        <div class="table-wrap" tabindex="0" role="region" aria-label="Режим дня Тони">
          <table>
            <caption>Режим дня Тони</caption>
            <thead>
              <tr>
                <th scope="col">Время</th>
                <th scope="col">Будни</th>
                <th scope="col">Выходные</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <th scope="row">07:00</th>
                <td>Прогулка, 20 минут</td>
                <td>Сплю</td>
              </tr>
              <tr>
                <th scope="row">08:00</th>
                <td colspan="2">Завтрак</td>
              </tr>
              <tr>
                <th scope="row">13:00</th>
                <td>Жду хозяина и сплю</td>
                <td>Прогулка в парке, 1 час</td>
              </tr>
              <tr>
                <th scope="row">19:00</th>
                <td colspan="2">Ужин</td>
              </tr>
              <tr>
                <th scope="row">21:00</th>
                <td colspan="2">Вечерняя прогулка</td>
              </tr>
            </tbody>
          </table>
        </div>
      </section>
      <section>
        <h2 id="progress">Мои успехи</h2>
        <p>Учу команду «Апорт»: пройдено 3 занятия из 5. <progress value="3" max="5">3 из 5</progress></p>
        <p>Прогулка сегодня: 40 минут. <meter value="40" min="0" max="90" low="30" high="60" optimum="75">40 из 90 минут</meter></p>
      </section>
      <section>
        <h2 id="walks">Где я гуляю</h2>
        <p>Я люблю гулять в сквере у дома.</p>
        <p>
          <a href="pages/walks.html" target="walk">Маршрут</a> |
          <a href="assets/tony.png" target="walk">Фото с прогулки</a>
        </p>
        <iframe src="pages/walks.html" name="walk" title="Маршрут и фото прогулки Тони" width="320" height="420"></iframe>
        <h3>Видео с прогулки</h3>
        <p>Субтитры помогут, если вы смотрите видео без звука.</p>
        <video width="180" height="320" poster="assets/walk.jpg" controls>
          <source src="assets/walk.mp4" type="video/mp4">
          <track src="assets/ru.vtt" kind="subtitles" srclang="ru" label="Русский" default>
          <p><a href="assets/walk.mp4">Скачать видео</a></p>
        </video>
        <h3>Как я лаю</h3>
        <p>Так я разговариваю с хозяином — у нас целый диалог!</p>
        <audio controls>
          <source src="assets/bark.mp3" type="audio/mpeg">
          <p><a href="assets/bark.mp3">Скачать запись</a></p>
        </audio>
      </section>
      <section>
        <h2 id="song">Песня для прогулок</h2>
        <p>Хозяин включает эту песню, когда мы выходим гулять.</p>
        <iframe width="480" height="270" src="https://www.youtube.com/embed/ojULkWEUsPs?start=17" title="Baha Men — Who Let the Dogs Out" loading="lazy" allow="fullscreen"></iframe>
        <p><a href="https://www.youtube.com/watch?v=ojULkWEUsPs">Baha Men — Who Let the Dogs Out на YouTube</a></p>
      </section>
      <section>
        <h2>Моя любимая книга</h2>
        <p>Хозяин читал мне вслух рассказ <cite>«Каштанка»</cite> Антона Чехова. Он начинается так:</p>
        <figure>
          <blockquote>
            <p>Молодая рыжая собака — помесь таксы с дворняжкой — очень похожая мордой на лисицу, бегала взад и вперёд по тротуару и беспокойно оглядывалась по сторонам.</p>
          </blockquote>
          <figcaption>— Антон Чехов, <cite>«Каштанка»</cite></figcaption>
        </figure>
      </section>
      <p><a href="#top">Наверх</a></p>
    </main>
    <footer>
      <p><small>&copy; 2026 Тони и его хозяин</small></p>
      <address>Написать хозяину Тони: <a href="mailto:tony@example.com">tony@example.com</a></address>
    </footer>
  </body>
</html>
pages/signup.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="Запишитесь на совместную прогулку с карликовым пинчером Тони.">
    <title>Запись на прогулку с Тони</title>
    <link rel="stylesheet" href="../css/style.css">
  </head>
  <body>
    <header>
      <nav>
        <ul>
          <li><a href="../index.html">Главная</a></li>
          <li><a href="toys.html">Мои игрушки</a></li>
          <li><a href="signup.html">Запись на прогулку</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <h1>Запись на прогулку</h1>
      <p>Гуляйте вместе с Тони! Оставьте заявку, и хозяин Тони напишет вам, чтобы договориться о встрече.</p>
      <p><a href="../index.html#walks">Где мы гуляем</a></p>
      <form>
        <fieldset>
          <legend>О вас</legend>
          <p>
            <label for="owner">Ваше имя</label>
            <input type="text" id="owner" name="owner" autocomplete="name" required>
          </p>
          <p>
            <label for="email">Электронная почта</label>
            <input type="email" id="email" name="email" autocomplete="email" placeholder="anna@example.com" required>
          </p>
        </fieldset>
        <fieldset>
          <legend>Ваша собака</legend>
          <p>
            <label for="breed">Порода</label>
            <input type="text" id="breed" name="breed" list="breeds">
            <datalist id="breeds">
              <option value="Карликовый пинчер"></option>
              <option value="Такса"></option>
              <option value="Йоркширский терьер"></option>
              <option value="Беспородная"></option>
            </datalist>
          </p>
          <p>
            <label for="dogs">Сколько собак придёт</label>
            <input type="number" id="dogs" name="dogs" min="1" max="3" value="1">
          </p>
        </fieldset>
        <fieldset>
          <legend>Когда гулять</legend>
          <p>
            <label for="date">Дата</label>
            <input type="date" id="date" name="date" required>
          </p>
          <label><input type="radio" name="time" value="morning" checked> Утром</label>
          <label><input type="radio" name="time" value="evening"> Вечером</label>
        </fieldset>
        <fieldset>
          <legend>Прогулка</legend>
          <p>
            <label for="leader">Ведёт прогулку</label>
            <input type="text" id="leader" name="leader" value="Тони" readonly>
          </p>
          <p>
            <label for="route">Маршрут</label>
            <select id="route" name="route">
              <optgroup label="В городе">
                <option value="square">Сквер у дома</option>
                <option value="pond" selected>Пруд с утками</option>
              </optgroup>
              <optgroup label="За городом">
                <option value="forest">Лес</option>
                <option value="river" disabled>Берег реки — закрыт до весны</option>
              </optgroup>
            </select>
          </p>
          <p>
            <label for="wishes">Пожелания</label>
            <textarea id="wishes" name="wishes" rows="4" cols="40" maxlength="300" placeholder="Например: моя собака боится велосипедов"></textarea>
          </p>
        </fieldset>
        <fieldset>
          <legend>Что взять с собой</legend>
          <label><input type="checkbox" name="take" value="ball"> Мячик</label>
          <label><input type="checkbox" name="take" value="rope"> Канат</label>
        </fieldset>
        <p>
          <button type="submit">Записаться на прогулку</button>
          <button type="reset">Очистить форму</button>
        </p>
      </form>
    </main>
    <footer>
      <p><small>&copy; 2026 Тони и его хозяин</small></p>
      <address>Написать хозяину Тони: <a href="mailto:tony@example.com">tony@example.com</a></address>
    </footer>
  </body>
</html>
pages/toys.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="Любимые игрушки Тони и правила игры в канат.">
    <title>Игрушки Тони</title>
    <link rel="stylesheet" href="../css/style.css">
  </head>
  <body>
    <header>
      <nav>
        <ul>
          <li><a href="../index.html">Главная</a></li>
          <li><a href="toys.html">Мои игрушки</a></li>
          <li><a href="signup.html">Запись на прогулку</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <h1>Мои игрушки</h1>
      <ul>
        <li>Канат с мячиком «Удав» — моя любимая игрушка</li>
        <li>Резиновая косточка — для вечеров, когда гулять уже поздно</li>
        <li>Пищащий ёжик — его я прячу <del>под диваном</del> <ins>в своей лежанке</ins></li>
      </ul>
      <img src="../assets/tony.png" alt="Тони сидит и смотрит в камеру" width="300" height="400">
      <h2>Как играть со мной в канат</h2>
      <ol>
        <li>Покажите мне канат.</li>
        <li>Бросьте его подальше.</li>
        <li>Подождите, пока я принесу его обратно.</li>
        <li>Похвалите меня — и всё сначала!</li>
      </ol>
      <section>
        <h2 id="album">Мой фотоальбом</h2>
        <ul class="gallery">
          <li>
            <figure>
              <img src="../assets/album-puppy.png" alt="Тони-щенок сидит и смотрит в камеру" width="300" height="400">
              <figcaption>Я в детстве</figcaption>
            </figure>
          </li>
          <li>
            <figure>
              <img src="../assets/album-squirrel.png" alt="Тони лежит в обнимку с плюшевой белкой" width="300" height="400">
              <figcaption>С плюшевой белкой</figcaption>
            </figure>
            <span class="badge">Любимая</span>
          </li>
          <li>
            <figure>
              <img src="../assets/album-hoodie.png" alt="Тони стоит в бежевой толстовке с капюшоном" width="300" height="400">
              <figcaption>В худи для холодной погоды</figcaption>
            </figure>
          </li>
          <li>
            <figure>
              <img src="../assets/album-harness.png" alt="Тони в оранжевой шлейке ждёт прогулку" width="300" height="400">
              <figcaption>Шлейка надета — идём гулять!</figcaption>
            </figure>
          </li>
          <li>
            <figure>
              <img src="../assets/album-laugh.png" alt="Тони лежит, запрокинув голову, и широко открыл пасть, будто смеётся" width="300" height="400">
              <figcaption>Смеюсь над шуткой хозяина</figcaption>
            </figure>
          </li>
          <li>
            <figure>
              <img src="../assets/album-sleep.png" alt="Тони спит, положив голову на лапы" width="300" height="319">
              <figcaption>После прогулки</figcaption>
            </figure>
          </li>
        </ul>
      </section>
      <p><a href="../index.html#commands">Какие команды я знаю</a></p>
      <p><a href="../index.html">Вернуться на главную</a></p>
    </main>
    <footer>
      <p><small>&copy; 2026 Тони и его хозяин</small></p>
      <address>Написать хозяину Тони: <a href="mailto:tony@example.com">tony@example.com</a></address>
    </footer>
  </body>
</html>
pages/walks.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="Маршрут прогулки Тони: сквер, пруд и поляна для игры с канатом.">
    <title>Где гуляет Тони</title>
    <link rel="stylesheet" href="../css/style.css">
  </head>
  <body>
    <h1>Мой маршрут</h1>
    <ol>
      <li>Сквер у дома.</li>
      <li>Пруд с утками.</li>
      <li>Поляна для игры с канатом.</li>
    </ol>
  </body>
</html>

Продолжите в VS Code

Внесите те же изменения в css/style.css своего сайта my-site и откройте страницы в браузере. Проверка: в режиме устройства на ширине 320 пикселей шапка тоже прилипает и не закрывает заголовок после перехода по оглавлению. На вкладке Elements у header видно правило с position: sticky. Если результат отличается, нажмите в редакторе «Показать решение» и сравните его со своим кодом.