Каталог, карточка товара, корзина, оформление заказа и блог на WooCommerce. Стили разложены по слоям: токены из Figma, база, компоненты, страницы, брейкпоинты. Цвета, типографика, радиусы и отступы взяты из Variables и текстовых стилей макета, ниже по слоям хардкода нет. Иконки — Phosphor, вставляются инлайном как SVG, символами ничего не рисуется. Стили WooCommerce отключены, вёрстка своя, !important не используется.
259 lines
5.8 KiB
CSS
259 lines
5.8 KiB
CSS
/*
|
||
* Drawn separators, interaction states, the mobile filter sheet and checkout validation
|
||
*/
|
||
|
||
/* ------------------------------------------------- glyph-free separators --- */
|
||
|
||
/*
|
||
* These were literal "•", "–" and "×" characters in the markup. They are
|
||
* separators, not content, so they are drawn instead of typed — which also
|
||
* keeps them out of the accessibility tree and out of copied text.
|
||
*/
|
||
.meta-separator {
|
||
display: inline-block;
|
||
width: 4px;
|
||
height: 4px;
|
||
margin: 0 10px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
opacity: 0.4;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.price-fields-separator {
|
||
width: 8px;
|
||
height: 1px;
|
||
flex: 0 0 8px;
|
||
background: var(--text-caption);
|
||
}
|
||
|
||
.multiply {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
margin: 0 2px;
|
||
background: currentColor;
|
||
vertical-align: baseline;
|
||
-webkit-mask: var(--icon-x) center / contain no-repeat;
|
||
mask: var(--icon-x) center / contain no-repeat;
|
||
}
|
||
|
||
/* ------------------------------------------------------------ interaction --- */
|
||
|
||
/*
|
||
* Hover feedback the design specifies through its own state tokens: list rows
|
||
* pick up bg-select-unit-hover, muted tabs and links resolve toward
|
||
* text-main / text-link-hover.
|
||
*/
|
||
.filter-option {
|
||
padding: 0 8px;
|
||
margin-inline: -8px;
|
||
border-radius: var(--br-control-m);
|
||
transition: background-color var(--motion-fast);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.filter-option:hover {
|
||
background: var(--bg-select-unit-hover);
|
||
}
|
||
|
||
.tab-list a {
|
||
transition: color var(--motion-fast);
|
||
}
|
||
|
||
.tab-list a:hover {
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.main-nav a {
|
||
transition: color var(--motion-fast);
|
||
}
|
||
|
||
.main-nav a:hover {
|
||
color: var(--text-body);
|
||
}
|
||
|
||
.breadcrumbs a {
|
||
transition: color var(--motion-fast);
|
||
}
|
||
|
||
.breadcrumbs a:hover {
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.product-card-title,
|
||
.cart-item-title a {
|
||
transition: color var(--motion-fast);
|
||
}
|
||
|
||
.product-card:hover .product-card-title {
|
||
color: var(--text-body);
|
||
}
|
||
|
||
.cart-item-remove {
|
||
transition:
|
||
background-color var(--motion-fast),
|
||
color var(--motion-fast);
|
||
}
|
||
|
||
.cart-item-remove:hover {
|
||
color: var(--notice-error);
|
||
}
|
||
|
||
/* ------------------------------------------------- mobile filter sheet --- */
|
||
|
||
/*
|
||
* The design keeps filters in a sidebar on desktop but moves them into a
|
||
* sheet on mobile, opened by a "Фильтр" button next to the sort control.
|
||
* The sheet reuses the same form, so filtering behaves identically.
|
||
*/
|
||
.filters-open,
|
||
.filters-sheet-head,
|
||
.filters-sheet-foot {
|
||
display: none;
|
||
}
|
||
|
||
@media (max-width: 767px) {
|
||
.filters-open {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: var(--pad-h-btn-m) var(--pad-w-text-m) var(--pad-h-btn-m) var(--pad-w-icon-m);
|
||
border: 0;
|
||
border-radius: var(--br-btn-l);
|
||
color: var(--text-main);
|
||
background: var(--bg-button-light);
|
||
font: var(--control-m);
|
||
transition: background-color var(--motion-fast);
|
||
}
|
||
|
||
.filters-open:hover {
|
||
background: var(--bg-button-light-hover);
|
||
}
|
||
|
||
.filters-open:active {
|
||
background: var(--bg-button-light-pressed);
|
||
}
|
||
|
||
.filters {
|
||
position: fixed;
|
||
z-index: 60;
|
||
inset: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* The desktop rule sets align-self: start. On an absolutely positioned
|
||
box that means "size to content" instead of stretching between top and
|
||
bottom, which would make the sheet taller than the viewport. */
|
||
align-self: stretch;
|
||
overflow: hidden;
|
||
border-radius: var(--br-modal) var(--br-modal) 0 0;
|
||
background: var(--bg-main);
|
||
/* Kept in the DOM so the form state survives; moved out of reach until
|
||
the sheet is opened. */
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
transform: translateY(2%);
|
||
transition:
|
||
opacity var(--motion-slow-duration) var(--motion-slow),
|
||
transform var(--motion-slow-duration) var(--motion-slow),
|
||
visibility 0s linear var(--motion-slow-duration);
|
||
}
|
||
|
||
.filters.is-open {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
transform: none;
|
||
transition-delay: 0s;
|
||
}
|
||
|
||
.filters-sheet-head {
|
||
display: flex;
|
||
padding: 16px var(--container-gutter);
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
border-bottom: 1px solid var(--border-main);
|
||
}
|
||
|
||
.filters-sheet-title {
|
||
margin: 0;
|
||
font: var(--h4);
|
||
letter-spacing: var(--ls-tight);
|
||
}
|
||
|
||
.filters-close {
|
||
width: 44px;
|
||
height: 44px;
|
||
}
|
||
|
||
.filters form {
|
||
flex: 1 1 auto;
|
||
padding: 0 var(--container-gutter);
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
}
|
||
|
||
.filters .filter-group {
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid var(--border-main);
|
||
}
|
||
|
||
.filters-sheet-foot {
|
||
display: block;
|
||
padding: 16px var(--container-gutter) calc(16px + env(safe-area-inset-bottom));
|
||
border-top: 1px solid var(--border-main);
|
||
}
|
||
|
||
.filters-apply {
|
||
width: 100%;
|
||
}
|
||
|
||
body.filters-open-lock {
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
|
||
|
||
.filter-input::-webkit-outer-spin-button,
|
||
.filter-input::-webkit-inner-spin-button {
|
||
margin: 0;
|
||
appearance: none;
|
||
}
|
||
|
||
/* --------------------------------------------------- checkout validation --- */
|
||
|
||
/*
|
||
* The design puts validation on the field itself: a red edge plus red caption
|
||
* text under it, per field. It does not use a banner listing every problem, so
|
||
* the WooCommerce notice group is suppressed once its errors have been mapped
|
||
* onto the fields.
|
||
*/
|
||
.checkout-input {
|
||
position: relative;
|
||
}
|
||
|
||
.checkout-input .field-error {
|
||
display: block;
|
||
margin-top: 4px;
|
||
color: var(--text-negative);
|
||
font: var(--caption);
|
||
}
|
||
|
||
.checkout-input .field-error:empty {
|
||
display: none;
|
||
}
|
||
|
||
.checkout-input.is-error input {
|
||
border-color: var(--border-input-error);
|
||
box-shadow: inset 0 0 0 1px var(--border-input-error);
|
||
}
|
||
|
||
.checkout-input input:focus {
|
||
box-shadow: inset 0 0 0 1px var(--border-input-focus);
|
||
}
|
||
|
||
.woocommerce-NoticeGroup-checkout.is-field-level,
|
||
.checkout .woocommerce-error.is-field-level {
|
||
display: none;
|
||
}
|