/* ---- src/styles/00-tokens.css ---- */
/*
 * WeSnack design tokens — Task F1 (docs/AGENT_RULES.md, docs/FIGMA_SPEC.md
 * §3 "Токены (точные)"), source of truth docs/FIGMA_SPEC.md, decision D21
 * (docs/PROJECT.md): exact Figma colours for brand/text/button/section
 * tokens, REPLACING the D20 darkened/lightened accessible variants that used
 * to live here (focus rings/aria/semantics are unaffected — see
 * src/styles/02-base.css's :focus-visible rule, unchanged).
 *
 * Per FIGMA_SPEC §3's colour table, almost every brand/text/button/section
 * colour is now IDENTICAL in both themes (Figma frame 1:292/10:48, the dark
 * home/about variants, reuse the exact same hex values as the light frames —
 * only the page background and a couple of surface/shadow tokens actually
 * change under [data-theme="dark"]). Tokens that stayed genuinely
 * theme-dependent (page bg, generic chrome --ws-surface used by menus/
 * modals/inputs outside this task's scope, shadows) keep their separate
 * :root / :root[data-theme="dark"] values below.
 *
 * Out of scope for this pass (left as-is, owned by the tasks that redesign
 * those sections per docs/AGENT_RULES.md §0 "found outside your task ->
 * report, don't fix"): --ws-surface's dark opacity (FIGMA_SPEC's "product
 * cards: white / white @ 80% opacity" applies to Bestsellers/ProductCard
 * specifically, not the generic chrome surface this token also drives for
 * header/modal/input chrome), --ws-border-peach* (Brands/Reviews cards use a
 * DIFFERENT border colour per card in Figma, not a single token).
 *
 * All custom properties are prefixed --ws- so they can never collide with
 * the legacy --display/--bg/--ink/... custom properties defined in
 * src/styles/legacy.css (see that file's header comment).
 */

:root {
  /* ---- surfaces / page bg (still theme-dependent) ---- */
  --ws-bg: #FFE7E4;
  --ws-surface: #FFFFFF;
  --ws-surface-border: transparent;

  /* ---- text (FIGMA_SPEC §3 — identical both themes) ---- */
  --ws-text: #333333;
  --ws-text-muted: #5C6066;
  --ws-heading: #824EBF;         /* заголовки секций: Bestsellers/Brands */
  --ws-heading-violet: #8A59C3;  /* заголовки секций: Feedback/about */
  --ws-product-name: #9C5EE5;
  --ws-eyebrow: #FF8A63;
  --ws-hero-title: #FF862F;
  --ws-accent-text: #FF862F;
  --ws-link-pink: #F15AB4;

  /* ---- buttons (FIGMA_SPEC §3/§9 — identical both themes) ---- */
  --ws-btn-primary-bg: #F15AB4;
  --ws-btn-primary-text: #FFFFFF;
  --ws-btn-accent-bg: #FF862F;
  --ws-btn-accent-text: #FFFFFF;
  --ws-btn-light-bg: #FFFFFF;
  --ws-btn-light-text: #F15AB4;
  --ws-btn-dark-bg: #3A4443;
  --ws-btn-dark-text: #FFFFFF;

  /* ---- section bands (same both themes) ---- */
  --ws-promo-bg: #9C5EE5;
  --ws-promo-text: #FF862F;
  --ws-promo-text-muted: #FF862F;
  --ws-cta-band-bg: #F15AB4;
  --ws-cta-band-text: #FFE7E4;
  /* Om oss "drip" hero (about-drip node 1:579) — fixed on-brand band, same in
     both site themes on purpose (docs/DESIGN.md §2 "Тёмная тема"). Per D21 +
     FIGMA_SPEC §3 "About drip": cream text, orange eyebrow (exact Figma —
     replaces the D20-mandated darkened plum #2A1633 that used to live here). */
  --ws-drip-top: #FF94E6;
  --ws-drip-bottom: #F05AB4;
  --ws-drip-text: #FFE7E4;
  --ws-footer-bg: #292C32;
  --ws-footer-text: #FFE7E4;
  --ws-footer-label: #9C5EE5;
  /* Footer newsletter pill — the footer is the same dark colour in both site
     themes (see above), so its email field needs its own fixed light chrome
     too, exactly like footer-bg/text/label already do. Figma node 1:267/
     1:268 (FIGMA_SPEC §3 footer row). */
  --ws-footer-input-bg: #F9FAFB;
  --ws-footer-input-border: #E5E7EB;
  --ws-footer-input-text: #292C32;
  --ws-footer-input-placeholder: #6F7278;
  --ws-footer-submit-bg: #3A4443;
  --ws-footer-submit-text: #FFFFFF;
  --ws-footer-newsletter-text: #FAFAFC;

  /* ---- decor (identical in both themes — logo, blobs, stars, focus ring) ---- */
  --ws-brand-pink: #F15AB4;
  --ws-brand-orange: #FF862F;
  --ws-brand-purple: #9C5EE5;
  /* Fixed cream swatch (FIGMA_SPEC §3 "Кремовый текст на розовом/кнопке
     героя") — same #FFE7E4 hex as --ws-bg's LIGHT value, but kept as its own
     token because it must NOT flip with the theme (the hero button's cream
     text stays cream on the dark frame 1:292 too, unlike --ws-bg itself). */
  --ws-cream: #FFE7E4;

  /* T4.4: peach/orange card outline — out of scope for Task F1 (see file
     header); Brands/Reviews cards need a DIFFERENT border colour per card in
     Figma (FIGMA_SPEC §3), not a single token, so this pair is left for the
     task that redesigns those sections. */
  --ws-border-peach: #B04A18;
  --ws-border-peach-fixed: #B04A18;

  /* ---- Blob.jsx variants (docs/TASKS.md T4.2b); dark recolour below ---- */
  --ws-blob-hero: #FF99C9;
  --ws-blob-about: #FF94E6;
  --ws-blob-promo: #824EBF;

  /* ---- typography ---- */
  --ws-font-display: "Mont", "Montserrat", system-ui, sans-serif;
  --ws-font-body: "Poppins", system-ui, sans-serif;

  /* Fluid scale: clamp(min, min + slope*vw, max), min/max viewport 400/1512px
     (DESIGN.md §2 desktop frame), except --ws-fs-hero which is the exact
     value given in the T4.2b brief. Non-fluid sizes (body/small/etc) are
     static — DESIGN.md gives them as single fixed sizes, not a range.
     Still used as the < 1024px fallback for components migrated to the --d
     design-unit system (src/styles/02-base.css §"Design unit") — at >=1024px
     those components compute their own calc(N * var(--d)) sizes instead. */
  --ws-fs-hero: clamp(2.5rem, 1.2rem + 4.6vw, 5.625rem);            /* H1 hero/promo: 40 -> 90px */
  --ws-fs-h2: clamp(2rem, 1.64rem + 1.44vw, 3rem);                  /* About H2: 32 -> 48px (Poppins 48 desktop) */
  --ws-fs-section-title: clamp(1.75rem, 1.48rem + 1.08vw, 2.5rem);  /* Section title: 28 -> 40px */
  --ws-fs-sub: clamp(1.125rem, 0.855rem + 1.08vw, 1.875rem);        /* Hero/promo subtitle: 18 -> 30px */
  --ws-fs-button: 1.25rem;    /* ~20px — comfortable button label size (Figma buttons run 30-40px display type; kept modest until T4.3 restyles real buttons) */
  --ws-fs-product-name: 1.25rem;   /* 20px */
  --ws-fs-review-title: 1.5rem;    /* 24px */
  --ws-fs-body: 1rem;         /* 16px */
  --ws-fs-small: 0.875rem;    /* 14px — eyebrow/footer labels */

  --ws-lh-tight: 1.1;     /* H1/H2 headings */
  --ws-lh-snug: 1.27;     /* subtitles */
  --ws-lh-normal: 1.5;    /* body text default */
  --ws-lh-relaxed: 1.4;   /* about/review paragraph copy */

  /* ---- radii ---- */
  --ws-radius-sm: 10px;     /* buttons: hero/card/promo/dark (FIGMA_SPEC §9 r=10) */
  --ws-radius-btn-light: 6px; /* buttons: light (white) — FIGMA_SPEC §9 r=6 */
  --ws-radius-md: 16px;     /* general cards (product/review) — not in DESIGN.md, reasonable value between sm/card, see T4.2b report */
  --ws-radius-card: 29px;   /* brand cards */
  --ws-radius-pill: 999px;  /* circle buttons, pills */

  /* ---- shadows ---- */
  --ws-shadow-button: 0 10px 24px -8px rgba(204, 59, 148, 0.35), 0 2px 6px rgba(31, 29, 28, 0.08);
  --ws-shadow-card: 0 4px 16px rgba(31, 29, 28, 0.06);
  --ws-shadow-popover: 0 20px 48px rgba(31, 29, 28, 0.16);
  /* FIGMA_SPEC §3 "Тени" — exact Figma effect values, identical both themes. */
  --ws-shadow-photo: -21px 15px 18px rgba(104, 104, 104, 0.25);      /* product photo compositions */
  --ws-shadow-hero-snacks: -22px 16px 41px rgba(161, 0, 0, 0.25);    /* hero flying snacks */
  --ws-shadow-btn-hero: 0 8px 28px rgba(183, 82, 142, 0.28);         /* hero pink button */
  --ws-shadow-btn-promo: 0 8px 28px rgba(130, 80, 189, 0.56);        /* promo orange button */
  --ws-shadow-product-card: 0 3px 15px rgba(0, 0, 0, 0.03);          /* bestsellers product card */

  /* ---- spacing scale ---- */
  --ws-space-3xs: 0.25rem;  /* 4px */
  --ws-space-2xs: 0.5rem;   /* 8px */
  --ws-space-xs: 0.75rem;   /* 12px */
  --ws-space-sm: 1rem;      /* 16px */
  --ws-space-md: 1.5rem;    /* 24px */
  --ws-space-lg: 2rem;      /* 32px */
  --ws-space-xl: 3rem;      /* 48px */
  --ws-space-2xl: 4rem;     /* 64px */
  --ws-space-3xl: 6rem;     /* 96px */

  /* ---- layout ---- */
  --ws-container: 1426px;
  /* 16px gutter under 1200px viewport width, ramping linearly to 43px AT
     exactly 1200px and staying there above it: slope = (43-16)/1200 = 2.25vw. */
  --ws-gutter: clamp(1rem, 1rem + 2.25vw, 2.6875rem);

  /* ---- z-index scale ---- */
  --ws-z-dropdown: 10;
  --ws-z-sticky: 20;
  --ws-z-header: 30;
  --ws-z-overlay: 40;
  --ws-z-modal: 50;
  --ws-z-toast: 60;

  /* ---- motion ---- */
  --ws-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ws-ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
  --ws-duration-fast: 150ms;
  --ws-duration-base: 250ms;
  --ws-duration-slow: 400ms;

  /* ---- focus ring (src/styles/02-base.css :focus-visible) ---- */
  --ws-focus-ring-color: var(--ws-brand-pink);
  --ws-focus-ring-width: 2px;
  --ws-focus-ring-offset: 2px;
}

:root[data-theme="dark"] {
  --ws-bg: #1F1D1C;
  --ws-surface: rgba(255, 255, 255, 0.06);
  --ws-surface-border: rgba(255, 231, 228, 0.14);

  /* Text/heading/eyebrow/button/section-band/footer/drip tokens are
     IDENTICAL to :root above per FIGMA_SPEC §3 (dark frame 1:292/10:48 reuses
     the same brand hex values) — nothing to redeclare here except the
     surface/shadow pair that stays genuinely theme-dependent. */

  --ws-border-peach: #FF8A63;
  --ws-border-peach-fixed: #B04A18;

  --ws-blob-hero: #4C3544;
  --ws-blob-about: #9C5EE5;
  --ws-blob-promo: #382A44;

  --ws-shadow-button: 0 10px 24px -8px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
  --ws-shadow-card: 0 4px 16px rgba(0, 0, 0, 0.35);
  --ws-shadow-popover: 0 20px 48px rgba(0, 0, 0, 0.5);
}

/* prefers-reduced-motion: collapse every duration to near-zero from one
   place, so any component that animates via var(--ws-duration-*) respects
   the user's preference automatically without its own media query. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --ws-duration-fast: 0.01ms;
    --ws-duration-base: 0.01ms;
    --ws-duration-slow: 0.01ms;
  }
}

/* ---- src/styles/01-fonts.css ---- */
/*
 * WeSnack self-hosted fonts — T4.2b (docs/TASKS.md).
 *
 * Poppins (body, D11) + Montserrat (display stand-in for the commercial
 * "Mont" until a licence is available — D11 in docs/PROJECT.md; the family
 * stack is `"Mont", "Montserrat", ...` in src/styles/00-tokens.css, so "Mont"
 * simply never resolves and every browser falls through to Montserrat).
 * Files self-hosted from /assets/fonts/ (see docs/DESIGN.md §4, D11) — no
 * Google Fonts / CDN requests (AGENT_RULES §1). The two files preloaded in
 * prerender/build.js's head template (montserrat-latin-700, poppins-latin-400
 * — the weights used above the fold: hero H1 + body copy) are named to match
 * the @font-face `src` URLs below exactly, so the preload actually gets
 * reused instead of double-fetched.
 *
 * unicode-range values are the standard Google Fonts latin / latin-ext
 * split (see the T4.2b task brief) — Norwegian bokmål's æ/ø/å live in the
 * Latin-1 Supplement block (U+00E6/U+00F8/U+00E5), covered by "latin" alone;
 * "latin-ext" only serves extra diacritics the site doesn't otherwise need,
 * kept for completeness/robustness (pasted foreign names, etc).
 */

/* ---- Poppins (body) ---- */

/* Weight 300 — added for Task F1 (docs/FIGMA_SPEC.md §3/D24): Figma's
   "Mont Light"/body-light pairings render as Poppins 300 in a couple of
   exact-spec spots (CTA eyebrow/title use Montserrat 300 — see below —
   but some reference call sites also cite Poppins Light; kept here so
   both families offer the same weight range). */
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-300-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-ext-300-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-400-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-ext-400-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-500-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-ext-500-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-600-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-ext-600-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-700-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/fonts/poppins-latin-ext-700-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---- Montserrat (display, stand-in for "Mont" — D11) ---- */

