Тема магазина по макету Figma

Каталог, карточка товара, корзина, оформление заказа и блог на WooCommerce.

Стили разложены по слоям: токены из Figma, база, компоненты, страницы,
брейкпоинты. Цвета, типографика, радиусы и отступы взяты из Variables и
текстовых стилей макета, ниже по слоям хардкода нет.

Иконки — Phosphor, вставляются инлайном как SVG, символами ничего не рисуется.
Стили WooCommerce отключены, вёрстка своя, !important не используется.
This commit is contained in:
f1den committed 2026-07-30 22:38:49 +03:00
commit 29d10ce4cf
72 files changed
+7465

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
/*
* Base layer: font faces, reset, document defaults and the type scale.
* Only element selectors and utility classes live here.
*/
@font-face {
font-family: Inter;
src: url("../fonts/Inter.ttf") format("truetype");
font-style: normal;
font-weight: 100 900;
font-display: swap;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
[hidden] {
display: none;
}
html {
scroll-behavior: smooth;
scrollbar-width: none;
}
html::-webkit-scrollbar {
width: 0;
height: 0;
}
body {
display: flex;
min-height: 100vh;
min-height: 100dvh;
margin: 0;
flex-direction: column;
color: var(--text-main);
background: var(--bg-main);
font: var(--body-m);
-webkit-font-smoothing: antialiased;
}
body.menu-open {
overflow: hidden;
}
a {
color: inherit;
text-decoration: none;
}
img,
svg {
display: block;
max-width: 100%;
}
img {
height: auto;
}
button,
input,
select,
textarea {
color: inherit;
font: inherit;
}
button {
cursor: pointer;
}
/* ------------------------------------------------------------ type scale --- */
h1,
.h1 {
margin: 0;
font: var(--h1);
letter-spacing: var(--ls-tight);
}
h2,
.h2 {
margin: 0;
font: var(--h2);
letter-spacing: var(--ls-tight);
}
h3,
.h3 {
margin: 0;
font: var(--h3);
letter-spacing: var(--ls-tight);
}
h4,
.h4 {
margin: 0;
font: var(--h4);
letter-spacing: var(--ls-tight);
}
h5,
.h5 {
margin: 0;
font: var(--h5);
letter-spacing: var(--ls-tight-s);
}
.lead {
font: var(--lead);
}
.control-m {
font: var(--control-m);
}
.control-s {
font: var(--control-s);
}
.caption {
font: var(--caption);
}
.upper-l,
.upper-m,
.upper-s {
letter-spacing: var(--ls-upper);
text-transform: uppercase;
}
.upper-l {
font: var(--upper-l);
}
.upper-m {
font: var(--upper-m);
}
.upper-s {
font: var(--upper-s);
}
.accent-m {
font: var(--accent-m);
}
/* --------------------------------------------------------------- layout --- */
.container {
width: min(calc(100% - var(--container-gutter) * 2), var(--container));
margin-inline: auto;
}
.site-main {
display: block;
min-height: 0;
flex: 1 0 auto;
}
.screen-reader-text {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 1ms !important;
animation-duration: 1ms !important;
scroll-behavior: auto !important;
}
}
+104
View File
@@ -0,0 +1,104 @@
/*
* Blog listing and article
*/
/* Blog and article */
.blog-page {
padding-bottom: 0;
}
.blog-page .page-heading {
margin-bottom: 24px;
}
.blog-page-spacer {
height: 64px;
}
.article-layout {
padding-bottom: 48px;
}
.article-layout .page-heading {
margin-bottom: 24px;
font: var(--h2);
letter-spacing: var(--ls-tight);
}
.article-prose {
width: 752px;
max-width: 100%;
}
.article-lead {
margin: 0 0 48px;
font: var(--lead);
}
.article-media,
.article-media-wide,
.prose .article-media {
display: block;
width: 1088px;
max-width: 100%;
height: 620px;
min-height: 0;
margin: 0;
border-radius: 0;
background: var(--border-secondary);
object-fit: cover;
}
.article-caption {
width: 1088px;
max-width: 100%;
margin: 8px 0 24px;
color: var(--text-caption);
font: var(--caption);
}
.article-layout .prose h2 {
margin: 32px 0 8px;
font: var(--h3);
letter-spacing: var(--ls-tight);
}
.article-layout .prose h3 {
margin: 24px 0 8px;
font: var(--h4);
letter-spacing: var(--ls-tight);
}
.article-layout .prose p {
margin: 0 0 16px;
}
.article-layout .prose ol,
.article-layout .prose ul {
margin: 0 0 24px;
padding-left: 36px;
}
.article-layout .prose li {
margin-bottom: 16px;
padding-left: 4px;
}
.article-layout .prose ul {
padding-left: 36px;
list-style: none;
}
.article-layout .prose ul li {
position: relative;
}
.article-layout .prose ul li::before {
position: absolute;
top: 0.7em;
left: -28px;
width: 16px;
height: 2px;
background: currentColor;
content: "";
}
+742
View File
@@ -0,0 +1,742 @@
/*
* Cart and checkout
*/
/* Cart / checkout */
.cart-page {
min-height: 460px;
padding-top: 16px;
padding-bottom: 48px;
}
.cart-page .page-heading {
margin-bottom: 28px;
}
.checkout-page {
/* The design height is a floor, not a cap: validation messages under the
fields make the page taller, and a fixed height would let the content run
under the footer instead. */
min-height: 1068px;
padding-top: 16px;
padding-bottom: 48px;
}
.checkout-page .page-heading {
margin-bottom: 44px;
}
.cart-layout,
.checkout-layout {
display: grid;
align-items: start;
}
.cart-layout {
grid-template-columns: minmax(0, 1fr) 432px;
gap: 40px;
}
.checkout-layout {
grid-template-columns: minmax(0, 1fr) 436px;
gap: 24px;
}
.cart-items {
display: grid;
gap: 16px;
}
.cart-item {
display: grid;
min-height: 152px;
padding: 16px;
grid-template-columns: 160px minmax(0, 1fr) 104px 144px 48px;
align-items: center;
gap: 16px;
border-radius: 16px;
box-shadow: inset 0 0 0 1px var(--border-main);
}
.cart-item-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.cart-item-title {
color: var(--text-main);
font: var(--control-m);
}
.cart-item-sku {
color: var(--text-caption);
font: var(--control-s);
}
.cart-item-price {
text-align: right;
font: var(--h5);
letter-spacing: var(--ls-tight-s);
}
.cart-item-price del {
display: block;
color: var(--text-caption);
font: var(--control-s);
}
.cart-item-price ins {
text-decoration: none;
}
.cart-item-quantity {
height: 48px;
}
/* Holds an inline SVG, so it has to centre its own child — the icon used to
be an absolutely positioned span. */
.cart-item-remove {
display: grid;
width: 48px;
height: 48px;
place-items: center;
border-radius: var(--br-btn-m);
color: var(--text-main);
background: var(--bg-secondary);
}
.cart-item-remove:hover,
.cart-item-remove:focus-visible {
background: var(--bg-button-light-hover);
}
.cart-item-remove .icon {
--icon-size: 20px;
}
.cart-table {
width: 100%;
border-collapse: collapse;
}
.cart-table th,
.cart-table td {
padding: 16px 12px;
border-bottom: 1px solid var(--border-main);
text-align: left;
}
.cart-product {
display: flex;
align-items: center;
gap: 14px;
}
.cart-thumb {
position: relative;
width: 160px;
height: 120px;
flex: 0 0 auto;
overflow: hidden;
border-radius: 8px;
background: var(--bg-select-unit-selected);
}
.cart-thumb .product-badge {
position: absolute;
z-index: 1;
top: 8px;
left: 8px;
}
.cart-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.order-card {
align-self: start;
padding: 24px;
border-radius: 16px;
background: var(--bg-secondary);
}
.cart-order-card {
display: flex;
width: 432px;
max-width: 100%;
min-height: 272px;
flex-direction: column;
background: var(--bg-main);
box-shadow: 0 16px 48px rgb(0 0 0 / 10%);
}
.cart-order-card .order-row {
display: grid;
grid-template-columns: auto minmax(16px, 1fr) auto;
align-items: center;
column-gap: 8px;
}
.cart-order-card .order-row::after {
grid-column: 2;
grid-row: 1;
border-top: 1px dashed var(--border-secondary);
content: "";
}
.cart-order-card .order-row > :first-child {
grid-column: 1;
}
.cart-order-card .order-row > :last-child {
grid-column: 3;
text-align: right;
}
.woocommerce .cart-order-card .button {
height: 56px;
margin-top: auto;
min-height: 56px;
}
.cart-page.is-updating {
opacity: 0.7;
transition: opacity 100ms ease-in-out;
}
.order-card h3 {
margin: 0 0 20px;
font: var(--h5);
letter-spacing: var(--ls-tight-s);
}
.order-row {
display: flex;
min-height: 32px;
padding: 4px 0;
justify-content: space-between;
gap: 20px;
font: var(--control-m);
}
/* In the design only the amount jumps to Header #4; the "Итого" label stays
on Control M like every other summary row. */
.order-row.total {
min-height: 40px;
margin-top: 4px;
padding: 8px 0 0;
align-items: baseline;
}
.order-row.total strong {
font: var(--h4);
letter-spacing: var(--ls-tight);
}
.order-card .button,
.order-card #place_order {
width: 100%;
margin-top: 16px;
}
.checkout-fields {
display: flex;
width: 432px;
max-width: 100%;
flex-direction: column;
}
.checkout-input {
display: block;
width: 432px;
max-width: 100%;
/* Height comes from the input so the field can grow by its error message
instead of clipping it. */
margin-bottom: 8px;
}
/*
* Checkout fields follow the Figma text-field: h-input-L tall, br-control-L
* radius, the border-input state ladder, and a label that stays with the field
* instead of vanishing as soon as you type. Empty, it sits centred at
* Control M; filled or focused, it shrinks to Caption and moves to the top
* with the value underneath.
*/
.checkout-input input {
width: 100%;
height: var(--h-input-l);
padding: 33px 20px 15px;
border: 1px solid var(--border-input);
border-radius: var(--br-control-l);
outline: 0;
color: var(--text-main);
background: var(--bg-main);
font: var(--control-m);
transition: border-color var(--motion-fast);
}
.checkout-input input::placeholder {
color: var(--text-caption);
}
.checkout-input input:hover {
border-color: var(--border-input-hover);
}
.checkout-input input:focus {
border-color: var(--border-input-focus);
}
.checkout-input input:disabled {
border-color: transparent;
color: var(--text-disable);
background: var(--bg-control-disable);
}
.checkout-input input[aria-invalid="true"],
.woocommerce-invalid .checkout-input input {
border-color: var(--border-input-error);
}
.checkout-delivery,
.checkout-payment-options {
min-width: 0;
margin: 20px 0 0;
padding: 0;
border: 0;
}
.checkout-delivery legend,
.checkout-payment-options legend {
margin-bottom: 16px;
padding: 0;
font: var(--h5);
letter-spacing: var(--ls-tight-s);
}
.delivery-switch {
position: relative;
display: flex;
width: 256px;
max-width: 100%;
height: 56px;
margin-bottom: 8px;
padding: 4px;
border-radius: 10px;
background: var(--bg-secondary);
}
.delivery-switch::before {
position: absolute;
top: 4px;
left: 4px;
width: calc((100% - 8px) / 2);
height: 48px;
border-radius: 8px;
background: var(--bg-main);
box-shadow: 0 2px 8px rgb(0 0 0 / 6%);
content: "";
transition: transform 180ms ease-in-out;
}
.delivery-switch.is-delivery::before {
transform: translateX(100%);
}
.delivery-switch label {
position: relative;
z-index: 1;
flex: 1 1 0;
cursor: pointer;
}
.delivery-switch input {
position: absolute;
opacity: 0;
}
.delivery-switch span {
display: grid;
height: 48px;
place-items: center;
border-radius: 8px;
transition: color 180ms ease-in-out;
font: var(--control-m);
}
.delivery-switch input:checked + span {
background: transparent;
box-shadow: none;
}
.checkout-radio {
display: flex;
min-height: 32px;
align-items: center;
gap: 8px;
font: var(--control-m);
}
.checkout-map {
display: block;
width: 432px;
max-width: 100%;
height: 300px;
margin-top: 16px;
border-radius: 8px;
object-fit: cover;
}
.delivery-address-fields {
margin-top: 16px;
}
.checkout-payment-options {
margin-top: 24px;
}
.checkout-order-products {
display: grid;
gap: 8px;
margin-bottom: 16px;
}
.checkout-order-product {
display: grid;
min-height: 72px;
grid-template-columns: 56px 1fr;
align-items: center;
gap: 16px;
border-bottom: 1px solid var(--border-secondary);
}
.checkout-order-thumb {
width: 56px;
height: 56px;
overflow: hidden;
border-radius: 6px;
background: var(--bg-select-unit-selected);
}
.checkout-order-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.checkout-order-product strong,
.checkout-order-product > div > span {
display: block;
font: var(--control-s);
}
.checkout-order-product > div > span {
color: var(--text-caption);
}
.checkout-order-totals .order-row {
border-bottom: 1px dashed var(--border-secondary);
}
.checkout-order-totals .order-row.total {
border-bottom: 0;
}
.checkout-consent {
display: flex;
margin-top: 16px;
align-items: center;
gap: 8px;
color: var(--text-caption);
font: var(--control-s);
}
.woocommerce form .form-row {
margin: 0 0 14px;
}
.woocommerce form .form-row label {
display: block;
margin-bottom: 6px;
font: var(--control-s);
}
.woocommerce-checkout-review-order-table {
width: 100%;
border-collapse: collapse;
}
.woocommerce-checkout-review-order-table th,
.woocommerce-checkout-review-order-table td {
padding: 9px 0;
border-bottom: 1px solid var(--border-secondary);
text-align: left;
}
.woocommerce-checkout-payment {
margin-top: 18px;
}
.woocommerce-checkout-payment ul {
margin: 0;
padding: 0;
list-style: none;
}
/*
* Notices. WooCommerce's own stylesheets are dequeued (see inc/assets.php),
* so these rules win on their own and no longer need `!important`. The status
* mark is the matching Phosphor vector carried as a mask — never a glyph.
*/
.woocommerce-message,
.woocommerce-info,
.woocommerce-error,
.woocommerce-warning {
--notice-accent: var(--notice-info);
--notice-mark: var(--icon-info-fill);
position: relative;
display: flex;
width: min(100%, 447px);
min-height: 64px;
margin: 16px 0;
padding: 16px 56px 16px 64px;
align-items: center;
border: 0;
border-radius: var(--br-modal);
color: var(--text-main-contrast);
background: var(--notice-accent);
font: var(--control-m);
list-style: none;
transition:
opacity 180ms ease-in-out,
transform 180ms ease-in-out;
}
.woocommerce-message {
--notice-accent: var(--notice-success);
--notice-mark: var(--icon-check-circle-fill);
}
.woocommerce-error {
--notice-accent: var(--notice-error);
--notice-mark: var(--icon-x-circle-fill);
}
.woocommerce-warning {
--notice-accent: var(--notice-warning);
--notice-mark: var(--icon-warning-fill);
}
.woocommerce-message::before,
.woocommerce-info::before,
.woocommerce-error::before,
.woocommerce-warning::before {
position: absolute;
top: 50%;
left: 16px;
width: 32px;
height: 32px;
background: currentColor;
content: "";
transform: translateY(-50%);
-webkit-mask: var(--notice-mark) center / contain no-repeat;
mask: var(--notice-mark) center / contain no-repeat;
}
.woocommerce-error li {
margin: 0;
}
.woocommerce-notice-dismiss {
position: absolute;
top: 50%;
right: 16px;
width: 24px;
height: 24px;
padding: 0;
border: 0;
color: var(--text-main-contrast);
background: transparent;
transform: translateY(-50%);
}
.woocommerce-notice-dismiss::before {
position: absolute;
inset: 0;
background: currentColor;
content: "";
-webkit-mask: var(--icon-x) center / contain no-repeat;
mask: var(--icon-x) center / contain no-repeat;
}
.woocommerce-notice-dismiss:focus-visible {
outline: 2px solid var(--text-main-contrast);
outline-offset: 2px;
}
.woocommerce-notice-closing {
opacity: 0;
transform: translateY(-8px);
}
.wc-block-components-notice-banner > svg {
display: none;
}
.shop-toast-stack {
position: fixed;
z-index: 1000;
top: 104px;
right: 24px;
display: grid;
width: min(calc(100vw - 48px), 447px);
gap: 12px;
pointer-events: none;
}
.home .shop-toast-stack {
top: 136px;
}
.shop-toast {
--toast-fade-duration: 300ms;
position: relative;
display: grid;
width: 100%;
min-height: 64px;
padding: 16px 56px 16px 64px;
grid-template-columns: 1fr;
align-items: center;
border-radius: 16px;
color: var(--text-main-contrast);
background: var(--notice-info);
box-shadow: 0 12px 32px rgb(0 0 0 / 14%);
opacity: 0;
pointer-events: auto;
transition: opacity var(--toast-fade-duration) ease-in-out;
font: var(--control-m);
}
.shop-toast-success {
background: var(--notice-success);
}
.shop-toast-error {
background: var(--notice-error);
}
.shop-toast.is-visible {
opacity: 1;
}
.shop-toast.is-leaving {
opacity: 0;
}
.shop-toast-icon {
--icon-size: 32px;
position: absolute;
top: 50%;
left: 16px;
display: grid;
width: 32px;
height: 32px;
place-items: center;
color: currentColor;
transform: translateY(-50%);
}
.shop-toast-dismiss {
position: absolute;
top: 50%;
right: 16px;
display: grid;
width: 24px;
height: 24px;
padding: 0;
place-items: center;
border: 0;
color: var(--text-main-contrast);
background: transparent;
transform: translateY(-50%);
}
.shop-toast-dismiss .icon {
--icon-size: 20px;
}
.shop-toast-dismiss:focus-visible {
outline: 2px solid var(--text-main-contrast);
outline-offset: 2px;
}
.empty-state {
padding: 60px 0;
text-align: center;
}
.cart-page-empty .cart-empty-message {
margin: 0;
font: var(--control-m);
}
.checkout-input .field-label {
position: absolute;
/* Half of the field, not of the wrapper — the wrapper grows when an error
message appears underneath and would drag the label down with it. */
top: calc(var(--h-input-l) / 2);
left: 21px;
/* The .field component sets width: 100%; here the label only needs to be as
wide as its own text, or it reaches past the field. */
width: auto;
max-width: calc(100% - 42px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-caption);
font-family: var(--font-sans);
font-size: 16px;
line-height: 24px;
pointer-events: none;
transform: translateY(-50%);
transition:
top var(--motion-fast),
font-size var(--motion-fast),
line-height var(--motion-fast),
transform var(--motion-fast);
}
.checkout-input input:focus + .field-label,
.checkout-input input:not(:placeholder-shown) + .field-label {
top: 16px;
font-size: 12px;
line-height: 16px;
transform: none;
}
/* The real placeholder is a single space so :placeholder-shown works; it must
never be visible. */
.checkout-input input::placeholder {
color: transparent;
}
.checkout-input.is-error .field-label {
color: var(--text-negative);
}
/*
* The delivery switch sizes to its labels rather than trusting a fixed width,
* so a longer word cannot crowd the segments.
*/
.delivery-switch span {
padding: 0 20px;
white-space: nowrap;
}
+576
View File
@@ -0,0 +1,576 @@
/*
* Catalog and category archive: filters, toolbar, pagination
*/
/* Catalog / archive */
.archive-shell {
display: grid;
padding: 0 0 72px;
grid-template-columns: 288px minmax(0, 1fr);
align-items: start;
gap: 48px;
}
.filters {
align-self: start;
}
.filter-group {
padding: 15px 0;
}
.filter-group:first-child {
padding-top: 0;
}
.filter-title {
display: inline-flex;
margin-bottom: 10px;
align-items: center;
gap: 8px;
font: var(--h5);
letter-spacing: var(--ls-tight-s);
}
button.filter-title {
width: auto;
padding: 0;
border: 0;
background: transparent;
text-align: left;
}
.filter-caret {
flex: 0 0 auto;
transition: transform 180ms ease-in-out;
}
.filter-collapsed .filter-title {
margin: 0;
}
.filter-collapsed .filter-caret {
transform: rotate(180deg);
}
.filter-content {
overflow: hidden;
opacity: 1;
transition:
height 180ms ease-in-out,
opacity 120ms ease-in-out;
}
.filter-content.is-animating {
will-change: height, opacity;
}
.price-fields {
display: grid;
grid-template-columns: 1fr 14px 1fr;
align-items: center;
gap: 8px;
}
.price-fields > span {
color: var(--text-caption);
text-align: center;
}
.filter-input,
.filter-select,
.field input,
.field textarea,
.field select,
.woocommerce form .form-row input.input-text,
.woocommerce form .form-row textarea,
.woocommerce form .form-row select {
width: 100%;
min-height: 48px;
padding: 10px 12px;
border: 1px solid var(--border-main);
border-radius: 7px;
outline: 0;
background: var(--bg-main);
}
.filter-input {
min-height: 56px;
padding: 14px;
font: var(--control-m);
appearance: textfield;
}
.price-range {
position: relative;
height: 20px;
margin-top: 8px;
--range-min: 0%;
--range-max: 66.3%;
}
.price-range::before {
position: absolute;
top: 50%;
right: 0;
left: 0;
height: 2px;
background: var(--border-secondary);
content: "";
transform: translateY(-50%);
}
.price-range::after {
position: absolute;
top: 50%;
right: calc(100% - var(--range-max));
left: var(--range-min);
height: 2px;
background: var(--text-main);
content: "";
transform: translateY(-50%);
}
.price-range input[type="range"] {
position: absolute;
z-index: 2;
inset: 0;
width: 100%;
height: 20px;
margin: 0;
padding: 0;
border: 0;
outline: 0;
background: transparent;
pointer-events: none;
appearance: none;
}
.price-range input[type="range"]::-webkit-slider-runnable-track {
height: 2px;
background: transparent;
}
.price-range input[type="range"]::-webkit-slider-thumb {
width: 20px;
height: 20px;
margin-top: -9px;
border: 2px solid var(--text-main);
border-radius: 50%;
background: var(--bg-main);
cursor: grab;
pointer-events: auto;
appearance: none;
}
.price-range input[type="range"]::-moz-range-track {
height: 2px;
background: transparent;
}
.price-range input[type="range"]::-moz-range-thumb {
width: 16px;
height: 16px;
border: 2px solid var(--text-main);
border-radius: 50%;
background: var(--bg-main);
cursor: grab;
pointer-events: auto;
}
.price-range input[type="range"]:active::-webkit-slider-thumb {
cursor: grabbing;
}
.price-range input[type="range"]:focus-visible::-webkit-slider-thumb {
outline: 2px solid var(--text-link);
outline-offset: 2px;
}
.filter-option {
display: flex;
min-height: 32px;
align-items: center;
gap: 8px;
font: var(--control-m);
}
.filter-more {
display: inline-block;
margin-top: 5px;
padding: 0;
border: 0;
border-bottom: 1px dashed var(--border-link);
color: var(--text-link);
background: transparent;
transition:
color 100ms ease-in-out,
border-color 100ms ease-in-out;
font: var(--control-m);
}
.filter-more:hover,
.filter-more:focus-visible {
border-bottom-color: var(--text-link);
}
.filter-empty {
margin: 48px 0;
color: var(--text-caption);
text-align: center;
}
.archive-results {
min-width: 0;
}
.archive-toolbar {
display: flex;
min-height: 32px;
margin-bottom: 16px;
align-items: center;
justify-content: space-between;
gap: 20px;
color: var(--text-caption);
font: var(--control-m);
}
.sort-control,
.per-page-control {
display: flex;
align-items: center;
gap: 8px;
}
.archive-toolbar .woocommerce-ordering {
margin: 0;
}
.archive-toolbar select {
min-height: 32px;
padding: 4px 28px 4px 4px;
border: 0;
outline: 0;
color: var(--text-main);
background: transparent var(--icon-caret-down) right 4px center / 14px no-repeat;
font: var(--control-s);
appearance: none;
}
.custom-select {
position: relative;
display: inline-flex;
min-width: 0;
}
.custom-select-native {
position: absolute;
width: 1px;
height: 1px;
min-height: 0;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.custom-select-trigger {
display: inline-flex;
min-height: 24px;
padding: 0 32px 0 0;
align-items: center;
border: 0;
color: var(--text-main);
background: transparent;
text-align: left;
font: var(--control-m);
}
.custom-select-trigger::after {
position: absolute;
top: 50%;
right: 0;
width: 24px;
height: 24px;
background: currentColor;
content: "";
transform: translateY(-50%);
transition: transform 100ms ease-in-out;
-webkit-mask: var(--icon-caret-down) center / contain no-repeat;
mask: var(--icon-caret-down) center / contain no-repeat;
}
.custom-select.is-open .custom-select-trigger::after {
transform: translateY(-50%) rotate(180deg);
}
.custom-select-trigger:focus-visible {
outline: 2px solid var(--text-link);
outline-offset: 2px;
}
.custom-select-menu {
position: absolute;
z-index: 80;
top: calc(100% + 8px);
left: 0;
width: max-content;
min-width: 240px;
max-height: 336px;
overflow: auto;
border-radius: 8px;
background: var(--bg-main);
box-shadow: 0 10px 30px rgb(0 0 0 / 14%);
scrollbar-width: thin;
}
.per-page-control .custom-select-menu {
right: 0;
left: auto;
min-width: 160px;
}
.custom-select-option {
display: flex;
width: 100%;
min-height: 56px;
padding: 16px;
align-items: center;
border: 0;
border-bottom: 1px solid var(--border-main);
color: var(--text-main);
background: var(--bg-main);
text-align: left;
transition: background-color 100ms ease-in-out;
font: var(--control-m);
}
.custom-select-option:last-child {
border-bottom: 0;
}
.custom-select-option:hover,
.custom-select-option:focus-visible,
.custom-select-option.is-selected {
background: var(--bg-secondary);
}
.archive-results .product-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
row-gap: 24px;
}
.archive-results .product-card {
height: 405px;
}
.pagination,
.woocommerce nav.woocommerce-pagination ul {
display: flex;
margin: 24px 0 0;
padding: 0;
gap: 6px;
border: 0;
list-style: none;
}
.pagination .nav-links {
display: flex;
gap: 6px;
}
.pagination .page-numbers,
.woocommerce nav.woocommerce-pagination ul li a,
.woocommerce nav.woocommerce-pagination ul li span {
display: grid;
width: 56px;
height: 56px;
padding: 0;
place-items: center;
border: 1px solid var(--border-control);
border-radius: var(--br-btn-m);
background: var(--bg-main);
font: var(--control-m);
transition:
background-color 100ms ease-in-out,
border-color 100ms ease-in-out;
}
.woocommerce nav.woocommerce-pagination ul li {
border: 0;
}
.pagination .current,
.woocommerce nav.woocommerce-pagination ul li span.current {
color: var(--text-main);
background: var(--bg-secondary);
}
.pagination .icon-arrow-fill {
width: 24px;
height: 24px;
}
.pagination a.page-numbers:hover,
.pagination a.page-numbers:focus-visible,
.woocommerce nav.woocommerce-pagination ul li a:hover,
.woocommerce nav.woocommerce-pagination ul li a:focus-visible {
background: var(--bg-button-light-hover);
}
/* ------------------------------------------------- subcategory cards --- */
/*
* A scrollable row above the filters. Measured off `sub-categorys` in the
* Category block: 216x80 cards on bg-secondary, 16px gap, a 64px media square,
* and a circular next button overlapping the right edge.
*/
.subcategories {
position: relative;
margin-bottom: 24px;
}
.subcategory-list {
display: flex;
align-items: stretch;
margin: 0;
padding: 0;
gap: 16px;
overflow-x: auto;
scroll-behavior: smooth;
scrollbar-width: none;
list-style: none;
}
.subcategory-list::-webkit-scrollbar {
height: 0;
}
.subcategory-card {
display: flex;
width: 216px;
/* Fixed, not min: the design keeps the row even when a name wraps. */
height: 80px;
padding: 8px;
align-items: center;
gap: 12px;
border-radius: var(--br-section-m);
background: var(--bg-secondary);
transition: background-color var(--motion-fast);
}
.subcategory-card:hover {
background: var(--bg-button-light-hover);
}
.subcategory-card:active {
background: var(--bg-button-light-pressed);
}
.subcategory-card-media {
display: block;
width: 64px;
height: 64px;
flex: 0 0 64px;
overflow: hidden;
border-radius: var(--br-control-m);
background: var(--bg-select-unit-selected);
}
.subcategory-card-media img {
width: 100%;
height: 100%;
object-fit: cover;
}
.subcategory-card-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.subcategory-card-title {
display: -webkit-box;
overflow: hidden;
color: var(--text-main);
font: var(--control-m);
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.subcategory-card-count {
color: var(--text-caption);
font: var(--caption);
}
.subcategory-next {
position: absolute;
z-index: 2;
top: 50%;
right: 0;
display: grid;
width: 40px;
height: 40px;
padding: 0;
place-items: center;
border: 0;
border-radius: 50%;
color: var(--text-main);
background: var(--bg-main);
box-shadow: var(--shadow-m);
transform: translateY(-50%);
transition: background-color var(--motion-fast);
}
.subcategory-next:hover {
background: var(--bg-button-light-hover);
}
.subcategory-next[hidden] {
display: none;
}
/* --------------------------------------------------- quick filters --- */
/*
* The `quick-filters-tabs` row: pills that wrap, 36px tall, on the container
* edge rather than filled, and the active one inverts.
*/
.quick-filters {
display: flex;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 8px;
}
.quick-filter {
padding: 6px 16px;
border: 1px solid var(--border-control);
border-radius: 999px;
color: var(--text-main);
background: var(--bg-main);
font: var(--control-m);
transition:
color var(--motion-fast),
border-color var(--motion-fast),
background-color var(--motion-fast);
}
.quick-filter:hover {
border-color: var(--border-control-hover);
background: var(--bg-button-light-hover);
}
.quick-filter.is-active {
border-color: transparent;
color: var(--text-main-contrast);
background: var(--bg-button);
}
.quick-filter:focus-visible {
outline: 2px solid var(--border-input-focus);
outline-offset: 2px;
}
+576
View File
@@ -0,0 +1,576 @@
/*
* Components.
*
* Every control here is built against the `Компоненты` matrices in Figma
* (DESKTOP + MOBILE, light theme) and uses tokens only. States follow the
* design's own vocabulary: default / hover / pressed / focus / error /
* disabled.
*/
/* ---------------------------------------------------------------- icon --- */
.icon {
display: inline-block;
width: var(--icon-size, 24px);
height: var(--icon-size, 24px);
flex: 0 0 auto;
color: inherit;
fill: currentColor;
vertical-align: middle;
}
.icon-20 {
--icon-size: 20px;
}
.icon-16 {
--icon-size: 16px;
}
/* Icons that read as "back" are the forward vector mirrored, never a
second asset. */
.icon-flip {
transform: scaleX(-1);
}
/* -------------------------------------------------------------- button --- */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: var(--pad-h-btn-m) var(--pad-w-text-m);
border: 1px solid transparent;
border-radius: var(--br-btn-m);
color: var(--text-main-contrast);
background: var(--bg-button);
font: var(--control-m);
text-align: center;
cursor: pointer;
transition:
color var(--motion-fast),
background-color var(--motion-fast),
border-color var(--motion-fast);
}
.button:hover {
background: var(--bg-button-hover);
}
.button:active {
background: var(--bg-button-pressed);
}
.button:disabled,
.button[aria-disabled="true"] {
color: var(--text-disable);
background: var(--bg-control-disable);
cursor: default;
}
/* Large button — the hero / checkout size. */
.button-l {
padding: var(--pad-h-btn-l) var(--pad-w-text-l);
border-radius: var(--br-btn-l);
}
/* An icon sitting next to the label tightens the padding on its side. */
.button:has(> .icon:first-child) {
padding-left: var(--pad-w-icon-m);
}
.button:has(> .icon:last-child) {
padding-right: var(--pad-w-icon-m);
}
.button-l:has(> .icon:first-child) {
padding-left: var(--pad-w-icon-l);
}
.button-l:has(> .icon:last-child) {
padding-right: var(--pad-w-icon-l);
}
/* Light (secondary) button. */
.button-light {
color: var(--text-main);
background: var(--bg-button-light);
}
.button-light:hover {
background: var(--bg-button-light-hover);
}
.button-light:active {
background: var(--bg-button-light-pressed);
}
/* Outline button — light fill with a visible edge. */
.button-outline {
color: var(--text-main);
border-color: var(--border-control);
background: var(--bg-main);
}
.button-outline:hover {
border-color: var(--border-control-hover);
background: var(--bg-button-light-hover);
}
.button-outline:active {
border-color: var(--border-control-pressed);
background: var(--bg-button-light-pressed);
}
/* Square icon-only control (cart, carousel arrows, favourite). */
.icon-button {
display: inline-grid;
width: 56px;
height: 56px;
padding: 0;
place-items: center;
border: 1px solid var(--border-control);
border-radius: var(--br-btn-m);
color: var(--text-main);
background: var(--bg-main);
cursor: pointer;
transition:
border-color var(--motion-fast),
background-color var(--motion-fast);
}
.icon-button:hover {
border-color: var(--border-control-hover);
background: var(--bg-button-light-hover);
}
.icon-button:active {
border-color: var(--border-control-pressed);
background: var(--bg-button-light-pressed);
}
.icon-button:disabled {
border-color: var(--border-control-disable);
color: var(--text-disable);
background: var(--bg-main);
cursor: default;
}
/* ---------------------------------------------------------- text link --- */
.text-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--text-link);
font: var(--control-m);
transition: color var(--motion-fast);
}
.text-link:hover {
color: var(--text-link-hover);
}
/* -------------------------------------------------- checkbox and radio --- */
/*
* Measured off the `radio-btn-icon` and `checkbox-icon` components: 20×20 box,
* 1px edge, 4px radius (`br-checkbox`), an 8px dot and a ~12px tick.
*
* The edge stays `border-checkbox` in every state, including checked — only
* the mark inside turns on. Turning the edge black as well is what made the
* radio read as a bullseye.
*
* Disabled fills with `bg-control-disable`, drops the edge, and dims the mark.
*/
.control-check {
display: inline-flex;
align-items: center;
gap: 12px;
font: var(--control-m);
cursor: pointer;
}
.control-check input[type="checkbox"],
.control-check input[type="radio"],
input[type="checkbox"],
input[type="radio"] {
position: relative;
width: 20px;
height: 20px;
margin: 0;
flex: 0 0 20px;
border: 1px solid var(--border-checkbox);
/* The mark is drawn in currentColor, so pin it here instead of inheriting
whatever colour the surrounding label happens to use. */
color: var(--text-main);
background: var(--bg-main);
cursor: pointer;
appearance: none;
transition:
border-color var(--motion-fast),
background-color var(--motion-fast);
}
input[type="checkbox"] {
border-radius: var(--br-checkbox);
}
input[type="radio"] {
border-radius: 50%;
}
input[type="checkbox"]:hover,
input[type="radio"]:hover {
border-color: var(--border-checkbox-hover);
}
/* The tick is the Phosphor `check-bold` vector, drawn as a mask so the mark
inherits `currentColor` and stays a vector at every zoom level. */
input[type="checkbox"]:checked::after {
position: absolute;
inset: 0;
background: currentColor;
content: "";
-webkit-mask: var(--icon-check) center / 12px 12px no-repeat;
mask: var(--icon-check) center / 12px 12px no-repeat;
}
input[type="radio"]:checked::after {
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
content: "";
transform: translate(-50%, -50%);
}
input[type="checkbox"]:disabled,
input[type="radio"]:disabled {
border-color: transparent;
color: var(--text-disable);
background: var(--bg-control-disable);
cursor: default;
}
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
outline: 2px solid var(--border-input-focus);
outline-offset: 2px;
}
.control-check:has(input:disabled) {
color: var(--text-disable);
cursor: default;
}
/* ---------------------------------------------------------- text field --- */
/*
* Figma text field: 72px tall on desktop / 64px on mobile, 16px radius,
* the label sits inside the field above the value.
*/
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.field-control {
position: relative;
display: flex;
min-height: var(--h-input-l);
padding: 12px 20px;
align-items: center;
flex-direction: column;
justify-content: center;
gap: 2px;
border: 1px solid var(--border-input);
border-radius: var(--br-control-l);
background: var(--bg-main);
transition: border-color var(--motion-fast);
}
.field-control:hover {
border-color: var(--border-input-hover);
}
.field-control:focus-within {
border-color: var(--border-input-focus);
box-shadow: inset 0 0 0 1px var(--border-input-focus);
}
.field-label {
width: 100%;
color: var(--text-caption);
font: var(--caption);
}
.field-control input,
.field-control textarea,
.field-input {
width: 100%;
padding: 0;
border: 0;
outline: 0;
color: var(--text-main);
background: transparent;
font: var(--control-m);
}
.field-control input::placeholder,
.field-control textarea::placeholder {
color: var(--text-caption);
}
.field-control textarea {
min-height: calc(var(--h-text-area-l) - 48px);
resize: vertical;
}
.field-error-text,
.field-error {
color: var(--text-negative);
font: var(--caption);
}
/* The slot only takes space once it carries a message. */
.field-error:empty {
display: none;
}
.field.is-error .field-control {
border-color: var(--border-input-error);
box-shadow: inset 0 0 0 1px var(--border-input-error);
}
.field.is-disabled .field-control,
.field-control:has(input:disabled),
.field-control:has(textarea:disabled) {
border-color: transparent;
background: var(--bg-control-disable);
}
.field.is-disabled .field-label,
.field-control:has(input:disabled) .field-label,
.field-control:has(input:disabled) input {
color: var(--text-disable);
}
/* Compact single-line input used inside filters and the cart. */
.input-compact {
min-height: 48px;
padding: 12px 16px;
border: 1px solid var(--border-input);
border-radius: var(--br-control-m);
outline: 0;
color: var(--text-main);
background: var(--bg-main);
font: var(--control-m);
transition: border-color var(--motion-fast);
}
.input-compact:hover {
border-color: var(--border-input-hover);
}
.input-compact:focus {
border-color: var(--border-input-focus);
}
/* --------------------------------------------------------------- select --- */
.custom-select {
position: relative;
}
.custom-select-trigger {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 0;
border: 0;
color: var(--text-main);
background: transparent;
font: var(--control-m);
cursor: pointer;
transition: color var(--motion-fast);
}
.custom-select-trigger .icon {
--icon-size: 20px;
transition: transform var(--motion-fast);
}
.custom-select.is-open .custom-select-trigger .icon {
transform: rotate(180deg);
}
.custom-select-menu {
position: absolute;
z-index: 20;
top: calc(100% + 8px);
right: 0;
min-width: 100%;
padding: 8px 0;
border: 1px solid var(--border-main);
border-radius: var(--br-control-m);
background: var(--bg-select-unit);
box-shadow: var(--shadow-m);
}
.custom-select-option {
display: block;
width: 100%;
padding: 10px 16px;
border: 0;
color: var(--text-main);
background: transparent;
font: var(--control-m);
text-align: left;
white-space: nowrap;
cursor: pointer;
transition: background-color var(--motion-fast);
}
.custom-select-option:hover {
background: var(--bg-select-unit-hover);
}
.custom-select-option[aria-selected="true"] {
background: var(--bg-select-unit-selected);
}
/* ----------------------------------------------------- quantity control --- */
.quantity-control {
display: grid;
min-height: 52px;
grid-template-columns: 40px minmax(0, 1fr) 40px;
align-items: center;
border-radius: var(--br-control-m);
background: var(--bg-secondary);
}
.quantity-control button {
display: grid;
height: 100%;
padding: 0;
place-items: center;
border: 0;
border-radius: inherit;
color: var(--text-main);
background: transparent;
cursor: pointer;
transition: background-color var(--motion-fast);
}
.quantity-control button .icon {
--icon-size: 20px;
}
.quantity-control button:hover {
background: var(--bg-button-light-hover);
}
.quantity-control button:active {
background: var(--bg-button-light-pressed);
}
.quantity-control button:disabled {
color: var(--text-disable);
cursor: default;
}
.quantity-control button:disabled:hover,
.quantity-control button:disabled:active {
background: transparent;
}
.quantity-control button:focus-visible {
z-index: 1;
outline: 2px solid var(--border-input-focus);
outline-offset: -2px;
}
.quantity-control input {
width: 100%;
padding: 0;
border: 0;
outline: 0;
background: transparent;
font: var(--control-m);
text-align: center;
appearance: textfield;
}
.quantity-control input::-webkit-outer-spin-button,
.quantity-control input::-webkit-inner-spin-button {
margin: 0;
appearance: none;
}
/* ------------------------------------------------------------- pill/tag --- */
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: var(--br-checkbox);
color: var(--text-main-contrast);
background: var(--bg-main-contrast);
font: var(--upper-s);
letter-spacing: var(--ls-upper);
text-transform: uppercase;
}
/* ----------------------------------------------------------- focus ring --- */
.button:focus-visible,
.icon-button:focus-visible,
.text-link:focus-visible,
.custom-select-trigger:focus-visible,
.custom-select-option:focus-visible {
outline: 2px solid var(--border-input-focus);
outline-offset: 2px;
}
/* ------------------------------------------------------------ carousel --- */
/*
* Applied by script once a carousel is wired up. Without JS the container
* stays the grid it was authored as, showing every item.
*/
.is-carousel {
overflow: hidden;
}
.carousel-track {
--carousel-gap: 24px;
display: flex;
flex-wrap: nowrap;
gap: var(--carousel-gap);
will-change: transform;
transition: transform var(--motion-slow-duration) var(--motion-slow);
}
.carousel-track > * {
flex: 0 0
calc(
(100% - (var(--carousel-per-view) - 1) * var(--carousel-gap)) /
var(--carousel-per-view)
);
min-width: 0;
}
/* While the track is being re-ordered behind the scenes. */
.carousel-track.is-settling {
transition: none;
}
+372
View File
@@ -0,0 +1,372 @@
/*
* Home page: hero, product cards, category cards, news cards
*/
/* Home hero */
.home .site-header {
height: 120px;
}
.hero {
height: 654px;
min-height: 654px;
padding: 48px 0 0;
background: var(--bg-secondary);
}
.hero-copy {
display: flex;
width: 100%;
max-width: 1008px;
flex-direction: column;
gap: 32px;
}
.hero-main {
display: flex;
flex-direction: column;
gap: 24px;
}
.hero-title-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.hero h1 {
max-width: 1008px;
margin: 0;
font: var(--h1);
letter-spacing: var(--ls-tight);
}
.hero-copy > p,
.hero-title-group > p {
max-width: 656px;
min-height: 84px;
margin: 0;
color: var(--text-main);
font: var(--lead);
}
.hero-features {
display: grid;
width: 100%;
max-width: 656px;
margin: 0;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
}
.hero-feature {
display: flex;
min-width: 0;
flex-direction: column;
gap: 8px;
font: var(--control-m);
}
.feature-mark {
--icon-size: 24px;
display: block;
}
.hero-button {
width: 144px;
height: 72px;
min-height: 72px;
padding: 0 28px;
font: var(--control-m);
}
/* Product cards */
.products-section {
height: 555px;
padding-top: 16px;
overflow: hidden;
}
.products-section .section-head {
margin-bottom: 24px;
}
.product-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 24px;
}
.product-card {
container-type: inline-size;
min-width: 0;
padding: 16px;
border: 0;
border-radius: 16px;
background: var(--bg-main);
box-shadow: inset 0 0 0 1px var(--border-main);
transition: box-shadow 400ms var(--motion-slow);
}
.product-card:hover,
.product-card:focus-within {
box-shadow: inset 0 0 0 1px var(--border-secondary);
}
.product-card-media {
position: relative;
display: block;
height: 220px;
overflow: hidden;
border-radius: 8px;
background: var(--bg-select-unit-selected);
}
.product-card-media img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-badge {
display: inline-block;
padding: 3px 6px;
color: var(--text-main-contrast);
background: var(--text-main);
font: var(--upper-s);
letter-spacing: var(--ls-upper);
text-transform: uppercase;
}
.product-card-media .product-badge {
position: absolute;
z-index: 2;
top: 10px;
left: 10px;
}
.product-card-body {
padding: 12px 0 0;
}
.stock {
color: var(--text-positive);
font: var(--control-s);
}
.product-card-title {
margin: 5px 0 1px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: var(--control-m);
}
.price {
display: flex;
min-height: 27px;
align-items: baseline;
gap: 10px;
font: var(--h5);
letter-spacing: var(--ls-tight-s);
}
.price ins {
order: -1;
text-decoration: none;
}
.price del {
color: var(--text-caption);
font: var(--control-s);
}
/*
* The stepper is 132px at the design width and gives that space back as the
* card narrows; min-content on the button track is its natural width, so the
* label is never squeezed. Below the point where both still fit they stack,
* ending up the same width as each other rather than one starving the other.
*/
.card-buy {
display: grid;
margin-top: 12px;
grid-template-columns: minmax(104px, 132px) minmax(min-content, 1fr);
gap: 8px;
}
@container (max-width: 251px) {
.card-buy {
grid-template-columns: minmax(0, 1fr);
}
}
.add-cart {
min-width: 0;
min-height: 52px;
padding: 10px 12px;
white-space: nowrap;
}
.add-cart .icon {
width: 20px;
height: 20px;
}
/* Category cards */
.categories-section {
padding-top: 60px;
}
.category-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}
.category-card {
position: relative;
display: grid;
height: 260px;
grid-template-columns: 400px 260px;
overflow: hidden;
border-radius: var(--br-section-m);
background: var(--bg-secondary);
}
.category-card::after {
position: absolute;
z-index: 2;
inset: 0;
border: 1px solid var(--border-secondary);
border-radius: inherit;
content: "";
opacity: 0;
pointer-events: none;
transition: opacity 400ms var(--motion-slow);
}
.category-card-copy {
display: flex;
padding: 24px;
align-items: flex-start;
flex-direction: column;
transition: background-color 400ms var(--motion-slow);
}
.category-card:hover,
.category-card:focus-visible {
outline: 0;
}
.category-card:hover::after,
.category-card:focus-visible::after {
opacity: 1;
}
.category-card:hover .category-card-copy,
.category-card:focus-visible .category-card-copy {
background: var(--bg-main);
}
.category-card h3 {
margin: 0 0 3px;
font: var(--h4);
letter-spacing: var(--ls-tight);
}
.category-card p {
margin: 0;
color: var(--text-caption);
font: var(--control-m);
}
.category-card .text-link {
margin-top: auto;
}
.category-card-media {
background: var(--bg-select-unit-selected);
}
/* News cards */
.news-section {
padding-top: 24px;
padding-bottom: 64px;
}
.news-section .section-head {
margin-bottom: 24px;
}
.news-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
}
.news-card {
position: relative;
display: flex;
height: 408px;
padding: 24px;
justify-content: flex-end;
flex-direction: column;
overflow: hidden;
border-radius: 10px;
color: var(--text-main-contrast);
background: linear-gradient(to bottom, var(--bg-select-unit-selected) 0 43%, var(--bg-select-unit-selected) 48%, #000 100%);
}
.news-card > * {
position: relative;
z-index: 1;
transition: transform 400ms var(--motion-slow);
}
.news-card:hover > *,
.news-card:focus-visible > * {
transform: translateY(-8px);
}
.news-card h3 {
margin: 0 0 4px;
font: var(--h5);
letter-spacing: var(--ls-tight-s);
}
.news-card p {
max-width: 340px;
margin: 0;
color: var(--text-main-contrast);
font: var(--control-s);
}
.all-news-link {
display: flex;
height: 56px;
margin-top: 24px;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: var(--br-btn-m);
color: var(--text-main);
background: var(--bg-secondary);
font: var(--control-m);
transition: background-color 100ms ease-in-out;
}
.all-news-link:hover,
.all-news-link:focus-visible {
background: var(--bg-button-light-hover);
}
.all-news-link:active {
background: var(--bg-button-light-pressed);
}
/* The two home grids double as carousels; the track replaces the grid flow. */
.product-grid.is-carousel,
.news-grid.is-carousel {
display: block;
}
+240
View File
@@ -0,0 +1,240 @@
/*
* Header, shared page furniture and the footer
*/
/* Header */
.site-header {
position: relative;
z-index: 30;
background: var(--bg-main);
}
.header-inner {
display: flex;
height: 88px;
align-items: center;
gap: 37px;
border-bottom: 1px solid var(--border-main);
}
.brand {
display: grid;
width: 105px;
height: 42px;
flex: 0 0 105px;
align-items: center;
color: var(--text-main);
}
.brand-mark,
.brand img {
width: 105px;
height: auto;
}
.main-nav {
margin-right: auto;
}
.main-nav ul {
display: flex;
margin: 0;
padding: 0;
align-items: center;
gap: 24px;
list-style: none;
}
.main-nav a {
display: block;
padding: 8px 0;
font: var(--control-m);
}
/* Square icon controls all share the .icon-button component. */
.cart-link,
.slider-controls button,
.gallery-arrow,
.gallery-favorite {
display: inline-grid;
width: 56px;
height: 56px;
padding: 0;
place-items: center;
border: 1px solid var(--border-control);
border-radius: var(--br-btn-m);
color: var(--text-main);
background: var(--bg-main);
cursor: pointer;
transition:
border-color var(--motion-fast),
background-color var(--motion-fast),
color var(--motion-fast);
}
.cart-link:hover,
.slider-controls button:hover,
.gallery-arrow:hover,
.gallery-favorite:hover {
border-color: var(--border-control-hover);
background: var(--bg-button-light-hover);
}
.cart-link:active,
.slider-controls button:active,
.gallery-arrow:active,
.gallery-favorite:active {
border-color: var(--border-control-pressed);
background: var(--bg-button-light-pressed);
}
.cart-link:focus-visible,
.slider-controls button:focus-visible,
.gallery-arrow:focus-visible,
.gallery-favorite:focus-visible {
outline: 2px solid var(--border-input-focus);
outline-offset: 2px;
}
.menu-toggle {
display: none;
width: 48px;
height: 48px;
padding: 0;
place-items: center;
border: 1px solid var(--border-control);
border-radius: var(--br-btn-m);
color: var(--text-main);
background: var(--bg-main);
transition:
border-color var(--motion-fast),
background-color var(--motion-fast);
}
.menu-toggle:hover {
border-color: var(--border-control-hover);
background: var(--bg-button-light-hover);
}
/* Shared typography and controls */
.breadcrumbs {
display: flex;
margin: 32px 0 16px;
padding: 0;
flex-wrap: wrap;
gap: 8px;
color: var(--text-caption);
list-style: none;
font: var(--control-s);
}
.breadcrumbs li:not(:last-child)::after {
display: inline-block;
width: 6px;
height: 6px;
margin-left: 8px;
border-radius: 50%;
background: var(--text-disable);
content: "";
vertical-align: 2px;
}
.breadcrumbs li:last-child {
color: var(--text-main);
}
.page-heading,
.section-title {
margin: 0;
font: var(--h2);
letter-spacing: var(--ls-tight);
}
.page-heading {
margin-bottom: 24px;
}
.section {
padding: 64px 0;
}
.section-head {
display: flex;
margin-bottom: 28px;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.slider-controls {
display: flex;
gap: 8px;
}
.slider-controls .icon {
width: 24px;
height: 24px;
}
.slider-controls button:disabled {
cursor: default;
opacity: 0.35;
}
.gallery-arrow .icon {
width: 24px;
height: 24px;
}
/* Footer */
.site-footer {
height: 176px;
padding: 30px 0 22px;
flex: 0 0 auto;
background: var(--bg-secondary);
}
.page-template-page-catalog .site-main {
padding-bottom: 24px;
}
.catalog-grid-section {
padding-top: 16px;
}
.products-section {
padding-bottom: 48px;
}
.categories-section {
padding-top: 16px;
padding-bottom: 48px;
}
.footer-top {
height: 70px;
border-bottom: 1px dashed var(--border-secondary);
}
.footer-bottom {
display: grid;
padding-top: 20px;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 24px;
font: var(--control-s);
}
.footer-bottom > span:nth-child(2) {
display: flex;
gap: 28px;
}
.footer-bottom a,
.footer-bottom > span:last-child {
color: var(--text-caption);
text-decoration: underline;
text-decoration-color: var(--border-secondary);
text-decoration-style: dashed;
text-underline-offset: 4px;
}
+258
View File
@@ -0,0 +1,258 @@
/*
* 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;
}
+300
View File
@@ -0,0 +1,300 @@
/*
* Product page: gallery, summary, tabs
*/
/* Product page */
.product-layout {
display: grid;
margin-bottom: 40px;
grid-template-columns: 776px 544px;
align-items: start;
gap: 24px;
}
.product-gallery {
position: relative;
height: 580px;
overflow: hidden;
border: 1px solid var(--border-main);
border-radius: 16px;
background: var(--bg-secondary);
}
.gallery-images {
position: absolute;
z-index: 0;
inset: 0;
}
.gallery-image {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.gallery-image-placeholder {
background: var(--bg-secondary);
}
.gallery-badges {
position: absolute;
z-index: 2;
top: 16px;
left: 16px;
display: flex;
gap: 4px;
}
.gallery-favorite {
position: absolute;
z-index: 2;
top: 10px;
right: 10px;
width: 48px;
height: 48px;
border-radius: 50%;
}
.gallery-favorite .icon {
--icon-size: 24px;
}
.gallery-favorite.is-active {
background: var(--bg-main);
}
.gallery-favorite.is-active {
color: var(--notice-error);
}
.gallery-arrow {
position: absolute;
z-index: 2;
top: 50%;
width: 48px;
height: 48px;
border-radius: 50%;
transform: translateY(-50%);
}
.gallery-arrow-prev {
left: 8px;
}
.gallery-arrow-next {
right: 8px;
}
.gallery-dots {
position: absolute;
bottom: 10px;
left: 50%;
display: flex;
gap: 4px;
transform: translateX(-50%);
}
.gallery-dots i {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-caption);
}
.gallery-dots i.active {
background: var(--text-main);
}
.product-summary {
min-height: 652px;
padding: 32px;
border-radius: 16px;
background: var(--bg-main);
box-shadow: 0 16px 38px rgb(0 0 0 / 8%);
}
.product-summary h1 {
margin: 6px 0 3px;
font: var(--h4);
letter-spacing: var(--ls-tight);
}
.product-meta-line {
color: var(--text-caption);
font: var(--control-s);
}
.product-summary .woocommerce-product-details__short-description {
margin-bottom: 14px;
color: var(--text-caption);
font: var(--control-m);
}
.product-summary .woocommerce-product-details__short-description p {
margin: 0;
}
.attribute-table {
width: 100%;
margin: 10px 0 18px;
border-collapse: collapse;
}
.attribute-table td {
padding: 8px 0;
border-top: 1px solid var(--border-main);
font: var(--control-m);
}
.attribute-table tr:last-child td {
border-bottom: 1px solid var(--border-main);
}
.attribute-table td:first-child {
width: 42%;
color: var(--text-caption);
}
.product-option {
position: relative;
display: block;
margin: 14px 0 20px;
}
.product-option > span {
position: absolute;
z-index: 1;
top: 10px;
left: 20px;
color: var(--text-caption);
font: var(--caption);
}
.product-option select {
width: 100%;
height: 60px;
padding: 23px 50px 7px 20px;
border: 1px solid var(--border-main);
border-radius: 10px;
outline: 0;
background: var(--bg-main) var(--icon-caret-down) right 20px center / 16px no-repeat;
appearance: none;
}
.product-option .custom-select {
display: flex;
width: 100%;
}
.product-option .custom-select-trigger {
width: 100%;
height: 60px;
padding: 23px 50px 7px 20px;
border: 1px solid var(--border-main);
border-radius: 10px;
background: var(--bg-main);
}
.product-option .custom-select-trigger::after {
right: 20px;
}
.product-option .custom-select-menu {
width: 100%;
}
.product-summary > .price {
margin: 0 0 12px;
font: var(--h3);
letter-spacing: var(--ls-tight);
}
.product-summary form.cart {
display: grid;
margin: 0;
grid-template-columns: 144px minmax(0, 1fr);
gap: 12px;
}
.product-summary form.cart .quantity-control,
.product-summary form.cart .single_add_to_cart_button {
min-height: 56px;
}
.product-summary .single_add_to_cart_button {
width: 100%;
}
.product-summary .single_add_to_cart_button .icon {
--icon-size: 20px;
}
.buy-one-click {
display: flex;
width: 100%;
min-height: 56px;
margin-top: 8px;
}
.product-tabs {
margin: 0 0 64px;
}
.single-product .product-tabs {
margin-bottom: 46px;
}
.single-product .section {
padding: 16px 0 90px;
}
.tab-list {
display: flex;
margin: 0 0 28px;
padding: 0;
gap: 30px;
border-bottom: 1px solid var(--border-main);
list-style: none;
}
.tab-list a {
display: block;
padding: 14px 0 12px;
color: var(--text-caption);
text-transform: uppercase;
font: var(--upper-m);
letter-spacing: var(--ls-upper);
text-transform: uppercase;
}
.tab-list .active a {
color: var(--text-main);
border-bottom: 1px solid var(--text-main);
}
.prose {
max-width: 800px;
font: var(--body-m);
}
.prose h2,
.prose h3 {
margin: 28px 0 8px;
}
.prose h2 {
font: var(--h4);
letter-spacing: var(--ls-tight);
}
.prose h3 {
font: var(--h5);
letter-spacing: var(--ls-tight-s);
}
.prose p {
margin: 0 0 14px;
}
+548
View File
@@ -0,0 +1,548 @@
/*
* Breakpoints. Must stay after every layer it overrides
*/
/* Responsive */
@media (max-width: 1439px) {
.container {
width: min(calc(100% - 48px), 1200px);
}
.product-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.category-card {
grid-template-columns: minmax(0, 1fr) 40%;
}
.archive-shell {
grid-template-columns: 250px minmax(0, 1fr);
gap: 32px;
}
.archive-results .product-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.product-layout {
grid-template-columns: minmax(0, 1.43fr) minmax(440px, 1fr);
}
}
@media (max-width: 1023px) {
.home .site-header {
height: 120px;
}
.hero-copy,
.hero h1 {
width: 100%;
max-width: 100%;
}
.hero h1 {
white-space: normal;
}
.header-inner {
gap: 16px;
}
.menu-toggle {
display: grid;
margin-left: auto;
}
.main-nav {
position: fixed;
z-index: 50;
inset: 88px 0 0;
display: none;
margin: 0;
padding: 30px 24px;
background: var(--bg-main);
}
.menu-open .main-nav {
display: block;
}
.main-nav ul {
align-items: stretch;
flex-direction: column;
gap: 0;
}
.main-nav a {
padding: 15px 0;
border-bottom: 1px solid var(--border-main);
font: var(--lead);
}
.product-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.category-grid,
.news-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.archive-shell,
.product-layout,
.cart-layout,
.checkout-layout {
grid-template-columns: 1fr;
}
.cart-order-card {
width: 100%;
}
.filters {
padding: 20px;
border-radius: 12px;
background: var(--bg-secondary);
}
.archive-results .product-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.product-gallery {
height: min(70vw, 580px);
}
.product-summary {
min-height: 0;
}
.footer-bottom {
grid-template-columns: 1fr auto;
}
.footer-bottom > span:nth-child(2) {
order: 3;
grid-column: 1 / -1;
}
}
@media (max-width: 639px) {
.home .site-header {
height: 72px;
}
.container {
width: calc(100% - 32px);
}
.header-inner {
height: 72px;
}
.main-nav {
inset: 72px 0 0;
}
.cart-link {
width: 48px;
height: 48px;
}
.shop-toast-stack,
.home .shop-toast-stack {
top: 88px;
right: 16px;
width: calc(100vw - 32px);
}
.section {
padding: 48px 0;
}
.hero {
height: auto;
min-height: 0;
padding: 44px 0 48px;
}
.hero-title-group > p,
.hero-features {
width: 100%;
max-width: 100%;
}
.hero-features {
grid-template-columns: 1fr;
gap: 14px;
}
.products-section {
height: auto;
overflow: visible;
}
.product-grid,
.category-grid,
.news-grid,
.archive-results .product-grid {
grid-template-columns: 1fr;
}
.category-card {
height: 230px;
grid-template-columns: minmax(0, 1fr) 38%;
}
.news-card {
height: 360px;
}
.pagination .nav-links,
.woocommerce nav.woocommerce-pagination ul {
flex-wrap: wrap;
}
.pagination .page-numbers,
.woocommerce nav.woocommerce-pagination ul li a,
.woocommerce nav.woocommerce-pagination ul li span {
width: 40px;
height: 40px;
}
.slider-controls {
display: none;
}
/* The design keeps the filter button and the sort control on one row; the
sort label stacks above its value so the pair fits at 360px. */
.archive-toolbar {
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
gap: 12px;
}
.archive-toolbar .sort-control {
display: flex;
min-width: 0;
margin-left: auto;
align-items: flex-end;
flex-direction: column;
gap: 0;
text-align: right;
}
.archive-toolbar .sort-control > span {
font: var(--caption);
}
/* The mobile layout does not expose the per-page control. */
.archive-toolbar .per-page-control {
display: none;
}
.product-gallery {
height: 400px;
}
.product-summary {
padding: 22px;
}
.product-summary form.cart {
grid-template-columns: 120px minmax(0, 1fr);
}
.tab-list {
overflow-x: auto;
}
.tab-list a {
white-space: nowrap;
}
.article-media,
.article-media-wide {
height: 62vw;
}
.article-lead {
margin-bottom: 32px;
}
.cart-table thead {
display: none;
}
.cart-table tr,
.cart-table td {
display: block;
}
.cart-table tr {
padding: 16px 0;
border-bottom: 1px solid var(--border-main);
}
.cart-table td {
padding: 7px 0;
border: 0;
}
.cart-page {
width: 100%;
height: 622px;
padding: 8px 16px 32px;
}
.checkout-fields {
width: 100%;
}
.checkout-page {
width: 100%;
min-height: 1278px;
padding: 8px 16px 32px;
}
.checkout-page .page-heading {
margin-bottom: 14px;
}
.checkout-layout {
gap: 32px;
}
.checkout-input,
.checkout-map {
width: 100%;
}
.checkout-input,
.checkout-input input {
height: 64px;
}
.checkout-delivery,
.checkout-payment-options {
margin-top: 24px;
}
.checkout-delivery legend,
.checkout-payment-options legend {
margin-bottom: 8px;
}
.delivery-switch {
height: 48px;
margin-bottom: 16px;
}
.delivery-switch span {
height: 40px;
}
.checkout-radio {
min-height: 24px;
}
.checkout-map {
height: auto;
margin-top: 16px;
aspect-ratio: 1.44;
}
.checkout-payment-options {
margin-top: 24px;
}
.checkout-order-card {
width: 100%;
min-height: 408px;
height: 408px;
padding: 16px;
}
.site-footer {
height: auto;
min-height: 210px;
}
.footer-bottom {
grid-template-columns: 1fr;
}
.footer-bottom > span:nth-child(2) {
grid-column: auto;
flex-direction: column;
gap: 10px;
}
}
@media (max-width: 767px) {
/* The mobile frame shows the same row, edge to edge and without the
desktop next button — a touch drag does that job. */
.subcategories {
margin-inline: calc(var(--container-gutter) * -1);
padding-inline: var(--container-gutter);
}
.subcategory-next {
display: none;
}
.subcategory-card {
width: 200px;
}
.quick-filters {
flex-wrap: nowrap;
margin-inline: calc(var(--container-gutter) * -1);
padding-inline: var(--container-gutter);
overflow-x: auto;
scrollbar-width: none;
}
.quick-filters::-webkit-scrollbar {
height: 0;
}
.quick-filter {
flex: 0 0 auto;
}
}
/*
* The cart row needs about 550px before its columns start colliding, and the
* summary beside it is a fixed 432px. Below the width where both fit, they
* stack — otherwise the summary ends up sitting on top of the items.
*/
@media (max-width: 1199px) {
.cart-layout,
.checkout-layout {
grid-template-columns: minmax(0, 1fr);
}
.cart-order-card {
width: 100%;
}
/* Once the summary drops below the fields the checkout is one column, so
centre it rather than leaving both blocks pinned to the left edge. */
.checkout-layout .checkout-fields,
.checkout-layout .checkout-order-card {
width: 432px;
max-width: 100%;
margin-inline: auto;
}
}
/*
* The cart row needs about 550px before its columns collide. Below that it
* takes the stacked shape from the mobile frame — thumbnail top left, copy
* beside it, price and controls on the bottom row.
*/
@media (max-width: 767px) {
.cart-page .page-heading {
margin-bottom: 8px;
}
.cart-layout {
display: flex;
flex-direction: column;
gap: 16px;
}
.cart-items {
width: 100%;
gap: 8px;
}
.cart-item {
position: relative;
display: block;
width: 100%;
min-height: 150px;
height: 150px;
padding: 12px;
}
.cart-thumb {
position: absolute;
top: 12px;
left: 12px;
width: 88px;
height: 70px;
}
.cart-thumb .product-badge {
display: none;
}
.cart-item-copy {
position: absolute;
top: 16px;
right: 12px;
left: 112px;
}
.cart-item-price {
position: absolute;
bottom: 16px;
left: 12px;
text-align: left;
}
.cart-item-quantity {
position: absolute;
right: 64px;
bottom: 16px;
width: 104px;
height: 44px;
}
.cart-item-remove {
position: absolute;
right: 12px;
bottom: 16px;
width: 44px;
height: 44px;
}
.cart-order-card {
width: 100%;
min-height: 212px;
height: 212px;
padding: 16px;
}
.cart-order-card .order-row {
min-height: 28px;
padding: 2px 0;
}
.cart-order-card .order-row.total {
min-height: 36px;
margin: 0;
padding-top: 4px;
}
.cart-order-card .button {
height: 56px;
margin-top: 12px;
}
}
@media (max-width: 767px) {
.checkout-input input {
padding: 25px 20px 15px;
}
.checkout-input input:focus + .field-label,
.checkout-input input:not(:placeholder-shown) + .field-label {
top: 10px;
}
}
+188
View File
@@ -0,0 +1,188 @@
/*
* Design tokens.
*
* A 1:1 transcription of the Figma variable collections (`colors`, `borders`,
* `size`) and text styles. Nothing below this file may invent a colour, a
* radius or a type size — everything resolves to a token defined here.
*/
:root {
/* text */
--text-main: #000;
--text-body: rgb(0 0 0 / 80%);
--text-caption: rgb(0 0 0 / 40%);
--text-disable: rgb(0 0 0 / 24%);
--text-main-contrast: #fff;
--text-body-contrast: rgb(255 255 255 / 88%);
--text-caption-contrast: rgb(255 255 255 / 48%);
--text-disable-contrast: rgb(255 255 255 / 24%);
--text-link: #3b67db;
--text-link-hover: #294ca7;
--text-negative: #d7202b;
--text-positive: #82b443;
--text-white: #fff;
--text-black: #000;
/* bg */
--bg-main: #fff;
--bg-secondary: #f4f4f4;
--bg-main-contrast: #000;
--bg-secondary-contrast: #212121;
--bg-button: #010101;
--bg-button-hover: #373737;
--bg-button-pressed: #000;
--bg-button-light: #f3f3f3;
--bg-button-light-hover: #e2e2e2;
--bg-button-light-pressed: #f0f0f0;
--bg-control-disable: rgb(0 0 0 / 6%);
--bg-select-unit: #fff;
--bg-select-unit-hover: #f4f4f4;
--bg-select-unit-selected: #ededed;
--bg-modal-shadow: rgb(0 0 0 / 64%);
/* border */
--border-main: rgb(0 0 0 / 8%);
--border-secondary: rgb(0 0 0 / 12%);
--border-link: rgb(48 93 209 / 24%);
--border-link-hover: rgb(41 76 167 / 32%);
--border-link-main: rgb(0 0 0 / 16%);
--border-link-main-hover: rgb(0 0 0 / 24%);
--border-checkbox: rgb(0 0 0 / 24%);
--border-checkbox-hover: rgb(0 0 0 / 32%);
--border-input: rgb(0 0 0 / 12%);
--border-input-hover: rgb(0 0 0 / 16%);
--border-input-focus: #393939;
--border-input-error: #d7202b;
--border-control: rgb(0 0 0 / 12%);
--border-control-hover: rgb(0 0 0 / 20%);
--border-control-pressed: rgb(0 0 0 / 12%);
--border-control-disable: rgb(0 0 0 / 8%);
/* notice */
--notice-info: #4299ff;
--notice-success: #8cca3e;
--notice-warning: #f1ab43;
--notice-error: #f5473c;
/* radii — desktop values, overridden for mobile below */
--br-control-l: 16px;
--br-control-m: 8px;
--br-btn-l: 12px;
--br-btn-m: 8px;
--br-section-l: 24px;
--br-section-m: 16px;
--br-modal: 16px;
--br-checkbox: 4px;
/* sizes */
--margin-main-top: 16px;
--margin-main-bottom: 48px;
--margin-header-bottom: 32px;
--h-input-l: 72px;
--h-text-area-l: 136px;
--pad-h-btn-l: 24px;
--pad-h-btn-m: 16px;
--pad-w-text-l: 28px;
--pad-w-text-m: 20px;
--pad-w-icon-l: 24px;
--pad-w-icon-m: 16px;
/* type scale — desktop */
--font-sans: "Inter", system-ui, -apple-system, "Segoe UI", arial, sans-serif;
--h1: 700 68px/72px var(--font-sans);
--h2: 700 48px/56px var(--font-sans);
--h3: 700 38px/42px var(--font-sans);
--h4: 700 28px/32px var(--font-sans);
--h5: 700 20px/24px var(--font-sans);
--lead: 400 20px/28px var(--font-sans);
--body-m: 400 16px/24px var(--font-sans);
--control-m: 400 16px/24px var(--font-sans);
--control-s: 400 14px/20px var(--font-sans);
--caption: 400 12px/16px var(--font-sans);
--upper-l: 600 16px/24px var(--font-sans);
--upper-m: 600 14px/24px var(--font-sans);
--upper-s: 600 12px/16px var(--font-sans);
--accent-m: 700 16px/24px var(--font-sans);
--ls-tight: -0.03em;
--ls-tight-s: -0.02em;
--ls-upper: 0.02em;
/* layout */
--container: 1344px;
--container-gutter: 24px;
/* motion — Figma "Slow" spring: mass 1, stiffness 80, damping 20, 400ms */
--motion-fast: 100ms ease-in-out;
--motion-slow-duration: 400ms;
--motion-slow: linear(
0 0%,
0.0257 6.25%,
0.0879 12.5%,
0.1696 18.75%,
0.2601 25%,
0.3523 31.25%,
0.4419 37.5%,
0.5265 43.75%,
0.6047 50%,
0.6759 56.25%,
0.7401 62.5%,
0.7976 68.75%,
0.8488 75%,
0.894 81.25%,
0.934 87.5%,
0.9691 93.75%,
1 100%
);
/* effects */
--shadow-s: 0 2px 8px rgb(0 0 0 / 6%);
--shadow-m: 0 8px 32px rgb(0 0 0 / 10%);
/*
* The checkbox tick. A native `input` cannot hold a child element, so the
* Phosphor `check-bold` vector is carried as a mask — still the real SVG
* path, tinted with `currentColor`, and it covers checkboxes WooCommerce
* prints for itself.
*/
--icon-check: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z"/></svg>');
--icon-x: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></svg>');
--icon-caret-down: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/></svg>');
--icon-caret-up: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z"/></svg>');
--icon-check-circle-fill: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"/></svg>');
--icon-info-fill: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm-4,48a12,12,0,1,1-12,12A12,12,0,0,1,124,72Zm12,112a16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40a8,8,0,0,1,0,16Z"/></svg>');
--icon-warning-fill: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM120,104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,88a12,12,0,1,1,12-12A12,12,0,0,1,128,192Z"/></svg>');
--icon-x-circle-fill: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm37.66,130.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></svg>');
}
@media (max-width: 767px) {
:root {
--br-control-l: 12px;
--br-btn-l: 8px;
--br-section-l: 16px;
--br-section-m: 12px;
--margin-main-top: 8px;
--margin-main-bottom: 32px;
--margin-header-bottom: 16px;
--h-input-l: 64px;
--h-text-area-l: 128px;
--pad-h-btn-l: 16px;
--pad-h-btn-m: 10px;
--pad-w-text-l: 24px;
--pad-w-text-m: 16px;
--pad-w-icon-l: 16px;
--pad-w-icon-m: 10px;
--h1: 700 32px/36px var(--font-sans);
--h2: 700 26px/30px var(--font-sans);
--h3: 700 22px/26px var(--font-sans);
--h4: 700 20px/22px var(--font-sans);
--h5: 700 16px/20px var(--font-sans);
--lead: 400 16px/24px var(--font-sans);
--upper-l: 600 14px/18px var(--font-sans);
--upper-m: 600 12px/16px var(--font-sans);
--upper-s: 600 10px/14px var(--font-sans);
--container-gutter: 16px;
}
}
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
# Local fonts
- `Inter.ttf` — Inter variable font by Rasmus Andersson, SIL Open Font License 1.1.
The files are distributed locally so the layout does not depend on a third-party font CDN.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,128a8,8,0,0,1-8,8H120v64a8,8,0,0,1-13.66,5.66l-72-72a8,8,0,0,1,0-11.32l72-72A8,8,0,0,1,120,56v64h96A8,8,0,0,1,224,128Z"/></svg>

After

Width:  |  Height:  |  Size: 223 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"/></svg>

After

Width:  |  Height:  |  Size: 248 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.7806 12.5307L14.0306 19.2807C13.9257 19.3857 13.792 19.4573 13.6465 19.4862C13.5009 19.5152 13.35 19.5004 13.2129 19.4436C13.0758 19.3868 12.9586 19.2905 12.8762 19.1671C12.7938 19.0436 12.7499 18.8985 12.75 18.7501V12.7501H3.75C3.55109 12.7501 3.36032 12.6711 3.21967 12.5304C3.07902 12.3898 3 12.199 3 12.0001C3 11.8012 3.07902 11.6104 3.21967 11.4698C3.36032 11.3291 3.55109 11.2501 3.75 11.2501H12.75V5.2501C12.7499 5.10168 12.7938 4.95656 12.8762 4.83312C12.9586 4.70967 13.0758 4.61345 13.2129 4.55664C13.35 4.49982 13.5009 4.48497 13.6465 4.51396C13.792 4.54294 13.9257 4.61447 14.0306 4.71948L20.7806 11.4695C20.8504 11.5391 20.9057 11.6218 20.9434 11.7129C20.9812 11.8039 21.0006 11.9015 21.0006 12.0001C21.0006 12.0987 20.9812 12.1963 20.9434 12.2873C20.9057 12.3784 20.8504 12.4611 20.7806 12.5307Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 942 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.7806 12.5306L14.0306 19.2806C13.8899 19.4213 13.699 19.5004 13.5 19.5004C13.301 19.5004 13.1101 19.4213 12.9694 19.2806C12.8286 19.1399 12.7496 18.949 12.7496 18.75C12.7496 18.551 12.8286 18.3601 12.9694 18.2194L18.4397 12.75H3.75C3.55109 12.75 3.36032 12.671 3.21967 12.5303C3.07902 12.3897 3 12.1989 3 12C3 11.8011 3.07902 11.6103 3.21967 11.4697C3.36032 11.329 3.55109 11.25 3.75 11.25H18.4397L12.9694 5.78061C12.8286 5.63988 12.7496 5.44901 12.7496 5.24999C12.7496 5.05097 12.8286 4.8601 12.9694 4.71936C13.1101 4.57863 13.301 4.49957 13.5 4.49957C13.699 4.49957 13.8899 4.57863 14.0306 4.71936L20.7806 11.4694C20.8504 11.539 20.9057 11.6217 20.9434 11.7128C20.9812 11.8038 21.0006 11.9014 21.0006 12C21.0006 12.0986 20.9812 12.1961 20.9434 12.2872C20.9057 12.3782 20.8504 12.461 20.7806 12.5306Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 933 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"/></svg>

After

Width:  |  Height:  |  Size: 188 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/></svg>

After

Width:  |  Height:  |  Size: 214 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z"/></svg>

After

Width:  |  Height:  |  Size: 200 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"/></svg>

After

Width:  |  Height:  |  Size: 219 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.4728 6.95062L20.0691 15.6019C19.9369 16.0745 19.6542 16.4911 19.2639 16.7885C18.8736 17.0859 18.397 17.2479 17.9062 17.25H8.64C8.14784 17.2498 7.66926 17.0886 7.27725 16.791C6.88523 16.4935 6.6013 16.0758 6.46875 15.6019L3.18 3.75H1.5C1.30109 3.75 1.11032 3.67098 0.96967 3.53033C0.829018 3.38968 0.75 3.19891 0.75 3C0.75 2.80109 0.829018 2.61032 0.96967 2.46967C1.11032 2.32902 1.30109 2.25 1.5 2.25H3.75C3.91397 2.24997 4.07343 2.30367 4.20398 2.40289C4.33452 2.50211 4.42895 2.64138 4.47281 2.79938L5.36156 6H21.75C21.8656 5.99998 21.9797 6.02669 22.0833 6.07805C22.1869 6.1294 22.2772 6.20401 22.3472 6.29605C22.4171 6.38809 22.4649 6.49506 22.4867 6.60861C22.5085 6.72216 22.5037 6.83922 22.4728 6.95062ZM8.25 18.75C7.95333 18.75 7.66332 18.838 7.41665 19.0028C7.16997 19.1676 6.97771 19.4019 6.86418 19.676C6.75065 19.9501 6.72094 20.2517 6.77882 20.5426C6.8367 20.8336 6.97956 21.1009 7.18934 21.3107C7.39912 21.5204 7.66639 21.6633 7.95736 21.7212C8.24834 21.7791 8.54994 21.7494 8.82403 21.6358C9.09811 21.5223 9.33238 21.33 9.4972 21.0834C9.66203 20.8367 9.75 20.5467 9.75 20.25C9.75 19.8522 9.59196 19.4706 9.31066 19.1893C9.02936 18.908 8.64782 18.75 8.25 18.75ZM18 18.75C17.7033 18.75 17.4133 18.838 17.1666 19.0028C16.92 19.1676 16.7277 19.4019 16.6142 19.676C16.5006 19.9501 16.4709 20.2517 16.5288 20.5426C16.5867 20.8336 16.7296 21.1009 16.9393 21.3107C17.1491 21.5204 17.4164 21.6633 17.7074 21.7212C17.9983 21.7791 18.2999 21.7494 18.574 21.6358C18.8481 21.5223 19.0824 21.33 19.2472 21.0834C19.412 20.8367 19.5 20.5467 19.5 20.25C19.5 19.8522 19.342 19.4706 19.0607 19.1893C18.7794 18.908 18.3978 18.75 18 18.75Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z"/></svg>

After

Width:  |  Height:  |  Size: 203 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"/></svg>

After

Width:  |  Height:  |  Size: 268 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.0306 9.53068L12.5306 17.0307C12.461 17.1004 12.3782 17.1557 12.2872 17.1935C12.1961 17.2312 12.0986 17.2506 12 17.2506C11.9014 17.2506 11.8038 17.2312 11.7128 17.1935C11.6217 17.1557 11.539 17.1004 11.4694 17.0307L3.96936 9.53068C3.82863 9.38995 3.74957 9.19907 3.74957 9.00005C3.74957 8.80103 3.82863 8.61016 3.96936 8.46943C4.1101 8.32869 4.30097 8.24963 4.49999 8.24963C4.69901 8.24963 4.88988 8.32869 5.03061 8.46943L12 15.4397L18.9694 8.46943C19.039 8.39974 19.1218 8.34447 19.2128 8.30676C19.3039 8.26904 19.4014 8.24963 19.5 8.24963C19.5985 8.24963 19.6961 8.26904 19.7872 8.30676C19.8782 8.34447 19.9609 8.39974 20.0306 8.46943C20.1003 8.53911 20.1556 8.62183 20.1933 8.71288C20.231 8.80392 20.2504 8.9015 20.2504 9.00005C20.2504 9.0986 20.231 9.19618 20.1933 9.28722C20.1556 9.37827 20.1003 9.46099 20.0306 9.53068Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 957 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M96.26,37.05A8,8,0,0,1,102,27.29a104.11,104.11,0,0,1,52,0,8,8,0,0,1-2,15.75,8.15,8.15,0,0,1-2-.26,88.09,88.09,0,0,0-44,0A8,8,0,0,1,96.26,37.05ZM53.79,55.14a104.05,104.05,0,0,0-26,45,8,8,0,0,0,15.42,4.27,88,88,0,0,1,22-38.09A8,8,0,0,0,53.79,55.14ZM43.21,151.55a8,8,0,1,0-15.42,4.28,104.12,104.12,0,0,0,26,45,8,8,0,0,0,11.41-11.22A88.14,88.14,0,0,1,43.21,151.55ZM150,213.22a88,88,0,0,1-44,0,8,8,0,1,0-4,15.49,104.11,104.11,0,0,0,52,0,8,8,0,0,0-4-15.49ZM222.65,146a8,8,0,0,0-9.85,5.58,87.91,87.91,0,0,1-22,38.08,8,8,0,1,0,11.42,11.21,104,104,0,0,0,26-45A8,8,0,0,0,222.65,146Zm-9.86-41.54a8,8,0,0,0,15.42-4.28,104,104,0,0,0-26-45,8,8,0,1,0-11.41,11.22A88,88,0,0,1,212.79,104.45Z"/></svg>

After

Width:  |  Height:  |  Size: 774 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.1929 15.2869C20.1362 15.4239 20.0401 15.5411 19.9168 15.6235C19.7934 15.7059 19.6484 15.75 19.5001 15.75H4.5001C4.35168 15.7501 4.20656 15.7062 4.08312 15.6238C3.95967 15.5414 3.86345 15.4242 3.80664 15.2871C3.74982 15.15 3.73497 14.9991 3.76396 14.8535C3.79294 14.708 3.86447 14.5743 3.96948 14.4694L11.4695 6.96937C11.5391 6.89964 11.6218 6.84432 11.7129 6.80658C11.8039 6.76883 11.9015 6.7494 12.0001 6.7494C12.0987 6.7494 12.1963 6.76883 12.2873 6.80658C12.3784 6.84432 12.4611 6.89964 12.5307 6.96937L20.0307 14.4694C20.1356 14.5743 20.207 14.708 20.2358 14.8535C20.2647 14.999 20.2498 15.1498 20.1929 15.2869Z" fill="black" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 768 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M230.6,49.53A15.81,15.81,0,0,0,216,40H40A16,16,0,0,0,28.19,66.76l.08.09L96,139.17V216a16,16,0,0,0,24.87,13.32l32-21.34A16,16,0,0,0,160,194.66V139.17l67.74-72.32.08-.09A15.8,15.8,0,0,0,230.6,49.53ZM40,56h0Zm106.18,74.58A8,8,0,0,0,144,136v58.66L112,216V136a8,8,0,0,0-2.16-5.47L40,56H216Z"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.5 9.5625C22.5 16.125 12.7697 21.4369 12.3553 21.6562C12.2461 21.715 12.124 21.7458 12 21.7458C11.876 21.7458 11.7539 21.715 11.6447 21.6562C11.2303 21.4369 1.5 16.125 1.5 9.5625C1.50174 8.02146 2.11468 6.54404 3.20436 5.45436C4.29404 4.36468 5.77146 3.75174 7.3125 3.75C9.24844 3.75 10.9434 4.5825 12 5.98969C13.0566 4.5825 14.7516 3.75 16.6875 3.75C18.2285 3.75174 19.706 4.36468 20.7956 5.45436C21.8853 6.54404 22.4983 8.02146 22.5 9.5625Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6875 3.75C14.7516 3.75 13.0566 4.5825 12 5.98969C10.9434 4.5825 9.24844 3.75 7.3125 3.75C5.77146 3.75174 4.29404 4.36468 3.20436 5.45436C2.11468 6.54404 1.50174 8.02146 1.5 9.5625C1.5 16.125 11.2303 21.4369 11.6447 21.6562C11.7539 21.715 11.876 21.7458 12 21.7458C12.124 21.7458 12.2461 21.715 12.3553 21.6562C12.7697 21.4369 22.5 16.125 22.5 9.5625C22.4983 8.02146 21.8853 6.54404 20.7956 5.45436C19.706 4.36468 18.2285 3.75174 16.6875 3.75ZM12 20.1375C10.2881 19.14 3 14.5959 3 9.5625C3.00149 8.41921 3.45632 7.32317 4.26475 6.51475C5.07317 5.70632 6.16921 5.25149 7.3125 5.25C9.13594 5.25 10.6669 6.22125 11.3062 7.78125C11.3628 7.91881 11.4589 8.03646 11.5824 8.11926C11.7059 8.20207 11.8513 8.24627 12 8.24627C12.1487 8.24627 12.2941 8.20207 12.4176 8.11926C12.5411 8.03646 12.6372 7.91881 12.6937 7.78125C13.3331 6.21844 14.8641 5.25 16.6875 5.25C17.8308 5.25149 18.9268 5.70632 19.7353 6.51475C20.5437 7.32317 20.9985 8.41921 21 9.5625C21 14.5884 13.71 19.1391 12 20.1375Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm-4,48a12,12,0,1,1-12,12A12,12,0,0,1,124,72Zm12,112a16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40a8,8,0,0,1,0,16Z"/></svg>

After

Width:  |  Height:  |  Size: 282 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"/></svg>

After

Width:  |  Height:  |  Size: 253 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/></svg>

After

Width:  |  Height:  |  Size: 243 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 12C21 12.1989 20.921 12.3897 20.7803 12.5303C20.6397 12.671 20.4489 12.75 20.25 12.75H3.75C3.55109 12.75 3.36032 12.671 3.21967 12.5303C3.07902 12.3897 3 12.1989 3 12C3 11.8011 3.07902 11.6103 3.21967 11.4697C3.36032 11.329 3.55109 11.25 3.75 11.25H20.25C20.4489 11.25 20.6397 11.329 20.7803 11.4697C20.921 11.6103 21 11.8011 21 12Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 465 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M209.66,122.34a8,8,0,0,1,0,11.32l-82.05,82a56,56,0,0,1-79.2-79.21L147.67,35.73a40,40,0,1,1,56.61,56.55L105,193A24,24,0,1,1,71,159L154.3,74.38A8,8,0,1,1,165.7,85.6L82.39,170.31a8,8,0,1,0,11.27,11.36L192.93,81A24,24,0,1,0,159,47L59.76,147.68a40,40,0,1,0,56.53,56.62l82.06-82A8,8,0,0,1,209.66,122.34Z"/></svg>

After

Width:  |  Height:  |  Size: 397 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 12C21 12.1989 20.921 12.3897 20.7803 12.5303C20.6397 12.671 20.4489 12.75 20.25 12.75H12.75V20.25C12.75 20.4489 12.671 20.6397 12.5303 20.7803C12.3897 20.921 12.1989 21 12 21C11.8011 21 11.6103 20.921 11.4697 20.7803C11.329 20.6397 11.25 20.4489 11.25 20.25V12.75H3.75C3.55109 12.75 3.36032 12.671 3.21967 12.5303C3.07902 12.3897 3 12.1989 3 12C3 11.8011 3.07902 11.6103 3.21967 11.4697C3.36032 11.329 3.55109 11.25 3.75 11.25H11.25V3.75C11.25 3.55109 11.329 3.36032 11.4697 3.21967C11.6103 3.07902 11.8011 3 12 3C12.1989 3 12.3897 3.07902 12.5303 3.21967C12.671 3.36032 12.75 3.55109 12.75 3.75V11.25H20.25C20.4489 11.25 20.6397 11.329 20.7803 11.4697C20.921 11.6103 21 11.8011 21 12Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 818 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.25 4.5H16.5V3.75C16.5 3.15326 16.2629 2.58097 15.841 2.15901C15.419 1.73705 14.8467 1.5 14.25 1.5H9.75C9.15326 1.5 8.58097 1.73705 8.15901 2.15901C7.73705 2.58097 7.5 3.15326 7.5 3.75V4.5H3.75C3.55109 4.5 3.36032 4.57902 3.21967 4.71967C3.07902 4.86032 3 5.05109 3 5.25C3 5.44891 3.07902 5.63968 3.21967 5.78033C3.36032 5.92098 3.55109 6 3.75 6H4.5V19.5C4.5 19.8978 4.65804 20.2794 4.93934 20.5607C5.22064 20.842 5.60218 21 6 21H18C18.3978 21 18.7794 20.842 19.0607 20.5607C19.342 20.2794 19.5 19.8978 19.5 19.5V6H20.25C20.4489 6 20.6397 5.92098 20.7803 5.78033C20.921 5.63968 21 5.44891 21 5.25C21 5.05109 20.921 4.86032 20.7803 4.71967C20.6397 4.57902 20.4489 4.5 20.25 4.5ZM9 3.75C9 3.55109 9.07902 3.36032 9.21967 3.21967C9.36032 3.07902 9.55109 3 9.75 3H14.25C14.4489 3 14.6397 3.07902 14.7803 3.21967C14.921 3.36032 15 3.55109 15 3.75V4.5H9V3.75ZM18 19.5H6V6H18V19.5ZM10.5 9.75V15.75C10.5 15.9489 10.421 16.1397 10.2803 16.2803C10.1397 16.421 9.94891 16.5 9.75 16.5C9.55109 16.5 9.36032 16.421 9.21967 16.2803C9.07902 16.1397 9 15.9489 9 15.75V9.75C9 9.55109 9.07902 9.36032 9.21967 9.21967C9.36032 9.07902 9.55109 9 9.75 9C9.94891 9 10.1397 9.07902 10.2803 9.21967C10.421 9.36032 10.5 9.55109 10.5 9.75ZM15 9.75V15.75C15 15.9489 14.921 16.1397 14.7803 16.2803C14.6397 16.421 14.4489 16.5 14.25 16.5C14.0511 16.5 13.8603 16.421 13.7197 16.2803C13.579 16.1397 13.5 15.9489 13.5 15.75V9.75C13.5 9.55109 13.579 9.36032 13.7197 9.21967C13.8603 9.07902 14.0511 9 14.25 9C14.4489 9 14.6397 9.07902 14.7803 9.21967C14.921 9.36032 15 9.55109 15 9.75Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm-8,56a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm8,104a12,12,0,1,1,12-12A12,12,0,0,1,128,184Z"/></svg>

After

Width:  |  Height:  |  Size: 242 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM120,104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,88a12,12,0,1,1,12-12A12,12,0,0,1,128,192Z"/></svg>

After

Width:  |  Height:  |  Size: 373 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm37.66,130.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></svg>

After

Width:  |  Height:  |  Size: 341 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></svg>

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.07 37.49" fill="currentColor" role="img" aria-label="ЛОГОТИП"><g transform="scale(0.78 1)"><path d="M3.374 37.492Q1.710 37.492 0.722 36.348Q-0.266 35.204 0.098 32.708Q1.606 32.708 2.516 31.096Q3.426 29.484 3.946 25.896Q4.466 22.308 4.778 16.484Q5.090 10.660 5.454 2.236Q5.506 1.508 5.740 1.144Q5.974 0.780 7.014 0.650Q8.054 0.520 10.446 0.520L19.702 0.520L19.702 35.880Q19.702 36.400 19.468 36.608Q19.234 36.816 18.012 36.868Q16.790 36.920 13.878 36.920L13.878 5.980L10.914 5.980Q10.498 16.848 9.666 23.842Q8.834 30.836 7.326 34.164Q5.818 37.492 3.374 37.492M31.714 37.440Q29.166 37.440 27.372 36.634Q25.578 35.828 24.486 33.748Q23.394 31.668 22.874 27.976Q22.354 24.284 22.354 18.512Q22.354 11.440 23.446 7.410Q24.538 3.380 26.748 1.690Q28.958 0 32.286 0Q34.834 0 36.602 0.858Q38.370 1.716 39.462 3.770Q40.554 5.824 41.048 9.360Q41.542 12.896 41.542 18.304Q41.542 25.636 40.502 29.796Q39.462 33.956 37.304 35.698Q35.146 37.440 31.714 37.440M32.026 31.408Q33.326 31.408 33.976 30.108Q34.626 28.808 34.886 25.948Q35.146 23.088 35.146 18.512Q35.146 13.988 34.964 11.232Q34.782 8.476 34.158 7.254Q33.534 6.032 32.182 6.032Q30.934 6.032 30.258 7.358Q29.582 8.684 29.296 11.700Q29.010 14.716 29.010 19.812Q29.010 23.868 29.218 26.416Q29.426 28.964 30.076 30.186Q30.726 31.408 32.026 31.408M49.082 36.920L44.194 36.920L44.194 1.560Q44.194 1.560 44.350 1.040Q44.506 0.520 45.234 0.520L57.298 0.520Q57.298 2.860 57.246 3.900Q57.194 4.940 56.986 5.200Q56.778 5.460 56.258 5.460L50.018 5.460L50.018 35.880Q50.018 35.880 49.914 36.400Q49.810 36.920 49.082 36.920M68.218 37.440Q65.670 37.440 63.876 36.634Q62.082 35.828 60.990 33.748Q59.898 31.668 59.378 27.976Q58.858 24.284 58.858 18.512Q58.858 11.440 59.950 7.410Q61.042 3.380 63.252 1.690Q65.462 0 68.790 0Q71.338 0 73.106 0.858Q74.874 1.716 75.966 3.770Q77.058 5.824 77.552 9.360Q78.046 12.896 78.046 18.304Q78.046 25.636 77.006 29.796Q75.966 33.956 73.808 35.698Q71.650 37.440 68.218 37.440M68.530 31.408Q69.830 31.408 70.480 30.108Q71.130 28.808 71.390 25.948Q71.650 23.088 71.650 18.512Q71.650 13.988 71.468 11.232Q71.286 8.476 70.662 7.254Q70.038 6.032 68.686 6.032Q67.438 6.032 66.762 7.358Q66.086 8.684 65.800 11.700Q65.514 14.716 65.514 19.812Q65.514 23.868 65.722 26.416Q65.930 28.964 66.580 30.186Q67.230 31.408 68.530 31.408M85.950 36.920L85.950 1.040Q85.950 1.040 86.080 1.040Q86.210 1.040 86.990 1.040L90.734 1.040Q91.514 1.040 91.644 1.040Q91.774 1.040 91.774 1.040L91.774 35.880Q91.774 36.400 91.514 36.608Q91.254 36.816 90.032 36.868Q88.810 36.920 85.950 36.920M97.286 5.980L79.606 5.980L79.606 1.560Q79.606 0.520 80.646 0.520L98.326 0.520L98.326 4.940Q98.326 5.564 98.092 5.772Q97.858 5.980 97.286 5.980M100.458 36.920L100.458 1.508Q100.458 0.988 100.666 0.806Q100.874 0.624 101.992 0.572Q103.110 0.520 105.918 0.520L105.918 15.028Q105.918 16.640 105.866 18.174Q105.814 19.708 105.684 21.476Q105.554 23.244 105.294 25.688Q106.022 22.828 106.854 20.358Q107.686 17.888 108.518 15.340L112.730 1.872Q112.938 1.196 113.276 0.910Q113.614 0.624 114.628 0.572Q115.642 0.520 117.878 0.520L117.878 35.932Q117.878 36.400 117.566 36.608Q117.254 36.816 116.110 36.868Q114.966 36.920 112.418 36.920L112.418 24.024Q112.418 22.932 112.470 21.450Q112.522 19.968 112.626 18.356Q112.730 16.744 112.860 15.184Q112.990 13.624 113.146 12.376Q112.626 15.132 111.976 17.680Q111.326 20.228 110.234 23.348L105.918 35.568Q105.710 36.244 105.398 36.530Q105.086 36.816 104.020 36.868Q102.954 36.920 100.458 36.920M121.102 36.920L121.102 1.560Q121.102 1.040 121.362 0.832Q121.622 0.624 122.844 0.572Q124.066 0.520 126.926 0.520L137.274 0.520L137.274 35.880Q137.274 36.400 137.014 36.608Q136.754 36.816 135.558 36.868Q134.362 36.920 131.450 36.920L131.450 5.980L126.926 5.980L126.926 35.880Q126.926 36.400 126.666 36.608Q126.406 36.816 125.210 36.868Q124.014 36.920 121.102 36.920"/></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+986
View File
@@ -0,0 +1,986 @@
(() => {
const body = document.body;
const menuButton = document.querySelector(".menu-toggle");
/*
* Icons rendered from script come from the same `assets/icons/*.svg` files
* the PHP component uses; the theme passes the markup through
* `window.vyatkaShopIcons`. Never draw an icon with a text glyph.
*/
const getIcon = (name) => {
const svg = (window.vyatkaShopIcons || {})[name];
return typeof svg === "string" ? svg : "";
};
menuButton?.addEventListener("click", () => {
const isOpen = body.classList.toggle("menu-open");
menuButton.setAttribute("aria-expanded", String(isOpen));
});
const syncQuantityControl = (input) => {
const wrapper = input?.closest(".quantity-control");
if (!wrapper) return;
const minimumValue = Number.parseInt(input.min || "1", 10);
const maximumValue = input.max ? Number.parseInt(input.max, 10) : Number.MAX_SAFE_INTEGER;
const currentValue = Number.parseInt(input.value || "1", 10);
const minimum = Number.isFinite(minimumValue) && minimumValue > 0 ? minimumValue : 1;
const maximum = Number.isFinite(maximumValue) && maximumValue > 0
? Math.max(maximumValue, minimum)
: Number.MAX_SAFE_INTEGER;
const current = Number.isFinite(currentValue) ? currentValue : minimum;
const decrease = wrapper.querySelector('[data-quantity-action="decrease"]');
const increase = wrapper.querySelector('[data-quantity-action="increase"]');
if (decrease) decrease.disabled = current <= minimum;
if (increase) increase.disabled = current >= maximum;
};
const clampQuantity = (input) => {
if (!input) return "1";
const minimumValue = Number.parseInt(input.min || "1", 10);
const maximumValue = input.max ? Number.parseInt(input.max, 10) : Number.MAX_SAFE_INTEGER;
const currentValue = Number.parseInt(input.value || "1", 10);
const minimum = Number.isFinite(minimumValue) && minimumValue > 0 ? minimumValue : 1;
const maximum = Number.isFinite(maximumValue) && maximumValue > 0
? Math.max(maximumValue, minimum)
: Number.MAX_SAFE_INTEGER;
const current = Number.isFinite(currentValue) ? currentValue : minimum;
input.value = String(Math.min(maximum, Math.max(minimum, current)));
syncQuantityControl(input);
return input.value;
};
document
.querySelectorAll('.quantity-control input[type="number"]')
.forEach((input) => syncQuantityControl(input));
document.addEventListener("input", (event) => {
const input = event.target.closest('.quantity-control input[type="number"]');
if (input) syncQuantityControl(input);
});
document.addEventListener("click", (event) => {
const button = event.target.closest("[data-quantity-action]");
if (!button) return;
const wrapper = button.closest(".quantity-control");
const input = wrapper?.querySelector('input[type="number"]');
if (!input) return;
const currentValue = Number.parseInt(input.value || "1", 10);
const minimumValue = Number.parseInt(input.min || "1", 10);
const maximumValue = input.max ? Number.parseInt(input.max, 10) : Number.MAX_SAFE_INTEGER;
const current = Number.isFinite(currentValue) ? currentValue : 1;
const minimum = Number.isFinite(minimumValue) && minimumValue > 0 ? minimumValue : 1;
const maximum = Number.isFinite(maximumValue) && maximumValue > 0
? Math.max(maximumValue, minimum)
: Number.MAX_SAFE_INTEGER;
const delta = button.dataset.quantityAction === "increase" ? 1 : -1;
input.value = String(Math.min(maximum, Math.max(minimum, current + delta)));
syncQuantityControl(input);
input.dispatchEvent(new Event("change", { bubbles: true }));
});
const getToastStack = () => {
let stack = document.querySelector(".shop-toast-stack");
if (stack) return stack;
stack = document.createElement("div");
stack.className = "shop-toast-stack";
stack.setAttribute("aria-live", "polite");
stack.setAttribute("aria-atomic", "false");
document.body.append(stack);
return stack;
};
const showToast = (message, type = "success") => {
const toast = document.createElement("div");
toast.className = `shop-toast shop-toast-${type}`;
toast.setAttribute("role", type === "error" ? "alert" : "status");
const icon = document.createElement("span");
icon.className = "shop-toast-icon";
icon.setAttribute("aria-hidden", "true");
icon.innerHTML = getIcon(type === "success" ? "check-circle-fill" : "x-circle-fill");
const copy = document.createElement("span");
copy.className = "shop-toast-copy";
copy.textContent = message;
const dismiss = document.createElement("button");
dismiss.className = "shop-toast-dismiss";
dismiss.type = "button";
dismiss.setAttribute("aria-label", "Закрыть уведомление");
dismiss.innerHTML = getIcon("x");
let fadeTimer;
let removeTimer;
const removeToast = (duration = 300) => {
window.clearTimeout(fadeTimer);
window.clearTimeout(removeTimer);
toast.style.setProperty("--toast-fade-duration", `${duration}ms`);
toast.classList.add("is-leaving");
window.setTimeout(() => toast.remove(), duration);
};
dismiss.addEventListener("click", () => removeToast());
toast.append(icon, copy, dismiss);
getToastStack().append(toast);
window.requestAnimationFrame(() => toast.classList.add("is-visible"));
fadeTimer = window.setTimeout(() => {
toast.style.setProperty("--toast-fade-duration", "1000ms");
toast.classList.add("is-leaving");
}, 6000);
removeTimer = window.setTimeout(() => toast.remove(), 7000);
};
const cartConfig = window.vyatkaShopCart || {
ajaxUrl: "/wp-admin/admin-ajax.php",
nonce: "",
};
let cartMutationQueue = Promise.resolve();
const sendCartMutation = async (payload) => {
const cartPage = document.querySelector(".cart-page");
cartPage?.classList.add("is-updating");
cartPage?.setAttribute("aria-busy", "true");
const requestBody = new URLSearchParams({
action: "vyatka_cart_mutation",
nonce: cartConfig.nonce,
...payload,
});
try {
const response = await fetch(cartConfig.ajaxUrl, {
method: "POST",
body: requestBody,
credentials: "same-origin",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
},
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result?.data?.message || "Не удалось обновить корзину");
}
const currentCartPage = document.querySelector(".cart-page");
if (currentCartPage && result.data?.html) {
currentCartPage.outerHTML = result.data.html;
}
return result.data;
} finally {
const currentCartPage = document.querySelector(".cart-page");
currentCartPage?.classList.remove("is-updating");
currentCartPage?.removeAttribute("aria-busy");
}
};
const queueCartMutation = (payload, successMessage = "") => {
cartMutationQueue = cartMutationQueue
.catch(() => {})
.then(() => sendCartMutation(payload))
.then((result) => {
if (successMessage) showToast(successMessage);
return result;
})
.catch((error) => {
showToast(error.message || "Нет соединения с сервером", "error");
return null;
});
return cartMutationQueue;
};
document.addEventListener("change", (event) => {
const input = event.target.closest('.quantity-control input[type="number"]');
if (!input) return;
const quantity = clampQuantity(input);
const cartItem = input.closest("[data-cart-item-key]");
if (!cartItem) return;
queueCartMutation({
operation: "update",
cart_item_key: cartItem.dataset.cartItemKey,
quantity,
});
});
document.addEventListener("focusout", (event) => {
const input = event.target.closest('.quantity-control input[type="number"]');
if (input) clampQuantity(input);
});
document.addEventListener("click", (event) => {
const remove = event.target.closest(".cart-item-remove");
if (!remove) return;
const cartItem = remove.closest("[data-cart-item-key]");
if (!cartItem) return;
event.preventDefault();
queueCartMutation({
operation: "remove",
cart_item_key: cartItem.dataset.cartItemKey,
});
});
document.addEventListener("submit", async (event) => {
const form = event.target.closest("form[data-ajax-add-to-cart]");
if (!form) return;
event.preventDefault();
if (form.dataset.addingToCart === "true") return;
const button = event.submitter || form.querySelector('[name="add-to-cart"]');
const productId = button?.value || form.querySelector('[name="add-to-cart"]')?.value;
if (!productId) {
showToast("Не удалось определить товар", "error");
return;
}
const quantity = clampQuantity(form.querySelector('input[name="quantity"]'));
form.dataset.addingToCart = "true";
if (button) {
button.disabled = true;
button.setAttribute("aria-busy", "true");
}
try {
await queueCartMutation(
{
operation: "add",
product_id: productId,
quantity,
},
"Товар успешно добавлен в корзину",
);
} finally {
form.dataset.addingToCart = "false";
if (button) {
button.disabled = false;
button.removeAttribute("aria-busy");
}
}
});
const productFilterForm = document.querySelector("[data-product-filters]");
const productCards = Array.from(document.querySelectorAll("[data-filter-results] [data-product-card]"));
const filterEmpty = document.querySelector("[data-filter-empty]");
const applyProductFilters = () => {
if (!productFilterForm || !productCards.length) return;
const selectedFeatures = Array.from(
productFilterForm.querySelectorAll('input[name="feature[]"]:checked')
)
.map((input) => input.value)
.filter((value) => value && value !== "all");
const selectedCollection = productFilterForm.querySelector('input[name="collection"]:checked')?.value || "";
const priceFilter = productFilterForm.querySelector("[data-price-filter]");
const priceActive = priceFilter?.dataset.priceActive === "true";
const minimumPrice = Number.parseFloat(priceFilter?.querySelector('[data-price-field="min"]')?.value || "0");
const maximumPrice = Number.parseFloat(priceFilter?.querySelector('[data-price-field="max"]')?.value || String(Number.MAX_SAFE_INTEGER));
let visibleProducts = 0;
productCards.forEach((card) => {
const features = (card.dataset.features || "").split(/\s+/).filter(Boolean);
const collectionMatches = !selectedCollection || card.dataset.collection === selectedCollection;
const featureMatches = !selectedFeatures.length || selectedFeatures.some((feature) => features.includes(feature));
const price = Number.parseFloat(card.dataset.price || "0");
const priceMatches = !priceActive || (price >= minimumPrice && price <= maximumPrice);
const visible = collectionMatches && featureMatches && priceMatches;
card.hidden = !visible;
if (visible) visibleProducts += 1;
});
if (filterEmpty) filterEmpty.hidden = visibleProducts > 0;
};
const updateProductFilterUrl = () => {
if (!productFilterForm) return;
const url = new URL(productFilterForm.action || window.location.href, window.location.href);
url.searchParams.delete("feature[]");
url.searchParams.delete("collection");
productFilterForm.querySelectorAll('input[name="feature[]"]:checked').forEach((input) => {
url.searchParams.append("feature[]", input.value);
});
const selectedCollection = productFilterForm.querySelector('input[name="collection"]:checked')?.value || "";
if (selectedCollection) url.searchParams.set("collection", selectedCollection);
const priceFilter = productFilterForm.querySelector("[data-price-filter]");
if (priceFilter?.dataset.priceActive === "true") {
url.searchParams.set("filter_min_price", priceFilter.querySelector('[data-price-field="min"]').value);
url.searchParams.set("filter_max_price", priceFilter.querySelector('[data-price-field="max"]').value);
} else {
url.searchParams.delete("filter_min_price");
url.searchParams.delete("filter_max_price");
}
window.history.replaceState({}, "", url);
};
document.querySelectorAll("[data-price-filter]").forEach((filter) => {
const minimumInput = filter.querySelector('[data-price-field="min"]');
const maximumInput = filter.querySelector('[data-price-field="max"]');
const minimumRange = filter.querySelector('[data-price-range="min"]');
const maximumRange = filter.querySelector('[data-price-range="max"]');
const range = filter.querySelector(".price-range");
if (!minimumInput || !maximumInput || !minimumRange || !maximumRange || !range) return;
const lowerBound = Number.parseInt(minimumRange.min, 10);
const upperBound = Number.parseInt(minimumRange.max, 10);
const gap = Number.parseInt(minimumRange.step || "1", 10);
const clamp = (value) => Math.min(upperBound, Math.max(lowerBound, value));
const syncRangeTrack = () => {
const min = Number.parseInt(minimumRange.value, 10);
const max = Number.parseInt(maximumRange.value, 10);
const size = upperBound - lowerBound;
range.style.setProperty("--range-min", `${((min - lowerBound) / size) * 100}%`);
range.style.setProperty("--range-max", `${((max - lowerBound) / size) * 100}%`);
minimumInput.value = String(min);
maximumInput.value = String(max);
};
minimumRange.addEventListener("input", () => {
filter.dataset.priceActive = "true";
minimumRange.value = String(Math.min(clamp(Number.parseInt(minimumRange.value, 10)), Number.parseInt(maximumRange.value, 10) - gap));
syncRangeTrack();
applyProductFilters();
});
maximumRange.addEventListener("input", () => {
filter.dataset.priceActive = "true";
maximumRange.value = String(Math.max(clamp(Number.parseInt(maximumRange.value, 10)), Number.parseInt(minimumRange.value, 10) + gap));
syncRangeTrack();
applyProductFilters();
});
const syncFields = () => {
let min = clamp(Number.parseInt(minimumInput.value || String(lowerBound), 10));
let max = clamp(Number.parseInt(maximumInput.value || String(upperBound), 10));
if (min > max - gap) min = max - gap;
if (max < min + gap) max = min + gap;
minimumRange.value = String(min);
maximumRange.value = String(max);
syncRangeTrack();
};
[minimumInput, maximumInput].forEach((input) => {
input.addEventListener("input", () => {
filter.dataset.priceActive = "true";
syncFields();
applyProductFilters();
});
input.addEventListener("change", () => {
updateProductFilterUrl();
productFilterForm?.requestSubmit();
});
});
minimumRange.addEventListener("change", () => {
updateProductFilterUrl();
productFilterForm?.requestSubmit();
});
maximumRange.addEventListener("change", () => {
updateProductFilterUrl();
productFilterForm?.requestSubmit();
});
syncFields();
});
/*
* Below the mobile breakpoint the filter sidebar becomes the sheet from the
* design. It stays the same <form>, so filtering behaves identically — only
* its presentation changes.
*/
const filterPanel = document.querySelector(".filters");
const filterOpener = document.querySelector("[data-filter-panel-open]");
const setFilterPanel = (open) => {
if (!filterPanel) return;
filterPanel.classList.toggle("is-open", open);
body.classList.toggle("filters-open-lock", open);
filterOpener?.setAttribute("aria-expanded", String(open));
if (open) {
filterPanel.querySelector("[data-filter-panel-close]")?.focus();
} else {
filterOpener?.focus();
}
};
filterOpener?.addEventListener("click", () => setFilterPanel(true));
document.querySelectorAll("[data-filter-panel-close]").forEach((button) => {
button.addEventListener("click", () => setFilterPanel(false));
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && filterPanel?.classList.contains("is-open")) {
setFilterPanel(false);
}
});
// Leaving the breakpoint must not strand the page in the locked state.
window.matchMedia("(min-width: 768px)").addEventListener("change", (event) => {
if (event.matches) setFilterPanel(false);
});
document.querySelectorAll("[data-filter-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const group = button.closest(".filter-group");
const content = group?.querySelector(".filter-content");
if (!group || !content) return;
window.clearTimeout(content.filterAnimationTimer);
if (content.filterAnimation) {
content.filterAnimation.cancel();
}
const collapse = !group.classList.contains("filter-collapsed");
const expandedHeight = content.scrollHeight;
const startHeight = collapse ? expandedHeight : 0;
const endHeight = collapse ? 0 : expandedHeight;
content.classList.add("is-animating");
content.hidden = false;
content.style.height = `${startHeight}px`;
content.style.opacity = collapse ? "1" : "0";
if (collapse) {
group.classList.add("filter-collapsed");
} else {
group.classList.remove("filter-collapsed");
}
button.setAttribute("aria-expanded", String(!collapse));
const animation = content.animate(
[
{ height: `${startHeight}px`, opacity: collapse ? 1 : 0 },
{ height: `${endHeight}px`, opacity: collapse ? 0 : 1 },
],
{
duration: 180,
easing: "ease-in-out",
fill: "forwards",
},
);
content.filterAnimation = animation;
const finishAnimation = () => {
if (content.filterAnimation !== animation) return;
window.clearTimeout(content.filterAnimationTimer);
content.classList.remove("is-animating");
content.hidden = collapse;
content.style.height = "";
content.style.opacity = "";
content.filterAnimation = null;
};
animation.finished.then(finishAnimation).catch(() => {});
content.filterAnimationTimer = window.setTimeout(finishAnimation, 240);
});
});
document.querySelectorAll("[data-auto-submit]").forEach((control) => {
control.addEventListener("change", () => control.form?.requestSubmit());
});
document.querySelectorAll("[data-facet-filter]").forEach((control) => {
control.addEventListener("change", () => {
applyProductFilters();
updateProductFilterUrl();
productFilterForm?.requestSubmit();
});
});
document.querySelectorAll("[data-filter-more]").forEach((button) => {
button.addEventListener("click", () => {
const content = button.closest(".filter-content");
const extras = content?.querySelectorAll("[data-filter-extra]");
if (!extras?.length) return;
const expanded = button.getAttribute("aria-expanded") !== "true";
extras.forEach((option) => {
option.hidden = !expanded;
});
button.setAttribute("aria-expanded", String(expanded));
button.textContent = expanded ? "Скрыть" : "Показать все";
});
});
const customSelects = [];
document.querySelectorAll("[data-custom-select], .woocommerce-ordering select").forEach((select) => {
if (select.dataset.customSelectReady === "true") return;
const wrapper = document.createElement("div");
const trigger = document.createElement("button");
const value = document.createElement("span");
const menu = document.createElement("div");
wrapper.className = "custom-select";
trigger.className = "custom-select-trigger";
trigger.type = "button";
trigger.setAttribute("aria-haspopup", "listbox");
trigger.setAttribute("aria-expanded", "false");
value.className = "custom-select-value";
menu.className = "custom-select-menu";
menu.setAttribute("role", "listbox");
menu.hidden = true;
select.parentNode.insertBefore(wrapper, select);
wrapper.append(select, trigger, menu);
select.classList.add("custom-select-native");
select.dataset.customSelectReady = "true";
trigger.append(value);
const close = () => {
wrapper.classList.remove("is-open");
trigger.setAttribute("aria-expanded", "false");
menu.hidden = true;
};
const sync = () => {
const selectedOption = select.options[select.selectedIndex];
value.textContent = selectedOption?.textContent || "";
menu.querySelectorAll(".custom-select-option").forEach((option) => {
const selected = option.dataset.value === select.value;
option.classList.toggle("is-selected", selected);
option.setAttribute("aria-selected", String(selected));
});
};
Array.from(select.options).forEach((sourceOption) => {
const option = document.createElement("button");
option.className = "custom-select-option";
option.type = "button";
option.setAttribute("role", "option");
option.dataset.value = sourceOption.value;
option.textContent = sourceOption.textContent;
option.addEventListener("click", () => {
select.value = sourceOption.value;
sync();
close();
select.dispatchEvent(new Event("change", { bubbles: true }));
});
menu.append(option);
});
trigger.addEventListener("click", () => {
const open = !wrapper.classList.contains("is-open");
customSelects.forEach((item) => {
if (item.wrapper !== wrapper) item.close();
});
wrapper.classList.toggle("is-open", open);
trigger.setAttribute("aria-expanded", String(open));
menu.hidden = !open;
});
trigger.addEventListener("keydown", (event) => {
if (event.key === "Escape") close();
if (event.key === "ArrowDown" && menu.hidden) {
event.preventDefault();
trigger.click();
menu.querySelector(".is-selected, .custom-select-option")?.focus();
}
});
select.addEventListener("change", sync);
if (select.closest(".woocommerce-ordering")) {
select.addEventListener("change", () => select.form?.requestSubmit());
}
customSelects.push({ wrapper, close });
sync();
});
document.addEventListener("click", (event) => {
customSelects.forEach((item) => {
if (!item.wrapper.contains(event.target)) item.close();
});
});
document.querySelectorAll(".gallery-favorite").forEach((button) => {
button.addEventListener("click", () => {
const active = button.classList.toggle("is-active");
button.setAttribute("aria-pressed", String(active));
});
});
document.querySelectorAll(
".woocommerce-message, .woocommerce-info, .woocommerce-error, .woocommerce-warning"
).forEach((notice) => {
if (notice.querySelector(".woocommerce-notice-dismiss")) return;
const dismiss = document.createElement("button");
dismiss.className = "woocommerce-notice-dismiss";
dismiss.type = "button";
dismiss.setAttribute("aria-label", "Закрыть сообщение");
dismiss.addEventListener("click", () => {
notice.classList.add("woocommerce-notice-closing");
window.setTimeout(() => notice.remove(), 190);
});
notice.append(dismiss);
});
document.querySelectorAll(".tab-list a[data-tab]").forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
const tabs = link.closest(".product-tabs");
tabs?.querySelectorAll(".tab-list li").forEach((item) => item.classList.remove("active"));
tabs?.querySelectorAll('.tab-list [role="tab"]').forEach((tab) => {
tab.setAttribute("aria-selected", String(tab === link));
});
link.parentElement?.classList.add("active");
tabs?.querySelectorAll("[data-tab-panel]").forEach((panel) => {
panel.hidden = panel.dataset.tabPanel !== link.dataset.tab;
});
});
});
document.querySelectorAll("[data-product-gallery]").forEach((galleryImages) => {
const gallery = galleryImages.closest(".product-gallery");
const slides = Array.from(galleryImages.querySelectorAll("[data-gallery-slide]"));
const dots = Array.from(gallery?.querySelectorAll("[data-gallery-dot]") || []);
const previous = gallery?.querySelector(".gallery-arrow-prev");
const next = gallery?.querySelector(".gallery-arrow-next");
let activeIndex = 0;
const render = () => {
slides.forEach((slide, index) => {
slide.hidden = index !== activeIndex;
});
dots.forEach((dot, index) => {
dot.classList.toggle("active", index === activeIndex);
});
};
previous?.addEventListener("click", () => {
activeIndex = (activeIndex - 1 + slides.length) % slides.length;
render();
});
next?.addEventListener("click", () => {
activeIndex = (activeIndex + 1) % slides.length;
render();
});
render();
});
document.querySelectorAll(".delivery-switch").forEach((deliverySwitch) => {
const form = deliverySwitch.closest("form");
const panels = Array.from(form?.querySelectorAll("[data-delivery-panel]") || []);
const radios = Array.from(deliverySwitch.querySelectorAll('input[name="delivery_type"]'));
const syncDeliveryPanels = () => {
const selected = radios.find((radio) => radio.checked)?.value || "pickup";
deliverySwitch.classList.toggle("is-delivery", selected === "delivery");
panels.forEach((panel) => {
const active = panel.dataset.deliveryPanel === selected;
panel.hidden = !active;
panel.querySelectorAll("input, select, textarea").forEach((control) => {
control.disabled = !active;
if (control.hasAttribute("data-delivery-required")) control.required = active;
});
});
};
radios.forEach((radio) => radio.addEventListener("change", syncDeliveryPanels));
syncDeliveryPanels();
});
/*
* Carousels.
*
* The track holds every item in one row and is translated by a page at a
* time. Instead of rewinding when it runs out, the items that just left the
* view are moved to the other end of the track while the transition is off,
* so the loop is endless and never snaps back to the start.
*
* Without JS the container stays the grid it was authored as and shows
* everything, so nothing depends on this running.
*/
document.querySelectorAll("[data-carousel]").forEach((carousel) => {
const controls = document.querySelector(`[data-carousel-controls="${carousel.id}"]`);
const previous = controls?.querySelector("[data-carousel-prev]");
const next = controls?.querySelector("[data-carousel-next]");
const cards = Array.from(carousel.children);
if (cards.length === 0) return;
const track = document.createElement("div");
track.className = "carousel-track";
cards.forEach((card) => track.append(card));
carousel.textContent = "";
carousel.append(track);
carousel.classList.add("is-carousel");
const perView = () => {
if (window.innerWidth <= 639) return 1;
if (window.innerWidth <= 1023) return 2;
if (carousel.classList.contains("news-grid")) return 3;
return window.innerWidth <= 1439 ? 3 : 4;
};
const items = () => Array.from(track.children);
const canScroll = () => items().length > perView();
const applyPerView = () => {
track.style.setProperty("--carousel-per-view", String(perView()));
const idle = !canScroll();
if (previous) previous.disabled = idle;
if (next) next.disabled = idle;
};
// Distance of one card plus the gap between cards.
const stride = () => {
const first = track.children[0];
if (!first) return 0;
const gap = Number.parseFloat(getComputedStyle(track).gap) || 0;
return first.getBoundingClientRect().width + gap;
};
// Re-order without animating, so the seam is never visible.
const settle = (mutate) => {
track.classList.add("is-settling");
mutate();
track.style.transform = "translateX(0px)";
void track.offsetWidth;
track.classList.remove("is-settling");
};
let busy = false;
const page = (direction) => {
if (busy || !canScroll()) return;
const count = Math.min(perView(), items().length - 1);
const distance = stride() * count;
if (distance <= 0) return;
busy = true;
const finish = () => {
track.removeEventListener("transitionend", finish);
window.clearTimeout(guard);
if (direction > 0) {
settle(() => {
for (let i = 0; i < count; i += 1) track.append(track.children[0]);
});
}
busy = false;
};
// A transition that never fires must not lock the control for good.
const guard = window.setTimeout(finish, 800);
track.addEventListener("transitionend", finish);
if (direction > 0) {
track.style.transform = `translateX(${-distance}px)`;
return;
}
// Going back: bring the tail to the front first, jump there without
// animating, then slide into place.
settle(() => {
for (let i = 0; i < count; i += 1) {
track.prepend(track.children[track.children.length - 1]);
}
track.style.transform = `translateX(${-distance}px)`;
});
void track.offsetWidth;
track.style.transform = "translateX(0px)";
};
previous?.addEventListener("click", () => page(-1));
next?.addEventListener("click", () => page(1));
window.addEventListener("resize", () => {
applyPerView();
settle(() => {});
});
applyPerView();
});
/*
* Checkout validation.
*
* The design shows problems on the field itself — a red edge and a red
* caption under it — not as one banner listing everything. So the fields are
* validated here first, and any error WooCommerce still returns is mapped
* back onto the field it belongs to.
*/
const checkoutForm = document.querySelector("form.checkout");
if (checkoutForm) {
const MESSAGES = {
valueMissing: "Заполните это поле",
typeMismatch: "Введены не верные данные",
patternMismatch: "Введены не верные данные",
tooShort: "Введены не верные данные",
};
const wrapperOf = (input) => input.closest(".checkout-input");
const setFieldError = (input, message) => {
const wrapper = wrapperOf(input);
if (!wrapper) return;
const slot = wrapper.querySelector("[data-field-error]");
wrapper.classList.toggle("is-error", Boolean(message));
input.setAttribute("aria-invalid", message ? "true" : "false");
if (slot) slot.textContent = message || "";
};
const messageFor = (input) => {
if (input.validity.valid) return "";
const key = Object.keys(MESSAGES).find((k) => input.validity[k]);
return MESSAGES[key] || "Введены не верные данные";
};
const validatableFields = () =>
Array.from(checkoutForm.querySelectorAll(".checkout-input input")).filter(
(input) => !input.disabled && input.willValidate
);
const validateField = (input) => {
const message = messageFor(input);
setFieldError(input, message);
return !message;
};
// Re-check as soon as the visitor leaves a field, and clear a shown error
// as soon as they start fixing it.
checkoutForm.addEventListener(
"blur",
(event) => {
const input = event.target.closest(".checkout-input input");
if (input) validateField(input);
},
true
);
checkoutForm.addEventListener("input", (event) => {
const input = event.target.closest(".checkout-input input");
if (input && wrapperOf(input)?.classList.contains("is-error")) validateField(input);
});
checkoutForm.addEventListener(
"submit",
(event) => {
const fields = validatableFields();
const invalid = fields.filter((input) => !validateField(input));
if (!invalid.length) return;
event.preventDefault();
event.stopImmediatePropagation();
invalid[0].focus();
invalid[0].scrollIntoView({ block: "center", behavior: "smooth" });
},
true
);
/*
* WooCommerce renders its own notice group when the server rejects the
* order. Each message starts with the field label, so it can be attached
* to that field; the banner is hidden only when every message found a home.
*/
const mapServerErrors = () => {
const group = document.querySelector(".woocommerce-NoticeGroup-checkout, .checkout .woocommerce-error");
if (!group) return;
const items = group.matches("ul") ? Array.from(group.querySelectorAll("li")) : Array.from(group.querySelectorAll("li, p"));
const texts = (items.length ? items : [group]).map((n) => n.textContent.trim()).filter(Boolean);
if (!texts.length) return;
let mappedAll = texts.length > 0;
texts.forEach((text) => {
const field = validatableFields().find((input) => {
const label = wrapperOf(input)?.querySelector(".field-label")?.textContent.trim();
return label && text.toLowerCase().startsWith(label.toLowerCase());
});
if (field) {
setFieldError(field, /обязательное/i.test(text) ? MESSAGES.valueMissing : MESSAGES.typeMismatch);
} else {
mappedAll = false;
}
});
if (mappedAll) {
document
.querySelectorAll(".woocommerce-NoticeGroup-checkout, .checkout .woocommerce-error")
.forEach((el) => el.classList.add("is-field-level"));
}
};
/*
* The consent box gates the order button: without it ticked there is no
* lawful basis to process the order, so the button is disabled rather
* than letting the submit fail somewhere later.
*/
const consent = checkoutForm.querySelector("[data-consent]");
const placeOrder = checkoutForm.querySelector("[data-consent-gated]");
if (consent && placeOrder) {
const syncConsent = () => {
placeOrder.disabled = !consent.checked;
placeOrder.setAttribute("aria-disabled", String(!consent.checked));
};
consent.addEventListener("change", syncConsent);
syncConsent();
}
mapServerErrors();
// WooCommerce replaces the notice group over AJAX rather than reloading.
document.body.addEventListener("checkout_error", () => window.setTimeout(mapServerErrors, 50));
new MutationObserver(() => mapServerErrors()).observe(checkoutForm, { childList: true, subtree: true });
}
/*
* Quick filters mirror the collection radio group in the sidebar. Clicking a
* chip selects the matching radio and submits the same form, so the two
* controls can never disagree.
*/
document.querySelectorAll("[data-quick-filter]").forEach((chip) => {
chip.addEventListener("click", () => {
const value = chip.dataset.quickFilter;
const radio = document.querySelector(`input[name="collection"][value="${CSS.escape(value)}"]`);
if (!radio) return;
radio.checked = true;
radio.dispatchEvent(new Event("change", { bubbles: true }));
});
});
/*
* Subcategory row. The next button pages the scroller by a whole card and
* hides itself once there is nothing further to reveal.
*/
document.querySelectorAll("[data-subcategories]").forEach((row) => {
const track = row.querySelector("[data-subcategory-track]");
const next = row.querySelector("[data-subcategory-next]");
if (!track || !next) return;
const step = () => {
const card = track.querySelector("li");
const gap = Number.parseFloat(getComputedStyle(track).gap) || 16;
return card ? card.getBoundingClientRect().width + gap : 232;
};
const sync = () => {
const atEnd = track.scrollLeft + track.clientWidth >= track.scrollWidth - 2;
const fits = track.scrollWidth <= track.clientWidth + 2;
next.hidden = fits;
next.querySelector(".icon")?.classList.toggle("icon-flip", atEnd && !fits);
};
next.addEventListener("click", () => {
const atEnd = track.scrollLeft + track.clientWidth >= track.scrollWidth - 2;
track.scrollBy({ left: atEnd ? -track.scrollWidth : step(), behavior: "smooth" });
});
track.addEventListener("scroll", sync, { passive: true });
window.addEventListener("resize", sync);
sync();
});
applyProductFilters();
})();