Пользовательские свойства (CSS-переменные)

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

В предыдущем уроке мы оформили форму, и цвет saddlebrown появился в таблице стилей ещё в нескольких правилах: у кнопок, флажков, рамки группы. Теперь он записан больше чем в десяти местах. Захоти хозяин Тони сменить акцентный цвет — придётся найти и исправить их все и не пропустить ни одного. Эту задачу решают пользовательские свойства, которые часто называют CSS-переменными.

Объявление и var()

Пользовательское свойство объявляют как обычное, но его имя начинается с двух дефисов: --accent, --text-muted, --space. Само по себе оно ничего не оформляет — только хранит значение. Чтобы подставить значение в обычное свойство, используют функцию var() с именем переменной:

CSS
:root {
  --accent: saddlebrown; /* объявление */
}

h2 {
  color: var(--accent); /* использование */
}

Общие переменные объявляют в правиле для псевдокласса :root — он выбирает корневой элемент документа, <html>. Оттуда значение наследуется всеми элементами страницы.

Объявлять переменные в :root необязательно. Если значение нужно только одному компоненту, объявите его прямо у этого компонента. Например, .card { --space: 1rem; padding: var(--space); } задаёт локальную переменную для карточки и её потомков; у соседних элементов она от этого не появится.

var(): попробуйте сами

Рамка, заголовок и кнопка обеих карточек берут цвет из --accent. Замените saddlebrown в :root на seagreen — поменяются все шесть мест сразу.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Объявление и var()</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
      :root {
        --accent: saddlebrown; /* объявление: имя начинается с двух дефисов */
      }
      .card {
        max-width: 22rem;
        margin-bottom: 1rem;
        padding: 1rem;
        border: 2px solid var(--accent);
        border-radius: 16px;
        background-color: #fffaf3;
      }
      h2 {
        margin-top: 0;
        color: var(--accent);
      }
      button {
        padding: 0.4rem 1rem;
        border: 0;
        border-radius: 999px;
        background-color: var(--accent);
        color: white;
        font: inherit;
      }
    </style>
  </head>
  <body>
    <article class="card">
      <h2>Канат «Удав»</h2>
      <p>Цвет рамки, заголовка и кнопки берётся из одной переменной --accent.</p>
      <button>Подробнее</button>
    </article>
    <article class="card">
      <h2>Резиновая косточка</h2>
      <p>Измените значение --accent в :root — поменяются обе карточки.</p>
      <button>Подробнее</button>
    </article>
  </body>
</html>

  • Имена чувствительны к регистру: --accent и --Accent — две разные переменные. В учебнике имена пишутся строчными буквами через дефис.
  • Значение — почти любой текст: цвет, длина, число, список шрифтов, даже целая тень. Браузер не проверяет его при объявлении, а подставляет в свойство как есть, когда встречает var().
  • var() работает только в значениях свойств. Переменную нельзя подставить в селектор, в имя свойства или в условие медиазапроса: @media (width >= var(--wide)) не сработает.

Наследование и переопределение

Пользовательские свойства наследуются, как color и font-family из урока «Наследование». Значение, объявленное у элемента, видно ему и всем его потомкам. Поэтому переменную можно переопределить для части страницы: новое значение действует внутри элемента, а за его пределами остаётся прежнее.

Переопределение: попробуйте сами

Все три карточки оформлены одними правилами. У карточки .favorite переменная --accent переопределена, поэтому она зелёная; соседние карточки остались коричневыми.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Наследование и переопределение</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
      :root {
        --accent: saddlebrown;
      }
      .favorite {
        --accent: seagreen; /* новое значение действует внутри этой карточки */
      }
      .card {
        max-width: 22rem;
        margin-bottom: 1rem;
        padding: 1rem;
        border: 2px solid var(--accent);
        border-radius: 16px;
        background-color: #fffaf3;
      }
      h2 {
        margin-top: 0;
        color: var(--accent);
      }
      button {
        padding: 0.4rem 1rem;
        border: 0;
        border-radius: 999px;
        background-color: var(--accent);
        color: white;
        font: inherit;
      }
    </style>
  </head>
  <body>
    <article class="card">
      <h2>Резиновая косточка</h2>
      <p>Обычная карточка: --accent унаследована от :root.</p>
      <button>Подробнее</button>
    </article>
    <article class="card favorite">
      <h2>Канат «Удав» — любимая</h2>
      <p>Здесь --accent переопределена: зелёными стали и рамка, и заголовок, и кнопка.</p>
      <button>Подробнее</button>
    </article>
    <article class="card">
      <h2>Пищащий ёжик</h2>
      <p>Снова saddlebrown: переопределение не выходит за пределы карточки .favorite.</p>
      <button>Подробнее</button>
    </article>
  </body>