/* Weight 300 — Task F1 (docs/FIGMA_SPEC.md §3/D24): Figma's "Mont Light" is
   used for CTA eyebrow/title (36/40px), section titles, Brands/Feedback
   headings, product-card copy, etc. — every place FIGMA_SPEC's typography
   table lists "Montserrat 300". */
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-300-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-ext-300-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-400-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-ext-400-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-500-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-ext-500-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-600-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-ext-600-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-700-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-ext-700-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-800-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url("/assets/fonts/montserrat-latin-ext-800-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---- src/styles/02-base.css ---- */
/*
 * WeSnack base styles — T4.2b (docs/TASKS.md). Deliberately light: this only
 * sets element-level defaults (body/headings/links/focus/selection) and two
 * small utility classes (.ws-container, .ws-visually-hidden) for the NEW
 * primitives (src/ui/) and future page-redesign tasks (T4.3+) to build on.
 *
 * Safe to load after legacy.css (see src/styles/legacy.css and
 * prerender/build.js's style auto-discovery): every legacy page's visible
 * content renders inside `.site-shell` (src/app.jsx), and legacy.css's
 * `.site-shell` / `.site-shell a` rules (class selectors) already
 * out-specificity the plain-element selectors below (`body`, `a`), so none
 * of this can visibly change how legacy-rendered pages look today — it only
 * takes effect once a page actually renders outside `.site-shell` (the
 * T4.2b QA-only primitives gallery today; real pages from T4.3 on).
 */

html {
  color-scheme: light dark;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Form controls: inherit typography; native appearance is replaced by the
   component styles (same global reset the legacy stylesheet had). */
button,
input {
  -webkit-appearance: none;
  appearance: none;
  font: inherit;
}

select,
textarea {
  font: inherit;
}

img {
  max-width: 100%;
  height: auto;
}

/*
 * ---- Design unit ("--d") — Task F1, docs/FIGMA_SPEC.md §2 "Система
 * «дизайн-единиц»" ----
 *
 * FIGMA_SPEC's geometry tables give every position/size in Figma desktop
 * pixels off a 1512px-wide frame (D25, docs/PROJECT.md). `--d` is "1 Figma
 * px" as a length: multiply any spec number by it to reproduce that geometry
 * responsively between 1024px and 1512px+ without a pile of breakpoints.
 *
 *   .ws-header__logo { width: calc(176 * var(--d)); }
 *   (176px at 1512, scales down to ~119px at 1024)
 *
 * How it works: `.site-shell` (src/app.jsx's root element) is a CSS size
 * container (`container-type: inline-size`); `--d` is then set on ITS
 * DESCENDANTS (not on `.site-shell` itself — a container cannot query its
 * own size, only its ancestor's, see the CSS Containment spec) as
 * `min(100cqw, 1512px) / 1512`, i.e. "the shell's own inline size, capped at
 * 1512px, divided into 1512 units". Above 1512px the value freezes at
 * exactly 1px/unit (D25: "content 1512 centred, backgrounds full width" —
 * matches docs/styles/02-base.css's `.ws-container` max-width pattern); below
 * it, every `calc(N * var(--d))` shrinks in lockstep with the viewport.
 *
 * Usage rules (per FIGMA_SPEC §2):
 *   - Only reach for `--d` at >=1024px (wrap the calc() rules in
 *     `@media (min-width: 1024px)`); below that, use ordinary responsive
 *     rules (stacked columns, fixed readable sizes) — the Figma desktop
 *     layout was never designed to scale down that far.
 *   - Never let a `--d`-scaled font size drop below 14px:
 *     `font-size: max(14px, calc(14 * var(--d)));`.
 *   - Absolute positioning is fine *inside* a section (as in Figma), but the
 *     section itself stays in normal flow with an explicit
 *     `height: calc(H * var(--d))` on desktop — sections must never
 *     overlap/collapse to zero height.
 */
.site-shell {
  container-type: inline-size;
}
.site-shell * {
  --d: calc(min(100cqw, 1512px) / 1512);
}

/* App shell rendered by src/app.jsx. `position: relative` is load-bearing
   for Task F1's Header (D22, docs/FIGMA_SPEC.md): src/layout/Header.jsx is
   `position: absolute; inset: 0 0 auto 0` so it overlays the top of whatever
   renders first (Hero / About drip hero / SubpageHero) instead of a sticky
   bar — it needs `.site-shell` itself, not some inner page wrapper, as its
   containing block so it always starts flush with the very top of the page. */
.site-shell {
  position: relative;
  /* <1024px height of the absolute header (16px padding + 79px logo + 16px):
     the first section of every page (Hero, SubpageHero, ProductPage, About
     drip band) starts its content below this so the logo never covers it.
     >=1024px each section uses its own calc(N * var(--d)) clearance instead. */
  --ws-header-clearance: 7rem;
  min-height: 100vh;
  background: var(--ws-bg);
  color: var(--ws-text);
  font-family: var(--ws-font-body);
}

body {
  margin: 0;
  overflow-x: hidden;
  background: var(--ws-bg);
  color: var(--ws-text);
  font-family: var(--ws-font-body);
  font-size: var(--ws-fs-body);
  line-height: var(--ws-lh-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--ws-font-display);
  color: var(--ws-heading);
  line-height: var(--ws-lh-tight);
  margin: 0;
}

p {
  margin: 0;
}

a {
  color: var(--ws-link-pink);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}

/* :focus-visible only (not :focus) so mouse/touch clicks stay quiet and the
   ring shows for keyboard/assistive-tech navigation — AGENT_RULES §3
   ("видимый focus"), DESIGN.md §2.1 --brand-pink. */
:focus-visible {
  outline: var(--ws-focus-ring-width) solid var(--ws-focus-ring-color);
  outline-offset: var(--ws-focus-ring-offset);
}

::selection {
  background: var(--ws-brand-pink);
  color: #FFFFFF;
}

/* Smooth colour cross-fade when ThemeToggle flips data-theme. Duration comes
   from the token layer (src/styles/00-tokens.css), which already collapses
   to ~0 under prefers-reduced-motion, so this needs no media query of its
   own. */
html {
  transition: background-color var(--ws-duration-base) var(--ws-ease), color var(--ws-duration-base) var(--ws-ease);
}
body {
  transition: background-color var(--ws-duration-base) var(--ws-ease), color var(--ws-duration-base) var(--ws-ease);
}

.ws-container {
  width: 100%;
  max-width: calc(var(--ws-container) + (var(--ws-gutter) * 2));
  margin-inline: auto;
  padding-inline: var(--ws-gutter);
  box-sizing: border-box;
}

/* Screen-reader-only utility (standard clip-based pattern) — e.g. CircleButton
   labels, ThemeToggle state text. */
.ws-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---- src/styles/03-motion.css ---- */
/*
 * Motion layer — Phase G (docs/MOTION.md). Pairs with src/lib/motion.js.
 * Everything here is opt-in via data attributes/classes and switches off
 * under prefers-reduced-motion. Figma has no prototype motion for this file
 * (get_motion_context returned no nodes), so timings follow one house style:
 * soft ease-out (--ws-ease), 0.6-0.9 s reveals, 5-8 s idle float loops.
 */

:root {
  --ws-motion-reveal: 0.8s;
  --ws-motion-shift: 28px;
  --ws-parallax-range: 26px;
}

/* ---------- Reveal on scroll ---------- */
/* Only .is-pending (added by JS to elements below the fold) is hidden, so the
   server-rendered page is fully visible without JavaScript. Never put
   data-reveal on an element that uses `transform` for its own layout — wrap it. */
@media (prefers-reduced-motion: no-preference) {
  [data-reveal].is-pending {
    opacity: 0;
    transform: translate3d(0, var(--ws-motion-shift), 0);
  }
  [data-reveal="fade"].is-pending {
    transform: none;
  }
  /* Sideways variants only where there is room: on narrow screens a
     full-width block shifted sideways widens the page while it waits. */
  @media (min-width: 1024px) {
    [data-reveal="left"].is-pending {
      transform: translate3d(calc(var(--ws-motion-shift) * -1.2), 0, 0);
    }
    [data-reveal="right"].is-pending {
      transform: translate3d(calc(var(--ws-motion-shift) * 1.2), 0, 0);
    }
  }
  [data-reveal="zoom"].is-pending {
    transform: scale(0.92);
  }
  [data-reveal].is-pending.is-revealed {
    opacity: 1;
    transform: none;
    transition:
      opacity var(--ws-motion-reveal) var(--ws-ease) var(--reveal-delay, 0ms),
      transform calc(var(--ws-motion-reveal) + 0.15s) var(--ws-ease) var(--reveal-delay, 0ms);
  }
}

/* ---------- Idle float (3D snacks, promo box, decorative pieces) ---------- */
/* Put .ws-float on an INNER element; position/parallax live on its parent.
   Tune per item with --float-y (lift), --float-x (drift), --float-r (sway),
   --float-dur and --float-delay. */
@keyframes ws-float {
  0%,
  100% {
    transform: translate3d(0, 0, 0) rotate(0deg);
  }
  25% {
    transform: translate3d(var(--float-x, 4px), calc(var(--float-y, 14px) * -0.55), 0) rotate(calc(var(--float-r, 3deg) * 0.6));
  }
  50% {
    transform: translate3d(0, calc(var(--float-y, 14px) * -1), 0) rotate(var(--float-r, 3deg));
  }
  75% {
    transform: translate3d(calc(var(--float-x, 4px) * -1), calc(var(--float-y, 14px) * -0.4), 0) rotate(calc(var(--float-r, 3deg) * 0.25));
  }
}
.ws-float {
  animation: ws-float var(--float-dur, 6.5s) ease-in-out var(--float-delay, 0s) infinite;
  will-change: transform;
}

/* ---------- Pointer + scroll parallax ---------- */
/* Children of [data-parallax-scope] opt in with .ws-depth and --depth (0..1.5).
   Uses the individual `translate` property, so it composes with any
   `transform`/`scale` animation on the same element. */
.ws-depth {
  translate:
    calc(var(--mx, 0) * var(--depth, 0.5) * var(--ws-parallax-range))
    calc(var(--my, 0) * var(--depth, 0.5) * var(--ws-parallax-range) - var(--sy, 0px) * var(--depth, 0.5) * var(--scroll-factor, 0.18));
}

/* ---------- Entrance (above-the-fold, CSS only) ---------- */
@keyframes ws-drop-in {
  from {
    opacity: 0;
    scale: 0.6;
  }
  60% {
    opacity: 1;
  }
  to {
    opacity: 1;
    scale: 1;
  }
}
@keyframes ws-rise-in {
  from {
    opacity: 0;
    transform: translate3d(0, 24px, 0);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.ws-enter-drop {
  animation: ws-drop-in 1s var(--ws-ease) var(--enter-delay, 0s) both;
}
.ws-enter-rise {
  animation: ws-rise-in 0.9s var(--ws-ease) var(--enter-delay, 0s) both;
}

/* ---------- 3D tilt ---------- */
[data-tilt] {
  transform: perspective(1000px) rotateX(var(--tilt-rx, 0deg)) rotateY(var(--tilt-ry, 0deg));
  transform-style: preserve-3d;
  transition: transform 0.6s var(--ws-ease);
}
[data-tilt].is-tilting {
  transition-duration: 0.15s;
}
/* A child that should float above the tilted surface. */
[data-tilt] .ws-tilt-pop {
  transform: translateZ(var(--tilt-pop, 40px));
  transition: transform 0.6s var(--ws-ease);
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  .ws-float,
  .ws-enter-drop,
  .ws-enter-rise {
    animation: none;
  }
  .ws-depth {
    translate: none;
  }
  [data-tilt],
  [data-tilt] .ws-tilt-pop {
    transform: none;
    transition: none;
  }
}

/* ---- src/styles/components/about.css ---- */
/*
 * Styles for src/pages/AboutPage.jsx (Om oss) — Task G3 (docs/MOTION.md),
 * replacing the F3 absolute-coordinate rebuild (`calc(N * var(--d))` on every
 * text/photo element) with a flow layout. Tokens only
 * (src/styles/00-tokens.css); class prefix `ws-`.
 *
 * Structure, both blocks: mobile-first flow rules below (readable stack,
 * <1024px, no Figma mobile design exists — FIGMA_SPEC §2/§7), then ONE
 * `@media (min-width: 1024px)` block per section switches to a CSS grid that
 * reproduces the Figma proportions from docs/FIGMA_SPEC.md §5:
 *   - drip frame (node 1:579): 2 columns, fraction-sized 515:605 (the exact
 *     Figma title-column/copy-column widths) so the ratio holds at every
 *     width, not just 1512 — title left, eyebrow/copy/team right.
 *   - light block (node 10:24): 2x2 `grid-template-areas` reproducing
 *     Figma's checkerboard placement (billboard photo top-left / copy
 *     top-right / contacts bottom-left / van photo bottom-right) instead of
 *     4 independently-coordinated elements.
 * Only the two purely decorative background layers (drip photo band, giant
 * logo blob) stay `position: absolute` inside their `overflow: hidden`
 * section — MOTION.md §2 rule 1 allows that for decor, never for text/photo
 * content. Grid/flow items that must paint above those absolute decor layers
 * get `position: relative` (no offset) so they win the stacking order
 * without needing z-index gymnastics beyond `z-index: 1`.
 *
 * Dark theme: no [data-theme="dark"] rules needed for text colour — --ws-bg
 * (light block background) already flips in src/styles/00-tokens.css; every
 * text/eyebrow colour used below (--ws-drip-text/--ws-brand-orange for the
 * drip block, --ws-heading-violet/--ws-eyebrow for the light block) is
 * identical in both themes per docs/FIGMA_SPEC.md §3's colour table (no dark
 * override exists for those tokens) — see src/pages/AboutPage.jsx's header
 * comment for the about-dark.png discrepancy this was checked against and
 * why it wasn't treated as the source of truth. The explicit dark-mode block
 * at the bottom of this file (unchanged from F3) only recolours the light
 * block per the separate node 10:48 spec.
 */

/* ================= Drip block — node 1:579 ================= */

/* The drip picture (1840x960) is always shown WHOLE (width 100%, natural
   height — never object-fit-cropped, which cut the drip tips off on wide
   screens). Its top rows are exactly --ws-drip-top and its bottom rows
   exactly --ws-drip-bottom, so the section paints those two colours split at
   the picture's middle: the picture can drift with the parallax without ever
   opening a seam against the header band or the text area below. */
.ws-about-drip {
  --ws-drip-img-top: var(--ws-header-clearance);
  --ws-drip-split: calc(var(--ws-drip-img-top) + 26.09cqw); /* image top + half its height (960/1840/2) */
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: linear-gradient(
    to bottom,
    var(--ws-drip-top) 0,
    var(--ws-drip-top) var(--ws-drip-split),
    var(--ws-drip-bottom) var(--ws-drip-split)
  );
}

.ws-about-drip__top-band {
  display: block;
  height: var(--ws-drip-img-top); /* <1024px: spacer under the cream header */
}

.ws-about-drip__image {
  display: block;
  width: 100%;
  height: auto;
}

.ws-about-drip__frame {
  position: relative;
  z-index: 1;
  padding: clamp(2rem, 6vw, 3rem) var(--ws-gutter) clamp(2.5rem, 8vw, 4rem);
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
}

.ws-about-drip__col--copy {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.ws-about-drip__eyebrow.ws-eyebrow {
  font-family: var(--ws-font-body); /* About eyebrow is Poppins, unlike every other section's Montserrat — FIGMA_SPEC §3 */
  color: var(--ws-brand-orange);
}

.ws-about-drip__title {
  margin: 0.75rem 0 0;
  font-family: var(--ws-font-body); /* About H1 is Poppins Regular, not the site's usual Mont/Montserrat display stack */
  font-weight: 400;
  font-size: clamp(1.75rem, 1.2rem + 4vw, 3rem);
  line-height: 1.1;
  color: var(--ws-drip-text);
  max-width: 30rem;
}

.ws-about-drip__p1,
.ws-about-drip__p2 {
  margin: 0;
  color: var(--ws-drip-text);
  font-size: 1rem;
  line-height: var(--ws-lh-relaxed);
  max-width: 38rem;
}

.ws-about-drip__team {
  display: flex;
  align-items: center;
  gap: 1rem;
}
.ws-about-drip__team-avatar {
  position: relative;
  flex-shrink: 0;
  width: 4rem;
  height: 4rem;
}
.ws-about-drip__team-shadow {
  position: absolute;
  left: -31%;
  top: -3%;
  width: 146%;
  height: 144%;
  max-width: none;
  pointer-events: none;
}
.ws-about-drip__team-photo {
  position: absolute;
  inset: 0;
  display: block;
  border-radius: 50%;
  overflow: hidden;
}
.ws-about-drip__team-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center bottom;
}
.ws-about-drip__team-name {
  font-family: var(--ws-font-body);
  font-size: 1.125rem;
  color: var(--ws-drip-text);
  white-space: nowrap;
}

/* ================= Light block — node 10:24 ================= */

.ws-about-light {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: var(--ws-bg);
  padding: clamp(2.5rem, 3vw + 2rem, 4rem) var(--ws-gutter) clamp(3rem, 4vw + 2rem, 5rem);
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.ws-about-light__blob {
  position: absolute;
  top: -4rem;
  right: -10rem;
  width: 26rem;
  height: 26rem;
  z-index: 0;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ws-about-light__blob img {
  display: block;
  width: 22rem;
  height: auto;
  max-width: none;
  transform: rotate(118.47deg);
}

.ws-about-light__intro {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.ws-about-light__eyebrow.ws-eyebrow {
  font-family: var(--ws-font-body);
  color: var(--ws-eyebrow);
}

.ws-about-light__title {
  margin: 0.75rem 0 0;
  font-family: var(--ws-font-body);
  font-weight: 400;
  font-size: clamp(1.75rem, 1.2rem + 4vw, 3rem);
  line-height: 1.1;
  color: var(--ws-heading-violet);
  max-width: 26rem;
}

.ws-about-light__p1,
.ws-about-light__p2 {
  margin: 0;
  color: var(--ws-heading-violet);
  font-size: 1rem;
  line-height: var(--ws-lh-relaxed);
  max-width: 38rem;
}

.ws-about-light__photo {
  position: relative;
  z-index: 1;
  margin: 0;
  border-radius: var(--ws-radius-md);
  overflow: hidden;
  box-shadow: var(--ws-shadow-card);
  line-height: 0;
}
.ws-about-light__photo img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
}
/* [data-tilt] gets its own perspective transform from 03-motion.css — the
   picture wrapper (.ws-tilt-pop) is the child that "pops" in Z. */
.ws-about-light__photo picture {
  display: block;
  line-height: 0;
}

.ws-about-light__contact {
  position: relative;
  z-index: 1;
}

/* ---- Contact block text (shared "Kontakt oss" heading/lines/social) ----
   Figma shows every line in this block — including "contact us" itself —
   at the SAME plain 16/22.4 Poppins Regular weight as the surrounding
   paragraphs (docs/design/figma-context/about-light.10-24.tsx's node
   10:46: one uniform text style, no bold/larger "heading" treatment) — see
   FIGMA_SPEC §5's "contacts block ... Poppins 16/22.4 #8A59C3". <h2>/<h3>
   are still used in the markup for a real document outline; only their
   VISUAL size/weight is reset to match here (AGENT_RULES §3 "семантические
   теги" + D21 "точные цвета/вёрстка Figma"). */
.ws-about-contact__heading {
  margin: 0;
  font-family: var(--ws-font-body);
  font-weight: 400;
  font-size: 1rem;
  line-height: var(--ws-lh-relaxed);
  color: var(--ws-heading-violet);
}
.ws-about-contact__heading--sub {
  margin-top: 1.25rem;
}
.ws-about-contact__line {
  margin: 0.375rem 0 0;
  color: var(--ws-heading-violet);
  font-size: 1rem;
  line-height: var(--ws-lh-relaxed);
}
.ws-about-contact__heading + .ws-about-contact__line {
  margin-top: 0.75rem;
}
.ws-about-contact__label {
  font-weight: 400;
}
.ws-about-contact__line a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}
.ws-about-contact__line a:hover {
  opacity: 0.75;
}

.ws-about-contact__social {
  display: flex;
  gap: var(--ws-space-sm);
  margin-top: 0.75rem;
}
.ws-about-contact__social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-surface);
  border: 1px solid var(--ws-surface-border);
  color: var(--ws-heading-violet); /* 4.89:1 on white --ws-surface — see docs/tools/contrast.mjs; well above the 3:1 non-text floor */
  transition: transform var(--ws-duration-fast) var(--ws-ease), opacity var(--ws-duration-fast) var(--ws-ease);
}
.ws-about-contact__social-link:hover {
  transform: translateY(-2px);
  opacity: 0.8;
}

@media (max-width: 520px) {
  .ws-about-light__blob {
    width: 16rem;
    height: 16rem;
    top: -2.5rem;
    right: -6rem;
  }
  .ws-about-light__blob img {
    width: 13rem;
  }
}

/* ================= >=1024px: grid layouts (Figma proportions) ================= */

@media (min-width: 1024px) {
  /* ---- Drip block (FIGMA_SPEC §5 "Верх (drip, узел 1:579)") ----
     Decorative bands/photo stay absolute, sized off --d so the coloured
     duotone transition matches Figma at 1512 and scales down cleanly. The
     text frame is a normal 2-column grid laid on top, sized generously
     enough (min-height) that it never runs out of decorated background. */
  .ws-about-drip {
    /* Figma 1:579 at 1512: picture rows 166..794 sit at y 184..699, i.e. the
       whole picture starts at y 47.6 — kept at every width; the picture then
       scales with the viewport instead of being cropped. */
    --ws-drip-img-top: calc(47.6 * var(--d));
    --ws-drip-img-h: min(52.17cqw, 1001px); /* 960/1840 of the width, capped at the 1920px value */
    --ws-drip-split: calc(var(--ws-drip-img-top) + var(--ws-drip-img-h) / 2);
    min-height: calc(1137 * var(--d));
  }

  .ws-about-drip__top-band {
    display: none;
  }

  .ws-about-drip__image {
    position: absolute;
    left: 0;
    top: var(--ws-drip-img-top);
    width: 100%;
    /* natural height up to 1920px wide; beyond that the box stops growing
       and the picture is cropped from its flat top rows only (bottom-anchored),
       so the drip tips always stay whole */
    height: var(--ws-drip-img-h);
    object-fit: cover;
    object-position: 50% 100%;
    z-index: 1;
  }

  .ws-about-drip__frame {
    display: grid;
    grid-template-columns: minmax(0, 515fr) minmax(0, 605fr);
    column-gap: clamp(2rem, 4vw, calc(106 * var(--d)));
    row-gap: 0;
    align-items: start;
    max-width: calc(1226.4 * var(--d));
    margin-inline: auto;
    /* text starts 90 design px below the drip tips (picture row 800 of 960,
       i.e. 160 rows = 8.696 % of the width above the picture's bottom):
       y 795 at 1512, lower on wider screens where the picture is taller */
    padding: calc(var(--ws-drip-img-top) + var(--ws-drip-img-h) - 8.696cqw + 90 * var(--d)) 0 clamp(2.5rem, 8vw, 4rem);
  }

  .ws-about-drip__col--title {
    display: flex;
    flex-direction: column;
    gap: calc(10 * var(--d));
  }

  .ws-about-drip__eyebrow.ws-eyebrow {
    font-size: max(14px, calc(14 * var(--d)));
  }

  .ws-about-drip__title {
    margin: 0;
    max-width: none;
    font-size: max(14px, calc(48 * var(--d)));
    line-height: calc(52.8 * var(--d));
  }

  .ws-about-drip__p1,
  .ws-about-drip__p2 {
    max-width: none;
    font-size: max(14px, calc(16 * var(--d)));
    line-height: calc(22.4 * var(--d));
  }

  .ws-about-drip__col--copy {
    gap: calc(48 * var(--d));
  }

  /* ---- Light block (FIGMA_SPEC §5 "Низ (светлая, узел 10:24)") ----
     2x2 checkerboard matching Figma's placement: billboard top-left, copy
     top-right, contacts bottom-left, van bottom-right. */
  .ws-about-light {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "photo1 text"
      "contact photo2";
    column-gap: calc(79 * var(--d));
    row-gap: calc(47 * var(--d));
    align-items: start;
    padding-inline: calc(81 * var(--d));
  }

  .ws-about-light__blob {
    top: calc(-1158 * var(--d));
    left: calc(602 * var(--d));
    right: auto;
    width: calc(1900.477 * var(--d));
    height: calc(2030.911 * var(--d));
  }
  .ws-about-light__blob img {
    width: calc(1611.955 * var(--d));
    height: calc(1287.845 * var(--d));
    max-width: none;
    transform: rotate(118.47deg);
  }

  .ws-about-light__photo--billboard {
    grid-area: photo1;
  }
  .ws-about-light__photo--van {
    grid-area: photo2;
    align-self: end;
  }

  .ws-about-light__intro {
    grid-area: text;
    gap: calc(20 * var(--d));
  }
  .ws-about-light__eyebrow.ws-eyebrow {
    font-size: max(14px, calc(14 * var(--d)));
  }
  .ws-about-light__title {
    margin: 0;
    max-width: none;
    font-size: max(14px, calc(48 * var(--d)));
    line-height: calc(52.8 * var(--d));
  }
  .ws-about-light__p1,
  .ws-about-light__p2 {
    max-width: none;
    font-size: max(14px, calc(16 * var(--d)));
    line-height: calc(22.4 * var(--d));
  }

  .ws-about-light__contact {
    grid-area: contact;
  }
  .ws-about-contact__heading,
  .ws-about-contact__line {
    font-size: max(14px, calc(16 * var(--d)));
    line-height: calc(22.4 * var(--d));
  }
}

/* ================= Dark theme — Figma node 10:48 =================
   The dark About frame recolours the light block: headline, paragraphs and
   contacts in #FF8A63, eyebrow in #8A59C3, purple logo blob, plum footer.
   The drip block and the header over it are identical in both themes. */
.ws-about-light__blob img.ws-about-light__blob-img--dark {
  display: none;
}
:root[data-theme="dark"] .ws-about-light__blob img.ws-about-light__blob-img--light {
  display: none;
}
:root[data-theme="dark"] .ws-about-light__blob img.ws-about-light__blob-img--dark {
  display: block;
}
:root[data-theme="dark"] .ws-about-light__title,
:root[data-theme="dark"] .ws-about-light__p1,
:root[data-theme="dark"] .ws-about-light__p2,
:root[data-theme="dark"] .ws-about-light__contact,
:root[data-theme="dark"] .ws-about-light__contact * {
  color: #FF8A63;
}
:root[data-theme="dark"] .ws-about-light__eyebrow.ws-eyebrow {
  color: #8A59C3;
}
:root[data-theme="dark"] .site-shell:has(.ws-about-light) .ws-footer {
  background: #4C3544;
}

/* ---- src/styles/components/bestsellers.css ---- */
/*
 * Styles for src/sections/Bestsellers.jsx — Task F2 (docs/FIGMA_SPEC.md
 * §3/§4, nodes 1:178-1:233), rebuilt flow-first in Phase G (docs/MOTION.md
 * §2 — see Bestsellers.jsx's header comment for why: no more
 * position:absolute grid pinned to a fixed-height frame).
 *
 * .ws-bestsellers__grid is ONE class, three `display` modes by breakpoint
 * (flex scroll-snap lane / 2-col grid / 4-col grid) — see Bestsellers.jsx's
 * comment for the reasoning. `.ws-container` (src/styles/00-tokens.css:
 * max-width 1426px content + the site's own responsive gutter) is what
 * keeps every width, including the scroll lane, off the viewport edges.
 */

.ws-bestsellers {
  position: relative; /* anchors .ws-bestsellers__scroll-cue */
  padding-block: var(--ws-space-2xl) var(--ws-space-3xl);
  /* See src/styles/components/hero.css's .ws-hero comment: repaints over
     legacy.css's non-theme-aware `.site-shell` background — also covers the
     ~26px of Hero's own decorative bg art that bleeds under this section's
     top edge on purpose (Hero.jsx's header comment). */
  background: var(--ws-bg);
}

.ws-bestsellers__head {
  margin-bottom: var(--ws-space-xl);
}

/* ---- <640px: horizontal scroll-snap lane ---- */
.ws-bestsellers__grid {
  display: flex;
  gap: var(--ws-space-md);
  margin-block: var(--ws-space-xl);
  padding-block: 0.25rem; /* room for the hover/focus lift + tilt glow to not get clipped */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.ws-bestsellers__grid::-webkit-scrollbar {
  display: none;
}
.ws-bestsellers__item {
  flex: 0 0 auto;
  /* ~70% of the viewport, capped — leaves a clearly visible (not just a
     hairline) peek of the next card at every width down to 360px, so it
     reads as "scroll for more" without showing two full cards (which would
     look like a broken 2-up grid instead of a lane). */
  width: min(70vw, 300px);
  scroll-snap-align: start;
}

/* ---- 640-1023px: 2 columns (2x2 for today's 4 products) ---- */
@media (min-width: 640px) {
  .ws-bestsellers__grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--ws-space-lg);
    overflow: visible;
  }
  .ws-bestsellers__item {
    width: auto;
  }
  /* Generic safety net, inert today (PRODUCTS.length is 4, an exact fit):
     if the catalog ever grows to a count that leaves exactly one orphan
     card alone in the grid's last row, centre it instead of stranding it in
     the start column (docs/MOTION.md §2 rule 5). */
  .ws-bestsellers__grid > .ws-bestsellers__item:nth-child(2n+1):last-child {
    grid-column: 1 / -1;
    max-width: 22rem;
    margin-inline: auto;
  }
}

/* ---- >=1024px: 4 columns, one row ---- */
@media (min-width: 1024px) {
  .ws-bestsellers__grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: max(1rem, calc(29 * var(--d)));
  }
  .ws-bestsellers__grid > .ws-bestsellers__item:nth-child(2n+1):last-child {
    /* Cancel the 640px fallback above: at 4 columns a remainder of 1 needs
       its own rule (below), not the 2-column one. */
    grid-column: auto;
    max-width: none;
    margin-inline: 0;
  }
  .ws-bestsellers__grid > .ws-bestsellers__item:nth-child(4n+1):last-child {
    grid-column: 1 / -1;
    max-width: 20rem;
    margin-inline: auto;
  }
}

.ws-bestsellers__link {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--ws-space-2xs);
  color: var(--ws-accent-text);
  font-family: var(--ws-font-display);
  font-weight: 600;
  font-size: 1.0625rem;
  text-decoration: none;
}
.ws-bestsellers__link:hover {
  text-decoration: underline;
}
.ws-bestsellers__link img {
  display: block;
  transition: translate var(--ws-duration-base) var(--ws-ease);
}
.ws-bestsellers__link:hover img {
  translate: 5px 0;
}
@media (prefers-reduced-motion: reduce) {
  .ws-bestsellers__link img {
    transition: none;
  }
}

@media (min-width: 1024px) {
  .ws-bestsellers__link {
    margin-top: var(--ws-space-lg);
    gap: calc(10 * var(--d));
    font-size: max(14px, calc(20 * var(--d)));
    line-height: 1.05; /* 21/20 */
  }
  .ws-bestsellers__link img {
    width: calc(20 * var(--d));
    height: auto;
  }
}

/* ---- Decorative circle arrow — straddles the seam with BoxPromo ---- */
.ws-bestsellers__scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 0;
  z-index: 2; /* above BoxPromo's own background, painted later in the DOM — both are z-index:auto sections, so this integer alone wins */
  transform: translate(-50%, 50%); /* pure layout placement, not motion — the idle wobble below animates `transform` too, so it's folded into the same @keyframes rather than stacked separately */
  animation: ws-scroll-cue-wobble 4.2s var(--ws-ease) infinite;
}
/* CircleButton's own `.ws-circle-btn:hover { translate: 0 -2px }`
   (src/styles/components/ui.css) already covers "on hover — a shift"
   (docs/MOTION.md §4 "Круглая стрелка") — `translate` and `transform` are
   independent properties, so it composes with the wobble below for free. */
@keyframes ws-scroll-cue-wobble {
  0%,
  100% {
    transform: translate(-50%, 50%) rotate(0deg);
  }
  50% {
    transform: translate(-50%, 50%) rotate(-6deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .ws-bestsellers__scroll-cue {
    animation: none;
  }
}

/* ---- src/styles/components/box-promo.css ---- */
/*
 * Styles for src/sections/BoxPromo.jsx — Task F2 (docs/FIGMA_SPEC.md §3/§4,
 * nodes 1:12-1:22 + 1:50), rebuilt flow-first + animated in Phase G
 * (docs/MOTION.md §2/§4 — see BoxPromo.jsx's header comment). --ws-promo-*
 * tokens are identical in both site themes (src/styles/00-tokens.css), so
 * this file has no [data-theme="dark"] overrides.
 *
 * <1024px: art above copy, centred. >=1024px: art beside copy (grid),
 * mirroring Hero's own two-column shape (src/styles/components/hero.css).
 * Both use `.ws-container` (max-width 1426px content + the site's own
 * gutter) so nothing ever touches the viewport edge — including the circle
 * scroll-cue from Bestsellers straddling this section's top edge
 * (bestsellers.css): `padding-top` here is deliberately generous so that
 * decoration never lands anywhere near the title text, at any width.
 */

.ws-box-promo {
  position: relative;
  overflow: clip; /* the blob is intentionally oversized/soft-edged; never let it cause horizontal page scroll */
  background: var(--ws-promo-bg);
  padding-block: clamp(5.5rem, 7rem + 4vw, 9rem) clamp(4rem, 5vw, 6rem);
}

/* FIGMA_SPEC's own blob only ever covered the left ~852px of the 1512-wide
   section; this task's brief asks for it full-bleed instead ("фиолетовый
   фон и клякса — на всю ширину") — object-fit:cover keeps its shape
   proportional while filling the whole section. */
.ws-box-promo__blob {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: 20% 50%;
  opacity: 0.7;
  pointer-events: none;
}

.ws-box-promo__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ws-space-xl);
  text-align: center;
}

/* ---- 3D art (docs/MOTION.md §4 "Промо «Lag din egen smaksboks»") ---- */
.ws-box-promo__art {
  width: min(26rem, 82vw);
}
.ws-box-promo__art-inner {
  display: block;
  will-change: transform;
}
/* Three layers of Figma group 1:50 (assets/brand/promo/layers.json), placed
   in % of the group's bounds so the composition scales as one piece. */
.ws-box-promo__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 691 / 643.34;
}
.ws-box-promo__layer {
  position: absolute;
  display: block;
  height: auto;
  max-width: none;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}
.ws-box-promo__layer--box-back {
  filter: drop-shadow(-18px 22px 30px rgba(56, 26, 90, 0.28));
}

/* The snacks bob inside the box: they rise a little above the front wall and
   settle back, on their own rhythm (the whole scene floats separately). Uses
   the individual `translate`/`rotate` properties so nothing else fights it. */
@keyframes ws-promo-snacks-bob {
  0%,
  100% {
    translate: 0 0;
    rotate: 0deg;
  }
  30% {
    translate: 0 -4.5%;
    rotate: -1.2deg;
  }
  55% {
    translate: 0 -1.5%;
    rotate: 0.6deg;
  }
  75% {
    translate: 0 -3%;
    rotate: 0.2deg;
  }
}
@media (prefers-reduced-motion: no-preference) {
  .ws-box-promo__layer--snacks {
    transform-origin: 50% 100%;
    animation: ws-promo-snacks-bob 3.8s ease-in-out infinite;
  }
}

.ws-box-promo__copy {
  position: relative;
  z-index: 1;
  max-width: 32rem;
}

.ws-box-promo__title {
  margin: 0 0 var(--ws-space-md);
  color: var(--ws-promo-text);
  font-size: var(--ws-fs-hero);
  font-weight: 700;
  line-height: 1.2;
}

.ws-box-promo__sub {
  margin: 0 0 var(--ws-space-lg);
  color: var(--ws-promo-text-muted);
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: var(--ws-fs-sub);
  line-height: var(--ws-lh-snug);
}

@media (min-width: 1024px) {
  .ws-box-promo__inner {
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: clamp(2.5rem, 6vw, 5.5rem);
    text-align: left;
  }

  .ws-box-promo__art {
    flex: 0 0 auto;
    width: min(30rem, 40%);
  }

  .ws-box-promo__copy {
    max-width: calc(657 * var(--d));
  }

  .ws-box-promo__title {
    max-width: calc(540 * var(--d)); /* ORC F5: the Norwegian copy needs a narrower box to break into three lines like the Figma reference */
    font-size: calc(90 * var(--d));
    line-height: 94.52%;
  }

  .ws-box-promo__sub {
    max-width: calc(589 * var(--d));
    font-size: max(18px, calc(32 * var(--d)));
    line-height: 1.288;
  }
}

