Files
site-portfolio/public/cases/design-system/index.html
T

1147 lines
56 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta
name="description"
content="Design System портфолио: цвета, типографика, сетка, компоненты интерфейса."
/>
<title>Design System — портфолио</title>
<script>
(function () {
var FONT_CSS = {
merriweather: "/assets/fonts/merriweather.css",
geologica: "/assets/fonts/geologica.css",
mono: "/assets/fonts/mono.css",
};
try {
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
var d = document.documentElement;
var m = t.mode;
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
var eff =
m === "system"
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: m;
d.dataset.theme = eff;
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
if (h !== null) d.style.setProperty("--theme-h", String(h));
if (s !== null) d.style.setProperty("--theme-s", String(s));
if (l !== null) {
var L = Number(l);
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
}
var hVal = h !== null ? h : 210;
var sVal = s !== null ? s : 90;
var lVal = l !== null ? Math.min(70, Math.max(20, Number(l))) : 40;
var H = (((hVal % 360) + 360) % 360) / 360;
var S = Math.min(100, Math.max(0, sVal)) / 100;
var Lv = Math.min(100, Math.max(0, lVal)) / 100;
var r, g, b;
if (S === 0) { r = g = b = Lv; }
else {
var q = Lv < 0.5 ? Lv * (1 + S) : Lv + S - Lv * S;
var p = 2 * Lv - q;
var hue2rgb = function(p, q, t) {
if (t < 0) t += 1; if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
r = hue2rgb(p, q, H + 1/3);
g = hue2rgb(p, q, H);
b = hue2rgb(p, q, H - 1/3);
}
var lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
d.style.setProperty("--on-accent", lum > 0.58 ? "hsl(220 14% 12%)" : "hsl(0 0% 98%)");
var rad = t.radius;
if (rad === "soft" || rad === "medium" || rad === "strong") {
d.dataset.radius = rad;
} else {
d.dataset.radius = "medium";
}
var f = t.font;
if (f === "onest") f = "geologica";
if (f === "anonymous") f = "mono";
if (f !== "merriweather" && f !== "mono" && f !== "geologica") f = "merriweather";
d.dataset.font = f;
var mb = t.meshBg;
if (mb === "off") {
d.dataset.meshBg = "off";
} else {
d.dataset.meshBg = "on";
}
var href = FONT_CSS[f];
if (href && !document.querySelector('link[data-theme-font="' + f + '"]')) {
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
link.setAttribute("data-theme-font", f);
document.head.appendChild(link);
}
} catch (e) {}
})();
</script>
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png" />
<link rel="manifest" href="/assets/favicon/site.webmanifest" />
<link rel="preload" href="/assets/fonts/merriweather.css" as="style" />
<link rel="preload" href="/css/main.css" as="style" />
<link rel="preload" href="/js/skill-connectors.js" as="script" />
<link rel="modulepreload" href="/js/main.js" />
<link rel="stylesheet" href="/css/main.css" />
<script src="/js/skill-connectors.js" defer></script>
<script type="module" src="/js/main.js"></script>
<style>
#main {
padding-bottom: clamp(3.5rem, 8vw, 5.5rem);
}
.case-article.ds-page {
max-width: 52rem;
margin: 0 auto;
}
.case-article.ds-page .case-page__h1 {
margin-bottom: 0.65rem;
}
.case-article.ds-page .case-page__sub {
margin-bottom: clamp(1.75rem, 4vw, 2.5rem);
}
.case-article.ds-page .section__prose {
margin-bottom: 1.15rem;
line-height: 1.65;
color: var(--text);
}
.case-article.ds-page > h2 {
margin: 0 0 1.15rem;
padding-top: clamp(2.75rem, 6vw, 4rem);
font-family: var(--font);
font-size: clamp(1.7rem, 3vw, 2rem);
font-weight: 700;
color: var(--text);
scroll-margin-top: 1.5rem;
border-top: 1px solid color-mix(in srgb, var(--line) 90%, transparent);
}
.case-article.ds-page > h2#colors {
padding-top: clamp(1.5rem, 4vw, 2.25rem);
border-top: none;
}
.case-article.ds-page > h3 {
margin: clamp(2rem, 4vw, 2.75rem) 0 0.85rem;
font-family: var(--font);
font-size: 1.55rem;
font-weight: 600;
color: var(--text);
}
.case-article.ds-page > h2 + .section__prose,
.case-article.ds-page > h2 + h3 {
margin-top: 0;
}
.case-article.ds-page > h2 + h3 {
margin-top: 0.25rem;
}
.case-article.ds-page #components h3:first-of-type {
margin-top: 1rem;
}
.case-article.ds-page a:not(.btn):not(.fab) {
color: var(--accent);
}
.sg-toc {
margin: clamp(1.5rem, 3vw, 2rem) 0 clamp(2.25rem, 5vw, 3.25rem);
padding: 1.15rem 1.35rem;
border-radius: var(--radius-md);
border: 1px solid var(--line);
background: color-mix(in srgb, var(--surface) 72%, transparent);
}
.sg-toc ol {
margin: 0;
padding-left: 1.25rem;
color: var(--text-muted);
}
.sg-toc li + li {
margin-top: 0.35rem;
}
.sg-toc a {
color: var(--accent-text);
text-decoration: none;
}
.sg-toc a:hover,
.sg-toc a:focus-visible {
text-decoration: underline;
}
.sg-blocks-card {
margin: 0 0 2.5rem;
padding: 1.15rem 1.35rem;
border-radius: var(--radius-md);
border: 1px solid var(--line);
background: color-mix(in srgb, var(--surface) 72%, transparent);
}
.sg-blocks-card p {
margin: 0 0 0.85rem;
line-height: 1.65;
color: var(--text);
}
.sg-blocks-card p:last-child {
margin-bottom: 0;
}
#blocks,
#mini-projects {
scroll-margin-top: calc(var(--header-offset, 4.25rem) + 0.75rem);
}
.sg-swatch-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
gap: 0.75rem;
margin: 1rem 0 1.75rem;
}
.sg-swatch {
border-radius: var(--radius-sm);
border: 1px solid var(--line);
overflow: hidden;
}
.sg-swatch__chip {
height: 3.25rem;
}
.sg-swatch__name {
display: block;
padding: 0.35rem 0.45rem;
font-size: 0.68rem;
line-height: 1.25;
font-family: var(--font);
color: var(--text-muted);
word-break: break-all;
}
.sg-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
margin: 1rem 0 2rem;
}
.sg-stack--row {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.sg-label {
margin: 0.35rem 0 0;
font-size: 0.78rem;
color: var(--text-muted);
font-family: var(--font);
}
.case-article.ds-page .sg-label {
display: block;
margin-bottom: 1.75rem;
}
.sg-table-wrap {
overflow-x: auto;
margin: 1rem 0 2rem;
}
.sg-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.sg-table th,
.sg-table td {
padding: 0.65rem 0.85rem;
border: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
.sg-table th {
background: color-mix(in srgb, var(--surface) 80%, transparent);
color: var(--text);
font-weight: 600;
}
.sg-table td {
color: var(--text-muted);
}
.sg-table code {
font-size: 0.82em;
}
.sg-table--type .sg-type-sample {
color: var(--text);
min-width: 11rem;
}
.sg-table--type .sg-type-sample > * {
margin: 0;
}
.sg-table--type td:first-child {
white-space: nowrap;
vertical-align: top;
}
.sg-table--type tbody td:last-child {
font-size: 0.88rem;
}
.sg-layout-demo {
margin: 1rem 0 2rem;
padding: 1.1rem;
border-radius: var(--radius-md);
border: 1px dashed var(--line);
background: color-mix(in srgb, var(--bg-elevated) 55%, transparent);
}
.sg-layout-demo__hero {
display: grid;
grid-template-columns: 1fr;
gap: 0.65rem;
}
@media (min-width: 860px) {
.sg-layout-demo__hero {
grid-template-columns: 1.15fr 0.85fr;
}
}
.sg-layout-demo__block {
min-height: 2.75rem;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--accent) 18%, var(--surface));
border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
color: var(--text-muted);
}
.sg-contact-demo {
list-style: none;
margin: 0;
padding: 0;
}
.sg-contact-demo .contact-card {
pointer-events: none;
cursor: default;
}
/* В компонентах только 2 карточки — без 4-колоночной сетки главной */
.sg-contact-components.contact-grid {
grid-template-columns: 1fr;
}
@media (min-width: 560px) {
.sg-contact-components.contact-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.sg-portfolio-wrap {
max-width: 100%;
}
.sg-portfolio-wrap .portfolio-card {
pointer-events: none;
}
.sg-section-demo.section {
margin: 0.75rem calc(-1 * max(var(--space), env(safe-area-inset-left, 0px))) 2rem;
padding: 1.35rem max(var(--space), env(safe-area-inset-right, 0px)) 1.35rem
max(var(--space), env(safe-area-inset-left, 0px));
border-radius: var(--radius-md);
background: var(--surface);
border: 1px solid color-mix(in srgb, var(--line) 85%, transparent);
}
.sg-section-demo .section__inner {
max-width: none;
margin: 0;
}
.sg-section-demo .section__title {
margin: 0 0 0.65rem;
padding: 0;
border: none;
font-size: clamp(1.02rem, 1.65vw, 1.28rem);
}
.sg-section-demo .section__prose {
margin: 0;
color: var(--text-muted);
}
.case-article.ds-page pre {
margin: 0.75rem 0 2rem;
}
.case-article.ds-page ul.section__prose,
.case-article.ds-page ol.section__prose {
margin-bottom: 1.5rem;
}
.case-article.ds-page .skill-task-card,
.case-article.ds-page .sg-portfolio-wrap {
margin-bottom: 0.5rem;
}
.case-article.ds-page .contact-grid {
margin-bottom: 0.25rem;
}
</style>
<script>
(function () {
try {
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
var d = document.documentElement;
var m = t.mode;
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
var eff =
m === "system"
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: m;
d.dataset.theme = eff;
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
if (h !== null) d.style.setProperty("--theme-h", String(h));
if (s !== null) d.style.setProperty("--theme-s", String(s));
if (l !== null) {
var L = Number(l);
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
}
var rad = t.radius;
if (rad === "soft" || rad === "medium" || rad === "strong") {
d.dataset.radius = rad;
} else {
d.dataset.radius = "medium";
}
var f = t.font;
if (f === "onest") f = "geologica";
if (f === "anonymous") f = "mono";
if (f === "merriweather" || f === "mono" || f === "geologica") {
d.dataset.font = f;
} else {
d.dataset.font = "merriweather";
}
var mb = t.meshBg;
if (mb === "off") {
d.dataset.meshBg = "off";
} else {
d.dataset.meshBg = "on";
}
} catch (e) {}
})();
</script>
</head>
<body>
<a class="skip-link" href="#main">К основному содержимому</a>
<header class="header" data-theme-picker>
<div class="header__inner">
<a class="logo" href="../../" aria-label="На главную">НМ</a>
<div class="header__cluster">
<nav class="nav" aria-label="Разделы страницы">
<a href="../../">На главную</a>
</nav>
</div>
</div>
</header>
<div class="fab-stack" aria-label="Быстрые действия">
<div class="theme-panel-fab-wrap">
<button type="button" class="fab fab--secondary glass-neutral" data-theme-panel-btn aria-expanded="false" aria-controls="theme-panel" aria-label="Настройки стиля">
<span class="theme-panel-fab__icon theme-panel-fab__icon--open-hide" aria-hidden="true"></span>
<svg class="theme-panel-fab__icon theme-panel-fab__icon--close" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
<div id="theme-panel" class="theme-picker__panel glass-neutral" role="dialog" aria-label="Тема и оформление" hidden data-theme-panel>
<div class="theme-picker__stack">
<section class="theme-picker__section" aria-labelledby="theme-accent-title">
<h3 id="theme-accent-title" class="theme-picker__section-title">Акцентный цвет</h3>
<div class="colorpicker" data-colorpicker>
<div class="colorpicker__plane-box">
<canvas class="colorpicker__plane" width="320" height="180" data-sl-canvas aria-label="Насыщенность и светлость акцента"></canvas>
<div class="colorpicker__knob" data-sl-knob></div>
</div>
<div class="colorpicker__hue-wrap">
<label class="colorpicker__hue-sr" for="theme-hue-slider">Оттенок акцента</label>
<input id="theme-hue-slider" class="colorpicker__hue" type="range" min="0" max="360" value="210" data-hue-slider aria-valuemin="0" aria-valuemax="360" />
</div>
</div>
</section>
<section class="theme-picker__section" aria-labelledby="theme-mode-title">
<h3 id="theme-mode-title" class="theme-picker__section-title">Тема</h3>
<div class="radius-seg theme-mode-seg" role="radiogroup" aria-labelledby="theme-mode-title">
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="system" aria-label="Как в системе"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--system"></span></span></button>
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="light" aria-label="Светлая тема"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--light"></span></span></button>
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="dark" aria-label="Тёмная тема"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--dark"></span></span></button>
</div>
</section>
<section class="theme-picker__section" aria-labelledby="theme-radius-title">
<h3 id="theme-radius-title" class="theme-picker__section-title">Скругление углов</h3>
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-radius-title">
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="soft" aria-label="Слабое скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--soft"></span></span></button>
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="medium" aria-label="Среднее скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--medium"></span></span></button>
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="strong" aria-label="Сильное скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--strong"></span></span></button>
</div>
</section>
<section class="theme-picker__section" aria-labelledby="theme-typography-title">
<h3 id="theme-typography-title" class="theme-picker__section-title">Типографика</h3>
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-typography-title">
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="geologica" aria-label="Sans"><span class="theme-font-seg__label">Sans</span></button>
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="merriweather" aria-label="Sans Serif"><span class="theme-font-seg__label">Sans Serif</span></button>
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="mono" aria-label="Mono"><span class="theme-font-seg__label">Mono</span></button>
</div>
</section>
<section class="theme-picker__section" aria-labelledby="theme-mesh-bg-title">
<h3 id="theme-mesh-bg-title" class="theme-picker__section-title">Фон страницы</h3>
<div class="radius-seg radius-seg--two" role="radiogroup" aria-labelledby="theme-mesh-bg-title">
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="on" aria-label="Показать фоновый градиент"><span class="theme-mesh-bg-seg__label">Вкл</span></button>
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="off" aria-label="Скрыть фоновый градиент"><span class="theme-mesh-bg-seg__label">Выкл</span></button>
</div>
</section>
</div>
</div>
</div>
<a class="fab fab--primary" href="../../#contact" aria-label="Связаться">
<span class="fab-primary__ico" aria-hidden="true"></span>
</a>
</div>
<div class="content-edit-fab-stack" aria-label="Правки контента" data-content-edit-ui>
<button
type="button"
class="fab fab--secondary glass-neutral content-edit-fab"
data-content-edit-mode-toggle
hidden
aria-pressed="false"
aria-label="Режим правок контента"
title="Режим правок"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 20h4l10.5-10.5a2.12 2.12 0 0 0 0-3L16.5 4.5a2.12 2.12 0 0 0-3 0L3 15v5z"
stroke="currentColor"
stroke-width="1.75"
stroke-linejoin="round"
/>
</svg>
<span class="content-edit-fab__badge" hidden aria-hidden="true">0</span>
</button>
</div>
<main id="main" class="section section--tight">
<div class="section__inner case-article ds-page">
<p class="hero__eyebrow">Документация · UI</p>
<h1 class="case-page__h1">Design System</h1>
<p class="case-page__sub">Токены, типографика, сетка, компоненты и блоки лонгрида кейса</p>
<p class="section__prose">
Переключайте тему, акцент и шрифт через плавающую панель справа. При глобальных правках стилей (токены,
компоненты, блоки лонгрида) обновляйте и эту страницу — живой каталог UI проекта.
</p>
<nav class="sg-toc" aria-label="Оглавление Design System">
<ol>
<li><a href="#colors">Цвета</a></li>
<li><a href="#fonts">Шрифты</a></li>
<li><a href="#layout">Сетка и адаптив</a></li>
<li><a href="#components">Компоненты</a></li>
<li><a href="#fab">FAB-кнопки</a></li>
<li><a href="#blocks">Блоки</a></li>
<li><a href="#mini-projects">Активность</a></li>
</ol>
</nav>
<h2 id="colors">Цвета</h2>
<p class="section__prose">
Акцент и «холод/тепло» всего UI задаются тремя числами в модели <strong>HSL</strong> (Hue — оттенок,
Saturation — насыщенность, Lightness — светлота) на <code>&lt;html&gt;</code>. Из них собирается
<code>--accent</code> и производные токены; нейтральные фоны слегка подмешивают тот же оттенок
(<code>--neutral-accent-tint</code>). Светлая/тёмная тема — <code>data-theme</code>, mesh —
<code>data-mesh-bg</code>. Меняется в FAB справа.
</p>
<h3>Что такое <code>--theme-h</code>, <code>--theme-s</code>, <code>--theme-l</code></h3>
<ul class="section__prose">
<li>
<code>--theme-h</code> — <strong>оттенок (Hue)</strong>, позиция на цветовом круге 0–360°. Сдвиг hue
перекрашивает кнопки, ссылки, mesh-градиент и квадратные акценты (<code>--accent-square-2</code> …
<code>4</code>), не трогая вручную каждый hex.
</li>
<li>
<code>--theme-s</code> — <strong>насыщенность</strong>: насколько «чистый» цвет, а не серый.
</li>
<li>
<code>--theme-l</code> — <strong>светлота</strong> акцента: темнее или ярче основной фирменный тон
(в панели обычно ограничен диапазоном, чтобы кнопки оставались читаемыми).
</li>
</ul>
<p class="section__prose">
В CSS это выглядит так:
<code>--accent: hsl(var(--theme-h) calc(var(--theme-s) * 1%) calc(var(--theme-l) * 1%))</code>.
Значения пишутся в <code>localStorage</code> (<code>siteThemeV1</code>) и подставляются inline-скриптом в
<code>head</code>, чтобы не было вспышки «чужого» цвета при загрузке.
</p>
<div class="sg-swatch-grid" aria-label="Палитра токенов">
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--bg)"></div>
<span class="sg-swatch__name">--bg</span>
</div>
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--bg-elevated)"></div>
<span class="sg-swatch__name">--bg-elevated</span>
</div>
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--surface)"></div>
<span class="sg-swatch__name">--surface</span>
</div>
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--text)"></div>
<span class="sg-swatch__name">--text</span>
</div>
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--text-muted)"></div>
<span class="sg-swatch__name">--text-muted</span>
</div>
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--line)"></div>
<span class="sg-swatch__name">--line</span>
</div>
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--accent)"></div>
<span class="sg-swatch__name">--accent</span>
</div>
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--accent-text)"></div>
<span class="sg-swatch__name">--accent-text</span>
</div>
<div class="sg-swatch">
<div class="sg-swatch__chip" style="background: var(--accent-soft)"></div>
<span class="sg-swatch__name">--accent-soft</span>
</div>
</div>
<p class="section__prose sg-label">
Дополнительно: квадратичные оттенки mesh — <code>--accent-square-2</code> … <code>4</code>; они зависят от hue.
</p>
<h2 id="fonts">Шрифты</h2>
<p class="section__prose">
Пара шрифтов — <code>data-font</code> на <code>&lt;html&gt;</code> (FAB «Типографика»): задаёт
<code>--font-accent</code> (заголовки) и <code>--font</code> (проза). Шкала в таблицах: класс, пример, назначение.
</p>
<h3>Семейства</h3>
<div class="sg-table-wrap">
<table class="sg-table">
<thead>
<tr>
<th scope="col"><code>data-font</code></th>
<th scope="col">Стек</th>
<th scope="col">Где</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>merriweather</code></td>
<td>Merriweather + Alice</td>
<td>Пара по умолчанию; Alice — основной текст, Merriweather — акцентные заголовки</td>
</tr>
<tr>
<td><code>mono</code></td>
<td>Source Code Pro + PT Mono</td>
<td>Моноширинная пара</td>
</tr>
<tr>
<td><code>geologica</code></td>
<td>Geologica + PT Sans</td>
<td>Гротеск + нейтральная проза</td>
</tr>
</tbody>
</table>
</div>
<h3>Типографическая шкала</h3>
<div class="sg-table-wrap">
<table class="sg-table sg-table--type" aria-label="Типографическая шкала">
<thead>
<tr>
<th scope="col">Класс</th>
<th scope="col">Пример</th>
<th scope="col">Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>.hero__eyebrow</code></td>
<td class="sg-type-sample"><p class="hero__eyebrow">Подпись секции</p></td>
<td>Hero, ввод кейса</td>
</tr>
<tr>
<td><code>.hero__title</code></td>
<td class="sg-type-sample">
<p class="hero__title" style="font-size: clamp(1.5rem, 3.5vw, 2.15rem); line-height: 1.08">
Display заголовок
</p>
</td>
<td>H1 главной; на DS уменьшен для колонки</td>
</tr>
<tr>
<td><code>.section__title</code></td>
<td class="sg-type-sample"><h2 class="section__title">Секция</h2></td>
<td>«Навыки», «Портфолио», контакты; основной шрифт, без капса</td>
</tr>
<tr>
<td><code>.case-page__h1</code></td>
<td class="sg-type-sample"><p class="case-page__h1">Заголовок страницы</p></td>
<td>H1 кейса / Design System</td>
</tr>
<tr>
<td><code>.case-page__sub</code></td>
<td class="sg-type-sample"><p class="case-page__sub">Лид под заголовком</p></td>
<td>Подзаголовок под H1</td>
</tr>
<tr>
<td><code>.section__prose</code></td>
<td class="sg-type-sample"><p class="section__prose">Основной текст абзаца.</p></td>
<td>Проза, списки, ссылки в статье</td>
</tr>
<tr>
<td><code>h2</code> в <code>.case-article</code></td>
<td class="sg-type-sample"><h2 style="margin: 0">Подсекция H2</h2></td>
<td>Разделы лонгрида (как «Цвета»)</td>
</tr>
<tr>
<td><code>h3</code> в <code>.case-article</code></td>
<td class="sg-type-sample"><h3 style="margin: 0">Подсекция H3</h3></td>
<td>Подразделы внутри H2; отступы в <code>main.css</code>: сверху 3.25rem, снизу 1.5rem, после <code>figure</code> 4rem, <code>h3+h3</code> 3.5rem</td>
</tr>
</tbody>
</table>
</div>
<h3>Ритм и код</h3>
<p class="section__prose">Пример ритма:</p>
<ul class="section__prose">
<li>маркированный список;</li>
<li><strong>акцент</strong> и <a href="#fonts">ссылка</a>;</li>
</ul>
<ol class="section__prose">
<li>нумерованный пункт;</li>
</ol>
<p class="section__prose">Инлайн <code>code</code> и блок:</p>
<pre><code>--font: "Alice", system-ui, serif;
--font-accent: "Merriweather", var(--font);</code></pre>
<p class="section__prose sg-label">
Заголовки используют <code>clamp()</code> — масштаб от вьюпорта; подробнее в разделе «Сетка».
</p>
<h2 id="layout">Сетка и адаптив</h2>
<p class="section__prose">
Отдельной 12-колоночной сетки нет: <strong>full-bleed</strong> layout с боковыми полями
<code>--space</code> и локальными CSS Grid внутри секций. Контентная колонка DS и кейсов —
<code>.case-article</code> (max-width 48rem).
</p>
<h3>Базовый каркас</h3>
<ul class="section__prose">
<li><code>--space: clamp(1rem, 4vw, 2.75rem)</code> — горизонтальные поля + <code>safe-area-inset</code>.</li>
<li><code>--max: none</code> — без фиксированной «коробки» 1200px; ширина = вьюпорт минус padding.</li>
<li><code>.section</code> — вертикальные отступы через <code>clamp()</code>; <code>.section__inner</code> центрирует блок.</li>
<li><code>.section--tight</code> — компактнее по вертикали (кейсы, эта страница).</li>
</ul>
<h3>Брейкпоинты</h3>
<div class="sg-table-wrap">
<table class="sg-table">
<thead>
<tr>
<th scope="col">От</th>
<th scope="col">Что меняется</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>860px</code></td>
<td><code>.hero__grid</code> → 2 колонки <code>1.15fr 0.85fr</code></td>
</tr>
<tr>
<td><code>760px</code> / <code>1180px</code></td>
<td><code>.projects-carousel</code>: <code>--projects-visible</code> 1 → 2 → 3 карточки</td>
</tr>
<tr>
<td><code>720px</code></td>
<td><code>.skills-category__grid</code> → 2 колонки</td>
</tr>
<tr>
<td><code>1024px</code></td>
<td>Навыки: 6-колоночная мозаика (<code>grid-column: span …</code>)</td>
</tr>
<tr>
<td><code>560px</code> / <code>1024px</code></td>
<td><code>.contact-grid</code> → 2 → 4 колонки</td>
</tr>
<tr>
<td><code>48rem</code> / <code>28rem</code></td>
<td>Галерея кейса: ≤2 col → 1 col</td>
</tr>
<tr>
<td><code>640px</code></td>
<td><code>case-media-gallery--wide</code> схлопывается в ширину колонки</td>
</tr>
</tbody>
</table>
</div>
<h3>Hero: одна → две колонки</h3>
<p class="section__prose">Схема <code>.hero__grid</code> (сжато до 860px):</p>
<div class="sg-layout-demo" aria-hidden="true">
<div class="sg-layout-demo__hero">
<div class="sg-layout-demo__block">Текст</div>
<div class="sg-layout-demo__block">Визуал</div>
</div>
</div>
<h3>Контакты: 1 → 2 → 4</h3>
<p class="section__prose">Демо <code>.contact-grid</code> (плейсхолдеры, без переходов):</p>
<ul class="contact-grid sg-contact-demo" aria-hidden="true">
<li>
<span class="contact-card contact-card--link contact-card--brand-tg">
<span class="contact-card__corner" aria-hidden="true"></span>
<span class="contact-card__icon" aria-hidden="true"></span>
<span class="contact-card__body">
<span class="contact-card__name">1 col</span>
<span class="contact-card__hint">&lt; 560px</span>
</span>
</span>
</li>
<li>
<span class="contact-card contact-card--link contact-card--brand-vk">
<span class="contact-card__corner" aria-hidden="true"></span>
<span class="contact-card__icon" aria-hidden="true"></span>
<span class="contact-card__body">
<span class="contact-card__name">2 col</span>
<span class="contact-card__hint">≥ 560px</span>
</span>
</span>
</li>
<li>
<span class="contact-card contact-card--link contact-card--brand-ya">
<span class="contact-card__corner" aria-hidden="true"></span>
<span class="contact-card__icon" aria-hidden="true"></span>
<span class="contact-card__body">
<span class="contact-card__name">4 col</span>
<span class="contact-card__hint">≥ 1024px</span>
</span>
</span>
</li>
<li>
<span class="contact-card contact-card--action contact-card--brand-email">
<span class="contact-card__corner contact-card__corner--copy" aria-hidden="true"></span>
<span class="contact-card__icon contact-card__icon--accent" aria-hidden="true"></span>
<span class="contact-card__body">
<span class="contact-card__name">Сетка</span>
<span class="contact-card__hint">на главной</span>
</span>
</span>
</li>
</ul>
<p class="section__prose">
Навыки: мозаика на широком экране — см.
<a href="../../#skills">главную, #skills</a>. Портфолио: карусель на
<code>100vw</code> с <code>--projects-card-width</code> и JS-прокруткой — на главной
<a href="../../#projects">#projects</a>.
</p>
<h3>Wide в кейсах</h3>
<p class="section__prose">
<code>.case-media--wide</code>, <code>.case-media-gallery--wide</code> и <code>.case-callout</code> шире
колонки статьи: <code>min(100vw − 2×padding, content + 8rem)</code>, с полями <code>--space</code>. На узком
экране (до 640px) wide схлопывается в ширину колонки. Кадры с <code>data-image-viewer</code> — обводка и hover
как у <code>.case-video-shell</code> (живые примеры — демо элементов кейса). Подпись под видео —
<code>figcaption</code> по центру колонки.
</p>
<h3>Типографика и motion</h3>
<p class="section__prose">
Fluid type: <code>clamp()</code> на <code>.hero__title</code>, <code>.section__title</code>,
<code>.case-page__h1</code>. Скругления (<code>data-radius</code>) сетку не меняют. Для длинных скринов и
анимаций учитывается <code>prefers-reduced-motion: reduce</code>.
</p>
<h2 id="components">Компоненты</h2>
<p class="section__prose">Живые примеры; оболочка (шапка, FAB, футер, cookie) уже подключена на этой странице.</p>
<h3>Кнопки</h3>
<div class="sg-stack">
<a class="btn btn--primary btn--flat" href="#components"><span class="btn__icon" aria-hidden="true"></span><span>Primary flat</span></a>
<a class="btn btn--primary btn--raised" href="#components"><span>Primary raised</span></a>
<a class="btn btn--secondary btn--flat" href="#components"><span>Secondary flat</span></a>
<a class="btn btn--secondary btn--raised" href="#components"><span>Secondary raised</span></a>
<button type="button" class="btn btn--ghost btn--flat">Ghost flat</button>
</div>
<p class="sg-label">.btn .btn--primary | secondary | ghost · .btn--flat | raised · .btn__icon</p>
<h3>FAB-кнопки</h3>
<p class="section__prose">
Плавающие кнопки в правом нижнем углу (<code>.fab-stack</code>). Иконка-only и с текстовой подписью.
</p>
<div class="sg-stack sg-stack--row">
<a class="fab fab--primary" href="#components" aria-label="Демо FAB"><span class="fab-primary__ico" aria-hidden="true"></span></a>
<button type="button" class="fab fab--primary fab--label"><span class="fab-primary__ico" aria-hidden="true"></span><span class="fab__label">Скачать в PDF</span></button>
<button type="button" class="fab fab--secondary" aria-label="Демо secondary FAB"><span class="theme-panel-fab__icon theme-panel-fab__icon--open-hide" aria-hidden="true"></span></button>
</div>
<p class="sg-label">.fab .fab--primary | secondary · .fab--label (с текстом) · .fab-primary__ico · .fab__label</p>
<h3>Теги</h3>
<ul class="hero__tags" aria-label="Демо тегов">
<li class="hero__tag">hero__tag</li>
<li class="hero__tag">UX/UI</li>
</ul>
<p class="sg-label">.hero__tags / .hero__tag</p>
<ul class="portfolio-card__tags" style="display: flex; flex-wrap: wrap; gap: 0.35rem; list-style: none; padding: 0; margin: 0.5rem 0">
<li class="portfolio-card__tag">portfolio-card__tag</li>
<li class="portfolio-card__tag">Web App</li>
</ul>
<p class="sg-label">.portfolio-card__tags / .portfolio-card__tag</p>
<h3>Карточка портфолио</h3>
<div class="sg-portfolio-wrap">
<a class="portfolio-card" href="#components" tabindex="-1" aria-hidden="true">
<div class="portfolio-card__body">
<h3 class="portfolio-card__title">portfolio-card</h3>
<p class="portfolio-card__summary">Краткое описание проекта в две-три строки для демо Design System.</p>
<ul class="portfolio-card__tags" aria-label="Теги">
<li class="portfolio-card__tag">Демо</li>
<li class="portfolio-card__tag">UI</li>
</ul>
</div>
<div class="portfolio-card__cover" aria-hidden="true">
<img
class="portfolio-card__image"
src="../../assets/shared/placeholder.webp"
alt=""
width="400"
height="225"
decoding="async"
/>
</div>
</a>
</div>
<p class="sg-label">.portfolio-card и дочерние BEM-классы</p>
<h3>Карточка портфолио — reflect (mp-card)</h3>
<div class="sg-portfolio-wrap">
<a class="mp-card mp-card--link mp-card--reflect" href="#components" tabindex="-1" aria-hidden="true" style="--card-cover:url('../../assets/shared/placeholder.webp')">
<div class="mp-card__media" aria-hidden="true">
<img
class="mp-card__image"
src="../../assets/shared/placeholder.webp"
alt=""
width="400"
height="225"
decoding="async"
/>
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">mp-card--reflect</h3>
<ul class="mp-card__tags" aria-label="Теги">
<li class="mp-card__tag">Демо</li>
<li class="mp-card__tag">UI</li>
</ul>
<p class="mp-card__desc">Картинка на всю карточку, зеркальное отражение снизу, прогрессивное размытие и тонировка снизу вверх; текст в нижней части.</p>
</div>
</a>
</div>
<p class="sg-label">.mp-card--reflect — полноразмерная картинка, отражение и размытие снизу</p>
<h3>Заглушка при загрузке изображений</h3>
<p class="section__prose">
Пока <code>img</code> ещё не загрузился, <code>initImageLoadPlaceholder</code> вешает
<code>.is-img-loading</code> на картинку и <code>.has-img-loading</code> на обёртку. Слой фона —
<code>::before</code>, один блеск слева направо — <code>::after</code> (<code>img-ph-shine-sweep</code>).
Иконки, SVG и мелкие картинки до 40px пропускаются;
отключить точечно: <code>data-no-img-placeholder</code>. Ниже — зафиксированное демо (на сайте классы снимаются
после <code>load</code>).
</p>
<div
class="case-media case-media--content has-img-loading img-ph-demo"
role="img"
aria-label="Демо: заглушка при загрузке изображения"
></div>
<p class="sg-label">.has-img-loading · .is-img-loading · .img-ph-demo (демо заглушки)</p>
<h3>TUI braille-спиннер</h3>
<p class="section__prose">
Загрузка тяжёлых изображений в просмотрщике (<code>.case-image-viewer</code>) показывается
моноширинным braille-спиннером — как в TUI. Чистый CSS, без JS: 10 кадров переключаются
через <code>steps()</code>-анимацию. Переиспользуемый класс <code>.tui-spinner</code>
(цвет — <code>--accent</code>, шрифт — системный mono).
</p>
<div class="sg-stack sg-stack--row sg-stack--center" style="gap: 1.5rem; padding: 1.25rem; background: color-mix(in srgb, var(--surface) 70%, transparent); border: 1px solid var(--line); border-radius: var(--radius-card, 14px);">
<span class="tui-spinner" aria-label="Загрузка" role="status"><span>⠋</span><span>⠙</span><span>⠹</span><span>⠸</span><span>⠼</span><span>⠴</span><span>⠦</span><span>⠧</span><span>⠇</span><span>⠏</span></span>
<code class="tui-spinner-seq" style="font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; color: var(--text-muted); font-size: 0.95rem;">⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏</code>
</div>
<p class="sg-label">.tui-spinner · кадры: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ · prefers-reduced-motion показывает ⠋</p>
<h3>Карточка навыка</h3>
<article class="skill-task-card">
<p class="skill-task-card__task">skill-task-card__task — формулировка задачи от бизнеса.</p>
<div class="skill-task-card__solutions">
<p class="skill-task-card__solution">skill-task-card__solution — первый подход к решению.</p>
<p class="skill-task-card__solution">Второй пункт в зоне решений.</p>
</div>
</article>
<p class="sg-label">.skill-task-card__task · __solutions · __solution</p>
<h3>Callout</h3>
<p class="section__prose">
Акцентный блок лонгрида: шире колонки (как wide-медиа), текст в <code>.case-callout__content</code> чуть
крупнее основного абзаца.
</p>
<aside class="case-callout" data-callout>
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
<div class="case-callout__content">
<p>case-callout — важная мысль или вывод. Слева только цветные emoji (📌 💡 ✨ ✅), не текстовые символы ✎ ✓ ◆; <code>case-callout__emoji--pulse</code> для лёгкой анимации.</p>
</div>
</aside>
<p class="sg-label">.case-callout · .case-callout__emoji · .case-callout__emoji--pulse · .case-callout__content</p>
<h3>Контактные карточки</h3>
<p class="section__prose sg-label" style="margin-top: 0">
На главной в сетке четыре карточки (1→2→4 колонки). Здесь два примера — сетка принудительно в две колонки.
</p>
<ul class="contact-grid sg-contact-components">
<li>
<a class="contact-card contact-card--link contact-card--brand-tg" href="https://t.me/mamadaevv" rel="noopener noreferrer">
<span class="contact-card__corner" aria-hidden="true"></span>
<span class="contact-card__icon" aria-hidden="true">
<img src="../../assets/shared/icons/tg.svg" alt="" width="24" height="24" loading="eager" decoding="async" />
</span>
<span class="contact-card__body">
<span class="contact-card__name">contact-card--link</span>
<span class="contact-card__hint">Внешняя ссылка</span>
</span>
</a>
</li>
<li>
<button
type="button"
class="contact-card contact-card--action contact-card--brand-email"
data-contact-copy
data-email-b64="bmlrQG1hbWFkYWV2di5ydQ=="
data-hint-default="Скопировать адрес"
aria-label="Демо: скопировать email"
>
<span class="contact-card__corner contact-card__corner--copy" aria-hidden="true"></span>
<span class="contact-card__icon contact-card__icon--accent" aria-hidden="true"></span>
<span class="contact-card__body">
<span class="contact-card__name">contact-card--action</span>
<span class="contact-card__hint" data-contact-copy-hint>Скопировать адрес</span>
</span>
</button>
</li>
</ul>
<p class="sg-label">.contact-card--link · --action · --brand-*</p>
<h3>Секция с фоном</h3>
<p class="section__prose">
На главной полоса <code>.section--surface</code> визуально читается через mesh; в кейсах фон страницы
ровный — ниже компактное демо с <code>background: var(--surface)</code>.
</p>
<section class="section section--surface section--tight sg-section-demo" aria-label="Демо секции">
<div class="section__inner">
<h2 class="section__title">section__title</h2>
<p class="section__prose">Фрагмент .section.section--surface + .section__inner</p>
</div>
</section>
<p class="sg-label">.section · .section--surface · .section--tight · .section__inner</p>
<h3>Разделители</h3>
<p class="section__prose">
Горизонтальные разделители: линия или просто воздух (отступ). Модификаторы <code>--line</code>, <code>--air</code>, <code>--air-lg</code>.
</p>
<div class="sg-stack">
<hr class="sep sep--line">
<hr class="sep sep--air">
<hr class="sep sep--air-lg">
</div>
<p class="sg-label">.sep · .sep--line · .sep--air · .sep--air-lg</p>
<h2 id="blocks">Блоки</h2>
<div class="sg-blocks-card">
<p>
Разметка лонгрида кейса: медиа и галереи, карточки ссылок, видео, callout, wide-блоки и прочие partials.
Живой эталон со всеми вариантами — на отдельной странице; оттуда копируют куски при сборке нового кейса.
</p>
<p>
<a class="btn btn--secondary btn--flat" href="../template-elements-demo/">Демо элементов кейса</a>
</p>
</div>
<h2 id="mini-projects">Активность</h2>
<div class="sg-blocks-card">
<p>
На главной в секции <code>#mini-projects</code> карточки <code>mp-card</code> в masonry-сетке. Типы, клетки и живая
раскладка вынесены на отдельную страницу.
</p>
<p>
<a class="btn btn--secondary btn--flat" href="../mini-projects-showcase/">Каталог карточек активности</a>
</p>
</div>
</div>
</main>
<footer class="footer">
<div class="footer__inner">
<span class="footer__chunk footer__chunk--date">© 2015–2026<span class="footer__sep" aria-hidden="true"> · </span></span>
<span class="footer__chunk footer__chunk--main">Дизайн и разработка сайта — Николай Мамадаев</span>
<span class="footer__chunk footer__chunk--meta footer__attr"><span class="footer__sep" aria-hidden="true"> · </span><a href="../../cases/design-system/">Design System</a><span class="footer__sep" aria-hidden="true"> · </span><a href="../../cookie-policy/">Политика cookie</a></span>
<span class="footer__chunk footer__chunk--lang"><span class="footer__sep" aria-hidden="true"> · </span><nav class="lang-switch" aria-label="Выбор языка" data-lang-switch>
<a class="lang-switch__btn" data-lang="ru" href="/">RU</a>
<a class="lang-switch__btn" data-lang="en" href="/en/">EN</a>
</nav>
<script>
(function () {
var p = location.pathname.replace(/^\/+/, "");
var isEn = p.indexOf("en/") === 0;
var base = isEn ? p.slice(3) : p;
var links = document.querySelectorAll("[data-lang-switch] [data-lang]");
for (var i = 0; i < links.length; i++) {
var b = links[i];
var l = b.getAttribute("data-lang");
b.setAttribute("href", (l === "en" ? "/en/" : "/") + base);
if ((l === "en") === isEn) b.setAttribute("aria-current", "true");
}
})();
</script>
</span>
</div>
</footer>
<aside class="cookie-notice glass-neutral" hidden data-cookie-notice>
<p class="cookie-notice__text">
Сайт использует cookie для комфортной работы. Подробнее — в
<a href="../../cookie-policy/" data-cookie-policy-link>cookie policy</a>.
</p>
<button type="button" class="btn btn--secondary btn--flat cookie-notice__accept" data-cookie-accept>
<span>Принять</span>
<span class="cookie-notice__accept-icon" aria-hidden="true">
<img src="../../assets/shared/icons/сookie.svg" alt="" width="20" height="20" loading="eager" decoding="async" />
</span>
</button>
</aside>
<div id="nav-more-menu-floating" class="nav-more__floating" hidden data-nav-more-menu-floating aria-hidden="true"></div>
</body>
</html>