Постепенное улучшение CSS-компонента
Современная возможность полезна, когда её отсутствие не лишает страницу основной задачи. Это и есть смысл постепенного улучшения: сначала человек может прочитать содержание и перейти к курсу, затем поддерживаемый механизм добавляет удобство. Последний урок продолжения собирает наши решения в явный контракт и отделяет базовое содержимое от конкретного современного оформления.
Полное современное состояние находится в lesson-32/index.html. Оно продолжает предыдущий снимок: шесть курсов, сетка, таблица, учебная форма, обычное пояснение, диалог и подсказка. Для сравнения рядом сохранены compatible.html и styles-compatible.css. Это отдельный плоский вариант, а не второй stylesheet, который нужно подключать поверх слоёв основного файла.
Содержание существует раньше улучшения
Названия, аннотации и реальные адреса записаны в HTML. Если JavaScript отключён, ссылки продолжают вести к оглавлениям, а две обычные секции помощи видны в документе. Кнопки дополнительных механизмов скрыты, пока их действие не установлено. Так основная задача не требует успешного сетевого запроса к API, разбора нового синтаксиса JavaScript или открытия диалога.
Учебная форма остаётся отдельным GET-примером на соседнюю receipt.html. Она показывает параметры адреса и не записывает заявку. Пояснение об этом остаётся рядом с формой, включая совместимый вариант. Постепенное улучшение не должно менять честное описание действия: красивый компонент не превращает локальную демонстрацию в подписку, серверный процесс или сохранённые данные.
У изображения есть исходные width и height, а название не спрятано внутрь декоративной картинки. Если ресурс не загрузился, курс остаётся определимым по обычному тексту. Если режим кадрирования недоступен, полное изображение может занять другую высоту. Основная обязанность сохраняется, хотя внешний вид не становится буквально одинаковым во всех ветках.
Отдельный плоский вариант оформления
Слои и вложенность являются способом записи и организации всего CSS. Если движок не понимает блок @layer, он не обязан применять его содержимое как плоские правила. Поэтому поставить все исходные размеры внутрь такого блока и назвать их универсальным запасным вариантом было бы неверно. Также обычные правила вне слоёв способны перекрывать их нормальные объявления, даже если записаны раньше.
Вместо случайного смешивания сохраним отдельный файл. Для совместимой страницы используйте одну строку подключения ниже вместо href="styles.css". Подключение ui.js остаётся прежним и тоже одно. Основной современный index.html не переписываем: два входа нужны для осмысленного сравнения, а не для автоматического угадывания браузера.
<link rel="stylesheet" href="styles-compatible.css">
Полное содержимое плоского файла приведено ниже. Его базой является знакомое оформление из первой части курса без горизонтального запроса контейнера; новые элементы получают простые самостоятельные правила. Скопируйте этот код в отдельный файл, а не добавляйте его после современного styles.css. Иначе обычные несгруппированные объявления изменят порядок каскада, который мы только что разобрали.
/* lesson-06: initial author styles */
.layout-lab { color: #e4e4e4; background: #1f202a; font-family: system-ui, sans-serif; }
.layout-lab a { color: #46d96f; }
.layout-lab .course-card { background: #252631; }
.layout-lab .course-count { color: #b7b9c9; }
/* lesson-07: class-based specificity */
.layout-lab .course-card { border: 1px solid #3b3c4c; }
.layout-lab .course-card--featured { border-color: #6c43af; }
/* lesson-08: predictable box sizes */
.layout-lab, .layout-lab *, .layout-lab *::before, .layout-lab *::after { box-sizing: border-box; }
.layout-lab { margin: 0; }
.layout-lab .course-card { padding: 1.25rem; border: 1px solid #3b3c4c; border-radius: .5rem; }
.layout-lab .course-card--featured { border-color: #6c43af; }
.layout-lab .course-cover { display: block; max-inline-size: 100%; block-size: auto; }
/* lesson-09: normal flow and local spacing */
.layout-lab { line-height: 1.6; }
.layout-lab .lab-shell { inline-size: min(100% - 2rem, 72rem); margin-inline: auto; }
.layout-lab .lab-header, .layout-lab .lab-footer { padding-block: 1.25rem; }
.layout-lab .lab-main { padding-block: 2rem; }
.layout-lab h1, .layout-lab h2, .layout-lab h3, .layout-lab p { margin-block: 0; }
.layout-lab .course-title { font-size: 1.25rem; line-height: 1.3; }
.layout-lab .course-card > * + * { margin-block-start: .75rem; }
.layout-lab .course-list { list-style: none; padding: 0; margin-block: 1.5rem 0; }
.layout-lab .course-list > li + li { margin-block-start: 1rem; }
.layout-lab .lab-intro > * + *, .layout-lab .lab-request > * + * { margin-block-start: 1rem; }
.layout-lab .lab-request { margin-block-start: 3rem; }
.layout-lab .course-description { overflow-wrap: anywhere; }
/* lesson-10: one-dimensional alignment */
.layout-lab .lab-header-row, .layout-lab .lab-nav, .layout-lab .lab-footer-row { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
.layout-lab .lab-header-row, .layout-lab .lab-footer-row { justify-content: space-between; }
.layout-lab .lab-brand { font-weight: 700; }
.layout-lab .course-meta { display: flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: baseline; }
/* lesson-11: two-dimensional course layout */
.layout-lab .course-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; }
.layout-lab .course-list > li + li { margin-block-start: 0; }
.layout-lab .course-card { block-size: 100%; }
/* lesson-12: page adaptations */
.layout-lab .lab-title { font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem); line-height: 1.15; }
.layout-lab .lab-intro { max-inline-size: 65ch; }
@media (max-width: 42rem) {
.layout-lab .lab-header-row { align-items: start; flex-direction: column; }
.layout-lab .lab-main { padding-block: 1.5rem; }
}
/* lesson-13: inherited component values */
.layout-lab { --lab-bg: #1f202a; --lab-surface: #252631; --lab-ink: #e4e4e4; --lab-muted: #b7b9c9; --lab-link: #46d96f; --lab-accent: #6c43af; --lab-line: #3b3c4c; --lab-gap: 1rem; background: var(--lab-bg); color: var(--lab-ink); }
.layout-lab a { color: var(--lab-link); }
.layout-lab .course-card { background: var(--lab-surface); border-color: var(--lab-line); }
.layout-lab .course-card--featured { border-color: var(--lab-accent); }
.layout-lab .course-count { color: var(--lab-muted); }
.layout-lab .course-list { gap: var(--lab-gap); }
/* lesson-14: native interaction states */
.layout-lab a:hover { text-decoration-thickness: .15em; }
.layout-lab :is(a, button, input, select, summary):focus-visible { outline: 3px solid #83eba0; outline-offset: 3px; }
.layout-lab .lab-request label { display: block; }
.layout-lab .lab-request form > * + * { margin-block-start: 1rem; }
.layout-lab .lab-request :is(input, select, button) { font: inherit; border: 1px solid var(--lab-line); border-radius: .35rem; padding: .6rem .75rem; max-inline-size: 100%; }
.layout-lab .lab-request :is(input, select) { background: var(--lab-surface); color: var(--lab-ink); }
.layout-lab .lab-request button { background: var(--lab-accent); color: #fff; cursor: pointer; }
.layout-lab .lab-request button:active { transform: translateY(1px); }
.layout-lab .lab-details { margin-block-start: 1.5rem; }
.layout-lab .lab-details summary { cursor: pointer; }
.layout-lab .lab-details[open] summary { margin-block-end: .5rem; }
/* lesson-16: page composition */
.layout-lab { min-block-size: 100vh; display: flex; flex-direction: column; }
.layout-lab .lab-main { flex: 1; }
.layout-lab .lab-intro { margin-block-end: 1.5rem; }
.layout-lab .lab-footer { border-block-start: 1px solid var(--lab-line); }
@media print {
.layout-lab { background: #fff; color: #111; display: block; }
.layout-lab .course-card { background: #fff; border-color: #aaa; break-inside: avoid; }
.layout-lab a, .layout-lab .course-count { color: #111; }
.layout-lab .lab-request, .layout-lab .lab-nav { display: none; }
}
/* Deliberately flat baseline: no layers or nesting in this separate variant. */
.layout-lab .course-slot { min-inline-size: 0; }
.layout-lab .course-card { position: relative; padding-block-start: 3rem; }
.layout-lab .course-badge { position: absolute; top: .75rem; right: .75rem; max-width: calc(100% - 1.5rem); background: #6c43af; color: #fff; padding-inline: .5rem; font-size: .875rem; }
.layout-lab .course-media { background: #30323f; }
.layout-lab .course-media .course-cover { inline-size: 100%; block-size: auto; }
.layout-lab .lab-section-nav { display: flex; flex-wrap: wrap; gap: 1rem; padding-block: .75rem; }
.layout-lab .lab-comparison, .layout-lab .lab-advice, .layout-lab .lab-help { margin-block-start: 2rem; }
.layout-lab .lab-table-scroll { overflow-x: auto; padding: .5rem; border: 1px solid #3b3c4c; }
.layout-lab table { inline-size: 100%; min-inline-size: 42rem; border-collapse: collapse; }
.layout-lab th, .layout-lab td { padding: .75rem; text-align: start; border-block-end: 1px solid #3b3c4c; }
.layout-lab [hidden] { display: none !important; }
.layout-lab .lab-dialog, .layout-lab .lab-help { padding: 1rem; background: #252631; color: #e4e4e4; }
.layout-lab .lab-dialog { width: 90%; max-width: 32rem; max-height: 80vh; overflow: auto; }
.layout-lab .lab-help[popover] { position: fixed; inset: 0; margin: auto; width: 90%; max-width: 30rem; max-height: 80vh; overflow: auto; }
.layout-lab :is(.lab-advice, .lab-dialog, .lab-help-controls, .lab-help) button { font: inherit; padding: .5rem .75rem; }
@supports (grid-template-rows: subgrid) {
.layout-lab .course-slot { display: grid; grid-row: span 4; grid-template-rows: subgrid; }
.layout-lab .course-card { display: grid; grid-row: 1 / -1; grid-template-rows: subgrid; gap: .75rem; }
.layout-lab .course-card > * + * { margin-block-start: 0; }
}
@supports (aspect-ratio: 16 / 9) and (object-fit: cover) {
.layout-lab .course-media { aspect-ratio: 16 / 9; position: relative; overflow: hidden; }
.layout-lab .course-media .course-cover { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; object-fit: cover; }
}
@supports (position: sticky) {
.layout-lab .lab-section-nav { position: sticky; top: 0; z-index: 10; background: #1f202a; }
}
@media print {
.layout-lab .lab-section-nav { position: static; }
.layout-lab .lab-help-controls, .layout-lab .lab-advice button { display: none; }
.layout-lab .lab-help[popover], .layout-lab .lab-dialog { display: none; }
.layout-lab .lab-table-scroll { overflow: visible; }
.layout-lab table { min-inline-size: 0; }
.layout-lab .course-count { background: transparent; }
.layout-lab .course-badge { background: transparent; color: #111; border: 1px solid #777; }
}
Этот вариант рассчитан на окружение, которое понимает использованные базовые HTML/CSS-возможности, в частности основную Flex- и Grid-компоновку. Он не является обещанием полной поддержки любого исторического браузера. Если проекту требуется более старое окружение, целевую границу и необходимые запасные размеры нужно определить отдельно. В нашем сравнении устраняется зависимость основной записи от слоёв и вложенности.
Условие относится к конкретному улучшению
Базовая карточка в совместимом файле имеет обычный поток и доступную картинку. Только при распознавании grid-template-rows: subgrid слоты и статьи переходят к четырём совместным рядам. Если условие не выполнено, курсы не исчезают, просто внутренние полосы не согласованы между соседями. Такой контракт ограничен одной обязанностью и легко читается по исходнику.
Аналогично отношение рамки и кадрирование изображения находятся вместе в условном блоке. Без него первая вертикальная картинка может быть выше остальных, зато показывается по естественному формату. Панель навигации сначала является обычным элементом потока, а при поддержке sticky получает локальное закрепление. Базовый путь к форме существует через обычную ссылку в обоих состояниях.
@supports проверяет распознавание декларации, а не проводит аудит всего компонента. Успешное условие не доказывает правильный перенос текста, доступность фокуса и отсутствие ошибки определённой версии движка. Неподдерживаемое условие оставляет предшествующую основу, когда она действительно записана вне улучшения. Обязанность такого запроса описана в MDN о supports.
Проверку CSS не нужно подменять поиском имени браузера в строке пользователя. Название браузера не гарантирует конкретный набор реализаций и может менять вид. В нашем учебном проекте нет такого распознавания. Выбор современного или плоского входа явный; отдельные функции внутри второго улучшаются по конкретным условиям. Это две понятные границы, а не скрытый список случайных исключений.
Поведение устанавливается независимо
ui.js сохранён из тридцатого урока без повторной регистрации обработчиков. Диалог проверяет наличие showModal, подсказка — наличие showPopover. Каждая часть имеет собственные обязательные узлы и включает только связанные с ней кнопки. Отсутствие одного механизма не прекращает настройку другого. Дополнительный общий скрипт для проверки всех CSS-возможностей сейчас не требуется.
Изменение поддержки во время работы не является нашим контрактом. Скрипт принимает решение при первоначальной настройке страницы, а пользовательская тема или ширина окна меняют только компоновку. Если реальный проект подгружает разные реализации или полифиллы позже, ему понадобится отдельный жизненный цикл. Не усложняем статический каталог таким механизмом без задачи, которую он должен выполнять.
Нативный popover остаётся немодальным, а модальный диалог блокирует фоновое взаимодействие. При сравнении нельзя считать их взаимозаменяемыми запасными реализациями одного действия. Основа для обоих — обычное пояснение, а улучшения имеют разные обязанности. Такой выбор сохраняет понятный смысл, вместо автоматической замены неподдерживаемого окна другим внешне похожим компонентом.
Вывод и последующее наблюдение
В основной современный stylesheet добавьте следующий полный блок. Он уточняет печатное состояние новых элементов; простая страница не должна печатать невидимую прокрутку или всплывающий блок как будто это отдельное окно поверх листа. Компактные подписи также получают прозрачный фон, чтобы выбранный тёмный цвет текста не оставался на прежней тёмной поверхности.
@layer components {
@media print {
.layout-lab .lab-help[popover] { display: none; }
.layout-lab .lab-help-controls, .layout-lab .lab-advice button { display: none; }
.layout-lab .lab-section-nav { position: static; }
.layout-lab .lab-table-scroll { overflow: visible; border: 0; }
.layout-lab table { min-inline-size: 0; }
.layout-lab .course-count { background: transparent; }
.layout-lab .course-badge { background: transparent; color: #111; border: 1px solid #777; }
}
}
Для будущего ручного просмотра есть несколько независимых состояний: современный вход со скриптом, тот же вход без скрипта, плоский вход и его состояние без скрипта. В каждом ожидаются шесть реальных ссылок и доступная обычная помощь. В поддерживаемых улучшениях дополнительно проверяются общие ряды, положение панели, кадрирование и открытие дополнительных блоков. Эти ожидания не являются отчётом о выполненных проверках.
Отдельно нужно посмотреть увеличенный текст, несколько ширин, крайние столбцы таблицы, видимый фокус и печать. Такая работа потребуется перед выпуском, но при подготовке продолжения ничего из этого не запускалось. Не называем набор исходников полным аудитом, а наличие ограниченных CSS-функций — подтверждением скорости загрузки. Учебные файлы дают воспроизводимые условия для будущих наблюдений.
Курс теперь показывает и основной каталог, и временные диагностические варианты, и явную более простую запись оформления. Следующего тридцать третьего урока в этой последовательности нет. Новые компоненты стоит добавлять по тому же правилу: назвать основную задачу, сохранить её доступной в базовом документе и лишь затем определить самостоятельное улучшение с понятной границей поддержки.
Полное состояние этого шага находится в layout-lab/lesson-32 внутри исходников продолжения. Каждый снимок открывается отдельно; подключать CSS разных уроков одновременно не нужно.