/* ---- src/styles/components/brands.css ---- */
/*
 * Styles for src/sections/Brands.jsx — Task F2 (docs/FIGMA_SPEC.md §3/§4,
 * node 1:234 "cooperation"), Phase G (docs/MOTION.md §2/§4 "Бренды"), Task G5
 * (interactive marquee — see Brands.jsx's header comment for the JS motion
 * model). Cards stay a fixed white/light surface in BOTH site themes on
 * purpose (docs/design/previews/home-dark.fonts.png: the logo cutouts assume
 * a light backdrop) — see --ws-btn-light-bg in src/styles/00-tokens.css,
 * which is itself fixed for the same reason; the border/glow colour comes
 * from each BRANDS entry (src/lib/content.js, `border`), not a token (a
 * different peach shade per card in Figma).
 *
 * `.ws-brands-reviews` + `.ws-brands-reviews__blob` (FIGMA_SPEC §4 "Клякса
 * справа" / "Brands" / "Feedback" rows): the shared wrapper + right-blob
 * decoration for src/sections/Brands.jsx AND src/sections/ReviewsSection.jsx
 * together (FIGMA_SPEC: "за брендами и отзывами" — the blob sits BEHIND
 * both sections, so it needs one wrapper spanning both, built in
 * src/pages/HomePage.jsx). Defined here rather than duplicated into
 * reviews.css because Brands owns the top of that combined region (the
 * wrapper's own top padding is what lines Brands' content up at the right
 * Figma y) — reviews.css only adds the small gap above ITS OWN section, see
 * that file. The blob is pure decoration (aria-hidden, pointer-events:none)
 * so it staying `position: absolute` is fine under docs/MOTION.md §2 rule 1
 * ("absolute is OK for decor inside a `position: relative` section with
 * `overflow-x: clip`, and it must never cover text") — the marquee below it
 * never overlaps the blob's own footprint on any width this file supports.
 *
 * G5 marquee: `.ws-brands__viewport` clips a `.ws-brands__track` holding
 * TWO copies of BRANDS back to back (src/sections/Brands.jsx, second copy
 * `aria-hidden`); a `useEffect` there sets `track.style.translate` every
 * frame (drag, autoplay, scroll-reactive, or an eased button tween — never a
 * CSS `animation`), so this file has no `@keyframes` for the loop itself.
 * Before that effect mounts (and as a no-JS fallback) the viewport is a
 * plain native-scrollable row (`overflow-x: auto`, duplicate hidden) —
 * `.ws-brands__viewport--live` (added by the effect) swaps that for the
 * clipped/dragged version and reveals the duplicate again.
 */

.ws-brands-reviews {
  position: relative;
  overflow-x: clip; /* the marquee track and the blob both bleed past the viewport edge on purpose */
  /* The wrapper paints the page background, not each section — .ws-brands
     and .ws-reviews (reviews.css) are transparent so .ws-brands-reviews__blob
     shows through both of them (Figma: the blob sits behind Brands AND
     Feedback, not clipped to one). */
  background: var(--ws-bg);
}

.ws-brands-reviews__blob {
  display: none; /* decorative only; skipped below 1024px to avoid mobile clutter/overflow */
  position: absolute;
  z-index: 0;
  pointer-events: none;
}

.ws-brands {
  position: relative;
  z-index: 1; /* above .ws-brands-reviews__blob */
  padding-block: var(--ws-space-2xl) var(--ws-space-3xl);
  /* Transparent on purpose — see .ws-brands-reviews above: the wrapper
     paints the shared page background so the blob shows through the gaps. */
  background: transparent;
}

/* ---- heading + prev/next row (matches .ws-reviews__top, reviews.css) ---- */
.ws-brands__top {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--ws-space-md);
  padding-inline: var(--ws-gutter);
  margin-bottom: var(--ws-space-lg);
}
.ws-brands__head {
  margin: 0;
}
.ws-brands__nav {
  display: flex;
  flex-shrink: 0;
  gap: var(--ws-space-sm);
}

/* ---- marquee ---- */
.ws-brands__viewport {
  position: relative;
  width: 100%;
  padding-block: var(--ws-space-lg) var(--ws-space-2xs);
  /* Pre-JS / no-JS fallback: a plain scrollable row. */
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.ws-brands__viewport::-webkit-scrollbar {
  display: none;
}
.ws-brands__viewport:focus-visible {
  outline: var(--ws-focus-ring-width) solid var(--ws-focus-ring-color);
  outline-offset: var(--ws-focus-ring-offset);
}
/* the aria-hidden duplicate only exists to feed the JS marquee's wrap
   illusion — hidden until the effect confirms it's driving the ribbon. */
.ws-brands__card[aria-hidden="true"] {
  display: none;
}

.ws-brands__viewport--live {
  overflow: hidden;
  touch-action: pan-y; /* horizontal drag is ours (pointer events); vertical page scroll still passes through */
  user-select: none;
  cursor: grab;
}
.ws-brands__viewport--live.is-dragging {
  cursor: grabbing;
}
.ws-brands__viewport--live .ws-brands__card[aria-hidden="true"] {
  display: flex;
}

.ws-brands__track {
  display: flex;
  align-items: center;
  gap: var(--ws-space-md);
  width: max-content;
  padding-left: var(--ws-gutter);
}

/* Cursor spotlight — subtle, off by default, only while the ribbon is
   hovered/focused (same trigger as the old CSS pause). Position comes from
   --ws-brands-spot-x/-y (px, set on pointermove in Brands.jsx, fine-pointer
   + motion-allowed only). */
/* Soft glow that follows the pointer BEHIND the cards (z-index 0 — on top
   it tinted the logos). The live viewport clips overflow, so the glow is
   also masked to fade out well before the viewport's top and bottom edges:
   no visible straight cut-off. */
.ws-brands__spotlight {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--ws-duration-slow) var(--ws-ease);
  background: radial-gradient(
    320px 220px at var(--ws-brands-spot-x, 50%) var(--ws-brands-spot-y, 50%),
    color-mix(in srgb, var(--ws-brand-pink) 30%, transparent) 0%,
    color-mix(in srgb, var(--ws-brand-pink) 12%, transparent) 45%,
    transparent 72%
  );
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 30%, #000 70%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 30%, #000 70%, transparent 100%);
}
.ws-brands__track {
  position: relative;
  z-index: 1;
}
.ws-brands__viewport--live:hover .ws-brands__spotlight,
.ws-brands__viewport--live:focus-within .ws-brands__spotlight {
  opacity: 1;
}

/* FIGMA_SPEC §3 "Карточки брендов": #FFFFFF opacity .8 — verified against
   the actual reference render (docs/design/previews/home-*.fonts.png) to be
   TRUE translucency (the card's fill+border blend with the page background
   behind it, matching the Figma source's own "opacity-80" Tailwind class on
   a background-only <div>), not a flat off-white. The logo <img> is a
   separate, always-full-opacity element in the Figma source (not a child of
   that faded rectangle) — reproduced here with `color-mix(...,transparent)`
   on just the background/border of this element, so the nested <img> below
   is never touched by it (only descendant opacity would fade the logo too,
   which the Figma source deliberately avoids).
 *
 * G5 interactivity lives entirely on `translate`/`box-shadow`/`border-color`
 * (hover lift + brand-colour glow) and `scale` (click bounce, see
 * @keyframes ws-brand-bounce below) — `transform` itself is owned by the
 * `[data-tilt]` engine (src/lib/motion.js sets --tilt-rx/--tilt-ry on this
 * same element), so none of this fights the tilt.
 */
.ws-brands__card {
  flex: 0 0 auto;
  box-sizing: border-box;
  width: 410px;
  height: 163px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--ws-space-md);
  background: color-mix(in srgb, var(--ws-btn-light-bg) 80%, transparent);
  border: 1.5px solid color-mix(in srgb, var(--ws-brand-card-border, var(--ws-border-peach-fixed)) 80%, transparent);
  border-radius: var(--ws-radius-card);
  translate: 0 0;
  transition:
    translate var(--ws-duration-base) var(--ws-ease),
    box-shadow var(--ws-duration-base) var(--ws-ease),
    border-color var(--ws-duration-base) var(--ws-ease);
}
.ws-brands__card:hover,
.ws-brands__card.is-tilting {
  translate: 0 -6px;
  border-color: var(--ws-brand-card-border, var(--ws-border-peach-fixed));
  box-shadow:
    0 22px 38px color-mix(in srgb, var(--ws-brand-card-border, var(--ws-brand-orange)) 38%, transparent),
    0 0 0 3px color-mix(in srgb, var(--ws-brand-card-border, var(--ws-brand-orange)) 28%, transparent);
}

@keyframes ws-brand-bounce {
  0% {
    scale: 1;
  }
  35% {
    scale: 1.08;
  }
  62% {
    scale: 0.96;
  }
  100% {
    scale: 1;
  }
}
.ws-brands__card.is-bounce {
  animation: ws-brand-bounce 0.45s var(--ws-ease);
}

/* The pop wrapper must fill the card's content box: the logo's own
   max-width/max-height: 100% only resolve against a definite parent size —
   with a shrink-to-fit wrapper the logos rendered at their intrinsic 320px
   and spilled out of the cards. */
.ws-brands__card .ws-tilt-pop {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
}

.ws-brands__card img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

@media (max-width: 700px) {
  .ws-brands__card {
    width: 300px;
    height: 120px;
    border-radius: calc(var(--ws-radius-card) * 0.75);
  }
}

@media (max-width: 420px) {
  .ws-brands__card {
    width: 250px;
    height: 100px;
    padding: var(--ws-space-sm);
  }
}

@media (min-width: 1024px) {
  .ws-brands-reviews {
    /* Lines Brands' content up at FIGMA_SPEC's absolute y=3305: this wrapper
       starts right where BoxPromo's flow ends (2238.49 + 984.5 = 3222.99),
       so 3305 - 3222.99 = 82.01. Padding (not margin) keeps this offset from
       shifting .ws-brands-reviews__blob's own `top` below, which is measured
       from the padding box's edge regardless of padding-top's size. */
    padding-top: calc(82.01 * var(--d));
  }

  .ws-brands-reviews__blob {
    display: block;
    left: calc(394 * var(--d));
    top: calc(170.01 * var(--d)); /* FIGMA_SPEC absolute y=3393 - wrapper's own top (3222.99) */
    width: calc(1118 * var(--d));
    height: calc(1002 * var(--d));
  }

  .ws-brands {
    padding-block: 0 calc(56 * var(--d));
  }

  .ws-brands__top {
    padding-inline: calc(31 * var(--d)) var(--ws-gutter);
    margin-bottom: calc(24 * var(--d));
  }
  .ws-brands__nav {
    gap: calc(12 * var(--d));
  }

  .ws-brands__viewport {
    padding-block: calc(20 * var(--d)) 0;
  }

  .ws-brands__track {
    padding-left: calc(31 * var(--d));
    gap: calc(38 * var(--d)); /* 481.15 - 412.225 column step */
  }

  .ws-brands__card {
    width: calc(412 * var(--d));
    height: calc(165 * var(--d));
    border-width: 2px;
    border-radius: calc(30 * var(--d));
  }
}

/* ---- reduced motion: no autoplay/scroll-react/spotlight (Brands.jsx skips
   them directly, `reduced` flag); the card lift stays (a short hover
   transition, not a decorative loop) but the click bounce is never added by
   JS, and the tilt itself is already zeroed by 03-motion.css. ---- */
@media (prefers-reduced-motion: reduce) {
  .ws-brands__spotlight {
    display: none;
  }
}

/* Room for the lifted card's brand-colour glow (22 px + 38 px blur) and the
   spotlight: the live viewport clips overflow, so it gets extra padding that
   the negative margin gives back — the layout does not move. */
.ws-brands__viewport.ws-brands__viewport--live {
  padding-block-start: calc(var(--ws-brands-pad-top, 0px) + 28px);
  padding-block-end: 76px;
  margin-block: -28px -76px;
}
.ws-brands__viewport--live {
  --ws-brands-pad-top: var(--ws-space-lg);
}
@media (min-width: 1024px) {
  .ws-brands__viewport--live {
    --ws-brands-pad-top: calc(20 * var(--d));
  }
}

/* ---- src/styles/components/bundle.css ---- */
/*
 * Styles for src/sections/BundleSection.jsx (Smaksboks builder) — T4.5a
 * (docs/TASKS.md), docs/DESIGN.md. Tokens only (src/styles/00-tokens.css);
 * no legacy.css classes. Responsive checkpoints per AGENT_RULES §3:
 * 360-390 / 768 / 1024 / 1512.
 *
 * Row media sizing targets a plain `img` selector scoped under
 * .ws-bundle-row__media: since T4.5b (docs/TASKS.md) this element renders a
 * plain src/ui/ProductImage.jsx img (the legacy wrapper primitive it used to
 * render through is deleted) — this rule stays a layout constraint on a
 * child element, not a dependency on any component's styling identity.
 */

.ws-bundle {
  background: var(--ws-bg);
  padding-block: clamp(1rem, 0.6vw + 1rem, 2rem) clamp(3rem, 2rem + 3vw, 5rem);
}

.ws-bundle__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.7fr) minmax(19rem, 1fr);
  gap: var(--ws-space-2xl);
  align-items: start;
}

.ws-bundle__title {
  font-family: var(--ws-font-display);
  font-size: var(--ws-fs-section-title);
  font-weight: 300;
  color: var(--ws-heading);
}
@media (min-width: 1024px) {
  .ws-bundle__title {
    font-size: calc(36 * var(--d));
  }
}

.ws-bundle__hint {
  margin-top: var(--ws-space-2xs);
  color: var(--ws-text-muted);
  font-size: var(--ws-fs-small);
  max-width: 46ch;
}
/* --ws-text-muted is fixed both themes and this sits directly on --ws-bg —
   same dark-mode fix as subpage-hero.css's .ws-subpage-hero__lead. */
:root[data-theme="dark"] .ws-bundle__hint {
  color: rgba(255, 231, 228, 0.65);
}

/* ---- Rows ---- */

.ws-bundle-rows {
  list-style: none;
  margin: var(--ws-space-lg) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
}

/* White r=8 card, exact Figma product-card shadow — Task F4
   (docs/FIGMA_SPEC.md §3), same treatment used across every other card on
   the redesigned inner pages (see product-page.css/craft.css/shop.css). */
.ws-bundle-row {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--ws-space-md);
  padding: var(--ws-space-sm);
  background: #FFFFFF;
  border-radius: 8px;
  box-shadow: var(--ws-shadow-product-card);
}
:root[data-theme="dark"] .ws-bundle-row {
  background: rgba(255, 255, 255, 0.8);
}

.ws-bundle-row__media {
  width: 4.5rem;
  height: 4.5rem;
  flex-shrink: 0;
  border-radius: 8px;
  overflow: hidden;
  background: var(--ws-bg);
  display: flex;
  align-items: center;
  justify-content: center;
}
.ws-bundle-row__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ws-bundle-row__info {
  min-width: 0;
}
/* Montserrat 400 20/28 #9C5EE5 — FIGMA_SPEC §3 "Название товара", exactly
   like the home product card's name (src/styles/components/product-card.css). */
.ws-bundle-row__name {
  display: flex;
  align-items: center;
  gap: var(--ws-space-2xs);
  font-family: var(--ws-font-display);
  font-weight: 400;
  font-size: 1.25rem;
  line-height: 1.4;
  color: var(--ws-product-name);
  margin: 0;
}
.ws-bundle-row__badge {
  flex-shrink: 0;
  padding: 0.15rem 0.5rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-bg);
  color: var(--ws-accent-text);
  font-family: var(--ws-font-body);
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.ws-bundle-row__desc {
  margin: 0.25rem 0 0;
  color: var(--ws-text-muted);
  font-size: 0.9375rem;
  line-height: var(--ws-lh-normal);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ws-bundle-row__quote {
  margin: 0.25rem 0 0;
  color: var(--ws-link-pink);
  font-size: 0.875rem;
  font-style: italic;
}

/* Montserrat 700 — FIGMA_SPEC §3 "prices Montserrat 700". */
.ws-bundle-row__price {
  font-family: var(--ws-font-display);
  font-weight: 700;
  color: var(--ws-text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ws-bundle-row__control {
  flex-shrink: 0;
}
/* .ws-btn--light (src/styles/components/ui.css) is a borderless white
   button, meant for use on a coloured band (CtaBand's pink) — invisible
   with no edge at all on this row's own white card. Same fix as
   src/styles/components/payment-return.css's .ws-payment-status__actions
   .ws-btn--light. */
.ws-bundle-row__control .ws-btn--light {
  border: 1.5px solid var(--ws-brand-pink);
}

/* ---- Stepper ---- */
/* "Steppers like above" (this task's brief, referring back to ProductPage's
   own qty stepper spec) — free-standing 44px circles, 2px #FF862F border,
   matching src/styles/components/product-page.css's .ws-pdp__qty exactly. */

.ws-bundle-stepper {
  display: inline-flex;
  align-items: center;
  gap: var(--ws-space-xs);
}
.ws-bundle-stepper__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem; /* 44px */
  height: 2.75rem;
  border: 2px solid var(--ws-brand-orange);
  border-radius: var(--ws-radius-pill);
  background: transparent;
  color: var(--ws-brand-orange);
  cursor: pointer;
  transition: background-color var(--ws-duration-fast) var(--ws-ease), color var(--ws-duration-fast) var(--ws-ease);
}
.ws-bundle-stepper__btn:hover:not(:disabled) {
  background: var(--ws-brand-orange);
  color: #FFFFFF;
}
.ws-bundle-stepper__btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.ws-bundle-stepper__value {
  min-width: 1.5rem;
  text-align: center;
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ---- Summary card ---- */
/* Purple promo style — this task's brief: "#9C5EE5 bg, texts #FF862F/white
   per Figma promo" (FIGMA_SPEC §3/§4 "Промо (фиолетовая)": bg --ws-promo-bg,
   H2/subtitle --ws-promo-text orange). The big numbers (count/price) get the
   same orange as the home promo's H2; supporting copy (label/shipping/list)
   is cream/white for legibility, echoing how CtaBand's cream band splits its
   own hierarchy (src/styles/components/cta-band.css). Button is variant
   "promo" in BundleSection.jsx — the one key conversion on this page. */
.ws-bundle-summary {
  position: sticky;
  top: calc(5.5rem + var(--ws-space-md));
  background: var(--ws-promo-bg);
  border-radius: var(--ws-radius-md);
  box-shadow: var(--ws-shadow-btn-promo);
  padding: var(--ws-space-lg);
}

.ws-bundle-summary__label {
  margin: 0;
  color: var(--ws-cream);
  opacity: 0.85;
  font-family: var(--ws-font-display);
  font-size: var(--ws-fs-small);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 700;
}
.ws-bundle-summary__count {
  margin: var(--ws-space-3xs) 0 0;
  font-family: var(--ws-font-display);
  font-weight: 400;
  font-size: 1.5rem;
  color: var(--ws-cream);
}
.ws-bundle-summary__price {
  margin: 0;
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: 2.25rem;
  /* the running total is the one number to read at a glance: cream on the
     promo purple (7:1) instead of the orange (1.7:1) */
  color: var(--ws-cream);
}

.ws-bundle-summary__shipping {
  margin-top: var(--ws-space-xs);
  color: var(--ws-cream);
  font-size: var(--ws-fs-small);
}
.ws-bundle-summary__track {
  margin-top: var(--ws-space-2xs);
  height: 0.5rem;
  border-radius: var(--ws-radius-pill);
  background: rgba(255, 255, 255, 0.28);
  overflow: hidden;
}
.ws-bundle-summary__track span {
  display: block;
  height: 100%;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-brand-orange);
  transition: width var(--ws-duration-base) var(--ws-ease-soft);
}

.ws-bundle-summary__unlock {
  margin-top: var(--ws-space-sm);
  padding: var(--ws-space-2xs) var(--ws-space-sm);
  border-radius: var(--ws-radius-sm);
  background: rgba(255, 255, 255, 0.92);
  color: var(--ws-promo-bg);
  font-family: var(--ws-font-display);
  font-size: var(--ws-fs-small);
  font-weight: 700;
}
.ws-bundle-summary__discount {
  margin-top: var(--ws-space-sm);
  color: var(--ws-promo-text);
  font-weight: 700;
  font-size: var(--ws-fs-small);
}

.ws-bundle-summary__items {
  list-style: none;
  margin: var(--ws-space-md) 0;
  padding-top: var(--ws-space-sm);
  border-top: 1px solid rgba(255, 255, 255, 0.28);
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-2xs);
  font-size: 0.9375rem;
}
.ws-bundle-summary__items li {
  display: flex;
  justify-content: space-between;
  gap: var(--ws-space-sm);
  color: var(--ws-cream);
}
.ws-bundle-summary__empty {
  color: var(--ws-cream) !important;
  opacity: 0.75;
  font-style: italic;
}

/* .ws-btn--promo's own >=1024px rule (src/styles/components/ui.css) fixes
   the Figma button to its literal 478x101 box — right for the home promo's
   short "Lag min boks" label, but this card's longer Norwegian label needs
   to fit a narrower sidebar column, so this scoped override (higher
   specificity, ui.css is out of this task's scope to edit) restores
   fullWidth and lets the label wrap instead of overflowing. */
.ws-bundle-summary .ws-btn.ws-btn--promo {
  width: 100%;
  white-space: normal;
  text-align: center;
  line-height: 1.2;
}
@media (min-width: 1024px) {
  .ws-bundle-summary .ws-btn.ws-btn--promo {
    height: auto;
    min-height: calc(70 * var(--d));
    padding-block: calc(14 * var(--d));
    font-size: max(15px, calc(22 * var(--d)));
  }
}

@media (max-width: 960px) {
  .ws-bundle__grid {
    grid-template-columns: 1fr;
  }
  .ws-bundle-summary {
    position: static;
  }
}

@media (max-width: 560px) {
  .ws-bundle-row {
    grid-template-columns: 3.75rem 1fr;
    grid-template-areas:
      "media info"
      "control control"
      "price price";
    row-gap: var(--ws-space-xs);
  }
  .ws-bundle-row__media { grid-area: media; width: 3.75rem; height: 3.75rem; }
  .ws-bundle-row__info { grid-area: info; }
  .ws-bundle-row__price { grid-area: price; }
  .ws-bundle-row__control { grid-area: control; }
  .ws-bundle-stepper { width: 100%; justify-content: space-between; }
}

/* ---- src/styles/components/cart-drawer.css ---- */
/*
 * CartDrawer — T4.6 (docs/TASKS.md), docs/DESIGN.md §2.1. Right-side panel on
 * desktop (~440px), full-screen bottom sheet on mobile (<=768px — same
 * breakpoint MobileStickyBuy switches on, src/styles/components/sticky-buy.css).
 * Tokens only (src/styles/00-tokens.css); no legacy.css classes/vars.
 *
 * Colour choices verified with `node docs/tools/contrast.mjs` (see the T4.6
 * report): --ws-text-muted doubles as the neutral divider/border colour
 * everywhere (5.36:1/9.86:1 on --ws-bg — comfortably clears the ≥3:1 UI-border
 * rule), --ws-heading for the product name (4.70:1 on --ws-bg), --ws-link-pink
 * for discount/promo emphasis (4.65:1 on --ws-bg, 5.49:1 on white, 4.62:1 on
 * the dark --ws-surface composite), --ws-accent-text for the order error.
 *
 * Vipps/Klarna pay buttons (.ws-pay-btn--vipps/--klarna) intentionally use the
 * providers' own fixed brand colours, NOT --ws-btn-* tokens (T4.6 brief: "must
 * stay recognisably on-brand for those providers ... do not recolour to
 * WeSnack colours"). Selectors are two classes deep (.ws-btn.ws-pay-btn--*) so
 * they out-specificity ui.css's .ws-btn--primary regardless of the two files'
 * relative load order (components/*.css loads alphabetically by full path —
 * prerender/build.js discoverStyleManifest — and "cart-drawer.css" sorts
 * before "ui.css", i.e. ui.css would otherwise win on source order alone).
 */

.ws-cart-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--ws-z-overlay);
  background: rgba(31, 29, 28, 0.45);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--ws-duration-base) var(--ws-ease);
}
.ws-cart-overlay--open {
  opacity: 1;
  pointer-events: auto;
}

.ws-cart-drawer {
  display: flex;
  flex-direction: column;
  height: 100%;
  width: min(100%, 27.5rem); /* ~440px, DESIGN.md brief */
  margin-left: auto;
  background: var(--ws-bg);
  color: var(--ws-text);
  box-shadow: var(--ws-shadow-popover);
  transform: translateX(100%);
  transition: transform var(--ws-duration-base) var(--ws-ease);
}
/* --ws-text is fixed dark grey in both themes (src/styles/00-tokens.css)
   and the drawer panel's own bg is --ws-bg, which DOES flip near-black —
   this default inherited colour (and every element below that relies on
   --ws-text/--ws-text-muted directly against the panel/footer surface
   rather than a white line card) needs the same dark-mode fix as every
   other inner page in this task (see src/styles/components/subpage-hero.css's
   .ws-subpage-hero__lead for the full rationale). */
:root[data-theme="dark"] .ws-cart-drawer {
  color: rgba(255, 231, 228, 0.92);
}
.ws-cart-overlay--open .ws-cart-drawer {
  transform: translateX(0);
}

@media (max-width: 768px) {
  .ws-cart-drawer {
    width: 100%;
    max-width: none;
  }
}

