Files
site-portfolio/public/en/cases/design-system/index.html
T
root daa623fe73 feat(mesh-bg): fade-in шейдера фона + modulepreload графа
- canvas стартует с opacity:0 и плавно проявляется (0.9s) при .is-ready
- initPageMeshShader ставит is-ready после первого кадра
- modulepreload page-mesh-shader.js для параллельной загрузки с main.js
- пересборка страниц (build:pages)
2026-09-24 21:37:40 +02:00

1257 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="en" 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 Portfolio: colors, typography, grid, interface components."
/>
<title>Design System — Portfolio</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="modulepreload" href="/js/modules/ui/page-mesh-shader.js" />
<link rel="stylesheet" href="/css/main.css" />
<link rel="canonical" href="https://mamadaevv.site/en/cases/design-system/" />
<link rel="alternate" hreflang="ru" href="https://mamadaevv.site/cases/design-system/" />
<link rel="alternate" hreflang="en" href="https://mamadaevv.site/en/cases/design-system/" />
<link rel="alternate" hreflang="x-default" href="https://mamadaevv.site/cases/design-system/" />
<meta name="author" content="Nikolai Mamadaev" />
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#0f1115" />
<!-- Open Graph -->
<meta property="og:type" content="article" />
<meta property="og:site_name" content="Nikolai Mamadaev — Product Designer" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="ru_RU" />
<meta property="og:title" content="Design System — Portfolio" />
<meta property="og:description" content="Design System Portfolio: colors, typography, grid, interface components." />
<meta property="og:url" content="https://mamadaevv.site/en/cases/design-system/" />
<meta property="og:image" content="https://mamadaevv.site/assets/og/og-default.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Design System — Portfolio" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Design System — Portfolio" />
<meta name="twitter:description" content="Design System Portfolio: colors, typography, grid, interface components." />
<meta name="twitter:image" content="https://mamadaevv.site/assets/og/og-default.png" />
<meta name="twitter:image:alt" content="Design System — Portfolio" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"@id": "https://mamadaevv.site/#person",
"name": "Nikolai Mamadaev",
"url": "https://mamadaevv.site",
"jobTitle": "Product Designer",
"description": "Product designer with 10+ years of experience. Designing digital products, interfaces and design systems.",
"knowsAbout": [
"UX/UI design",
"product design",
"design systems",
"prototyping",
"Figma",
"no-code"
],
"sameAs": [
"https://t.me/mamadaevv",
"https://vk.com/mamadaevv",
"https://github.com/mamadaevv"
]
},
{
"@type": "WebSite",
"@id": "https://mamadaevv.site/#website",
"url": "https://mamadaevv.site/",
"name": "Nikolai Mamadaev — Product Designer",
"inLanguage": "en",
"author": {
"@id": "https://mamadaevv.site/#person"
},
"publisher": {
"@id": "https://mamadaevv.site/#person"
}
},
{
"@type": "CreativeWork",
"@id": "https://mamadaevv.site/en/cases/design-system/#page",
"url": "https://mamadaevv.site/en/cases/design-system/",
"name": "Design System — Portfolio",
"description": "Design System Portfolio: colors, typography, grid, interface components.",
"isPartOf": {
"@id": "https://mamadaevv.site/#website"
},
"author": {
"@id": "https://mamadaevv.site/#person"
},
"inLanguage": "en"
},
{
"@type": "BreadcrumbList",
"@id": "https://mamadaevv.site/en/cases/design-system/#breadcrumbs",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Nikolai Mamadaev — Product Designer",
"item": "https://mamadaevv.site/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Cases",
"item": "https://mamadaevv.site/cases/"
},
{
"@type": "ListItem",
"position": 3,
"name": "Design System — Portfolio",
"item": "https://mamadaevv.site/cases/design-system/"
}
]
}
]
}
</script>
<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 .mp-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>
<svg class="liquid-glass-defs" aria-hidden="true" width="0" height="0" focusable="false" style="position:absolute;width:0;height:0;overflow:hidden;pointer-events:none">
<filter id="liquid-glass-displacement" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
<feImage href="../../../assets/liquid-glass/displacement.png" xlink:href="../../../assets/liquid-glass/displacement.png" preserveAspectRatio="none" result="lgMap"/>
<feDisplacementMap in="SourceGraphic" in2="lgMap" scale="14" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<filter id="liquid-glass-displacement-btn" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
<feImage href="../../../assets/liquid-glass/displacement.png" xlink:href="../../../assets/liquid-glass/displacement.png" preserveAspectRatio="none" result="lgMapBtn"/>
<feDisplacementMap in="SourceGraphic" in2="lgMapBtn" scale="6" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<a class="skip-link" href="#main">Skip to main content</a>
<header class="header" data-theme-picker>
<div class="header__inner">
<a class="logo" href="../../" aria-label="Home">NM</a>
<div class="header__cluster">
<nav class="nav" aria-label="Page sections">
<a href="../../">Home</a>
</nav>
</div>
</div>
</header>
<div class="fab-stack" aria-label="Quick Actions">
<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="Style Settings">
<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="Theme and Design" 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">Accent Color</h3>
<div class="colorpicker" data-colorpicker>
<div class="colorpicker__plane-box">
<canvas class="colorpicker__plane" width="320" height="180" data-sl-canvas aria-label="Saturation and brightness of the accent"></canvas>
<div class="colorpicker__knob" data-sl-knob></div>
</div>
<div class="colorpicker__hue-wrap">
<label class="colorpicker__hue-sr" for="theme-hue-slider">Accent shade</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">Theme</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="As in the system"><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="Light-colored theme"><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="Dark Theme"><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">Corner Rounding</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="Slight rounding"><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="Average rounding"><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="Significant rounding"><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">Typography</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">Page Background</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="Show background gradient"><span class="theme-mesh-bg-seg__label">On</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="Hide the background gradient"><span class="theme-mesh-bg-seg__label">Off</span></button>
</div>
</section>
</div>
</div>
</div>
<a class="fab fab--primary" href="../../#contact" aria-label="Contact Us">
<span class="fab-primary__ico" aria-hidden="true"></span>
</a>
</div>
<div class="content-edit-fab-stack" aria-label="Content Edits" 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="Content Editing Mode"
title="Edit Mode"
>
<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">Documentation · UI</p>
<h1 class="case-page__h1">Design System</h1>
<p class="case-page__sub">Tokens, typography, grid, components, and longgrid blocks</p>
<p class="section__prose">
Switch between themes, accents, and fonts using the floating panel on the right. When making global style changes (tokens,
components, longgrid blocks), be sure to update this page as well—it’s the project’s live UI catalog.
</p>
<nav class="sg-toc" aria-label="Table of Contents: Design System">
<ol>
<li><a href="#colors">Colors</a></li>
<li><a href="#fonts">Fonts</a></li>
<li><a href="#layout">Grid and Responsive Design</a></li>
<li><a href="#components">Components</a></li>
<li><a href="#fab">FAB Buttons</a></li>
<li><a href="#blocks">Blocks</a></li>
<li><a href="#activity">Activity</a></li>
</ol>
</nav>
<h2 id="colors">Colors</h2>
<p class="section__prose">
The accent and “cool/warm” tones of the entire UI are defined by three numbers in the <strong>HSL</strong> model (Hue,
Saturation, Lightness) at <code>&lt;html&gt;</code>. From these,
<code>--accent</code> and derived tokens; neutral backgrounds are slightly tinted with the same hue
(<code>--neutral-accent-tint</code>). Light/dark theme — <code>data-theme</code>, mesh —
<code>data-mesh-bg</code>. Can be changed via the FAB on the right.
</p>
<h3>What is <code>--theme-h</code>, <code>--theme-s</code>, <code>--theme-l</code></h3>
<ul class="section__prose">
<li>
<code>--theme-h</code> — <strong>hue (Hue)</strong>, position on the color wheel 0–360°. A hue shift
recolors buttons, links, the mesh gradient, and square accents (<code>--accent-square-2</code> …
<code>4</code>), without having to manually adjust each hex value.
</li>
<li>
<code>--theme-s</code> — <strong>Saturation</strong>: how “pure” the color is, rather than gray.
</li>
<li>
<code>--theme-l</code> — Accent <strong>lightness</strong>: darker or lighter than the primary brand color
(usually limited by the panel’s range to ensure buttons remain readable).
</li>
</ul>
<p class="section__prose">
In CSS, it looks like this:
<code>--accent: hsl(var(--theme-h) calc(var(--theme-s) * 1%) calc(var(--theme-l) * 1%))</code>.
Values are written in <code>localStorage</code> (<code>siteThemeV1</code>) and are inserted by an inline script into
<code>head</code>to prevent a “foreign” color from flashing during loading.
</p>
<div class="sg-swatch-grid" aria-label="Token Palette">
<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">
Additionally: quadratic mesh shades — <code>--accent-square-2</code> … <code>4</code>; they depend on hue.
</p>
<h2 id="fonts">Fonts</h2>
<p class="section__prose">
A pair of fonts — <code>data-font</code> on <code>&lt;html&gt;</code> (FAB “Typography”): sets
<code>--font-accent</code> (headings) and <code>--font</code> (body text). Scale in tables: class, example, purpose.
</p>
<h3>Font families</h3>
<div class="sg-table-wrap">
<table class="sg-table">
<thead>
<tr>
<th scope="col"><code>data-font</code></th>
<th scope="col">Stack</th>
<th scope="col">Where</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>merriweather</code></td>
<td>Merriweather + Alice</td>
<td>Default pair; Alice—body text, Merriweather—emphasized headings</td>
</tr>
<tr>
<td><code>mono</code></td>
<td>Source Code Pro + PT Mono</td>
<td>Monospace pair</td>
</tr>
<tr>
<td><code>geologica</code></td>
<td>Geologica + PT Sans</td>
<td>Sans-serif + neutral body text</td>
</tr>
</tbody>
</table>
</div>
<h3>Typographic scale</h3>
<div class="sg-table-wrap">
<table class="sg-table sg-table--type" aria-label="Typographic scale">
<thead>
<tr>
<th scope="col">Class</th>
<th scope="col">Example</th>
<th scope="col">Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>.hero__eyebrow</code></td>
<td class="sg-type-sample"><p class="hero__eyebrow">Section caption</p></td>
<td>Hero, case input</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 main heading
</p>
</td>
<td>H1 for the main page; reduced on DS for the column</td>
</tr>
<tr>
<td><code>.section__title</code></td>
<td class="sg-type-sample"><h2 class="section__title">Section</h2></td>
<td>“Skills,” “Portfolio,” contacts; main font, no caps</td>
</tr>
<tr>
<td><code>.case-page__h1</code></td>
<td class="sg-type-sample"><p class="case-page__h1">Page title</p></td>
<td>H1 for case studies / Design System</td>
</tr>
<tr>
<td><code>.case-page__sub</code></td>
<td class="sg-type-sample"><p class="case-page__sub">Lead text under the heading</p></td>
<td>Subheading below H1</td>
</tr>
<tr>
<td><code>.section__prose</code></td>
<td class="sg-type-sample"><p class="section__prose">Main paragraph text.</p></td>
<td>Prose, lists, and links in the article</td>
</tr>
<tr>
<td><code>h2</code> in <code>.case-article</code></td>
<td class="sg-type-sample"><h2 style="margin: 0">H2 subsection</h2></td>
<td>Long-form sections (such as “Colors”)</td>
</tr>
<tr>
<td><code>h3</code> in <code>.case-article</code></td>
<td class="sg-type-sample"><h3 style="margin: 0">H3 subsection</h3></td>
<td>Subsections within H2; margins in <code>main.css</code>: 3.25rem top, 1.5rem bottom, after <code>figure</code> 4rem, <code>h3+h3</code> 3.5rem</td>
</tr>
</tbody>
</table>
</div>
<h3>Rhythm and Code</h3>
<p class="section__prose">Rhythm example:</p>
<ul class="section__prose">
<li>bulleted list;</li>
<li><strong>emphasis</strong> and <a href="#fonts">link</a>;</li>
</ul>
<ol class="section__prose">
<li>numbered item;</li>
</ol>
<p class="section__prose">Inline <code>code</code> and block:</p>
<pre><code>--font: "Alice", system-ui, serif;
--font-accent: "Merriweather", var(--font);</code></pre>
<p class="section__prose sg-label">
Headings use <code>clamp()</code> — scale relative to the viewport; see the “Grid” section for more details.
</p>
<h2 id="layout">Grid and Responsive</h2>
<p class="section__prose">
There is no separate 12-column grid: a <strong>full-bleed</strong> layout with side margins
<code>--space</code> and local CSS Grid within sections. The content column for DS and case studies —
<code>.case-article</code> (max-width 48rem).
</p>
<h3>Basic framework</h3>
<ul class="section__prose">
<li><code>--space: clamp(1rem, 4vw, 2.75rem)</code> — horizontal margins + <code>safe-area-inset</code>.</li>
<li><code>--max: none</code> — no fixed 1200px “box”; width = viewport minus padding.</li>
<li><code>.section</code> — vertical spacing via <code>clamp()</code>; <code>.section__inner</code> centers the block.</li>
<li><code>.section--tight</code> — More compact vertically (case studies, this page).</li>
</ul>
<h3>Breakpoints</h3>
<div class="sg-table-wrap">
<table class="sg-table">
<thead>
<tr>
<th scope="col">From</th>
<th scope="col">What Changes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>860px</code></td>
<td><code>.hero__grid</code> → 2 columns <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 cards</td>
</tr>
<tr>
<td><code>720px</code></td>
<td><code>.skills-category__grid</code> → 2 columns</td>
</tr>
<tr>
<td><code>1024px</code></td>
<td>Skills: 6-column mosaic (<code>grid-column: span …</code>)</td>
</tr>
<tr>
<td><code>560px</code> / <code>1024px</code></td>
<td><code>.contact-grid</code> → 2 → 4 columns</td>
</tr>
<tr>
<td><code>48rem</code> / <code>28rem</code></td>
<td>Case gallery: ≤2 col → 1 col</td>
</tr>
<tr>
<td><code>640px</code></td>
<td><code>case-media-gallery--wide</code> collapses to the width of a column</td>
</tr>
</tbody>
</table>
</div>
<h3>Hero: one → two columns</h3>
<p class="section__prose">Layout <code>.hero__grid</code> (compressed to 860px):</p>
<div class="sg-layout-demo" aria-hidden="true">
<div class="sg-layout-demo__hero">
<div class="sg-layout-demo__block">Text</div>
<div class="sg-layout-demo__block">Visual</div>
</div>
</div>
<h3>Contacts: 1 → 2 → 4</h3>
<p class="section__prose">Demo <code>.contact-grid</code> (placeholders, no transitions):</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>
<span class="contact-card contact-card--link contact-card--brand-tg"><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>
<span class="contact-card contact-card--link contact-card--brand-vk"><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>
<span class="contact-card contact-card--link contact-card--brand-ya"><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>
<span class="contact-card contact-card--action contact-card--brand-email"><span class="contact-card__body"><span class="contact-card__name">Grid</span>
on<span class="contact-card__hint"> the homepage</span>
</span></span></li>
</ul>
<p class="section__prose">
Skills: mosaic layout on wide screens—see
<a href="../../#skills">the homepage, #skills</a>. Portfolio: carousel on
<code>100vw</code> with <code>--projects-card-width</code> and JS scrolling — on the homepage
<a href="../../#projects">#projects</a>.
</p>
<h3>Wide in case studies</h3>
<p class="section__prose">
<code>.case-media--wide</code>, <code>.case-media-gallery--wide</code> and <code>.case-callout</code> wider
article columns: <code>min(100vw − 2×padding, content + 8rem)</code>, with margins <code>--space</code>. On a narrow screen (up to 640px), &quot;wide&quot; collapses to the width of the column. Images with <code>data-image-viewer</code> — the outline and hover effects
are the same as <code>.case-video-shell</code> (live examples — case study element demo). The caption below the video is
<code>figcaption</code> centered in the column.
</p>
<h3>Typography and Motion</h3>
<p class="section__prose">
Fluid type: <code>clamp()</code> on <code>.hero__title</code>, <code>.section__title</code>,
<code>.case-page__h1</code>. Rounded corners (<code>data-radius</code>) do not change the grid. For long screenshots and
animations, the <code>prefers-reduced-motion: reduce</code>.
</p>
<h2 id="components">Components</h2>
<p class="section__prose">Live examples; the shell (header, FAB, footer, cookie) is already integrated on this page.</p>
<h3>Buttons</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 buttons</h3>
<p class="section__prose">
Floating buttons in the lower-right corner (<code>.fab-stack</code>). Icon-only and with a text label.
</p>
<div class="sg-stack sg-stack--row">
<a class="fab fab--primary" href="#components" aria-label="FAB Demo"><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">Download as PDF</span></button>
<button type="button" class="fab fab--secondary" aria-label="Secondary FAB Demo"><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 (with text) · .fab-primary__ico · .fab__label</p>
<h3>Tags</h3>
<ul class="hero__tags" aria-label="Tag Demo">
<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="mp-card__tags" aria-label="Tag Demo" style="display: flex; flex-wrap: wrap; gap: 0.35rem 0.45rem; list-style: none; padding: 0; margin: 0.5rem 0">
<li class="mp-card__tag">mp-card__tag</li>
<li class="mp-card__tag">Web App</li>
</ul>
<p class="sg-label">.mp-card__tags / .mp-card__tag</p>
<h3>Portfolio Card</h3>
<div class="sg-portfolio-wrap">
<a class="mp-card mp-card--link mp-card--stack-bottom" href="#components" tabindex="-1" aria-hidden="true">
<div class="mp-card__media" aria-hidden="true">
<img
class="mp-card__image"
src="../../../assets/shared/placeholder.webp"
alt=""
width="400"
height="225"
loading="lazy"
decoding="async"
/>
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">mp-card — portfolio</h3>
<ul class="mp-card__tags" aria-label="Tags">
<li class="mp-card__tag">Demo</li>
<li class="mp-card__tag">UI</li>
</ul>
<p class="mp-card__desc">A brief two- or three-line description of the project for the Design System demo.</p>
</div>
</a>
</div>
<p class="sg-label">.mp-card .mp-card--stack-bottom — text on top, cover at the bottom</p>
<script>
(function () {
function hashStr(s) {
var h = 0;
for (var i = 0; i < s.length; i++) { h = (h * 31 + s.charCodeAt(i)) >>> 0; }
return h;
}
function colorTags(root) {
var tags = root.querySelectorAll('.mp-card--stack-bottom .mp-card__tag');
for (var i = 0; i < tags.length; i++) {
var t = tags[i];
var key = (t.textContent || '').trim().toLowerCase();
if (!key) continue;
var hue = hashStr(key) % 360;
t.style.setProperty('--tag-hue', String(hue));
}
}
colorTags(document);
})();
</script>
<h3>Placeholder while images are loading</h3>
<p class="section__prose">
While <code>img</code> hasn&#x27;t loaded yet, <code>initImageLoadPlaceholder</code> it
<code>.is-img-loading</code> on the image and <code>.has-img-loading</code> the wrapper. The background layer is
<code>::before</code>, a single glow from left to right— <code>::after</code> (<code>img-ph-shine-sweep</code>).
Icons, SVGs, and small images up to 40px are skipped;
disable selectively: <code>data-no-img-placeholder</code>. Below is a fixed demo (on the website, the classes are removed
after <code>load</code>).
</p>
<div
class="case-media case-media--content has-img-loading img-ph-demo"
role="img"
aria-label="Demo: Placeholder when loading an image"
></div>
<p class="sg-label">.has-img-loading · .is-img-loading · .img-ph-demo (placeholder demo)</p>
<h3>TUI Braille spinner</h3>
<p class="section__prose">
Loading large images in the viewer (<code>.case-image-viewer</code>) is indicated by a monospaced Braille spinner—just like in TUI. Pure CSS, no JS: 10 frames cycle through an animation. Reusable class: <code>steps()</code>an animation. Reusable class <code>.tui-spinner</code>
(color — <code>--accent</code>, font—system 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="Loading" 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 · frames: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ · prefers-reduced-motion displays ⠋</p>
<h3>Skill card</h3>
<article class="skill-task-card">
<p class="skill-task-card__task">skill-task-card__task — the task description provided by the business.</p>
<div class="skill-task-card__solutions">
<p class="skill-task-card__solution">skill-task-card__solution — the first approach to the solution.</p>
<p class="skill-task-card__solution">The second item in the solutions section.</p>
</div>
</article>
<p class="sg-label">.skill-task-card__task · __solutions · __solution</p>
<h3>Callout</h3>
<p class="section__prose">
Longread accent block: wider than the column (like wide media), text <code>.case-callout__content</code> slightly
larger than the main paragraph.
</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 — an important idea or conclusion. On the left, only colored emojis (📌 💡 ✨ ✅), not text symbols ✎ ✓ ◆; <code>case-callout__emoji--pulse</code> for a subtle animation.</p>
</div>
</aside>
<p class="sg-label">.case-callout · .case-callout__emoji · .case-callout__emoji--pulse · .case-callout__content</p>
<h3>Contact Cards</h3>
<p class="section__prose sg-label" style="margin-top: 0">
On the home page, there are four cards in a grid (1→2→4 columns). Here are two examples—the grid is forced into two columns.
</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></a>
<a class="contact-card contact-card--link contact-card--brand-tg" href="https://t.me/mamadaevv" rel="noopener noreferrer"><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></a>
<a class="contact-card contact-card--link contact-card--brand-tg" href="https://t.me/mamadaevv" rel="noopener noreferrer"><span class="contact-card__body"><span class="contact-card__name">contact-card--link</span>
<span class="contact-card__hint">External link</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="Copy the address"
aria-label="Demo: Copy 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>Copy address</span>
</span></button>
</li>
</ul>
<p class="sg-label">.contact-card--link · --action · --brand-*</p>
<h3>Section with a background</h3>
<p class="section__prose">
On the homepage, the strip <code>.section--surface</code> is visually presented using mesh; in other cases, the page background is
solid—see the compact demo below with <code>background: var(--surface)</code>.
</p>
<section class="section section--surface section--tight sg-section-demo" aria-label="Section Demos">
<div class="section__inner">
<h2 class="section__title">section__title</h2>
<p class="section__prose">Snippet: .section.section--surface + .section__inner</p>
</div>
</section>
<p class="sg-label">.section · .section--surface · .section--tight · .section__inner</p>
<h3>Separators</h3>
<p class="section__prose">
Horizontal separators: a line or simply white space (indentation). Modifiers <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">Blocks</h2>
<div class="sg-blocks-card">
<p>
Longgrid case layout: media and galleries, link cards, videos, callouts, wide blocks, and other partials.
A live example with all variations is available on a separate page; use it to copy sections when building a new case.
</p>
<p>
<a class="btn btn--secondary btn--flat" href="../template-elements-demo/">Case element demo
</a></p>
</div>
<h2 id="activity">Activity</h2>
<div class="sg-blocks-card">
<p>
On the homepage in the section <code>#mini-projects</code> card <code>mp-card</code> section on the homepage in a masonry grid. Types, cells, and the live
layout are on a separate page.
</p>
<p>
<a class="btn btn--secondary btn--flat" href="../mini-projects-showcase/">Activity Card Catalog
</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">Website design and development — Nikolay Mamadaev</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 policy</a></span> ·<span class="footer__chunk footer__chunk--lang"><span class="footer__sep" aria-hidden="true">
</span><nav class="lang-switch" aria-label="Choose language" data-lang-switch>
<a class="lang-switch__btn" data-lang="ru" href="/">Русский</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 l = links[i].getAttribute("data-lang");
links[i].setAttribute("href", (l === "en" ? "/en/" : "/") + base);
}
})();
</script>
</span></div>
</footer>
<aside class="cookie-notice glass-neutral" hidden data-cookie-notice>
<p class="cookie-notice__text">
This site uses cookies to ensure a smooth user experience. For more information, see the
<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>Accept</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>