</html>

Так строят варианты компонентов: правила кнопки или карточки пишут один раз через переменные, а для «любимой», «предупреждения» или «тёмной» версии меняют только значения. Объявление переменной подчиняется каскаду, как любое другое: при равной специфичности побеждает записанное ниже, у более специфичного селектора — своё значение.

При этом каскад сравнивает объявления для одного и того же элемента. Если переменная задана непосредственно у карточки, она использует собственное значение вместо унаследованного. Специфичность селектора родителя здесь не сравнивается со специфичностью селектора карточки: даже значение от родителя с ID не помешает переопределить переменную у потомка обычным классом.

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

Переменная --token
Корень #demo-root
Секция #demo-section
Карточка #demo-card
Резервные значения

    Цепочка от корня к карточке
    ЭлементОбъявление --tokenВычислено браузеромИсточник

    CSS примера

    HTML дерева элементов

    @property и светлая и тёмная тема — в инструменте CSS-переменных

    Запасное значение

    Второй аргумент var() — запасное значение: оно используется, если вычисленное значение переменной недействительно для подстановки. Обычный случай — переменная не объявлена ни у элемента, ни у его предков. У незарегистрированного пользовательского свойства такое состояние возникает и при initial или циклической зависимости переменных. Запасным значением может быть и другая переменная:

    CSS
    color: var(--brand, royalblue); /* --brand нет — royalblue */
    color: var(--brand, var(--accent)); /* --brand нет — значение --accent */
    

    Важная тонкость: запасное значение не проверяет, подходит ли подставляемый текст обычному свойству. Если значение переменной не подходит свойству — например, --size: зелёный в font-size, — браузер не вернётся ни к запасному значению, ни к предыдущему объявлению этого свойства. Объявление становится недействительным уже после подстановки, и свойство получает значение по умолчанию: унаследованное, если свойство наследуется, или начальное.

    Запасное значение: попробуйте сами

    Первый абзац коричневый — переменная есть. Второй синий — сработало запасное значение. Третий коричневый — запасное значение взято из другой переменной. В четвёртом --size неподходящая: размер не 2rem и не 1.5rem, а унаследованный от body.

    index.html
    HTML
    <!doctype html>
    <html lang="ru">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Запасное значение и неподходящее значение</title>
        <style>
          body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
          :root {
            --accent: saddlebrown;
            --size: зелёный; /* значение есть, но для font-size не подходит */
          }
          p { margin: 0 0 0.75rem; padding: 0.5rem; background-color: #fffaf3; }
          .known {
            color: var(--accent, royalblue); /* переменная есть — запасное значение не нужно */
          }
          .missing {
            color: var(--brand, royalblue); /* переменной нет — берётся royalblue */
          }
          .nested {
            color: var(--brand, var(--accent)); /* запасное значение — другая переменная */
          }
          .invalid {
            font-size: 2rem;
            font-size: var(--size, 1.5rem); /* переменная есть, запасное значение не сработает */
          }
          .initial {
            --brand: initial;
            color: var(--brand, royalblue);
          }
          .cycle {
            --a: var(--b);
            --b: var(--a);
            color: var(--a, royalblue);
          }
        </style>
      </head>
      <body>
        <p class="known">var(--accent, royalblue) — коричневый: переменная объявлена.</p>
        <p class="missing">var(--brand, royalblue) — синий: переменной --brand нет.</p>
        <p class="nested">var(--brand, var(--accent)) — коричневый из второй переменной.</p>
        <p class="invalid">var(--size, 1.5rem) — обычный размер, а не 2rem и не 1.5rem.</p>
        <p class="initial">--brand: initial — запасной royalblue у незарегистрированного свойства.</p>
        <p class="cycle">--a и --b ссылаются друг на друга — цикл, срабатывает royalblue.</p>
      </body>
    </html>
    

    Переменные в расчётах

    Переменные удобно сочетать с функцией calc() из урока «Единицы измерения». Например, задать шаг шкалы отступов и выводить из него все отступы карточки: тогда, изменив один шаг, вы пропорционально меняете весь компонент.

    calc() с переменными: попробуйте сами

    Отступы, скругление и расстояние между карточками вычисляются от --space. У плотной карточки --space вдвое меньше — и всё в ней вдвое меньше. Попробуйте --space: 1rem.

    index.html
    HTML
    <!doctype html>
    <html lang="ru">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Переменные в calc(): шкала отступов</title>
        <style>
          body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
          :root {
            --space: 0.5rem; /* шаг шкалы отступов */
          }
          .box {
            max-width: 22rem;
            margin-bottom: calc(var(--space) * 2);
            padding: calc(var(--space) * 3);
            border-radius: calc(var(--space) * 2);
            background-color: #fffaf3;
            border: 1px solid tan;
          }
          .compact {
            --space: 0.25rem; /* вдвое плотнее — меняется одно значение */
          }
        </style>
      </head>
      <body>
        <div class="box">Обычная карточка: отступы считаются от --space: 0.5rem.</div>
        <div class="box compact">Плотная карточка: --space: 0.25rem, все отступы и скругление уменьшились вдвое.</div>
      </body>
    </html>
    

    Темы оформления

    Самое известное применение переменных — темы. Правила оформления пишут один раз через переменные цвета, а тёмная тема переопределяет только значения. В уроке «Адаптивная вёрстка. Часть 2» мы встречали медиазапрос prefers-color-scheme: dark: внутри него и переопределяют переменные в :root. В примере для наглядности тема включена классом .dark.

    Тема на переменных: попробуйте сами

    Обе секции оформлены одними правилами. У второй класс .dark переопределил четыре переменные — фон, поверхность, текст и акцент.

    index.html
    HTML
    <!doctype html>
    <html lang="ru">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Тёмная тема на переменных</title>
        <style>
          body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
          :root {
            --bg: linen;
            --surface: #fffaf3;
            --text: #2b211b;
            --accent: saddlebrown;
          }
          .dark {
            /* те же имена, другие значения; в настоящем сайте — в @media (prefers-color-scheme: dark) */
            --bg: #1f1915;
            --surface: #2b231d;
            --text: #f3e9df;
            --accent: #e0a46f;
          }
          .page {
            margin-bottom: 1rem;
            padding: 1rem;
            border-radius: 16px;
            background-color: var(--bg);
            color: var(--text);
          }
          .page h2 { margin: 0 0 0.5rem; color: var(--accent); }
          .page p { margin: 0; padding: 0.5rem; border-radius: 8px; background-color: var(--surface); }
        </style>
      </head>
      <body>
        <section class="page">
          <h2>Светлая тема</h2>
          <p>Правила оформления одни и те же — меняются только значения переменных.</p>
        </section>
        <section class="page dark">
          <h2>Тёмная тема</h2>
          <p>Класс .dark переопределил четыре переменные, и всё оформление перекрасилось.</p>
        </section>
      </body>
    </html>
    

    Регистрация переменной правилом @property позволяет задать ей тип, начальное значение и отключить наследование. Это углублённая тема; попробовать её можно в инструменте CSS-переменных.

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

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

    1. Как правильно объявить пользовательское свойство с цветом акцента?
    Показать ответ

    :root { --accent: saddlebrown; } — Имя начинается с двух дефисов; в :root переменная видна всей странице.

    2. У :root задано --accent: saddlebrown, у .favorite — --accent: seagreen. Какого цвета заголовок с color: var(--accent) внутри .favorite?
    Показать ответ

    Зелёного: ближайшее объявление у предка — у .favorite — Переменные наследуются, и значение у ближайшего предка перекрывает значение корня.

    3. Когда сработает запасное значение в var(--brand, royalblue)?
    Показать ответ

    Когда вычисленное значение --brand недействительно для подстановки, например переменная не объявлена или участвует в цикле — Это также возможно при initial у незарегистрированного свойства. Если обычному свойству не подходит уже подставленный текст, запасное значение var() не используется.

    4. У :root задано --size: зелёный, а у абзаца — font-size: 2rem; font-size: var(--size, 1.5rem);. Какой размер получит абзац?
    Показать ответ

    Унаследованный от родителя — Объявление с var() стало недействительным после подстановки, и свойство получило значение по умолчанию — для font-size это наследование.

    5. Почему не сработает @media (width >= var(--wide)) { … }?
    Показать ответ

    var() работает только в значениях свойств, а не в условиях медиазапросов — Переменные подставляются в значения объявлений; селекторы и условия @media их не принимают.

    6. Какое имя переменной лучше для акцентного цвета сайта?
    Показать ответ

    --accent — Имя по назначению остаётся верным, даже если сам цвет поменяется.

    7. Как сделать отступы карточки вдвое плотнее, если все они записаны как calc(var(--space) * N)?
    Показать ответ

    Задать карточке --space вдвое меньше — Все отступы вычисляются от одного шага и меняются пропорционально.

    Задание

    Ожидаемый результат: сайт выглядит точно так же, как до правки, но saddlebrown записан в файле один раз — в :root. При временной замене на seagreen все акценты на всех четырёх страницах меняются вместе.

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

    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;
    }
    
    /* Шапка прилипает к верхнему краю окна. Фон нужен, чтобы под шапкой не просвечивал текст страницы */
    header {
      position: sticky;
      top: 0;
      z-index: 10; /* выше карточек альбома: они тоже позиционированы и записаны в HTML позже шапки */
      background-color: linen;
    }
    
    /* Липкая шапка должна быть низкой: пункты меню не сжимаются и не переносятся на вторую строку,
       а если им не хватает места, меню прокручивается внутри шапки */
    nav ul {
      flex-wrap: nowrap;
      overflow-x: auto;
    }
    
    nav li {
      flex-shrink: 0;
    }
    
    /* Заголовок, к которому ведёт ссылка-якорь, не прячется под липкой шапкой.
       В rem: при увеличении текста растут и шапка, и отступ */
    [id] {
      scroll-margin-top: 4.5rem;
    }
    
    /* Ссылка, которая открывается в новой вкладке, помечена стрелкой — посетитель знает, что страница не сменится */
    a[target="_blank"]::after {
      content: " ↗";
    }
    
    /* Ссылка на электронную почту — с конвертом */
    a[href^="mailto:"]::before {
      content: "✉ ";
    }
    
    /* Карточка альбома, в которой есть значок, выделена рамкой — без отдельного класса в HTML */
    .gallery li:has(.badge) {
      outline: 3px solid tan;
      outline-offset: 4px;
      border-radius: 8px;
    }
    
    /* Первая ссылка меню — «Главная» — подписана лапкой без изменения HTML */
    nav li:first-child a::before {
      content: "🐾 ";
    }
    
    /* Первый абзац вводного блока крупнее остальных. :first-child здесь не подойдёт — первым стоит заголовок h1 */
    .intro p:first-of-type {
      font-size: 1.2rem;
    }
    
    /* Список игрушек: вместо стандартного кружка — косточка */
    .toy-list li::marker {
      content: "🦴 ";
    }
    
    /* Номера шагов в нумерованных списках — в цвет заголовков и жирные; текст пунктов не меняется */
    ol li::marker {
      color: saddlebrown;
      font-weight: bold;
    }
    
    /* Таблица режима дня: соседние рамки объединяются в одну линию */
    table {
      border-collapse: collapse;
    }
    
    /* Ячейки с воздухом и линией снизу; текст — от левого края и сверху, даже если в соседней ячейке две строки */
    th,
    td {
      padding: 0.6rem 0.8rem;
      border-bottom: 1px solid tan;
      text-align: left;
      vertical-align: top;
    }
    
    /* Шапка таблицы — в цвет заголовков сайта */
    thead th {
      background-color: saddlebrown;
      color: white;
    }
    
    /* Чётные строки тела таблицы — на светлом фоне: глаз не теряет строку */
    tbody tr:nth-child(even) {
      background-color: hsl(30, 80%, 95%);
    }
    
    /* Отдельные рамки без промежутков позволяют скруглить крайние ячейки. Линия только снизу не удваивается. */
    table {
      border-collapse: separate;
      border-spacing: 0;
    }
    thead th:first-child {
      border-top-left-radius: 8px;
    }
    
    thead th:last-child {
      border-top-right-radius: 8px;
    }
    
    /* Форма записи: текстовые поля, список и область текста в едином стиле */
    input:not([type="radio"], [type="checkbox"]),
    select,
    textarea {
      padding: 0.5rem 0.75rem;
      border: 1px solid #8a7660;
      border-radius: 8px;
      background-color: white;
    }
    
    /* Поле «Ведёт прогулку» менять нельзя — фон показывает это */
    input[readonly] {
      background-color: hsl(30, 80%, 95%);
    }
    
    /* Отмеченные флажки и переключатели — в цвет сайта */
    input[type="radio"],
    input[type="checkbox"] {
      accent-color: saddlebrown;
    }
    
    /* Неверно заполненное поле — после того как посетитель с ним поработал */
    input:user-invalid,
    select:user-invalid,
    textarea:user-invalid {
      border-color: firebrick;
    }
    
    /* Группы полей — светлые блоки с тонкой рамкой вместо рельефной рамки браузера */
    fieldset {
      margin-bottom: 1rem;
      border: 1px solid #8a7660;
      border-radius: 16px;
    }
    
    /* Группа полей, в которой сейчас работает посетитель */
    fieldset:focus-within {
      border-color: saddlebrown;
    }
    
    /* Кнопки-«таблетки»: основная залита цветом, второстепенная — только рамка */
    button {
      padding: 0.7rem 1.4rem;
      border: 2px solid saddlebrown;
      border-radius: 999px;
      background-color: saddlebrown;
      color: white;
      font-weight: bold;
      cursor: pointer;
    }
    
    button[type="reset"] {
      background-color: transparent;
      color: saddlebrown;
    }
    
    /* Текст ошибок связан с полями через aria-describedby и не полагается только на цвет рамки. */
    .field-error {
      display: none;
      flex-basis: 100%;
      color: firebrick;
      font-weight: bold;
    }
    input:user-invalid + .field-error {
      display: block;
    }
    
    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 aria-describedby="owner-error">
                <span class="field-error" id="owner-error">Введите ваше имя.</span>
              </p>
              <p>
                <label for="email">Электронная почта</label>
                <input type="email" id="email" name="email" autocomplete="email" placeholder="anna@example.com" required aria-describedby="email-error">
                <span class="field-error" id="email-error">Введите адрес электронной почты, например anna@example.com.</span>
              </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" aria-describedby="dogs-error">
                <span class="field-error" id="dogs-error">Укажите число собак от 1 до 3.</span>
              </p>
            </fieldset>
            <fieldset>
              <legend>Когда гулять</legend>
              <p>
                <label for="date">Дата</label>
                <input type="date" id="date" name="date" required aria-describedby="date-error">
                <span class="field-error" id="date-error">Выберите дату прогулки.</span>
              </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 class="toy-list">
            <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>
    

    Закрепите локальное переопределение

    Вернитесь к примеру с тремя карточками. Добавьте первой карточке класс special и задайте для него --accent: royalblue. Не меняйте правила рамки, заголовка и кнопки: они уже используют var(--accent). Проверка: у первой карточки рамка, заголовок и кнопка стали синими, вторая осталась зелёной, а третья — коричневой. Затем измените --accent в :root на darkviolet: из трёх карточек перекраситься должна только третья, поскольку первые две имеют собственные значения.

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

    Внесите те же изменения в css/style.css своего сайта my-site. Проверка: поиск по файлу находит saddlebrown ровно один раз. На вкладке Styles инструментов разработчика у заголовка видно color: var(--accent), а у :root — объявление переменной. Если результат отличается, нажмите в редакторе «Показать решение» и сравните его со своим кодом.