.ws-cart-drawer__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ws-space-sm);
  padding: var(--ws-space-md) var(--ws-space-lg);
  border-bottom: 1px solid var(--ws-surface-border);
  flex-shrink: 0;
}
.ws-cart-drawer__title {
  font-size: 1.5rem;
  margin: 0;
}
.ws-cart-drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  flex-shrink: 0;
  border: none;
  border-radius: var(--ws-radius-pill);
  background: transparent;
  color: var(--ws-text);
  cursor: pointer;
  transition: background-color var(--ws-duration-fast) var(--ws-ease);
}
.ws-cart-drawer__close:hover {
  background: var(--ws-surface);
}
:root[data-theme="dark"] .ws-cart-drawer__close {
  color: rgba(255, 231, 228, 0.92);
}

.ws-cart-drawer__body {
  flex: 1;
  overflow-y: auto;
  padding: 0 var(--ws-space-lg) var(--ws-space-lg);
}

.ws-cart-drawer__done {
  height: 100%;
  display: grid;
  align-content: center;
  gap: var(--ws-space-sm);
  padding: var(--ws-space-lg);
}
.ws-cart-drawer__done-title {
  font-size: 1.5rem;
}
.ws-cart-drawer__order-id {
  color: var(--ws-text);
  font-weight: 600;
  line-height: 1.5;
  margin: 0;
}
.ws-cart-drawer__done-copy {
  color: var(--ws-text-muted);
  line-height: 1.6;
  margin: 0;
}
:root[data-theme="dark"] .ws-cart-drawer__order-id {
  color: rgba(255, 231, 228, 0.92);
}
:root[data-theme="dark"] .ws-cart-drawer__done-copy {
  color: rgba(255, 231, 228, 0.65);
}

.ws-cart-drawer__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 17.5rem;
  color: var(--ws-text-muted);
  text-align: center;
  line-height: 1.5;
}
:root[data-theme="dark"] .ws-cart-drawer__empty {
  color: rgba(255, 231, 228, 0.65);
}

.ws-cart-lines {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
}

/* White r=8 line card — this task's brief: "white r=8 line cards". */
.ws-cart-line {
  display: grid;
  grid-template-columns: 4.5rem 1fr auto;
  gap: var(--ws-space-sm);
  padding: var(--ws-space-sm);
  background: #FFFFFF;
  border-radius: 8px;
  box-shadow: var(--ws-shadow-product-card);
}
:root[data-theme="dark"] .ws-cart-line {
  background: rgba(255, 255, 255, 0.8);
}

.ws-cart-line__media {
  width: 4.5rem;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
  overflow: hidden;
  background: var(--ws-bg);
  flex-shrink: 0;
}
.ws-cart-line__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ws-cart-line__info {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-3xs);
}
/* Montserrat names #9C5EE5 — this task's brief, FIGMA_SPEC §3 "Название
   товара" (same colour/weight as the home product card's name). */
.ws-cart-line__name {
  font-family: var(--ws-font-display);
  font-size: 1.25rem; /* 20px — matches FIGMA_SPEC §3 "Название товара" exactly, and clears the WCAG large-text threshold for --ws-product-name's 4.11:1 on the dark background */
  font-weight: 400;
  color: var(--ws-product-name);
  margin: 0;
}
.ws-cart-line__variant {
  color: var(--ws-text-muted);
  font-size: var(--ws-fs-small);
  margin: 0 0 var(--ws-space-3xs);
}

.ws-cart-line__price {
  font-family: var(--ws-font-display);
  font-weight: 700;
  white-space: nowrap;
  align-self: start;
  font-variant-numeric: tabular-nums;
}

/* ---- Qty stepper ----
   Same "circle, 2px #FF862F border" language as ProductPage/BundleSection
   (src/styles/components/product-page.css's .ws-pdp__qty, bundle.css's
   .ws-bundle-stepper) but scaled down to fit inline in a compact cart line —
   32px circles rather than the full 44px, still unmistakably the same
   control. */
.ws-qty-stepper {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  width: fit-content;
}
.ws-qty-stepper__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1.5px solid var(--ws-brand-orange);
  border-radius: var(--ws-radius-pill);
  background: transparent;
  color: var(--ws-brand-orange);
  cursor: pointer;
  transition: background-color var(--ws-duration-fast) var(--ws-ease), color var(--ws-duration-fast) var(--ws-ease);
}
.ws-qty-stepper__btn:hover {
  background: var(--ws-brand-orange);
  color: #FFFFFF;
}
.ws-qty-stepper__btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.ws-qty-stepper__value {
  min-width: 1.25rem;
  text-align: center;
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-size: var(--ws-fs-small);
}

/* ---- Gift wrap ---- */

.ws-gift-wrap {
  margin-top: var(--ws-space-sm);
  padding-top: var(--ws-space-sm);
  border-top: 1px solid var(--ws-text-muted);
}
.ws-gift-wrap__label {
  display: flex;
  align-items: center;
  gap: var(--ws-space-2xs);
  font-size: var(--ws-fs-small);
  cursor: pointer;
}
.ws-gift-wrap__checkbox {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  accent-color: var(--ws-btn-primary-bg);
  cursor: pointer;
}
/* "Like the footer newsletter field" — see checkout.css's .ws-field__input
   header comment for the token choice. */
.ws-gift-wrap__message {
  box-sizing: border-box;
  width: 100%;
  margin-top: var(--ws-space-2xs);
  padding: var(--ws-space-xs);
  border: 1px solid var(--ws-footer-input-border);
  border-radius: 8px;
  background: var(--ws-footer-input-bg);
  color: var(--ws-text);
  font-family: var(--ws-font-body);
  font-size: 1rem;
  line-height: 1.5;
  resize: vertical;
}
.ws-gift-wrap__message::placeholder {
  color: var(--ws-footer-input-placeholder);
}
.ws-gift-wrap__message:focus-visible {
  border-color: var(--ws-brand-pink);
}

/* ---- Footer ---- */

.ws-cart-drawer__footer {
  flex-shrink: 0;
  padding: var(--ws-space-md) var(--ws-space-lg);
  background: var(--ws-surface);
  border-top: 1px solid var(--ws-surface-border);
}

.ws-shipping-progress {
  margin-bottom: var(--ws-space-sm);
}
.ws-shipping-progress__label {
  margin: 0 0 var(--ws-space-2xs);
  font-size: var(--ws-fs-small);
  color: var(--ws-text-muted);
}
:root[data-theme="dark"] .ws-shipping-progress__label {
  color: rgba(255, 231, 228, 0.65);
}
/* "#F15AB4 on #FFE7E4" — this task's brief. --ws-link-pink/--ws-bg are
   those exact hex values in the light theme (src/styles/00-tokens.css). */
.ws-shipping-progress__track {
  height: 0.5rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-bg);
  overflow: hidden;
}
.ws-shipping-progress__fill {
  display: block;
  height: 100%;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-link-pink);
  transition: width var(--ws-duration-base) var(--ws-ease-soft);
}

.ws-bundle-notice {
  margin-bottom: var(--ws-space-sm);
  padding: var(--ws-space-2xs) var(--ws-space-sm);
  border-radius: var(--ws-radius-sm);
  background: var(--ws-btn-primary-bg);
  color: var(--ws-btn-primary-text);
  font-size: var(--ws-fs-small);
  font-weight: 600;
  text-align: center;
}

.ws-totals {
  margin-bottom: var(--ws-space-sm);
}
.ws-totals__row {
  display: flex;
  justify-content: space-between;
  gap: var(--ws-space-sm);
  font-size: var(--ws-fs-small);
  margin-bottom: var(--ws-space-2xs);
}
.ws-totals__row--discount {
  color: var(--ws-link-pink);
  font-weight: 600;
}
.ws-totals__row--strong {
  margin-top: var(--ws-space-xs);
  padding-top: var(--ws-space-sm);
  border-top: 1px solid var(--ws-text-muted);
  font-family: var(--ws-font-display);
  font-size: 1.375rem;
  font-weight: 700;
  color: var(--ws-heading);
}

.ws-order-error {
  margin-bottom: var(--ws-space-sm);
  padding: var(--ws-space-xs) var(--ws-space-sm);
  border-radius: var(--ws-radius-sm);
  border: 1px solid var(--ws-accent-text);
  color: var(--ws-accent-text);
  font-size: var(--ws-fs-small);
  line-height: 1.45;
}

.ws-cart-drawer__pay-actions {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-2xs);
}

/* ---- Vipps / Klarna payment buttons ---- */
/* Sized "lg" + weight 700 in the component (not just 600 — the .ws-btn
   default) so the label text clears the WCAG "large text" bar (>=19px
   bold): white-on-#FF5B24 measures 3.10:1, which only satisfies AA at the
   large-text 3:1 threshold, not the 4.5:1 normal-text one. See the T4.6
   report for the full contrast table — this is a Vipps brand-colour
   constraint, not something recolouring can fix per the task brief. */
.ws-btn.ws-pay-btn {
  font-weight: 700;
}
.ws-btn.ws-pay-btn--vipps {
  background: #FF5B24;
  color: #FFFFFF;
}
.ws-btn.ws-pay-btn--vipps:hover {
  background: #E84E18;
}
.ws-btn.ws-pay-btn--klarna {
  background: #FFB3C7;
  color: #000000;
}
.ws-btn.ws-pay-btn--klarna:hover {
  background: #FFA7C0;
}
.ws-pay-btn__wordmark {
  font-weight: 800;
}

/* ---- src/styles/components/checkout.css ---- */
/*
 * CheckoutForm — T4.6 (docs/TASKS.md), docs/DESIGN.md §2.1. Renders inside
 * CartDrawer's body (src/styles/components/cart-drawer.css), so it inherits
 * --ws-bg from the drawer panel rather than setting its own background.
 * Tokens only; no legacy.css classes/vars. Divider/border colour choices
 * mirror cart-drawer.css: --ws-text-muted is the one neutral border used
 * everywhere (verified >=3:1 on both themes with docs/tools/contrast.mjs),
 * --ws-accent-text for invalid state + error text (5.43:1 on --ws-bg,
 * 6.41:1 on white, 6.96:1 on dark — see the T4.6 report).
 */

.ws-checkout-form {
  display: grid;
  gap: var(--ws-space-sm);
  padding-top: var(--ws-space-md);
}
.ws-checkout-form__title {
  font-size: 1.5rem;
}

/* Aggregated error summary (T4.6 brief: "error summary styling in the same
   pink/purple language"). Built from the same emailError/phoneError strings
   the inline field errors already use — no new validation logic. */
.ws-checkout-form__summary {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-3xs);
  padding: var(--ws-space-sm);
  border-radius: var(--ws-radius-sm);
  border-left: 3px solid var(--ws-accent-text);
  background: var(--ws-surface);
  color: var(--ws-accent-text);
  font-size: var(--ws-fs-small);
  line-height: 1.4;
}
.ws-checkout-form__summary-title {
  font-weight: 700;
}

.ws-field {
  display: grid;
  gap: var(--ws-space-3xs);
}
.ws-field__label {
  font-family: var(--ws-font-display);
  font-size: var(--ws-fs-small);
  font-weight: 600;
  color: var(--ws-text-muted);
}
/* --ws-text-muted is fixed both themes and this label sits directly on the
   cart drawer's --ws-bg panel — same dark-mode fix as everywhere else in
   this task (see src/styles/components/subpage-hero.css's
   .ws-subpage-hero__lead for the full rationale). */
:root[data-theme="dark"] .ws-field__label {
  color: rgba(255, 231, 228, 0.65);
}
/* "Like the footer newsletter field" — this task's brief — reuses the exact
   --ws-footer-input-* tokens (src/styles/00-tokens.css, FIGMA_SPEC §3 footer
   row) instead of the generic --ws-surface chrome: fixed bg #F9FAFB, 1px
   #E5E7EB border, r=8, Poppins 16, both themes (same reasoning the footer
   itself already documents — a light input chrome reads correctly regardless
   of site theme). */
.ws-field__input {
  box-sizing: border-box;
  width: 100%;
  min-height: 3.25rem; /* large touch target */
  padding: 0 var(--ws-space-sm);
  border: 1px solid var(--ws-footer-input-border);
  border-radius: 8px;
  background: var(--ws-footer-input-bg);
  color: var(--ws-text);
  font-family: var(--ws-font-body);
  font-size: 1rem;
  transition: border-color var(--ws-duration-fast) var(--ws-ease);
}
.ws-field__input::placeholder {
  color: var(--ws-footer-input-placeholder);
}
.ws-field__input:focus-visible {
  border-color: var(--ws-brand-pink);
}
.ws-field--invalid .ws-field__input {
  border-color: var(--ws-accent-text);
}
.ws-field__error {
  color: var(--ws-accent-text);
  font-size: 0.8125rem;
  line-height: 1.35;
}

.ws-field-group {
  position: relative;
}

.ws-addr-suggestions {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 0.25rem);
  z-index: var(--ws-z-dropdown);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--ws-footer-input-border);
  border-radius: 8px;
  background: var(--ws-footer-input-bg);
  box-shadow: var(--ws-shadow-popover);
}
.ws-addr-suggestion {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--ws-space-xs) var(--ws-space-sm);
  border: 0;
  border-bottom: 1px solid var(--ws-footer-input-border);
  background: transparent;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--ws-duration-fast) var(--ws-ease);
}
.ws-addr-suggestion:last-child {
  border-bottom: 0;
}
.ws-addr-suggestion:hover,
.ws-addr-suggestion:focus-visible {
  background: var(--ws-bg);
}
.ws-addr-suggestion__main {
  color: var(--ws-text);
  font-size: 0.875rem;
  font-weight: 500;
}
.ws-addr-suggestion__meta {
  color: var(--ws-text-muted);
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* ---- src/styles/components/craft.css ---- */
/*
 * Styles for src/sections/CraftSection.jsx — T4.5b (docs/TASKS.md),
 * docs/DESIGN.md. Tokens only (src/styles/00-tokens.css); no legacy.css
 * classes. Responsive checkpoints per AGENT_RULES §3: 360-390 / 768 / 1024 /
 * 1512. Replaces the pre-redesign carousel (`.craft`/`.story-card` etc. in
 * src/styles/legacy.css, now unused dead CSS left for the orchestrator to
 * retire with the rest of legacy.css at the end of Phase 4) with a plain
 * responsive grid — see CraftSection.jsx's header comment for why.
 */

.ws-craft {
  background: var(--ws-bg);
  padding-block: var(--ws-space-2xl) var(--ws-space-3xl);
}

.ws-craft__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--ws-space-lg);
}
.ws-craft__head .ws-section-heading__title em {
  color: var(--ws-hero-title);
  font-style: italic;
}

.ws-craft__lead {
  max-width: 26rem;
  margin: 0;
  color: var(--ws-text-muted);
  font-size: 1.0625rem;
  line-height: var(--ws-lh-relaxed);
}
/* --ws-text-muted is fixed both themes and this sits directly on --ws-bg —
   same dark-mode fix as subpage-hero.css's .ws-subpage-hero__lead. */
:root[data-theme="dark"] .ws-craft__lead {
  color: rgba(255, 231, 228, 0.65);
}

.ws-craft__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--ws-space-lg);
  margin-top: var(--ws-space-xl);
}

/* White r=8 card, exact Figma product-card shadow — Task F4
   (docs/FIGMA_SPEC.md §3), same treatment as src/styles/components/
   product-page.css's gallery/soldout cards and shop.css's box offers. */
.ws-craft-card {
  display: flex;
  flex-direction: column;
  background: #FFFFFF;
  border-radius: 8px;
  box-shadow: var(--ws-shadow-product-card);
  overflow: hidden;
}
:root[data-theme="dark"] .ws-craft-card {
  background: rgba(255, 255, 255, 0.8);
}

.ws-craft-card__media {
  aspect-ratio: 1 / 1; /* the three source photos are all native 1024x1024 */
  overflow: hidden;
}
.ws-craft-card__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ws-craft-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-2xs);
  padding: var(--ws-space-lg);
}

.ws-craft-card__number {
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: var(--ws-fs-small);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ws-eyebrow);
}
/* Dark theme: these cards stay light (white 80 %), where the orange eyebrow
   drops to 1.5:1 — use the heading purple the same cards already use. */
:root[data-theme="dark"] .ws-craft-card__number {
  color: var(--ws-heading);
}

.ws-craft-card__title {
  font-family: var(--ws-font-display);
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--ws-heading);
}

.ws-craft-card__text {
  margin: 0;
  color: var(--ws-text-muted);
  font-size: 0.9375rem;
  line-height: var(--ws-lh-relaxed);
}

@media (max-width: 900px) {
  .ws-craft__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 560px) {
  .ws-craft__head {
    flex-direction: column;
    align-items: flex-start;
  }
  .ws-craft__lead {
    max-width: none;
  }
  .ws-craft__grid {
    grid-template-columns: 1fr;
  }
}

/* ---- src/styles/components/cta-band.css ---- */
/*
 * Styles for src/sections/CtaBand.jsx — Task F1 (docs/FIGMA_SPEC.md §4 node
 * 1:276). --ws-cta-band-bg/--ws-cta-band-text are identical in both site
 * themes (src/styles/00-tokens.css), so this file has no [data-theme="dark"]
 * overrides.
 *
 * Figma: 399px band, a 199px-tall centered content block starting 100px from
 * the top — i.e. 100px of padding above AND below the content, which is
 * exactly what `padding-block: calc(100 * var(--d))` on the band itself
 * gives us for free (no need to also force the content block to exactly
 * 199px — "as close as possible", FIGMA_SPEC §8).
 */

.ws-cta-band {
  background: var(--ws-cta-band-bg);
  color: var(--ws-cta-band-text);
  padding-block: clamp(2.5rem, 1.7rem + 3.5vw, 5rem);
}

.ws-cta-band__frame {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ws-space-md);
  max-width: 44rem;
  margin-inline: auto;
  padding-inline: var(--ws-gutter);
  text-align: center;
}

.ws-cta-band__heading.ws-section-heading--center {
  align-items: center;
}
.ws-cta-band__heading .ws-eyebrow {
  color: var(--ws-cta-band-text);
  font-weight: 300; /* Figma "CTA надзаголовок": Montserrat 300 — the one eyebrow that ISN'T weight 700 */
}
/* Compound selector (.ws-cta-band__heading PLUS the colour modifier, both on
   the SAME <div> — see src/ui/SectionHeading.jsx) so this reliably beats
   components/ui.css's `.ws-section-heading--purple .ws-section-heading__title`
   on specificity (0,3,0 > 0,2,0) — a plain `.ws-cta-band__heading
   .ws-section-heading__title` (0,2,0) used to be enough back when ui.css had
   no colour-modifier classes, but ties on specificity now and "components/
   cta-band.css" sorts BEFORE "components/ui.css" alphabetically, so ui.css
   would otherwise win the cascade and the title would render heading-purple
   instead of cream. */
.ws-cta-band__heading.ws-section-heading--purple .ws-section-heading__title,
.ws-cta-band__heading.ws-section-heading--violet .ws-section-heading__title {
  color: var(--ws-cta-band-text);
}

.ws-cta-band__text {
  color: var(--ws-cta-band-text);
  font-family: var(--ws-font-display);
  font-weight: 400;
  font-size: 1.125rem; /* 18px */
  line-height: 1.2;    /* 21.6/18 */
  max-width: 34rem;
}

.ws-cta-band__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: var(--ws-space-xs);
}
/* Button renders these as <a> (Button.jsx's `href` path), and legacy.css's
   `.site-shell a { color: inherit }` (specificity 0,1,1) otherwise beats
   ui.css's `.ws-btn--light` (0,1,0), leaving the link inheriting this
   section's cream --ws-cta-band-text instead of --ws-btn-light-text. Two
   classes (0,2,0) fixes it. */
.ws-cta-band__actions .ws-btn--light {
  color: var(--ws-btn-light-text);
}

@media (min-width: 1024px) {
  .ws-cta-band {
    padding-block: calc(100 * var(--d));
  }
  .ws-cta-band__frame {
    max-width: none;
    padding-inline: 0;
    gap: calc(10 * var(--d));
  }
  .ws-cta-band__text {
    font-size: calc(18 * var(--d));
    max-width: calc(574 * var(--d));
  }
}

@media (max-width: 520px) {
  .ws-cta-band__actions {
    width: 100%;
    max-width: 20rem;
    flex-direction: column;
    align-items: stretch;
  }
}

/* ---- src/styles/components/delivery.css ---- */
/*
 * Styles for src/sections/QuickStrip.jsx (delivery/benefit cards) — T4.5a
 * (docs/TASKS.md), docs/DESIGN.md. Tokens only. Responsive checkpoints per
 * AGENT_RULES §3: 360-390 / 768 / 1024 / 1512.
 */

.ws-quick-strip {
  background: var(--ws-bg);
  padding-block: clamp(1rem, 0.6vw + 1rem, 2rem) clamp(3rem, 2vw + 2rem, 4.5rem);
}

.ws-quick-strip__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--ws-space-md);
}

/* Peach r=30 card, like Brands (src/styles/components/brands.css) — this
   task's brief: "info cards peach r=30 (Brands card style)". Same
   fixed-both-themes white/peach chrome as Brands and shop.css's box offers. */
.ws-quick-item {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-xs);
  padding: var(--ws-space-lg) var(--ws-space-md);
  background: var(--ws-btn-light-bg);
  border: 1.5px solid var(--ws-border-peach-fixed);
  border-radius: var(--ws-radius-card);
}

.ws-quick-item__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-bg);
  /* --ws-link-pink, not the decor-only --ws-brand-pink: these icons carry
     meaning (truck/gift/lock/check per item), so WCAG 1.4.11 non-text
     contrast applies (>=3:1). --ws-brand-pink measures only 2.59:1 on
     --ws-bg (node docs/tools/contrast.mjs "#F15AB4 on #FFE7E4") — a real
     fail, not just a style choice. */
  color: var(--ws-link-pink);
}

.ws-quick-item__title {
  font-family: var(--ws-font-display);
  font-weight: 600;
  font-size: 1.0625rem;
  color: var(--ws-heading);
}

.ws-quick-item__text {
  color: var(--ws-text-muted);
  font-size: 0.9375rem;
  line-height: var(--ws-lh-relaxed);
}

@media (max-width: 1024px) {
  .ws-quick-strip__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 560px) {
  .ws-quick-strip__grid {
    grid-template-columns: 1fr;
  }
}

/* ---- src/styles/components/faq.css ---- */
/*
 * Styles for src/sections/FAQSection.jsx — T4.4 (docs/TASKS.md),
 * docs/CONTENT.md "FAQ", restyled in Task F2 (docs/FIGMA_SPEC.md) into the
 * Figma visual language (white product-card-style shadow/radius, pink
 * chevron) even though FAQ has no Figma node of its own.
 *
 * Phase G (docs/MOTION.md §4 "FAQ"): the accordion is now a
 * <button aria-expanded> + <div role="region"> pair (see FAQSection.jsx's
 * header comment for why this replaced <details>/<summary>). The smooth
 * open/close is the standard `grid-template-rows: 0fr -> 1fr` trick:
 * `.ws-faq__answer-wrap` is the grid (row height set inline per item's open
 * state), `.ws-faq__answer-inner` is its single row/column child with
 * `overflow: hidden` and no min-height, so a 0fr row genuinely collapses to
 * zero instead of showing clipped text.
 */

.ws-faq {
  padding-block: var(--ws-space-2xl) var(--ws-space-3xl);
  /* See src/styles/components/hero.css's .ws-hero comment: repaints over
     legacy.css's non-theme-aware `.site-shell` background. */
  background: var(--ws-bg);
}

.ws-faq__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.ws-faq__list {
  width: 100%;
  max-width: 48rem;
  margin-top: var(--ws-space-xl);
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
}

/* Task F2 (docs/FIGMA_SPEC.md): restyled to the same white-card language as
   the product cards (--ws-shadow-product-card = FIGMA_SPEC §3 "Тени" ->
   "Карточка товара"; the radius matches src/styles/components/
   product-card.css's own 8px). FAQ has no Figma node of its own, so this
   deliberately reuses another section's exact numbers rather than
   inventing new ones. */
.ws-faq__item {
  background: var(--ws-surface);
  border: 1px solid var(--ws-surface-border);
  border-radius: 12px;
  box-shadow: var(--ws-shadow-product-card);
  padding-inline: var(--ws-space-md);
}

.ws-faq__q-heading {
  margin: 0;
  font: inherit; /* neutralise the default <h3> styling — the visible text lives on the button below */
}

.ws-faq__question {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ws-space-md);
  width: 100%;
  border: none;
  background: transparent;
  cursor: pointer;
  padding-block: var(--ws-space-sm);
  font-family: var(--ws-font-display);
  font-weight: 500;
  font-size: 1.125rem;
  color: var(--ws-heading);
  text-align: left;
}

/* FIGMA_SPEC: FAQ chevron is pink (#F15AB4 = --ws-link-pink/--ws-brand-pink,
   same hex both themes), not the orange used elsewhere for decorative
   chevrons/arrows. */
.ws-faq__chevron {
  flex-shrink: 0;
  color: var(--ws-link-pink);
  transition: rotate var(--ws-duration-base) var(--ws-ease);
}
.ws-faq__question[aria-expanded="true"] .ws-faq__chevron {
  rotate: 180deg;
}

/* Phase G: the grid-rows collapse trick — 0fr/1fr set inline per item
   (FAQSection.jsx), .ws-faq__answer-inner is the single grid row/column
   child that actually gets clipped to zero height. */
.ws-faq__answer-wrap {
  display: grid;
  transition: grid-template-rows var(--ws-duration-slow) var(--ws-ease);
}
.ws-faq__answer-inner {
  overflow: hidden;
}

.ws-faq__answer {
  margin: 0;
  padding-bottom: var(--ws-space-md);
  color: var(--ws-text);
  font-size: var(--ws-fs-body);
  line-height: var(--ws-lh-relaxed);
}

/* Dark theme: the item surface turns near-black (--ws-surface is white 6 %),
   but --ws-text/--ws-heading stay dark (no dark override), which left the
   answers at 1.1:1 and the questions at 2.6:1. FAQ has no Figma node, so it
   takes the cream-on-dark pairing the dark design already uses (CTA band,
   About drip). */
:root[data-theme="dark"] .ws-faq__question {
  color: var(--ws-cream);
}
:root[data-theme="dark"] .ws-faq__answer {
  color: rgba(255, 231, 228, 0.82);
}

@media (prefers-reduced-motion: reduce) {
  .ws-faq__answer-wrap {
    transition: none;
  }
}

@media (max-width: 520px) {
  .ws-faq__question {
    font-size: 1rem;
  }
}

/* ---- src/styles/components/footer.css ---- */
/*
 * Styles for src/layout/Footer.jsx (SiteFooter) — Task F1 (docs/FIGMA_SPEC.md
 * §4 footer row, node 1:253). The footer is the same dark colour in both
 * site themes (--ws-footer-*, src/styles/00-tokens.css), so nothing here
 * reacts to [data-theme="dark"] — including the newsletter pill's own fixed
 * light input/submit tokens.
 *
 * >=1024px: Phase G (docs/MOTION.md §2 rule 1) replaces the old
 * absolutely-positioned-children block (fixed 410px height, every column
 * pinned with `left`/`top`) with a 3-column grid (logo | pages | newsletter)
 * in normal flow, still sized with `calc(N * var(--d))` (see
 * src/styles/02-base.css's design-unit comment) so column widths/gaps track
 * the Figma geometry between 1024px and 1512px. Two concrete bugs this
 * fixes (docs/MOTION.md, user report): the "Meld på" button was clipped at
 * 1024px because it had a fixed `width: calc(76 * var(--d))` (~51px at
 * 1024px, too narrow for its own label) — it's `width: auto` + padding now;
 * and the "SIDER" label sat visibly off from its own link column because
 * both were positioned independently by absolute `left`/`top` that rounded
 * differently as `--d` shrank — they're both flow children of the same
 * `.ws-footer__pages` column now, so they can't drift apart.
 * `.ws-footer__legal` is a small bottom row of our own (Task F1 brief point
 * 6: "bottom row may add <=40px"), stacked in normal flow right after the
 * grid rather than growing it. <1024px: a plain responsive grid (own
 * layout, Figma has no mobile footer).
 */

.ws-footer {
  background: var(--ws-footer-bg);
  color: var(--ws-footer-text);
}

.ws-footer__inner {
  width: 100%;
  max-width: calc(var(--ws-container) + (var(--ws-gutter) * 2));
  margin-inline: auto;
  padding-inline: var(--ws-gutter);
}

/* ---- <1024px: responsive grid (own layout) ---- */
.ws-footer__frame {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--ws-space-xl) var(--ws-space-lg);
  padding-block: var(--ws-space-2xl) var(--ws-space-lg);
}
.ws-footer__logo {
  grid-column: 1 / -1;
  display: inline-flex;
  line-height: 0;
}
.ws-footer__logo .ws-logo {
  width: clamp(8rem, 6.4rem + 5vw, 12.5rem);
  height: auto;
  color: var(--ws-brand-purple);
}

.ws-footer__pages {
  grid-column: 1;
}
.ws-footer__newsletter {
  grid-column: 2;
}

/* Compound selector (.ws-eyebrow + a component class, both on the same
   <Eyebrow className="..."> element) so this reliably outranks
   components/ui.css's plain `.ws-eyebrow` rule on specificity (0,2,0 > 0,1,0)
   regardless of file load order. */
.ws-eyebrow.ws-footer__pages-label,
.ws-eyebrow.ws-footer__newsletter-label {
  color: var(--ws-footer-label);
  margin-bottom: var(--ws-space-sm);
  /* Figma 1:256 / 1:273: Poppins Regular 14/19.6, letter-spacing 2px */
  font-family: var(--ws-font-body);
  font-weight: 400;
  line-height: 1.4;
}

.ws-footer__links {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
}
.ws-footer__links a {
  color: var(--ws-footer-text);
  font-family: var(--ws-font-display);
  font-size: 1.0625rem;
  text-decoration: none;
}
.ws-footer__links a:hover {
  text-decoration: underline;
}

.ws-footer__newsletter-text {
  color: var(--ws-footer-newsletter-text);
  font-size: var(--ws-fs-body);
  line-height: var(--ws-lh-relaxed);
  margin: 0 0 var(--ws-space-md);
  max-width: 30rem;
}

.ws-footer__newsletter-form {
  max-width: 30rem;
}

.ws-footer__newsletter-row {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  background: var(--ws-footer-input-bg);
  border: 1px solid var(--ws-footer-input-border);
  border-radius: 8px;
  padding: 0.25rem 0.25rem 0.25rem 1rem;
}
.ws-footer__newsletter-row input {
  flex: 1 1 auto;
  min-width: 0;
  border: none;
  background: transparent;
  color: var(--ws-footer-input-text);
  font-family: var(--ws-font-body);
  font-size: var(--ws-fs-body);
  padding-block: 0.625rem;
}
.ws-footer__newsletter-row input::placeholder {
  color: var(--ws-footer-input-placeholder);
}
.ws-footer__newsletter-row input:focus-visible {
  outline: none;
}
.ws-footer__newsletter-row:focus-within {
  outline: var(--ws-focus-ring-width) solid var(--ws-focus-ring-color);
  outline-offset: var(--ws-focus-ring-offset);
}
.ws-footer__newsletter-row button {
  flex-shrink: 0;
  border: none;
  border-radius: 10px;
  background: var(--ws-footer-submit-bg);
  color: var(--ws-footer-submit-text);
  font-family: var(--ws-font-body);
  font-weight: 400;
  font-size: 1rem;
  padding: 0.625rem 1.125rem;
  cursor: pointer;
  transition: opacity var(--ws-duration-fast) var(--ws-ease);
  white-space: nowrap;
}
.ws-footer__newsletter-row button:hover:not(:disabled) {
  opacity: 0.88;
}
.ws-footer__newsletter-row button:disabled,
.ws-footer__newsletter-row input:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.ws-footer__newsletter-status {
  min-height: 1.25em;
  margin: var(--ws-space-2xs) 0 0;
  font-size: var(--ws-fs-small);
  color: var(--ws-footer-text);
}

.ws-footer__fineprint {
  color: var(--ws-footer-text);
  opacity: 0.65;
  font-size: 0.8125rem;
  line-height: var(--ws-lh-normal);
  margin-top: var(--ws-space-sm);
  max-width: 26rem;
}

.ws-footer__social {
  display: flex;
  gap: var(--ws-space-sm);
  margin-top: var(--ws-space-md);
}
/* .ws-footer__social-link alone is one class (0,1,0); legacy.css's
   `.site-shell a { color: inherit }` (0,1,1) would otherwise win over the
   icon colour below. .ws-footer .ws-footer__social-link (0,2,0) is
   order-independent. */
.ws-footer .ws-footer__social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 8px;
  background: var(--ws-footer-input-bg);
  color: var(--ws-footer-input-text);
  transition: transform var(--ws-duration-fast) var(--ws-ease);
}
.ws-footer__social-link:hover {
  transform: translateY(-2px);
}

/* ---- bottom legal row (our addition — Task F1 brief point 6) ---- */
.ws-footer__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--ws-space-sm);
  padding-block: 0.625rem;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  font-family: var(--ws-font-body);
  font-size: 0.8125rem;
  color: var(--ws-footer-text);
  opacity: 0.7;
}
.ws-footer__legal-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ws-space-md);
}
.ws-footer__legal-links a {
  color: var(--ws-footer-text);
  text-decoration: none;
}
.ws-footer__legal-links a:hover {
  text-decoration: underline;
}
.ws-footer__legal-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ws-space-md);
}

@media (max-width: 700px) {
  .ws-footer__frame {
    /* minmax(0, ...): the newsletter <input>'s intrinsic width would
       otherwise set a ~335px minimum and overflow 320px screens. */
    grid-template-columns: minmax(0, 1fr);
  }
  /* The explicit grid-column: 2 above would otherwise create an implicit
     second column and push the newsletter ~150px past the right edge. */
  .ws-footer__pages,
  .ws-footer__newsletter {
    grid-column: 1 / -1;
  }
  .ws-footer__legal {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* ---- >=1024px: flow grid (logo | pages | newsletter), Figma sizes via --d ---- */
@media (min-width: 1024px) {
  .ws-footer__inner {
    max-width: calc(1512 * 1px);
    padding-inline: 0;
  }

  .ws-footer__frame {
    display: grid;
    /* logo column fits its own 207px-wide logo; newsletter column matches
       Figma's 480px width; pages takes whatever is left between them. */
    grid-template-columns: calc(207 * var(--d)) minmax(calc(120 * var(--d)), 1fr) calc(480 * var(--d));
    column-gap: calc(40 * var(--d));
    align-items: start;
    padding-block: calc(84 * var(--d)) calc(60 * var(--d));
    padding-inline: calc(84 * var(--d)) calc(134 * var(--d));
  }

  .ws-footer__logo {
    grid-column: 1;
    grid-row: 1;
  }
  .ws-footer__logo .ws-logo {
    width: calc(207 * var(--d));
  }

  /* The "SIDER" label sits directly above its own link list — both flow
     children of the same grid cell, so they can never drift apart the way
     two independently absolutely-positioned blocks did (see the file header
     comment). This trades Figma's side-by-side label/links placement for a
     guaranteed-aligned stacked one; flagged in the task report. */
  .ws-footer__pages {
    grid-column: 2;
    grid-row: 1;
  }
  .ws-eyebrow.ws-footer__pages-label {
    margin: 0 0 calc(14 * var(--d)) 0;
    font-size: max(12px, calc(14 * var(--d)));
  }
  .ws-footer__links {
    gap: calc(10 * var(--d));
  }
  .ws-footer__links a {
    font-size: max(14px, calc(17 * var(--d)));
  }

  .ws-footer__newsletter {
    grid-column: 3;
    grid-row: 1;
  }
  .ws-eyebrow.ws-footer__newsletter-label {
    margin: 0;
    font-size: max(12px, calc(14 * var(--d)));
  }
  .ws-footer__newsletter-text {
    margin: calc(2 * var(--d)) 0 0;
    max-width: none;
    font-size: max(14px, calc(16 * var(--d)));
  }
  .ws-footer__newsletter-form {
    margin-top: calc(10 * var(--d));
    max-width: none;
  }
  .ws-footer__newsletter-row {
    height: auto;
    min-height: max(44px, calc(53 * var(--d)));
    padding-left: calc(20 * var(--d));
    border-radius: 8px;
  }
  /* Bug fix (docs/MOTION.md, user report): this used to be a FIXED
     `width: calc(76 * var(--d))` (~51px at 1024px) with no room for its own
     "Meld på…" label — the button now sizes to its content (`width: auto` +
     padding, `white-space: nowrap`) at every width, with a 44px floor so it
     never drops below the accessible tap-target minimum either. */
  .ws-footer__newsletter-row button {
    width: auto;
    height: auto;
    min-height: max(44px, calc(39 * var(--d)));
    padding-inline: calc(18 * var(--d));
    text-align: center;
    white-space: nowrap;
    font-size: max(13px, calc(16 * var(--d)));
  }
  .ws-footer__social {
    margin-top: calc(26 * var(--d));
    gap: calc(26 * var(--d));
  }

  .ws-footer__legal {
    max-width: none;
    /* align with the logo column (x=84) and the newsletter column's right edge (x=1378) */
    padding-inline: calc(84 * var(--d)) calc(134 * var(--d));
  }
}

/* ---- src/styles/components/header.css ---- */
/*
 * Styles for src/layout/Header.jsx — Task F1 (docs/AGENT_RULES.md,
 * docs/FIGMA_SPEC.md §4 "Меню" / §5 "Om oss", decision D22). Class prefix
 * `ws-` throughout.
 *
 * D22: the header is NOT a sticky bar — it's absolutely positioned over the
 * top of the page (transparent, no shadow) so it scrolls away with the
 * content instead of staying pinned. `.site-shell` is the positioning
 * ancestor (`position: relative`, src/styles/02-base.css); `.ws-header` is
 * `position: absolute; inset: 0 0 auto 0` so it always starts flush with
 * whatever section renders first for that route (Hero for `/`, the About
 * drip hero for `/om-oss`, SubpageHero elsewhere).
 *
 * Two independent variant axes, both set per-route by src/app.jsx:
 *   tone: default (`#F15AB4` ink) | cream (`#FFE7E4` ink, for the pink/drip
 *         About hero)
 *   size: home (logo 176x145 at (30,30), nav 30px at y=83) | compact
 *         (logo 158x130 at (30,36), nav 25px at y=69) — About-style pages
 *
 * >=1024px: Phase G (docs/MOTION.md §2 rule 1 — "no absolute positioning of
 * text/buttons/cards, flow only") replaces the old three-absolutely-
 * positioned-children layout with a single flex row (logo | nav | actions),
 * sized with `calc(N * var(--d))` (see src/styles/02-base.css's design-unit
 * comment block) so it still scales exactly like the rest of a --d-driven
 * layout between 1024px and 1512px and holds at the Figma pixel values above
 * that. The row's height and the logo/nav/icon sizes still match Figma at
 * 1512px; the previous version's exact nav `left`/icon `right` offsets are
 * now a flex gap instead (nav sits directly after the logo, icons are
 * pushed to the far right by `margin-left: auto`) — visually equivalent,
 * but nothing can overlap or clip anymore as the row narrows towards 1024px.
 * Menu text has an explicit 16px floor here (docs/MOTION.md §2 rule 3) —
 * bigger than the old 14px floor, which read as genuinely small at 1024px;
 * icon buttons have a 44px floor (was `max(24px, ...)`, well under the
 * 44px hit-target minimum the same rule requires).
 * <1024px: a plain flex row (logo left, burger+icons right) — Figma has no
 * mobile header, so this is our own adaptive layout, not a scaled-down copy.
 */

.ws-header {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: var(--ws-z-header);
  background: transparent;
  pointer-events: none; /* re-enabled on the logo/nav/actions individually below, so the transparent gaps between them (and <1024px, the empty space below a short mobile bar) never eat clicks meant for the hero underneath */
}
.ws-header__logo,
.ws-header__nav,
.ws-header__actions {
  pointer-events: auto;
}

/* ---- ink colour per tone (logo fill via currentColor, nav text, icons) ---- */
.ws-header--tone-default {
  --ws-header-ink: var(--ws-brand-pink);
}
.ws-header--tone-cream {
  --ws-header-ink: #FFE7E4;
}

/* ---- <1024px: simple flex row ---- */
.ws-header__frame {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ws-space-md);
  padding: var(--ws-space-sm) var(--ws-gutter);
}

.ws-header__logo {
  display: inline-flex;
  flex-shrink: 0;
  line-height: 0;
}
.ws-header__logo .ws-logo {
  width: 6rem; /* ~96px, per the F1 brief's <1024px note */
  height: auto;
  color: var(--ws-header-ink);
}

.ws-header__nav {
  display: none;
}
.ws-header__nav a {
  font-family: var(--ws-font-display);
  color: var(--ws-header-ink);
  text-decoration: none;
  white-space: nowrap;
}
.ws-header__nav a[aria-current="page"] {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.3em;
}
.ws-header__nav a:hover {
  opacity: 0.75;
}

.ws-header__actions {
  display: flex;
  align-items: center;
  gap: var(--ws-space-2xs);
  flex-shrink: 0;
}

/* Icon buttons — D22: 40x40, no background/border, icon colour = tone ink
   (overrides ThemeToggle's own default chip look, see src/styles/components/
   ui.css's .ws-theme-toggle — that default styling is still used as-is in
   src/layout/MobileMenu.jsx's footer, so it's overridden here, not changed
   at the source). */
.ws-header__icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  flex-shrink: 0;
  border-radius: var(--ws-radius-pill);
  border: none;
  background: transparent;
  color: var(--ws-header-ink);
  cursor: pointer;
  transition:
    transform var(--ws-duration-fast) var(--ws-ease),
    opacity var(--ws-duration-fast) var(--ws-ease);
}
.ws-header__icon-btn:hover {
  transform: translateY(-2px);
}
/* ui.css (.ws-theme-toggle, same specificity) loads after this file and was
   repainting the toggle as a surface chip — near-invisible sun icon in the
   dark theme. Only the look is bumped here; sizes stay on the rules above. */
.ws-header .ws-header__icon-btn {
  border: none;
  background: transparent;
  color: var(--ws-header-ink);
}

.ws-header__cart {
  position: relative;
}
.ws-header__cart-badge {
  position: absolute;
  top: 0.1rem;
  right: 0.1rem;
  min-width: 1.125rem;
  height: 1.125rem;
  padding-inline: 0.25rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-btn-primary-bg);
  color: var(--ws-btn-primary-text);
  font-family: var(--ws-font-display);
  font-size: 0.6875rem;
  font-weight: 700;
  line-height: 1.125rem;
  text-align: center;
}

.ws-header__burger {
  display: inline-flex;
}

/* ---- >=1024px: flow row (logo | nav | actions), Figma sizes via --d ---- */
@media (min-width: 1024px) {
  .ws-header__frame {
    display: flex;
    align-items: center;
    position: relative;
    width: 100%;
    max-width: calc(1512 * 1px);
    margin-inline: auto;
    min-height: calc(206 * var(--d));
    /* left gutter matches the Figma logo x=30; right gutter approximates
       the icons' right edge (Figma x=42 from the frame's right edge). */
    padding: 0 calc(42 * var(--d)) 0 calc(30 * var(--d));
    gap: 0;
  }

  .ws-header__logo {
    flex-shrink: 0;
    /* Figma: logo right edge (30 + 176 = 206) to nav start (283) = 77px. */
    margin-right: calc(77 * var(--d));
  }
  .ws-header__logo .ws-logo {
    width: calc(176 * var(--d));
  }

  .ws-header__nav {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    align-items: baseline;
    gap: calc(64 * var(--d));
  }
  .ws-header__nav a {
    /* docs/MOTION.md §2 rule 3: menu text floor is 16px at 1024-1280px. */
    font-size: max(16px, calc(30 * var(--d)));
  }

  .ws-header__actions {
    flex-shrink: 0;
    margin-left: auto;
    gap: calc(8 * var(--d));
  }
  .ws-header__icon-btn {
    /* docs/MOTION.md §2 rule 3: icon buttons stay >=44px, even though the
       Figma 40px icon would otherwise shrink under that at 1024-1280px. */
    width: max(44px, calc(40 * var(--d)));
    height: max(44px, calc(40 * var(--d)));
  }

  .ws-header__burger {
    display: none;
  }

  /* ---- size="compact" (About-style pages, FIGMA_SPEC §5) ---- */
  .ws-header--size-compact .ws-header__frame {
    min-height: calc(176 * var(--d));
  }
  .ws-header--size-compact .ws-header__logo {
    /* Figma compact: logo right edge (30 + 158 = 188) to nav start (262) = 74px. */
    margin-right: calc(74 * var(--d));
  }
  .ws-header--size-compact .ws-header__logo .ws-logo {
    width: calc(158 * var(--d));
  }
  .ws-header--size-compact .ws-header__nav {
    gap: calc(54 * var(--d));
  }
  .ws-header--size-compact .ws-header__nav a {
    font-size: max(16px, calc(25 * var(--d)));
  }
}

/* ---- src/styles/components/hero.css ---- */
/*
 * Hero — Phase G (src/sections/Hero.jsx, docs/MOTION.md). Flow layout that
 * matches Figma node 1:8 at 1512px and adapts without absolute text:
 *   <640px     copy, then the 3D scene centred below it
 *   640-1023   two columns, scene beside the copy
 *   >=1024     Figma columns via --d: copy from x=140, scene box at x=855
 *              (layers.json bounds), section height 1116 (Bestsellers starts
 *              right after, FIGMA_SPEC y=1116)
 * The header is absolute (D22), so the copy clears it with
 * --ws-header-clearance (<1024) or the Figma 206px header band (>=1024).
 */

.ws-hero {
  position: relative;
  isolation: isolate;
  background: var(--ws-bg);
  /* horizontal clip only: floating snacks may drift past the edge, the page must not scroll sideways */
  overflow-x: clip;
}

.ws-hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: 50% 0;
  pointer-events: none;
}

.ws-hero__inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1rem, 4vw, 2.5rem);
  max-width: 1512px;
  margin-inline: auto;
  padding: calc(var(--ws-header-clearance) + clamp(0.25rem, 1.5vw, 1rem)) var(--ws-gutter) clamp(2.5rem, 7vw, 4.5rem);
}

.ws-hero__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
}

.ws-hero__title {
  margin: 0;
  color: var(--ws-hero-title);
  font-size: var(--ws-fs-hero);
  font-weight: 700;
  line-height: 1.2; /* FIGMA_SPEC: 90/1.2 */
}

.ws-hero__sub {
  margin: clamp(0.75rem, 2vw, 1.25rem) 0 0;
  max-width: 26rem;
  color: var(--ws-accent-text);
  font-family: var(--ws-font-display);
  font-size: var(--ws-fs-sub);
  line-height: var(--ws-lh-snug);
}

.ws-hero__cta-wrap {
  margin-top: clamp(1.25rem, 4vw, 2.5rem);
}

/* ---- 3D scene ---- */
.ws-hero__art {
  position: relative;
  justify-self: center;
  width: min(100%, 19rem);
  aspect-ratio: 597 / 966;
  --ws-parallax-range: 34px;
  --scroll-factor: 0.2;
}

.ws-hero__layer {
  position: absolute;
  pointer-events: none;
}
.ws-hero__layer img {
  display: block;
  width: 100%;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
}

@media (min-width: 640px) {
  .ws-hero__inner {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    align-items: center;
    column-gap: clamp(1rem, 3vw, 2rem);
  }
  .ws-hero__art {
    width: 100%;
    max-width: 30rem;
  }
}

@media (min-width: 1024px) {
  .ws-hero {
    min-height: calc(1116 * var(--d));
  }

  .ws-hero__inner {
    /* 140 | copy 715 | scene 597 | 60  =  1512 */
    grid-template-columns: minmax(0, 715fr) minmax(0, 597fr);
    column-gap: 0;
    align-items: start;
    padding: calc(206 * var(--d)) calc(60 * var(--d)) 0 calc(140 * var(--d));
  }

  .ws-hero__copy {
    padding-top: calc(57 * var(--d)); /* H1 top at y=263 */
  }

  .ws-hero__title {
    max-width: calc(711 * var(--d));
    font-size: calc(90 * var(--d));
  }

  .ws-hero__sub {
    max-width: calc(455 * var(--d));
    margin-top: calc(22 * var(--d));
    font-size: max(18px, calc(30 * var(--d)));
  }

  .ws-hero__cta-wrap {
    margin-top: calc(76 * var(--d)); /* button top at y=750 */
  }

  .ws-hero__art {
    max-width: none;
    /* scene box top at Figma y=17.67, i.e. under the header band */
    margin-top: calc((17.67 - 206) * var(--d));
  }
}

/* ---- src/styles/components/legal.css ---- */
/*
 * Styles for src/pages/SalgsbetingelserPage.jsx — T4.5a (docs/TASKS.md),
 * docs/DESIGN.md. Tokens only. Readable legal typography: body copy capped
 * at ~72ch (this task's brief). Responsive checkpoints per AGENT_RULES §3:
 * 360-390 / 768 / 1024 / 1512.
 */

.ws-legal {
  position: relative;
  overflow: hidden;
  background: var(--ws-bg);
  padding-block: 0 clamp(3rem, 2vw + 2rem, 5rem);
}

/* "group27-right-blob.svg behind long content" (this task's brief) — the
   long legal body is the clearest case of "long content" on the site.
   Tasteful/subtle: low opacity, behind everything (z-index 0), never
   intercepts clicks/taps. */
.ws-legal__blob {
  position: absolute;
  top: 8%;
  right: -12%;
  z-index: 0;
  width: min(58rem, 70vw);
  height: auto;
  opacity: 0.35;
  pointer-events: none;
  user-select: none;
}

.ws-legal__layout {
  position: relative;
  z-index: 1;
}

.ws-legal__layout {
  display: grid;
  grid-template-columns: 16rem minmax(0, 1fr);
  gap: var(--ws-space-2xl);
  align-items: start;
}

/* Peach r=30 card, like Brands (src/styles/components/brands.css) — this
   task's brief: "TOC as peach-bordered card r=30 with orange links". */
.ws-legal-toc {
  position: sticky;
  top: calc(5.5rem + var(--ws-space-md));
  padding: var(--ws-space-md);
  background: var(--ws-btn-light-bg);
  border: 1.5px solid var(--ws-border-peach-fixed);
  border-radius: var(--ws-radius-card);
  max-height: calc(100vh - 7.5rem);
  overflow-y: auto;
}

.ws-legal-toc__list {
  list-style: none;
  margin: var(--ws-space-sm) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.ws-legal-toc__link {
  display: block;
  padding: 0.375rem var(--ws-space-2xs);
  border-radius: var(--ws-radius-sm);
  color: var(--ws-accent-text);
  font-size: 0.875rem;
  line-height: 1.4;
  text-decoration: none;
  transition: background-color var(--ws-duration-fast) var(--ws-ease), color var(--ws-duration-fast) var(--ws-ease);
}
.ws-legal-toc__link:hover {
  background: var(--ws-bg);
  text-decoration: underline;
}

.ws-legal-body {
  max-width: 72ch;
  display: flex;
  flex-direction: column;
}

.ws-legal-section {
  padding-block: var(--ws-space-lg);
  border-top: 1px solid var(--ws-surface-border);
}
.ws-legal-section:first-child {
  padding-top: 0;
  border-top: none;
}

/* Montserrat 700 22px #824EBF — this task's brief. */
.ws-legal-section h2 {
  font-size: 1.375rem;
  font-weight: 700;
  color: var(--ws-heading);
  margin-bottom: var(--ws-space-sm);
  scroll-margin-top: 6rem;
}

/* Poppins 16/26 --ws-text — this task's brief. */
.ws-legal-section p {
  color: var(--ws-text);
  font-family: var(--ws-font-body);
  font-size: 1rem;
  line-height: 1.625;
  white-space: pre-line;
}
/* --ws-text is a fixed dark grey in both themes (src/styles/00-tokens.css)
   and this paragraph sits directly on --ws-bg, which DOES flip near-black —
   same dark-mode contrast fix as every other inner page this task touches
   (see src/styles/components/subpage-hero.css's .ws-subpage-hero__lead for
   the full rationale). */
:root[data-theme="dark"] .ws-legal-section p {
  color: rgba(255, 231, 228, 0.85);
}
.ws-legal-section p + p {
  margin-top: var(--ws-space-sm);
}

@media (max-width: 900px) {
  .ws-legal__layout {
    grid-template-columns: 1fr;
  }
  .ws-legal-toc {
    position: static;
    max-height: none;
  }
}

/* ---- src/styles/components/mobile-menu.css ---- */
/*
 * Styles for src/layout/MobileMenu.jsx — T4.3 (docs/TASKS.md). Always
 * rendered by Header.jsx; hidden above 900px (matches header.css's nav
 * breakpoint) and, while closed, visually + functionally hidden via
 * `visibility` rather than `display:none` so the transform/opacity
 * transitions can run — see the MobileMenu.jsx header comment for why this
 * (not aria-hidden) is what actually keeps it out of the tab order.
 */

.ws-mobile-menu {
  position: fixed;
  inset: 0;
  z-index: var(--ws-z-modal);
  pointer-events: none;
}
.ws-mobile-menu.is-open {
  pointer-events: auto;
}

@media (min-width: 901px) {
  .ws-mobile-menu {
    display: none;
  }
}

.ws-mobile-menu__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(20, 16, 15, 0.45);
  opacity: 0;
  transition: opacity var(--ws-duration-base) var(--ws-ease);
}
.ws-mobile-menu.is-open .ws-mobile-menu__backdrop {
  opacity: 1;
}

.ws-mobile-menu__panel {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: min(22rem, 86vw);
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-lg);
  padding: var(--ws-space-md) var(--ws-space-lg) var(--ws-space-xl);
  background: var(--ws-bg);
  color: var(--ws-text);
  box-shadow: var(--ws-shadow-popover);
  transform: translateX(100%);
  visibility: hidden;
  transition:
    transform var(--ws-duration-slow) var(--ws-ease),
    visibility 0s linear var(--ws-duration-slow);
}
.ws-mobile-menu.is-open .ws-mobile-menu__panel {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--ws-duration-slow) var(--ws-ease), visibility 0s linear 0s;
}

.ws-mobile-menu__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.ws-mobile-menu__eyebrow {
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: var(--ws-fs-small);
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--ws-eyebrow);
}

.ws-mobile-menu__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  flex-shrink: 0;
  border-radius: var(--ws-radius-pill);
  border: 1.5px solid var(--ws-surface-border);
  background: var(--ws-surface);
  color: var(--ws-text);
  cursor: pointer;
}
/* Same reason as ui.css's dark .ws-theme-toggle rule: --ws-text has no dark
   override, and here the chip sits on the panel's own dark background. */
:root[data-theme="dark"] .ws-mobile-menu__close {
  color: var(--ws-cream);
}

.ws-mobile-menu__nav {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
}
.ws-mobile-menu__nav a {
  font-family: var(--ws-font-display);
  font-size: 1.5rem;
  color: var(--ws-link-pink);
  text-decoration: none;
}
.ws-mobile-menu__nav a[aria-current="page"] {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.25em;
}

.ws-mobile-menu__footer {
  margin-top: auto;
  display: flex;
}

/* ---- src/styles/components/modal.css ---- */
/*
 * InterestModal ("Meld interesse" / vegansk restock notify) — T4.6
 * (docs/TASKS.md), docs/DESIGN.md §2.1. Centered dialog with backdrop.
 * Tokens only. Entrance animation duration is a token
 * (--ws-duration-slow/base) so it collapses to ~0 automatically under
 * prefers-reduced-motion (src/styles/00-tokens.css), no extra media query
 * needed here.
 */

.ws-interest-modal {
  position: fixed;
  inset: 0;
  z-index: var(--ws-z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--ws-space-lg);
  animation: ws-modal-fade var(--ws-duration-slow) var(--ws-ease-soft);
}
.ws-interest-modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(31, 29, 28, 0.5);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
/* White r=8 modal card — this task's brief: "modal cards white r=8". */
.ws-interest-modal__card {
  position: relative;
  width: 100%;
  max-width: 28.75rem;
  border-radius: 8px;
  background: #FFFFFF;
  color: var(--ws-text);
  box-shadow: var(--ws-shadow-popover);
  overflow: hidden;
  animation: ws-modal-rise var(--ws-duration-slow) var(--ws-ease-soft);
}
:root[data-theme="dark"] .ws-interest-modal__card {
  background: rgba(255, 255, 255, 0.8);
}
/* A fixed neutral wash (NOT --ws-bg, which flips near-black in dark mode
   and would hide the also-fixed-dark --ws-text icon against it) — reads as
   a soft grey chip against the white/80%-opacity card in both themes. */
.ws-interest-modal__close {
  position: absolute;
  top: 0.875rem;
  right: 0.875rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border: none;
  border-radius: var(--ws-radius-pill);
  background: rgba(31, 29, 28, 0.06);
  color: var(--ws-text);
  cursor: pointer;
  transition: transform var(--ws-duration-fast) var(--ws-ease), background-color var(--ws-duration-fast) var(--ws-ease);
}
.ws-interest-modal__close:hover {
  background: var(--ws-brand-pink);
  color: #FFFFFF;
  transform: scale(1.05);
}

.ws-interest-modal__body {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
  padding: var(--ws-space-2xl) var(--ws-space-xl) var(--ws-space-lg);
}
.ws-interest-modal__body--done {
  align-items: center;
  text-align: center;
  padding-top: var(--ws-space-lg);
}

.ws-interest-modal__check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 4rem;
  height: 4rem;
  margin: 0 auto 0.25rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-brand-pink);
  color: #FFFFFF;
  box-shadow: var(--ws-shadow-button);
}

/* Montserrat 300 — this task's brief: "titles Montserrat 300". */
.ws-interest-modal__title {
  font-family: var(--ws-font-display);
  font-weight: 300;
  font-size: 1.75rem;
  line-height: 1.2;
  margin: 0;
  color: var(--ws-heading);
}
.ws-interest-modal__title em {
  color: var(--ws-hero-title);
  font-style: italic;
}
.ws-interest-modal__lead {
  margin: 0;
  color: var(--ws-text-muted);
  font-size: 0.9375rem;
  line-height: 1.6;
}

.ws-interest-modal__field {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-3xs);
  margin-top: var(--ws-space-3xs);
}
.ws-interest-modal__field-label {
  font-family: var(--ws-font-display);
  font-size: 0.8125rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ws-text-muted);
  font-weight: 600;
}
/* "Like the footer newsletter field" — see src/styles/components/checkout.css's
   .ws-field__input header comment for the token choice. */
.ws-interest-modal__field input {
  box-sizing: border-box;
  width: 100%;
  min-height: 3.25rem;
  padding: 0 var(--ws-space-sm);
  border: 1px solid var(--ws-footer-input-border);
  border-radius: 8px;
  background: var(--ws-footer-input-bg);
  color: var(--ws-text);
  font-family: var(--ws-font-body);
  font-size: 1rem;
  transition: border-color var(--ws-duration-fast) var(--ws-ease);
}
.ws-interest-modal__field input:focus-visible {
  border-color: var(--ws-brand-pink);
}
.ws-interest-modal__field input:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.ws-interest-modal__error {
  margin: 0;
  padding: var(--ws-space-xs) var(--ws-space-sm);
  border-radius: var(--ws-radius-sm);
  border-left: 3px solid var(--ws-accent-text);
  background: var(--ws-surface);
  color: var(--ws-accent-text);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.ws-interest-modal__submit {
  margin-top: var(--ws-space-3xs);
}

.ws-interest-modal__fineprint {
  margin: var(--ws-space-2xs) 0 0;
  color: var(--ws-text-muted);
  font-size: 0.75rem;
  line-height: 1.55;
  text-align: center;
}

@keyframes ws-modal-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes ws-modal-rise {
  from { opacity: 0; transform: translateY(1.125rem) scale(0.97); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@media (max-width: 520px) {
  .ws-interest-modal {
    padding: var(--ws-space-sm);
    align-items: flex-end;
  }
  .ws-interest-modal__card {
    max-width: 100%;
    border-radius: var(--ws-radius-md) var(--ws-radius-md) 0 0;
  }
  .ws-interest-modal__body {
    padding: var(--ws-space-xl) var(--ws-space-lg) var(--ws-space-lg);
  }
  .ws-interest-modal__title {
    font-size: 1.3125rem;
  }
}

/* ---- src/styles/components/payment-return.css ---- */
/*
 * Styles for src/pages/PaymentReturnPage.jsx (status card) — Task F4
 * (docs/FIGMA_SPEC.md §3). Tokens only. No dedicated "success/error" colour
 * exists in the palette (only pink/orange/purple brand accents) — the icon
 * shape (check/clock/close) plus the message text carry the meaning, not
 * colour alone: every state fills its icon circle with a brand colour
 * ("large status icon circle (pink/orange)", this task's brief) — pink by
 * default/in-progress, orange for the positive "paid" outcome (echoing
 * --ws-btn-accent-* already being the closest on-brand positive accent).
 * Responsive checkpoints per AGENT_RULES §3: 360-390 / 768 / 1024 / 1512.
 */

.ws-payment-status {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ws-space-sm);
  margin-top: var(--ws-space-lg);
  padding: var(--ws-space-2xl) var(--ws-space-lg) var(--ws-space-xl);
  width: 100%;
  max-width: 26rem;
  background: #FFFFFF;
  border-radius: 8px;
  box-shadow: var(--ws-shadow-product-card);
  text-align: center;
}
:root[data-theme="dark"] .ws-payment-status {
  background: rgba(255, 255, 255, 0.8);
}

.ws-payment-status__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 6rem;
  height: 6rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-brand-pink);
  color: #FFFFFF;
}
.ws-payment-status--paid .ws-payment-status__icon {
  background: var(--ws-btn-accent-bg);
  color: var(--ws-btn-accent-text);
}

.ws-payment-status__spinner {
  animation: ws-payment-status-spin 1.1s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ws-payment-status__spinner {
    animation: none;
  }
}
@keyframes ws-payment-status-spin {
  to { transform: rotate(360deg); }
}

.ws-payment-status__message {
  margin: 0;
  color: var(--ws-text);
  font-size: 1.0625rem;
  line-height: var(--ws-lh-relaxed);
}

/* A fixed neutral wash (NOT --ws-bg, which flips near-black in dark mode
   and would hide the also-fixed-dark --ws-text against it) — reads as a
   soft chip against the white/80%-opacity status card in both themes. */
.ws-payment-status__order {
  margin: 0;
  padding: var(--ws-space-2xs) var(--ws-space-sm);
  border-radius: var(--ws-radius-sm);
  background: rgba(31, 29, 28, 0.06);
  color: var(--ws-text);
  font-size: var(--ws-fs-small);
}
.ws-payment-status__order strong {
  color: var(--ws-heading);
}

/* Stacked, full-width (Button `fullWidth` in PaymentReturnPage.jsx) — a
   single-column CTA stack reads cleaner in a centered ~26rem card than a
   wrapping row of two 70px-tall Figma card buttons. */
.ws-payment-status__actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--ws-space-sm);
  width: 100%;
  margin-top: var(--ws-space-2xs);
}
/* .ws-btn--light (src/styles/components/ui.css) is a WHITE button with no
   border of its own — by design it's meant for use on a coloured band (CTA
   band's pink, FIGMA_SPEC §9), where it pops against the surrounding
   colour. Sitting on this page's own white/80%-opacity status card instead,
   that combination has no visible edge at all — a real "invisible button"
   bug caught while checking this page live, not just in a screenshot.
   Scoped border restores its outline without touching the shared variant
   (out of this task's scope to edit). */
.ws-payment-status__actions .ws-btn--light {
  border: 1.5px solid var(--ws-brand-pink);
}

/* ---- src/styles/components/product-card.css ---- */
/*
 * Styles for src/ui/ProductCard.jsx — Task F2 (docs/FIGMA_SPEC.md §3/§4,
 * node 1:181 "Rectangle"/1:186 photo/1:183 name/1:184-185 button), motion
 * added in Phase G (docs/MOTION.md §4 "Карточки товаров"). Shared by the
 * home Bestsellers grid (src/sections/Bestsellers.jsx), /butikk
 * (src/pages/ShopPage.jsx) and the product page's "Flere smaker"
 * (src/pages/ProductPage.jsx) — kept width-agnostic (no fixed `width`, only
 * `height` at >=1024px) so it still renders correctly whatever column width
 * those OTHER grids (both G3-owned, not touched by this task) give it.
 *
 * Phase G: name/button used to be `position: absolute` inside the
 * fixed-height card — docs/MOTION.md §2 rule 1 bans that for text/buttons.
 * They are flex-flow children now (the card is `display: flex;
 * flex-direction: column` at every width, already true below 1024px); at
 * >=1024px `padding-top: calc(296 * var(--d))` reserves the same room the
 * old `top` offset used, so the name still lands at Figma's y=296 and the
 * CTA still docks flush to the card's bottom edge via the unconditional
 * `margin-top: auto` below — no JS/measurement needed, the math is exact:
 * 403 - 296 = 107 = the name's ~28px line box + Figma's 9px gap + the
 * button's 70px (calc(70*var(--d)) from ui.css's `.ws-btn--card`), so the
 * leftover flex space that auto margin eats reproduces the 9px gap for free.
 * The PRODUCT PHOTO stays `position: absolute; top: calc(-47 * var(--d))`
 * on purpose: it is decorative (an image, not text/an interactive control —
 * the allowed exception in MOTION.md §2 rule 1) and shop.css's/
 * product-page.css's own grids (`.ws-shop__grid`/`.ws-pdp-more__grid`)
 * already reserve `margin-top: calc(47 * var(--d))` specifically to
 * compensate for it — changing how the photo is positioned would break
 * that cross-file contract.
 */

.ws-product-card {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Grid/flex items default to `min-width: auto`, which lets a nowrap
     descendant (the "Kjøp nå · N kr" button below, `white-space: nowrap`
     via .ws-btn) force this card's min-content width above whatever the
     grid column actually gives it — widening the WHOLE grid (and the page)
     past the viewport at narrower breakpoints. This opts back into normal
     shrinking. */
  min-width: 0;
  height: 100%;
  /* FIGMA_SPEC §3 "Карточки товаров": #FFFFFF in BOTH themes (only opacity
     drops in dark, below) — a fixed value, not the theme-dependent
     --ws-surface token (which is a faint rgba(255,255,255,.06) in dark
     mode, not solid white), same reasoning as Brands'/Reviews' fixed-white
     cards (see brands.css / reviews.css). */
  background: #FFFFFF;
  border: 1px solid var(--ws-surface-border);
  border-radius: 12px;
  box-shadow: var(--ws-shadow-product-card);
  overflow: visible; /* the photo pokes above the card's own top edge at >=1024px */
  /* Hover/focus lift below uses `translate`, never `transform` — `transform`
     on this element is owned by [data-tilt] (src/lib/motion.js,
     docs/MOTION.md §3), which sets it directly as an inline custom
     property read, not a class toggle, so it can't be listed here. */
  transition: translate var(--ws-duration-base) var(--ws-ease), box-shadow var(--ws-duration-base) var(--ws-ease);
}
:root[data-theme="dark"] .ws-product-card {
  background: rgba(255, 255, 255, 0.8);
}

/* ---- Phase G motion (docs/MOTION.md §4) ---- */
.ws-product-card:hover,
.ws-product-card:focus-within {
  translate: 0 max(-6px, calc(-10 * var(--d)));
  box-shadow: var(--ws-shadow-popover);
}

/* Soft pointer-tracked highlight — motion.js writes --tilt-gx/--tilt-gy
   (0-100%) while the card is being tilted, and toggles .is-tilting. Brand
   pink, not white: the card itself is already white/near-white (both
   themes), so a white glare would be invisible on it. */
.ws-product-card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  border-radius: inherit;
  background: radial-gradient(circle at var(--tilt-gx, 50%) var(--tilt-gy, 50%), rgba(241, 90, 180, 0.22), transparent 60%);
  opacity: 0;
  transition: opacity var(--ws-duration-base) var(--ws-ease);
  pointer-events: none; /* never steals clicks from the photo/name/button underneath */
}
.ws-product-card.is-tilting::after {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .ws-product-card {
    transition: none;
  }
  .ws-product-card:hover,
  .ws-product-card:focus-within {
    translate: none;
  }
}

.ws-product-card__badge {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  z-index: 2;
  background: var(--ws-btn-primary-bg);
  color: var(--ws-btn-primary-text);
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: 0.6875rem;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 0.3rem 0.65rem;
  border-radius: var(--ws-radius-pill);
}

.ws-product-card__photo {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--ws-space-lg) var(--ws-space-md) var(--ws-space-sm);
}
.ws-product-card__photo img {
  display: block;
  width: auto;
  max-width: 70%;
  height: auto;
  max-height: 11rem;
  object-fit: contain;
  filter: drop-shadow(var(--ws-shadow-photo));
  transition: transform var(--ws-duration-base) var(--ws-ease);
}
.ws-product-card--sold-out .ws-product-card__photo img {
  opacity: 0.55;
}
.ws-product-card__photo:hover img {
  transform: scale(1.03);
}

.ws-product-card__name {
  margin: 0 0 var(--ws-space-sm);
  padding-inline: var(--ws-space-sm);
  text-align: center;
  font-family: var(--ws-font-display);
  font-weight: 400;
  font-size: var(--ws-fs-product-name);
  line-height: 1.4; /* 28/20 */
}
.ws-product-card__name a {
  color: var(--ws-product-name);
  text-decoration: none;
}
.ws-product-card__name a:hover {
  text-decoration: underline;
}

.ws-product-card__cta {
  margin-top: auto;
}
/* Two independent overrides of ui.css's `.ws-btn`/`.ws-btn--full` rules,
   both via a doubled selector so they win on specificity rather than
   relying on file load order (ui.css loads AFTER this file alphabetically,
   so same-specificity rules here would otherwise lose):
    1) `.ws-btn--full` sets `width: 100%` — combined with a horizontal
       margin for side-inset (the natural way to inset a full-width button
       from its card), the margin ADDS to that 100% instead of being
       absorbed by it, so the button's total box was 2*--ws-space-sm wider
       than its own card — enough to push a 2-column mobile/tablet grid
       (src/styles/components/bestsellers.css) past the viewport. Using
       `calc(100% - 2 * inset)` bakes the inset into the width itself.
    2) `.ws-btn`'s base rule sets `white-space: nowrap` (fine for short
       static labels) — but this one's label is dynamic ("Kjøp nå · N kr",
       D23); letting it wrap is a second, independent safety net against
       the same class of overflow if a future price ever needs it. */
.ws-product-card__cta.ws-product-card__cta {
  width: calc(100% - 2 * var(--ws-space-sm));
  margin-inline: var(--ws-space-sm);
  margin-bottom: var(--ws-space-sm);
  white-space: normal;
  text-align: center;
  line-height: 1.15;
}

@media (min-width: 1024px) {
  .ws-product-card {
    height: calc(403 * var(--d));
    /* Reserves the photo's own visible height inside the card (327 - 47 =
       280) plus Figma's 16px gap to the name (296) — see this file's header
       comment for the rest of the math. */
    padding-top: calc(296 * var(--d));
    border-radius: calc(8 * var(--d));
  }

  .ws-product-card__badge {
    top: calc(12 * var(--d));
    right: calc(12 * var(--d));
  }

  .ws-product-card__photo {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(-47 * var(--d));
    height: calc(327 * var(--d));
    padding: 0;
  }
  .ws-product-card__photo img {
    max-width: 62%;
    max-height: 100%;
  }

  .ws-product-card__name {
    margin: 0;
    padding-inline: calc(20 * var(--d));
    font-size: max(14px, calc(20 * var(--d)));
  }

  /* .ws-product-card__cta's `margin-top: auto` (base rule, unconditional)
     docks it to the card's bottom edge inside the flex column — see this
     file's header comment for why that reproduces Figma's y=333 exactly.
     Beats the doubled mobile-width override above on specificity (three
     classes > two): full width again, flush with the card, no side inset. */
  .ws-product-card__cta.ws-product-card__cta.ws-product-card__cta {
    width: auto;
    margin: 0;
  }
}

/* ---- src/styles/components/product-page.css ---- */
/*
 * Styles for src/pages/ProductPage.jsx — T4.5b (docs/TASKS.md),
 * docs/DESIGN.md. Tokens only (src/styles/00-tokens.css); no legacy.css
 * classes. Responsive checkpoints per AGENT_RULES §3: 360-390 / 768 / 1024 /
 * 1512. Every section here repaints `background: var(--ws-bg)` itself (same
 * reasoning as src/styles/components/hero.css's `.ws-hero` comment: painting
 * over legacy.css's non-theme-aware `.site-shell` background).
 *
 * The qty stepper reuses .ws-qty-stepper/.ws-qty-stepper__btn/__value as-is
 * (src/styles/components/cart-drawer.css) rather than duplicating those
 * rules — .ws-pdp__qty only adds PDP-specific sizing on top, scoped so it
 * can't leak back into the cart drawer's compact version.
 */

/* ============ Hero: gallery | info ============ */

.ws-pdp {
  background: var(--ws-bg);
  padding-block: calc(var(--ws-header-clearance) + clamp(0.5rem, 0.2rem + 1.5vw, 1.5rem)) clamp(2.5rem, 1.6rem + 3.6vw, 4rem);
}
/* ProductPage renders its own two-column hero instead of the shared
   SubpageHero (src/sections/SubpageHero.jsx) — see docs/FRONTEND.md
   "Фактическое состояние после Task F1": since src/layout/Header.jsx is
   absolutely positioned over the top of the page (D22), every page's own
   first section must reserve that clearance itself. Matches SubpageHero's
   own calc(200 * var(--d)) exactly. */
@media (min-width: 1024px) {
  .ws-pdp {
    padding-top: calc(200 * var(--d));
  }
}

.ws-pdp__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--ws-space-2xl);
  align-items: start;
}

/* ---- Gallery ---- */

.ws-pdp-gallery {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
  position: sticky;
  top: calc(5.5rem + var(--ws-space-md));
  /* Headroom for the product photo overflowing the card's top edge above
     (.ws-pdp-gallery__main--product .ws-pdp-gallery__image, inset -7%). */
  margin-top: var(--ws-space-lg);
}

/* Gallery main frame — Task F4 (docs/FIGMA_SPEC.md §3/§4): the exact "white
   r=8 card, shadow 0 3px 15px rgba(0,0,0,.03)" home-card treatment
   (--ws-shadow-product-card is that literal token). A product cutout photo
   (data/catalog.json gallery kind="product", the default) is absolutely
   positioned OVER the card and pulled up past its top edge, matching the
   home product-card composition (FIGMA_SPEC §4: "фото товара выходит за
   верх карточки"); its own drop-shadow is --ws-shadow-photo (FIGMA_SPEC §3
   "Тени" — "Фото товаров/снеков в композициях"), the same numbers as a
   box-shadow but usable as `filter: drop-shadow()` since it clips to the
   cutout's own alpha instead of the padding box. Lifestyle/detail photos
   (kind="photo") stay a plain filled card (object-fit: cover, no overflow)
   — see ProductGallery in src/pages/ProductPage.jsx for the kind switch. */
.ws-pdp-gallery__main {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: 8px;
  overflow: visible;
  background: #FFFFFF;
  box-shadow: var(--ws-shadow-product-card);
}
:root[data-theme="dark"] .ws-pdp-gallery__main {
  background: rgba(255, 255, 255, 0.8);
}

.ws-pdp-gallery__main--photo {
  overflow: hidden;
}
.ws-pdp-gallery__main--product .ws-pdp-gallery__image {
  position: absolute;
  inset: -7% 9% 5%;
  width: 82%;
  height: 104%;
  object-fit: contain;
  filter: drop-shadow(var(--ws-shadow-photo));
}
.ws-pdp-gallery__main--photo .ws-pdp-gallery__image {
  object-fit: cover;
  border-radius: 8px;
}
.ws-pdp-gallery__image {
  display: block;
  width: 100%;
  height: 100%;
}
/* Task G3 (docs/MOTION.md §4): .ws-pdp-gallery__main carries data-tilt="6"
   (~6deg toward the pointer); this image is the .ws-tilt-pop child that
   pops slightly forward in Z (03-motion.css). */
.ws-pdp-gallery__image.ws-tilt-pop {
  --tilt-pop: 22px;
}

.ws-pdp-gallery__thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ws-space-xs);
  margin-top: var(--ws-space-sm);
}
.ws-pdp-gallery__thumb {
  width: 4.75rem;
  height: 4.75rem;
  flex-shrink: 0;
  padding: 0;
  border-radius: 8px;
  overflow: hidden;
  background: #FFFFFF;
  border: 2px solid transparent;
  cursor: pointer;
  transition: border-color var(--ws-duration-fast) var(--ws-ease), transform var(--ws-duration-fast) var(--ws-ease);
}
:root[data-theme="dark"] .ws-pdp-gallery__thumb {
  background: rgba(255, 255, 255, 0.8);
}
.ws-pdp-gallery__thumb:hover {
  transform: translateY(-2px);
}
.ws-pdp-gallery__thumb.is-active {
  border-color: var(--ws-brand-pink);
}
.ws-pdp-gallery__thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Info column ---- */

.ws-pdp__info {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
}

/* Montserrat 700, #FF862F — "like hero H1 colour" (this task's brief; the
   home Hero H1 is FIGMA_SPEC's Montserrat 700 90px --ws-hero-title). Scaled
   with --d at >=1024px to land at calc(52 * var(--d)), the middle of the
   brief's 48-56px range. */
.ws-pdp__title {
  margin-top: var(--ws-space-2xs);
  font-family: var(--ws-font-display);
  font-size: clamp(1.75rem, 1.2rem + 2.4vw, 3rem);
  font-weight: 700;
  line-height: 1.12;
  color: var(--ws-hero-title);
}

.ws-pdp__price-row {
  display: flex;
  align-items: baseline;
  gap: var(--ws-space-sm);
}
/* Montserrat 700 32px #9C5EE5 (this task's brief), scaled with --d. */
.ws-pdp__price {
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: 1.75rem;
  color: var(--ws-product-name);
}

@media (min-width: 1024px) {
  .ws-pdp__title {
    font-size: calc(52 * var(--d));
  }
  .ws-pdp__price {
    font-size: calc(32 * var(--d));
  }
}
.ws-pdp__weight {
  color: var(--ws-text-muted);
  font-size: var(--ws-fs-small);
}

.ws-pdp__lead {
  color: var(--ws-text-muted);
  font-size: 1.0625rem;
  line-height: var(--ws-lh-relaxed);
  max-width: 40ch;
}

/* ---- Buy row / sold-out ---- */

.ws-pdp__buy {
  display: flex;
  gap: var(--ws-space-sm);
  margin-top: var(--ws-space-2xs);
}
/* min-width: 0 overrides the flex item default of min-width:auto, which
   otherwise refuses to shrink a nowrap-text flex child below its own
   content width — exactly what was pushing this row (and the whole page)
   wider than the viewport at ~1024-1300px once the button label grew to
   include the price ("Legg i handlekurv · N kr", this task's brief).
   white-space: normal + a moderated font-size (smaller than the literal
   Figma card-button 30px, which was sized for a much shorter label) let the
   label wrap onto two lines instead of forcing the row wider. */
.ws-pdp__buy .ws-btn--full {
  flex: 1;
  min-width: 0;
}
.ws-pdp__buy .ws-btn.ws-btn--card {
  white-space: normal;
  text-align: center;
  line-height: 1.15;
}
@media (min-width: 1024px) {
  .ws-pdp__buy .ws-btn.ws-btn--card {
    height: auto;
    min-height: calc(70 * var(--d));
    padding-block: calc(10 * var(--d));
    font-size: max(14px, calc(21 * var(--d)));
  }
}
/* Qty stepper — "circles 44px with 2px #FF862F border like the carousel
   buttons" (this task's brief, echoing src/ui/CircleButton.jsx's
   .ws-circle-btn--outline look, FIGMA_SPEC §10 — a bigger 60px sibling of
   this same idea). Two free-standing circular buttons flanking the number,
   not a single pill container. */
.ws-pdp__qty {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--ws-space-sm);
  border: none;
  background: none;
  padding: 0;
}
.ws-pdp__qty .ws-qty-stepper__btn {
  width: 2.75rem; /* 44px */
  height: 2.75rem;
  border: 2px solid var(--ws-brand-orange);
  border-radius: var(--ws-radius-pill);
  background: transparent;
  color: var(--ws-brand-orange);
}
.ws-pdp__qty .ws-qty-stepper__btn:hover {
  background: var(--ws-brand-orange);
  color: #FFFFFF;
}
.ws-pdp__qty .ws-qty-stepper__value {
  min-width: 1.75rem;
  font-family: var(--ws-font-display);
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--ws-text);
  text-align: center;
}

.ws-pdp__soldout {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--ws-space-sm);
  margin-top: var(--ws-space-2xs);
  padding: var(--ws-space-md);
  background: #FFFFFF;
  border-radius: 8px;
  box-shadow: var(--ws-shadow-product-card);
}
:root[data-theme="dark"] .ws-pdp__soldout {
  background: rgba(255, 255, 255, 0.8);
}
.ws-pdp__badge {
  padding: 0.3rem 0.75rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-btn-primary-bg);
  color: var(--ws-btn-primary-text);
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: var(--ws-fs-small);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.ws-pdp__soldout-note {
  margin: 0;
  color: var(--ws-text-muted);
  font-size: 0.9375rem;
  line-height: var(--ws-lh-relaxed);
}
.ws-pdp__soldout .ws-btn {
  width: 100%;
  /* "Varsle meg når den er tilbake" is wider than a 320-414px column: wrap
     instead of pushing the page sideways (same fix as .ws-pdp__buy below). */
  white-space: normal;
  text-align: center;
  line-height: 1.15;
  padding-block: 0.75rem;
}

/* ---- Delivery / payment notes ---- */

.ws-pdp__delivery-note,
.ws-pdp__payment-hint {
  display: flex;
  align-items: center;
  gap: var(--ws-space-2xs);
  margin: 0;
  color: var(--ws-text-muted);
  font-size: 0.9375rem;
}
.ws-pdp__payment-hint {
  font-size: var(--ws-fs-small);
}
/* --ws-link-pink, not the decor-only --ws-brand-pink: same reasoning as
   src/styles/components/delivery.css's .ws-quick-item__icon (>=3:1 non-text
   contrast; --ws-brand-pink only measures 2.59:1 on --ws-bg). */
.ws-pdp__delivery-note .ws-icon,
.ws-pdp__payment-hint .ws-icon {
  flex-shrink: 0;
  color: var(--ws-link-pink);
}

/* ---- Info tabs ---- */
/* Pill buttons — this task's brief: "active #F15AB4 bg white text, inactive
   white bg #F15AB4 text", reusing the exact Button light/card token pairs
   (src/styles/components/ui.css) so these tabs render in precisely the same
   pink as every other Figma-exact control on the page. */

.ws-pdp-tabs {
  margin-top: var(--ws-space-sm);
  padding-top: var(--ws-space-lg);
  border-top: 1px solid var(--ws-surface-border);
}
.ws-pdp-tabs__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ws-space-xs);
}
.ws-pdp-tabs__tab {
  position: relative;
  padding: 0.625rem 1.25rem;
  border: none;
  border-radius: var(--ws-radius-pill);
  cursor: pointer;
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: 0.9375rem;
  background: var(--ws-btn-light-bg);
  color: var(--ws-btn-light-text);
  transition: background-color var(--ws-duration-fast) var(--ws-ease), color var(--ws-duration-fast) var(--ws-ease);
}
.ws-pdp-tabs__tab:hover {
  filter: brightness(0.97);
}
.ws-pdp-tabs__tab.is-active {
  background: var(--ws-btn-primary-bg);
  color: var(--ws-btn-primary-text);
}
.ws-pdp-tabs__panel {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-2xs);
  padding-top: var(--ws-space-md);
  color: var(--ws-text);
  font-size: 0.9375rem;
  line-height: var(--ws-lh-relaxed);
}
.ws-pdp-tabs__panel p {
  margin: 0;
}

/* ============ Content: "what's actually in the plate" ============ */

.ws-pdp-content {
  background: var(--ws-bg);
  padding-block: var(--ws-space-xl) var(--ws-space-2xl);
}
.ws-pdp-content__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: var(--ws-space-2xl);
  align-items: start;
}
.ws-pdp-content__title {
  margin-top: var(--ws-space-2xs);
  font-family: var(--ws-font-display);
  font-size: var(--ws-fs-section-title);
  font-weight: 300; /* FIGMA_SPEC §3: every section title is Montserrat 300 */
  color: var(--ws-heading);
}
@media (min-width: 1024px) {
  .ws-pdp-content__title {
    font-size: calc(40 * var(--d));
  }
}
.ws-pdp-content__allergens {
  margin-top: var(--ws-space-sm);
  color: var(--ws-text-muted);
  font-size: 1rem;
  line-height: var(--ws-lh-relaxed);
  max-width: 36ch;
}

/* "Review-like" fact-list card — this task's brief: r=4, 2px #FF8A63 border,
   "where lists of facts are shown" (echoes ReviewsSection's own card
   language, src/styles/components/reviews.css, for a list of ingredient
   facts instead of a list of quotes). --ws-border-peach so it recolours for
   dark mode the same way review cards do. */
.ws-pdp-ingredients {
  list-style: none;
  margin: 0;
  padding: var(--ws-space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
  background: var(--ws-surface);
  border: 2px solid var(--ws-border-peach);
  border-radius: 4px;
}
.ws-pdp-ingredients__label {
  display: flex;
  justify-content: space-between;
  gap: var(--ws-space-sm);
  font-size: 0.9375rem;
  color: var(--ws-text);
}
.ws-pdp-ingredients__label strong {
  color: var(--ws-heading);
  font-variant-numeric: tabular-nums;
}
.ws-pdp-ingredients__track {
  margin-top: 0.375rem;
  height: 0.5rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-bg);
  border: 1px solid var(--ws-surface-border);
  overflow: hidden;
}
/* --ws-link-pink for the same accessible-progress-fill reason as
   src/styles/components/cart-drawer.css's .ws-shipping-progress__fill and
   src/styles/components/bundle.css's .ws-bundle-summary__track span. */
.ws-pdp-ingredients__fill {
  display: block;
  height: 100%;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-link-pink);
}

/* ============ "Flere smaker" ============ */

.ws-pdp-more {
  background: var(--ws-bg);
  padding-block: var(--ws-space-xl) var(--ws-space-3xl);
}
.ws-pdp-more__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--ws-space-lg);
  margin-top: var(--ws-space-xl);
}
@media (min-width: 560px) {
  .ws-pdp-more__grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
/* Same 3-column Figma geometry as /butikk (src/styles/components/shop.css's
   .ws-shop__grid) — "«Flere smaker» grid like Bestsellers". */
@media (min-width: 1024px) {
  .ws-pdp-more__grid {
    grid-template-columns: repeat(3, 1fr);
    gap: calc(23 * var(--d)) calc(29 * var(--d));
    margin-top: calc(47 * var(--d));
  }
}

/* ============ Responsive ============ */

@media (max-width: 960px) {
  .ws-pdp__grid {
    grid-template-columns: minmax(0, 1fr);
  }
  .ws-pdp-gallery {
    position: static;
    max-width: 26rem;
    margin-inline: auto;
    width: 100%;
  }
  .ws-pdp-content__grid {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 640px) {
  .ws-pdp-gallery {
    max-width: none;
  }
  .ws-pdp__lead {
    max-width: none;
  }
}

@media (max-width: 480px) {
  .ws-pdp__buy {
    flex-direction: column;
  }
  .ws-pdp__qty {
    width: 100%;
    justify-content: center;
  }
  .ws-pdp-tabs__list {
    gap: var(--ws-space-md);
  }
}

/* ============ Dark-theme text-contrast fixes ============ */
/* --ws-text/--ws-text-muted are a fixed dark grey in BOTH themes
   (src/styles/00-tokens.css never gives them a [data-theme="dark"]
   override), so every spot on this page that sits directly on --ws-bg
   (which DOES flip near-black) rather than one of this page's white cards
   reads as near-invisible dark-on-near-black. Same scoped fix everywhere on
   this page set: a translucent cream, echoing this design system's existing
   light-text-on-dark convention (CTA band / About drip —
   --ws-cream/--ws-cta-band-text). Cards (.ws-pdp-gallery__main,
   .ws-pdp__soldout, .ws-craft-card, etc.) are unaffected — their background
   is a fixed white/80%-opacity in both themes, so --ws-text stays correct
   there. */
:root[data-theme="dark"] .ws-pdp__lead,
:root[data-theme="dark"] .ws-pdp-tabs__panel,
:root[data-theme="dark"] .ws-pdp-content__allergens {
  color: rgba(255, 231, 228, 0.85);
}
:root[data-theme="dark"] .ws-pdp__weight,
:root[data-theme="dark"] .ws-pdp__delivery-note,
:root[data-theme="dark"] .ws-pdp__payment-hint {
  color: rgba(255, 231, 228, 0.65);
}
:root[data-theme="dark"] .ws-pdp__qty .ws-qty-stepper__value {
  color: rgba(255, 231, 228, 0.92);
}
/* .ws-pdp-ingredients sits on --ws-surface (a near-transparent dark wash in
   dark mode, not one of the fixed-white cards) — same fix for its label
   text; the percentage in <strong> already uses --ws-heading (bright
   purple, unaffected — see ProductIngredientBars in
   src/pages/ProductPage.jsx). */
:root[data-theme="dark"] .ws-pdp-ingredients__label {
  color: rgba(255, 231, 228, 0.85);
}

/* ---- src/styles/components/promo.css ---- */
/*
 * PromoCodeField — T4.6 (docs/TASKS.md), docs/DESIGN.md §2.1. Renders inside
 * CartDrawer's cart-mode body (--ws-bg background). Tokens only. Applied-state
 * badge/amount use --ws-link-pink (verified 4.65:1 on --ws-bg / 5.49:1 on
 * white / 4.62:1 on the dark --ws-surface composite — docs/tools/contrast.mjs,
 * see the T4.6 report), matching the discount-row colour in cart-drawer.css.
 */

.ws-promo {
  margin-top: var(--ws-space-sm);
  padding-top: var(--ws-space-sm);
  border-top: 1px solid var(--ws-text-muted);
}

.ws-promo__row {
  display: flex;
  gap: var(--ws-space-2xs);
}
/* "Like the footer newsletter field" — see src/styles/components/checkout.css's
   .ws-field__input header comment for the token choice. */
.ws-promo__input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  min-height: 3.25rem; /* matches Button size="md" (.ws-btn--md) for a flush row */
  padding: 0 var(--ws-space-sm);
  border: 1px solid var(--ws-footer-input-border);
  border-radius: 8px;
  background: var(--ws-footer-input-bg);
  color: var(--ws-text);
  font-family: var(--ws-font-body);
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.ws-promo__input::placeholder {
  color: var(--ws-footer-input-placeholder);
  text-transform: none;
  letter-spacing: normal;
}
.ws-promo__input:focus-visible {
  border-color: var(--ws-brand-pink);
}

.ws-promo__error {
  margin-top: var(--ws-space-2xs);
  color: var(--ws-accent-text);
  font-size: 0.8125rem;
  line-height: 1.4;
}

.ws-promo__applied {
  display: flex;
  align-items: center;
  gap: var(--ws-space-2xs);
  padding: var(--ws-space-xs) var(--ws-space-sm);
  border: 1px solid var(--ws-link-pink);
  border-radius: var(--ws-radius-sm);
  background: var(--ws-surface);
}
.ws-promo__badge {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ws-link-pink);
  font-weight: 600;
  font-size: 0.9rem;
}
.ws-promo__amount {
  color: var(--ws-link-pink);
  font-weight: 600;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ws-promo__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
  border: none;
  border-radius: var(--ws-radius-pill);
  background: transparent;
  color: var(--ws-text-muted);
  cursor: pointer;
  transition: background-color var(--ws-duration-fast) var(--ws-ease), color var(--ws-duration-fast) var(--ws-ease);
}
.ws-promo__remove:hover {
  background: var(--ws-bg);
  color: var(--ws-text);
}
/* --ws-text(-muted) fixed both themes, sitting directly on the drawer's
   --ws-bg — same dark-mode fix as elsewhere in this task. */
:root[data-theme="dark"] .ws-promo__remove {
  color: rgba(255, 231, 228, 0.65);
}
:root[data-theme="dark"] .ws-promo__remove:hover {
  background: var(--ws-brand-pink);
  color: #FFFFFF;
}

/* ---- src/styles/components/reviews.css ---- */
/*
 * Styles for src/sections/ReviewsSection.jsx — Task F2 (docs/FIGMA_SPEC.md
 * §3/§4, node 1:72 "Frame" + 1:176/1:177 arrows). Card surface is a FIXED
 * white/opacity-70 + blur in BOTH site themes (FIGMA_SPEC §3 "Карточки
 * отзывов" — same hex in the dark frame 1:292 as the light one), unlike the
 * pre-F2 version which reused the theme-dependent --ws-surface/
 * --ws-border-peach pair; same reasoning as Brands' fixed-white cards (see
 * brands.css). Border colour comes from each review's own
 * --ws-review-card-border custom property (src/sections/ReviewsSection.jsx —
 * a different peach/orange shade per card position in Figma, not a token).
 *
 * Card height is intentionally CONTENT-DRIVEN (FIGMA_SPEC: "content-driven
 * height allowed >= 224") rather than a rigid absolute box: WeSnack's real
 * (short) Norwegian review copy renders nowhere near as tall as Figma's
 * Lorem-ipsum placeholders, so internal elements (stars/title/body/author)
 * are normal flow with Figma-matching gaps rather than each pinned to its
 * own absolute y.
 *
 * Phase G (docs/MOTION.md §2 rule 1): the old >=1024px layout pinned the
 * heading, the track and the prev/next nav as three independent
 * `position: absolute` blocks. `.ws-reviews__top` (src/sections/
 * ReviewsSection.jsx) replaces that with one flex row — heading left, nav
 * right of it — and the track is a normal flow sibling below; card widths
 * still come from `calc(N * var(--d))`.
 */

.ws-reviews {
  position: relative;
  z-index: 1; /* above .ws-brands-reviews__blob (brands.css) */
  padding-block: var(--ws-space-2xl) var(--ws-space-3xl);
  overflow: hidden; /* contain the track's own scroll area only, not the page */
  /* Transparent on purpose (brands.css's .ws-brands-reviews comment): the
     shared wrapper paints the page background so .ws-brands-reviews__blob
     shows through both Brands and this section's own gaps. */
  background: transparent;
}

.ws-reviews__top {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--ws-space-md);
  padding-inline: var(--ws-gutter);
  margin-bottom: var(--ws-space-lg);
}

.ws-reviews__head {
  margin: 0;
}

.ws-reviews__track {
  display: flex;
  gap: var(--ws-space-lg);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-left: var(--ws-gutter);
  padding-right: var(--ws-gutter);
  padding-bottom: var(--ws-space-2xs);
  scrollbar-width: none;
}
.ws-reviews__track::-webkit-scrollbar {
  display: none;
}

.ws-review-card {
  flex: 0 0 auto;
  box-sizing: border-box;
  width: min(26rem, 82vw);
  min-height: 14rem;
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
  padding: var(--ws-space-lg);
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  /* Verified against the reference render (like Brands' card — see
     brands.css's comment): the border blends at the SAME opacity .7 as the
     background, not a flat/fully-saturated colour. */
  border: 2px solid color-mix(in srgb, var(--ws-review-card-border, var(--ws-brand-orange)) 70%, transparent);
  border-radius: var(--ws-radius-sm);
  scroll-snap-align: start;
}

.ws-review-card__stars {
  display: inline-flex;
  gap: 2px;
}
.ws-review-card__stars img {
  display: block;
}

.ws-review-card__title {
  margin: 0;
  font-family: var(--ws-font-display);
  font-weight: 500;
  font-size: var(--ws-fs-review-title);
  color: var(--ws-heading);
  line-height: 1.2; /* 28.8/24 */
}

.ws-review-card__body {
  flex: 1;
  margin: 0;
  color: var(--ws-heading);
  font-size: 0.9375rem; /* 15px */
  line-height: 1.493; /* 22.4/15 */
}

.ws-review-card__author {
  display: flex;
  align-items: center;
  gap: var(--ws-space-xs);
  margin-top: auto;
  padding-top: var(--ws-space-2xs);
  color: var(--ws-heading);
  font-family: var(--ws-font-display);
  font-weight: 300;
  font-size: 1rem; /* 16px */
}

.ws-review-card__bar {
  width: 2px;
  height: 1.5rem;
  flex-shrink: 0;
  background: var(--ws-brand-orange);
}

.ws-reviews__nav {
  display: flex;
  flex-shrink: 0;
  gap: var(--ws-space-sm);
}

@media (max-width: 640px) {
  .ws-reviews__top {
    align-items: flex-start;
  }
}

@media (min-width: 1024px) {
  .ws-reviews {
    /* Gap after Brands within the shared wrapper (.ws-brands-reviews,
       brands.css): 3638 (Feedback's own absolute y) - 3625 (Brands' bottom,
       82.01 + 320) = 12.99. */
    margin-top: calc(12.99 * var(--d));
    padding-block: calc(3 * var(--d)) calc(64 * var(--d));
  }

  .ws-reviews__top {
    padding-inline: calc(31 * var(--d)) var(--ws-gutter);
    margin-bottom: calc(64 * var(--d)); /* head top (3) + track top (82) - a little breathing room */
  }

  .ws-reviews__track {
    gap: calc(15 * var(--d));
    padding-left: calc(31 * var(--d));
    padding-right: var(--ws-gutter);
    padding-bottom: calc(8 * var(--d));
  }

  .ws-review-card {
    width: calc(584 * var(--d));
    min-height: calc(224 * var(--d));
    gap: calc(13 * var(--d));
    padding: calc(31 * var(--d)) calc(31 * var(--d)) calc(24 * var(--d));
    border-radius: calc(4 * var(--d));
  }

  .ws-review-card__stars img {
    width: calc(18 * var(--d));
    height: calc(18 * var(--d));
  }

  .ws-review-card__title {
    font-size: max(16px, calc(24 * var(--d)));
  }

  .ws-review-card__body {
    font-size: max(13px, calc(15 * var(--d)));
  }

  .ws-review-card__author {
    font-size: max(14px, calc(16 * var(--d)));
  }
  .ws-review-card__bar {
    width: calc(2 * var(--d));
    height: calc(24 * var(--d));
  }

  .ws-reviews__nav {
    gap: calc(12 * var(--d));
  }
}

/* ORC F5 adjustments: Mont Regular review titles, Figma card height, no visible scrollbar. */
.ws-review-card__title {
  font-weight: 400;
}
.ws-reviews__track {
  scrollbar-width: none;
}
.ws-reviews__track::-webkit-scrollbar {
  display: none;
}
@media (min-width: 1024px) {
  .ws-review-card {
    min-height: calc(286 * var(--d));
  }
}

/* ORC F5 adjustment: keep the first review card at its padding edge (Figma x=31), not scrolled to x=0. */
.ws-reviews__track {
  scroll-padding-inline-start: var(--ws-gutter);
}
@media (min-width: 1024px) {
  .ws-reviews__track {
    scroll-padding-inline-start: calc(31 * var(--d));
  }
}

/* ---- src/styles/components/service-notice.css ---- */
/*
 * ServiceNotice ("driftsmelding" popup) — T4.6 (docs/TASKS.md),
 * docs/DESIGN.md §2.1. Same dialog language as InterestModal
 * (src/styles/components/modal.css) but kept in its own file/classes per the
 * task brief's 1:1 component<->stylesheet mapping. Tokens only.
 */

.ws-service-notice {
  position: fixed;
  inset: 0;
  z-index: var(--ws-z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--ws-space-lg);
  animation: ws-notice-fade var(--ws-duration-slow) var(--ws-ease-soft);
}
.ws-service-notice__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(31, 29, 28, 0.5);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
/* White r=8 modal card — Task F4 (docs/FIGMA_SPEC.md §3), same treatment as
   src/styles/components/modal.css's InterestModal card. */
.ws-service-notice__card {
  position: relative;
  width: 100%;
  max-width: 27.5rem;
  border-radius: 8px;
  background: #FFFFFF;
  color: var(--ws-text);
  box-shadow: var(--ws-shadow-popover);
  overflow: hidden;
  animation: ws-notice-rise var(--ws-duration-slow) var(--ws-ease-soft);
}
:root[data-theme="dark"] .ws-service-notice__card {
  background: rgba(255, 255, 255, 0.8);
}
/* A fixed neutral wash (NOT --ws-bg, which flips near-black in dark mode
   and would hide the also-fixed-dark --ws-text icon against it) — reads as
   a soft grey chip against the white/80%-opacity card in both themes. */
.ws-service-notice__close {
  position: absolute;
  top: 0.875rem;
  right: 0.875rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border: none;
  border-radius: var(--ws-radius-pill);
  background: rgba(31, 29, 28, 0.06);
  color: var(--ws-text);
  cursor: pointer;
  transition: transform var(--ws-duration-fast) var(--ws-ease), background-color var(--ws-duration-fast) var(--ws-ease);
}
.ws-service-notice__close:hover {
  background: var(--ws-brand-pink);
  color: #FFFFFF;
  transform: scale(1.05);
}

.ws-service-notice__body {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-sm);
  padding: var(--ws-space-2xl) var(--ws-space-xl) var(--ws-space-lg);
}
/* Montserrat 300 — this task's brief: "titles Montserrat 300". */
.ws-service-notice__title {
  font-family: var(--ws-font-display);
  font-weight: 300;
  font-size: 1.75rem;
  line-height: 1.2;
  margin: 0;
  color: var(--ws-heading);
}
.ws-service-notice__message {
  margin: 0;
  color: var(--ws-text-muted);
  font-size: 0.9375rem;
  line-height: 1.6;
  white-space: pre-wrap;
}
.ws-service-notice__ok {
  margin-top: var(--ws-space-3xs);
}

@keyframes ws-notice-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes ws-notice-rise {
  from { opacity: 0; transform: translateY(1.125rem) scale(0.97); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@media (max-width: 520px) {
  .ws-service-notice {
    padding: var(--ws-space-sm);
    align-items: flex-end;
  }
  .ws-service-notice__card {
    max-width: 100%;
    border-radius: var(--ws-radius-md) var(--ws-radius-md) 0 0;
  }
  .ws-service-notice__body {
    padding: var(--ws-space-xl) var(--ws-space-lg) var(--ws-space-lg);
  }
  .ws-service-notice__title {
    font-size: 1.3125rem;
  }
}

/* ---- src/styles/components/shop.css ---- */
/*
 * Styles for src/pages/ShopPage.jsx (Butikk, /butikk) — Task F4
 * (docs/FIGMA_SPEC.md §4 "Сетка", docs/CONTENT.md "Butikk"). Tokens only
 * (src/styles/00-tokens.css); no legacy.css classes. Responsive checkpoints
 * per AGENT_RULES §3: 360-390 / 768 / 1024 / 1512.
 *
 * .ws-shop__grid reproduces the home Bestsellers grid's exact Figma geometry
 * (node bbox x=43..1469 (1426 wide), cards 456x403, columns at x=0/485/970 —
 * i.e. THREE fractional columns that resolve to exactly 456px each at the
 * 1512 design width, calc(29 * var(--d)) column gap / calc(23 * var(--d))
 * row gap) rather than sharing a class with bestsellers.css: F2 owns that
 * file concurrently and Bestsellers only ever shows 4 fixed products (3 +
 * 1 centered), while /butikk lists the whole catalog and needs an ordinary
 * wrapping grid. `margin-top: calc(47 * var(--d))` reserves the headroom
 * ProductCard's product photo needs to overflow each card's top edge by
 * 47px without colliding with the SubpageHero content above it — the photo
 * treatment itself is ProductCard's own CSS (F2, not touched here).
 */

.ws-shop {
  background: var(--ws-bg);
  padding-block: var(--ws-space-lg) var(--ws-space-3xl);
}

.ws-shop__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--ws-space-lg);
  margin-top: var(--ws-space-xl);
}

/* Reveal wrapper around each ProductCard (Task G3, docs/MOTION.md) — kept
   outside ProductCard itself (F2/G1 own that component) so the grid cell can
   opt into data-reveal without editing src/ui/ProductCard.jsx. Deliberately
   NOT display:flex/grid: a flex/grid container only stretches its
   children along the CROSS axis by default, so ProductCard (a plain
   block-level flex column, no width of its own — it's always relied on
   its immediate parent being the stretched grid cell) would shrink to its
   own min-content width instead of filling the column — confirmed live
   (the /butikk grid collapsed to hairline-width cards at 1024px before
   this fix). A plain block box has no such issue: block-level children
   fill 100% of their parent's content width by default. */
.ws-shop__card-reveal {
  display: block;
  min-width: 0;
}
@media (min-width: 560px) {
  .ws-shop__grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
/* Phase G (docs/MOTION.md §2 rule 5): the catalog has 4 plates, so 3 columns
   left "Vegansk" alone on a second row — 4 columns, same as the home
   Bestsellers grid (bestsellers.css). A future odd count still gets its last
   card centred instead of stuck at the left. */
@media (min-width: 560px) and (max-width: 1023px) {
  .ws-shop__grid > :nth-child(2n+1):last-child {
    grid-column: 1 / -1;
    justify-self: center;
    width: calc(50% - var(--ws-space-lg) / 2);
  }
}
@media (min-width: 1024px) {
  .ws-shop__grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: calc(23 * var(--d)) calc(29 * var(--d));
    margin-top: calc(47 * var(--d));
  }
}

/* ---- Box offers: peach r=30 cards, like Brands (src/styles/components/
   brands.css) — same fixed-both-themes white/peach chrome, since these are
   purchase-decision tiles that should read as siblings of the product grid
   above, not a themed chrome surface. ---- */

.ws-shop-boxes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--ws-space-lg);
  margin-top: var(--ws-space-2xl);
}

.ws-shop-box {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-md);
  padding: var(--ws-space-xl) var(--ws-space-lg) var(--ws-space-lg);
  background: var(--ws-btn-light-bg);
  border: 1.5px solid var(--ws-border-peach-fixed);
  border-radius: var(--ws-radius-card);
}

.ws-shop-box__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-bg);
  color: var(--ws-link-pink);
}

.ws-shop-box__body {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.ws-shop-box__title {
  margin: 0;
  font-family: var(--ws-font-display);
  font-weight: 400;
  font-size: 1.5rem;
  line-height: 1.2;
  color: var(--ws-product-name);
}

.ws-shop-box__price {
  margin: 0.125rem 0 0;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: 1.75rem;
  color: var(--ws-text);
}

.ws-shop-box__note {
  font-family: var(--ws-font-body);
  font-weight: 600;
  font-size: var(--ws-fs-small);
  color: var(--ws-link-pink);
}

.ws-shop-box .ws-btn {
  margin-top: auto;
}

@media (max-width: 640px) {
  .ws-shop-boxes {
    grid-template-columns: 1fr;
  }
}

/* ---- src/styles/components/sticky-buy.css ---- */
/*
 * MobileStickyBuy — T4.6 (docs/TASKS.md), docs/DESIGN.md §2.1. Bottom bar,
 * <=768px only (matches the breakpoint cart-drawer.css uses for the
 * full-screen sheet). Tokens only.
 */

/* "Sticky bar white with card button" — this task's brief: a fixed white
   chrome (both themes), like the modal/line-card surfaces elsewhere on the
   redesigned inner pages, rather than the generic theme-aware --ws-surface. */
.ws-sticky-buy {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--ws-z-sticky);
  align-items: center;
  justify-content: space-between;
  gap: var(--ws-space-sm);
  padding: var(--ws-space-sm) var(--ws-space-md);
  padding-bottom: max(var(--ws-space-sm), env(safe-area-inset-bottom));
  background: #FFFFFF;
  box-shadow: var(--ws-shadow-popover);
}
:root[data-theme="dark"] .ws-sticky-buy {
  /* Same white/80%-opacity language as every other Figma card on this page
     set (product-page.css, bundle.css, cart-drawer.css, modal.css) rather
     than a one-off dark bar — "white" stays legible/on-brand in both themes. */
  background: rgba(255, 255, 255, 0.85);
}

@media (max-width: 768px) {
  .ws-sticky-buy {
    display: flex;
  }
  /* The bar is position:fixed, so without this the last ~84px of the page
     (the footer's legal row) stay permanently hidden under it. The selector
     only matches while the bar is actually rendered — src/commerce/
     MobileStickyBuy.jsx returns null on an empty cart — same `:has()` scope
     pattern as about.css's footer override. */
  .site-shell:has(.ws-sticky-buy) .ws-footer {
    padding-bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px));
  }
}

.ws-sticky-buy__info {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ws-sticky-buy__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ws-text-muted);
  font-size: 0.75rem;
}
.ws-sticky-buy__price {
  font-family: var(--ws-font-display);
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--ws-heading);
  font-variant-numeric: tabular-nums;
}

.ws-sticky-buy .ws-btn {
  flex-shrink: 0;
}

/* ---- src/styles/components/subpage-hero.css ---- */
/*
 * Styles for src/sections/SubpageHero.jsx — Task F4 (docs/FIGMA_SPEC.md §3
 * typography table, §4 "Фон героя"). Shared top banner for EVERY inner page
 * (Butikk, product, Smaksboks, Levering, Betaling, Salgsbetingelser). Tokens
 * only (src/styles/00-tokens.css); no legacy.css classes/vars. Responsive
 * checkpoints per AGENT_RULES §3: 360-390 / 768 / 1024 / 1512.
 *
 * Title is Montserrat 300 (FIGMA_SPEC §3 lists every section/page title as
 * weight 300) scaled with the --d design unit at >=1024px, landing at
 * calc(64 * var(--d)) — the middle of this task's brief "60-72px" range and
 * visually one notch below the home Hero's own 90px H1 (FIGMA_SPEC "H1
 * героя" node), which is correct: SubpageHero is a page banner, not a hero.
 * Lead is Poppins 18/28 (FIGMA_SPEC's generic body-lead pairing), colour
 * --ws-text (#333, matches the brief's literal "#333/--ws-text" — they're
 * the same value).
 */

.ws-subpage-hero {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: var(--ws-bg);
  padding-block: calc(var(--ws-header-clearance) + clamp(0.5rem, 0.2rem + 1.5vw, 1.5rem)) clamp(1.75rem, 1.3rem + 1.8vw, 2.75rem);
}

/* Task F1 (docs/FIGMA_SPEC.md, decision D22): src/layout/Header.jsx is now
   absolutely positioned OVER the top of the page instead of pushing it down
   (no more sticky bar taking up flow height) — every page's first section
   is responsible for its own top clearance. Home's Hero starts at y=0 in
   Figma itself (the header overlays its own background artwork), but every
   OTHER page uses this shared SubpageHero, so it needs real top padding
   roughly matching the header's own height (176-206px desktop, see
   src/styles/components/header.css) so the H1 never renders under the
   logo/nav. <1024px the top padding above adds --ws-header-clearance (the
   mobile header's 111px height, src/styles/02-base.css). Bottom padding is
   deliberately compact (this task's brief: "no huge empty area") — a flat
   64px rather than scaling all the way up with viewport width. */
@media (min-width: 1024px) {
  .ws-subpage-hero {
    padding-top: calc(200 * var(--d));
    padding-bottom: calc(64 * var(--d));
  }
}

/* Real Figma export (assets/brand/figma/hero-mask-group-bg.svg, the same art
   the home Hero uses full-bleed) anchored top-right at a reduced scale —
   replaces the old hand-authored <Blob variant="about">. The artwork's own
   "клякса" detail already sits on the right side of its 1512-wide frame
   (FIGMA_SPEC §4), so simply shrinking the whole asset and flushing its
   right edge to the section's right edge reproduces that look without any
   cropping. Never over text: text column caps at 46rem/40rem (below) and
   this sits behind it (z-index 0) with pointer-events none.
   Task G3: positioning lives on the wrapper (.ws-depth, parallax) so the
   image itself is free to carry .ws-float's own `transform` keyframe
   without the two fighting over the same property. */
.ws-subpage-hero__bg-wrap {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 0;
  width: clamp(19rem, 14rem + 24vw, 40rem);
  pointer-events: none;
  user-select: none;
}
.ws-subpage-hero__bg {
  display: block;
  width: 100%;
  height: auto;
  opacity: 0.55;
}

.ws-subpage-hero__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  max-width: 46rem;
}
.ws-subpage-hero--center .ws-subpage-hero__inner {
  max-width: 40rem;
  margin-inline: auto;
  align-items: center;
  text-align: center;
}

.ws-subpage-hero__title {
  margin-top: var(--ws-space-sm);
  font-family: var(--ws-font-display);
  font-size: clamp(2.25rem, 1.65rem + 3vw, 4rem); /* <1024px fallback: ~36->64px */
  font-weight: 300;
  line-height: 1.08; /* matches SectionHeading's xl tier, FIGMA_SPEC §3 */
  color: var(--ws-heading);
}

.ws-subpage-hero__lead {
  margin-top: var(--ws-space-md);
  color: var(--ws-text);
  font-family: var(--ws-font-body);
  font-size: 1.125rem; /* 18px */
  line-height: 1.5556; /* 28/18 */
  max-width: 42ch;
}
/* --ws-text is a fixed dark grey in BOTH themes (src/styles/00-tokens.css —
   not a bug specific to this file, that token simply never got a dark
   override), so it reads as near-invisible dark-on-near-black wherever it
   sits directly on --ws-bg (which DOES flip dark) instead of one of this
   page set's white cards. Every such spot across this task's files gets the
   same scoped fix: a translucent cream in dark mode, echoing the cream this
   design system already uses for light-text-on-dark contexts (CTA band, the
   About drip hero — src/styles/00-tokens.css's --ws-cream/--ws-cta-band-text). */
:root[data-theme="dark"] .ws-subpage-hero__lead {
  color: rgba(255, 231, 228, 0.88);
}
.ws-subpage-hero--center .ws-subpage-hero__lead {
  max-width: 38ch;
}

/* Small pill note under the lead — e.g. BundlePage's "4 plater: N kr". */
.ws-subpage-hero__note {
  display: inline-flex;
  align-items: center;
  margin-top: var(--ws-space-md);
  padding: 0.625rem 1.125rem;
  border-radius: var(--ws-radius-pill);
  background: var(--ws-btn-light-bg);
  border: 1.5px solid var(--ws-border-peach-fixed);
  color: var(--ws-heading);
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: var(--ws-fs-small);
}

@media (min-width: 1024px) {
  .ws-subpage-hero__title {
    font-size: calc(64 * var(--d));
  }
}

@media (max-width: 520px) {
  .ws-subpage-hero__bg-wrap {
    width: 14rem;
  }
  .ws-subpage-hero__bg {
    opacity: 0.45;
  }
}

/* Entrance is per-child now (ws-enter-rise on eyebrow/title/lead in
   SubpageHero.jsx, docs/MOTION.md §3) instead of one whole-block animation. */

/* ---- src/styles/components/ui.css ---- */
/*
 * Styles for the primitives in src/ui/ — Logo, Icons, Button, Eyebrow,
 * SectionHeading, CircleButton, Stars, ThemeToggle, Blob. Class prefix `ws-`
 * throughout (AGENT_RULES §3 — CSS only via tokens). Loads after
 * src/styles/00-tokens.css/01-fonts.css/02-base.css (path sort — "00-"/
 * "01-"/"02-" precede "components/", see prerender/build.js).
 *
 * Button/SectionHeading/CircleButton were rebuilt in Task F1 to the exact
 * Figma variants (docs/FIGMA_SPEC.md §9/§8/§10). Every PRE-F1 variant name
 * (primary/accent/outline/ghost) still works — each is mapped to the new
 * variant with the closest Figma look (see the Button section below) rather
 * than renamed, so call sites in sections this task doesn't touch (Hero,
 * ProductCard, BoxPromo, ...) keep working unmodified, per this task's
 * constraints ("map to the closest look, don't redesign").
 */

/* ---- Logo ---- */

.ws-logo {
  display: inline-block;
  color: inherit;
}

/* ---- Icon ---- */

.ws-icon {
  display: inline-block;
  flex-shrink: 0;
}

/* ---- Button ---- */

.ws-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ws-space-2xs);
  border: none;
  border-radius: var(--ws-radius-sm);
  font-family: var(--ws-font-display);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  /* Phase G (docs/MOTION.md §2/§4): hover/active move via the `translate`/
     `scale` individual transform properties, never the shorthand `transform`
     — so a Button that ends up inside a data-tilt/data-reveal wrapper later
     (both of which animate `transform`) never fights this rule. */
  translate: 0 0;
  scale: 1;
  transition:
    background-color var(--ws-duration-fast) var(--ws-ease),
    color var(--ws-duration-fast) var(--ws-ease),
    translate var(--ws-duration-fast) var(--ws-ease),
    scale var(--ws-duration-fast) var(--ws-ease),
    box-shadow var(--ws-duration-fast) var(--ws-ease),
    opacity var(--ws-duration-fast) var(--ws-ease),
    filter var(--ws-duration-fast) var(--ws-ease);
}

.ws-btn--lg {
  min-height: 4rem;
  padding: 0 var(--ws-space-lg);
  font-size: var(--ws-fs-button);
}
.ws-btn--md {
  min-height: 3.25rem;
  padding: 0 var(--ws-space-md);
  font-size: 1.0625rem;
}
.ws-btn--sm {
  min-height: 2.5rem;
  padding: 0 var(--ws-space-sm);
  font-size: var(--ws-fs-small);
}
.ws-btn--full {
  width: 100%;
}

/* ---- legacy alias: primary -> card look (FIGMA_SPEC §9 "card": bg #F15AB4,
   white bold text, r=10) — keeps its OWN sm/md/lg box model (Hero's lg CTA,
   modal/sticky-buy full-width buttons, ...) rather than card's fixed
   404x86/full-width-70 box, since those callers aren't the bestsellers card
   this look was designed for. See src/ui/Button.jsx callers. ---- */
.ws-btn--primary,
.ws-btn--card {
  background: var(--ws-btn-primary-bg);
  color: var(--ws-btn-primary-text);
  font-weight: 700;
}
.ws-btn--primary:hover,
.ws-btn--card:hover {
  filter: brightness(0.94);
}

/* ---- legacy alias: accent -> promo look (FIGMA_SPEC §9 "promo": bg
   #FF862F, white text, promo shadow) — keeps its own sm/md/lg box model. ---- */
.ws-btn--accent,
.ws-btn--promo {
  background: var(--ws-btn-accent-bg);
  color: var(--ws-btn-accent-text);
  box-shadow: var(--ws-shadow-btn-promo);
}
.ws-btn--accent:hover,
.ws-btn--promo:hover {
  filter: brightness(0.96);
}

.ws-btn--light {
  background: var(--ws-btn-light-bg);
  color: var(--ws-btn-light-text);
  font-family: var(--ws-font-body);
  font-weight: 400;
  font-size: 1rem;
  height: 44px;
  min-height: 44px;
  padding-inline: 1.5rem;
  border-radius: var(--ws-radius-btn-light);
}
.ws-btn--light:hover {
  filter: brightness(0.96);
}

.ws-btn--dark {
  background: var(--ws-btn-dark-bg);
  color: var(--ws-btn-dark-text);
  font-family: var(--ws-font-body);
  font-weight: 400;
  font-size: 1rem;
  height: 44px;
  min-height: 44px;
  padding-inline: 1.5rem;
  border-radius: var(--ws-radius-sm);
}
.ws-btn--dark:hover {
  filter: brightness(1.15);
}

/* ---- hero (FIGMA_SPEC §9: 404x86, r=10, Montserrat 400 35px, cream text,
   node 1:29/1:30) — Hero.jsx's own task hasn't switched to it yet (still
   "primary"), ready for that task to just flip the variant name. ---- */
.ws-btn--hero {
  background: var(--ws-btn-primary-bg);
  color: var(--ws-cream);
  font-weight: 400;
  font-size: clamp(1.125rem, 0.9rem + 1vw, 2.1875rem);
  min-height: clamp(3rem, 2.5rem + 2vw, 5.375rem);
  padding-inline: var(--ws-space-lg);
  box-shadow: var(--ws-shadow-btn-hero);
}
.ws-btn--hero:hover {
  filter: brightness(0.96);
}

/* Phase G (docs/MOTION.md §4 "Кнопки"): hover lifts, active settles back
   with a scale-down instead of the old translateY(1px) press. `:not(:disabled)`
   keeps a disabled/aria-disabled button inert (no lift, no nudge). */
.ws-btn:hover:not(:disabled):not([aria-disabled="true"]) {
  translate: 0 -2px;
  box-shadow: 0 10px 22px rgba(20, 16, 15, 0.18);
}
.ws-btn:active:not(:disabled):not([aria-disabled="true"]) {
  translate: 0 0;
  scale: 0.98;
}
.ws-btn:disabled,
.ws-btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  box-shadow: none;
  translate: 0 0;
  scale: 1;
  pointer-events: none;
}

.ws-btn__icon {
  display: inline-flex;
}

@media (min-width: 1024px) {
  .ws-btn--hero {
    width: calc(404 * var(--d));
    height: calc(86 * var(--d));
    min-height: 0;
    font-size: calc(35 * var(--d));
    padding-inline: 0;
  }
  .ws-btn--card {
    width: 100%;
    height: calc(70 * var(--d));
    min-height: 0;
    font-size: calc(30 * var(--d));
  }
  .ws-btn--promo {
    width: calc(478 * var(--d));
    height: calc(101 * var(--d));
    min-height: 0;
    font-size: calc(40 * var(--d));
    padding-inline: 0;
  }
}

/* ---- Eyebrow ---- */

.ws-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--ws-space-2xs);
  font-family: var(--ws-font-display);
  font-weight: 700;
  font-size: var(--ws-fs-small);
  line-height: 1.4; /* 19.6/14 */
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--ws-eyebrow);
  margin: 0;
}

/* ---- SectionHeading ---- */

.ws-section-heading {
  display: flex;
  flex-direction: column;
  gap: var(--ws-space-2xs);
}
.ws-section-heading--center {
  align-items: center;
  text-align: center;
}
.ws-section-heading--left {
  align-items: flex-start;
  text-align: left;
}
.ws-section-heading__title {
  font-family: var(--ws-font-display);
  font-weight: 300; /* Figma "Mont Light" for every section title */
  margin: 0;
}
.ws-section-heading--purple .ws-section-heading__title {
  color: var(--ws-heading);
}
.ws-section-heading--violet .ws-section-heading__title {
  color: var(--ws-heading-violet);
}

/* size="xl" — Bestsellers (FIGMA_SPEC node 1:178/1:179): 40/43.2px. Default
   size, so today's existing SectionHeading callers (Bestsellers, CTA
   pre-F1, FAQ, ProductPage "Flere smaker") render at this size unless they
   explicitly opt into "lg". */
.ws-section-heading--xl .ws-section-heading__title {
  font-size: var(--ws-fs-section-title); /* <1024px fallback: clamp 28->40px */
  line-height: 1.08; /* 43.2/40 */
}
/* size="lg" — Brands/Feedback/CTA (FIGMA_SPEC nodes 1:236/1:175/1:284): 36/43.2px. */
.ws-section-heading--lg .ws-section-heading__title {
  font-size: clamp(1.625rem, 1.38rem + 1vw, 2.25rem); /* <1024px fallback: 26->36px */
  line-height: 1.2; /* 43.2/36 */
}

@media (min-width: 1024px) {
  .ws-section-heading--xl .ws-section-heading__title {
    font-size: calc(40 * var(--d));
  }
  .ws-section-heading--lg .ws-section-heading__title {
    font-size: calc(36 * var(--d));
  }
}

/* ---- CircleButton ---- */

.ws-circle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border-radius: var(--ws-radius-pill);
  cursor: pointer;
  /* Phase G (docs/MOTION.md §4): `translate`, not `transform` — this button
     is also the one Bestsellers wiggles decoratively (its own `transform`
     keyframe, out of this task's files), so the hover/active nudge here must
     stay on a separate property to compose instead of overwrite it. */
  translate: 0 0;
  transition:
    translate var(--ws-duration-fast) var(--ws-ease),
    background-color var(--ws-duration-fast) var(--ws-ease),
    box-shadow var(--ws-duration-fast) var(--ws-ease);
}
/* variant="purple" — FIGMA_SPEC §10 (node 1:69 "Group 28"): the exported SVG
   already contains the circle, arrow and drop shadow inside a 183x183
   canvas (127x127 visible circle + shadow padding), so the button itself is
   just a transparent, unstyled hit area sized to match. */
.ws-circle-btn--purple {
  width: 11.4375rem; /* 183px */
  height: 11.4375rem;
  background: transparent;
}
.ws-circle-btn__art {
  display: block;
  width: 100%;
  height: 100%;
}

/* variant="outline" — FIGMA_SPEC §10 (node 1:171/1:172): 60x60, 2px orange
   ring, arrow1.svg rotated per `direction`. */
.ws-circle-btn--outline {
  width: 3.75rem; /* 60px */
  height: 3.75rem;
  background: var(--ws-surface);
  border: 2px solid var(--ws-brand-orange);
}
.ws-circle-btn__arrow {
  display: block;
  width: 45%;
  height: auto;
}
.ws-circle-btn__arrow--left {
  transform: rotate(180deg);
}
.ws-circle-btn__arrow--down {
  transform: rotate(90deg);
}

.ws-circle-btn:hover:not(:disabled) {
  translate: 0 -2px;
}
.ws-circle-btn:active:not(:disabled) {
  translate: 0 0;
}
/* T4.4: reviews carousel prev/next at the ends (src/ui/CircleButton.jsx's
   `disabled` prop, src/sections/ReviewsSection.jsx). */
.ws-circle-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
  pointer-events: none;
}

/* ---- Stars ---- */

.ws-stars {
  display: inline-flex;
  align-items: center;
  gap: var(--ws-space-2xs);
  color: var(--ws-brand-orange);
  font-family: var(--ws-font-body);
}
.ws-stars__icons {
  display: inline-flex;
  gap: 2px;
}
.ws-stars__count {
  color: var(--ws-text-muted);
  font-size: var(--ws-fs-small);
}

/* ---- ThemeToggle ---- */

.ws-theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--ws-radius-pill);
  border: 1.5px solid var(--ws-surface-border);
  background: var(--ws-surface);
  color: var(--ws-text);
  cursor: pointer;
  transition:
    background-color var(--ws-duration-fast) var(--ws-ease),
    transform var(--ws-duration-base) var(--ws-ease),
    color var(--ws-duration-fast) var(--ws-ease);
}
.ws-theme-toggle:hover {
  transform: rotate(12deg);
}
/* --ws-text has no dark override, so the chip's icon (mobile menu footer)
   vanished on the dark surface. The header's own toggle keeps its ink colour. */
:root[data-theme="dark"] .ws-theme-toggle:not(.ws-header__icon-btn) {
  color: var(--ws-cream);
}

/* ---- Blob ---- */

.ws-blob {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
}
/* Figma marks the hero/reviews blobs semi-transparent; promo/about read as
   flatter, more opaque shapes — docs/DESIGN.md §2. */
.ws-blob--hero {
  opacity: 0.55;
}

/* ORC F5 adjustments (Figma): eyebrow sits directly on the title (≈0–2px gap);
   hero/promo buttons use Mont Regular labels. */
@media (min-width: 1024px) {
  .ws-section-heading {
    gap: 0;
  }
}
.ws-btn--hero,
.ws-btn--promo {
  font-weight: 400;
}

