/*
Theme Name: Lyli Shop
Theme URI: https://lylishop.online
Author: lylishop developer
Author URI: https://github.com/ducanh8888/lylishop_v2
Description: Child theme V1 storefront for Lyli Shop (Botiga Free parent). Brand identity, typography, color, spacing, product archive/single presentation, Classic Cart/Checkout styling, responsive/accessibility. Presentation only — no order, payment, voucher or business logic (PLAN.md §6.1).
Version: 1.12.0
Requires at least: 7.0.2
Tested up to: 7.0
Requires PHP: 8.3
Template: botiga
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: shop-child
Tags: e-commerce, custom-menu, custom-logo, grid-layout, featured-images, right-sidebar, custom-colors, translation-ready

Official six-color palette is canonical in theme.json — docs/THEME-DECISION.md §11.
Text, muted, border and WooCommerce state colors are functional/accessibility neutrals.
*/

/* ==========================================================================
   1. Design tokens (CSS custom properties)
   ========================================================================== */

:root {
    --lyli-color-primary: var(--wp--preset--color--lyli-primary);
    --lyli-color-warm-white: var(--wp--preset--color--lyli-warm-white);
    --lyli-color-cream: var(--wp--preset--color--lyli-cream);
    --lyli-color-blush: var(--wp--preset--color--lyli-blush);
    --lyli-color-sage: var(--wp--preset--color--lyli-sage);
    --lyli-color-lavender: var(--wp--preset--color--lyli-lavender);
    --lyli-color-text: var(--wp--preset--color--lyli-text);
    --lyli-color-muted: var(--wp--preset--color--lyli-text-muted);
    --lyli-color-border: var(--wp--preset--color--lyli-border);
    --lyli-color-action-hover: var(--wp--custom--lyli--color--action-hover);
    --lyli-color-surface: var(--lyli-color-warm-white);
    --lyli-color-surface-soft: var(--lyli-color-cream);
    --lyli-color-canvas: var(--lyli-color-warm-white);
    --lyli-font-heading: var(--wp--preset--font-family--lyli-heading);
    --lyli-font-body: var(--wp--preset--font-family--lyli-body);
    --lyli-radius-sm: 4px;
    --lyli-radius-md: 8px;
    --lyli-radius-lg: 14px;
    --lyli-shadow-card: 0 1px 3px rgba(45, 42, 38, 0.08), 0 4px 14px rgba(45, 42, 38, 0.05);
    --lyli-shadow-card-hover: 0 4px 12px rgba(45, 42, 38, 0.12), 0 10px 28px rgba(45, 42, 38, 0.10);
    --lyli-motion-fast: 120ms;
    --lyli-motion-base: 200ms;
    --lyli-motion-slow: 320ms;
    /* Section tier (450-750ms): hero entrance, section reveal. Formalizes a
       duration hero-entrance and reveal.js's CSS already used as a magic
       number (600ms), so nothing outside the token system floats free. */
    --lyli-motion-section: 600ms;
    --lyli-ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1);
    --lyli-ease-out: cubic-bezier(0.32, 0.08, 0.24, 1);
    --lyli-lift-sm: -1px;
    --lyli-lift-md: -2px;
    --lyli-transition: var(--lyli-motion-base) var(--lyli-ease-standard);
    --lyli-content-width: 1140px;
    --lyli-gutter: clamp(14px, 4vw, 40px);
}

/* Base integration and accessibility. Layout remains owned by Botiga. */
button,
input,
select,
textarea {
    font-family: var(--lyli-font-body);
}

.lyli-announcement {
    padding: 8px 16px;
    background: var(--lyli-color-primary);
    color: var(--lyli-color-surface);
    text-align: center;
    font-size: 0.9rem;
}

.lyli-announcement p {
    margin: 0;
}

input[type="text"],
input[type="email"],
input[type="url"],
input[type="tel"],
input[type="number"],
input[type="password"],
select,
textarea {
    border-color: var(--lyli-color-border);
    border-radius: var(--lyli-radius-sm);
}

:focus-visible {
    outline: 3px solid var(--lyli-color-primary);
    outline-offset: 2px;
}

.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100000;
    padding: 8px 16px;
    background: var(--lyli-color-primary);
    color: var(--lyli-color-surface);
}

.skip-link:focus {
    left: 8px;
    top: 8px;
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/*
 * The blanket rule above only compresses duration to near-zero; it does not
 * neutralize the end state, so a reduced-motion user would still see these
 * transforms jump instantly instead of not moving at all. Target only the
 * decorative transforms (never :active, focus, or state-carrying rules).
 */
@media (prefers-reduced-motion: reduce) {
    .wp-block-button__link:hover,
    .woocommerce a.button:hover,
    .woocommerce button.button:hover,
    .woocommerce input.button:hover,
    .wc-block-components-button:hover,
    .lyli-category-card:hover,
    .woocommerce ul.products li.product:hover img {
        transform: none;
    }
}

/* ===========================================================================
   Product-ready V1 visual system
   ========================================================================== */

body {
    background: var(--lyli-color-canvas);
    font-size: 16px;
    -webkit-font-smoothing: antialiased;
}

a { color: var(--lyli-color-primary); transition: color var(--lyli-transition); }
a:hover { color: var(--lyli-color-action-hover); text-decoration-thickness: 2px; }

.container,
.site-main,
.entry-content {
    min-width: 0;
}

img,
svg,
video,
iframe,
table,
form {
    max-width: 100%;
}

.content-wrapper,
.site-main > .page,
.site-main > article {
    margin-top: clamp(28px, 5vw, 68px);
    margin-bottom: clamp(56px, 8vw, 108px);
}

.home .site-main > .page,
.home .site-main > article,
.home .content-wrapper {
    margin-top: 0;
}

.home .entry-header,
.home .page-header {
    display: none;
}

.home .entry-content {
    max-width: none;
    margin: 0;
}

/* Header */
.bhfb-header {
    position: sticky;
    top: 0;
    z-index: 999;
    background: color-mix(in srgb, var(--lyli-color-warm-white) 96%, transparent);
    border-bottom: 1px solid color-mix(in srgb, var(--lyli-color-primary) 12%, transparent);
    box-shadow: 0 8px 28px rgba(45, 42, 38, 0.045);
}

.admin-bar .bhfb-header { top: 32px; }
.bhfb-header .bhfb-row { min-height: 64px; }
.bhfb-header .container { max-width: 1240px; }
.bhfb-header .site-title { margin: 0; font-size: clamp(1.55rem, 2.3vw, 2rem); }
.bhfb-header .site-title a { color: var(--lyli-color-primary); text-decoration: none; }
.bhfb-header .site-description { display: none; }
.bhfb-header .custom-logo { max-height: 54px; width: auto; }
.bhfb-header .main-navigation ul { gap: clamp(14px, 2.2vw, 30px); }
.bhfb-header .main-navigation a {
    font-size: 0.92rem;
    font-weight: 500;
    letter-spacing: 0.01em;
    padding-block: 12px;
    /*
     * Botiga's generated color rules for this link never declare a
     * transition, so duration otherwise falls back to whichever `a`
     * base rule wins the cascade tie. Declare it explicitly here instead
     * of depending on stylesheet load order.
     */
    transition: color var(--lyli-transition);
}
/*
 * The current-page indicator must not depend on hover: it is the only
 * thing that tells a keyboard or touch user which nav item they are on.
 * wp_nav_menu() adds both the class and the aria attribute by default.
 */
.bhfb-header .main-navigation .current-menu-item > a.botiga-dropdown-link,
.bhfb-header .main-navigation a[aria-current="page"] {
    color: var(--lyli-color-primary);
}
/*
 * Browser-verified: this menu's items are custom links, several of which
 * point at in-page anchors on the homepage (#categories, #about, #contact).
 * WordPress marks every menu item whose URL resolves to the current page
 * as current — including anchor-only links — so on the homepage 4 of 6
 * nav items lit up as "active" simultaneously (getComputedStyle confirmed
 * all four resolved to the same color). Only a link to a distinct page
 * (or the bare homepage link itself) should carry the current-page color;
 * exclude anything whose href contains a fragment.
 */
.bhfb-header .main-navigation .current-menu-item > a.botiga-dropdown-link[href*="#"],
.bhfb-header .main-navigation a.botiga-dropdown-link[aria-current="page"][href*="#"] {
    color: var(--lyli-color-text);
}

.mobile-header .row,
.mobile-header .header-elements,
.mobile-header .site-branding {
    min-width: 0;
}

.mobile-header .header-elements {
    gap: 4px;
}

.mobile-header .site-branding {
    overflow: hidden;
}

.mobile-header .site-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mobile-header .header-item,
.mobile-header .menu-toggle {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

@media (min-width: 1025px) {
    .bhfb-header .bhfb-main_header_row { min-height: 76px; }
    .bhfb-header .bhfb-below_header_row {
        min-height: 48px;
        border-top: 1px solid color-mix(in srgb, var(--lyli-color-primary) 10%, transparent);
    }
    .bhfb-header .bhfb-below_header_row .bhfb-row { min-height: 48px; }
    .bhfb-header .bhfb-below_header_row .bhfb-column-1 { justify-content: center; }
    .bhfb-header .bhfb-below_header_row .main-navigation ul {
        flex-wrap: nowrap;
        justify-content: center;
        gap: clamp(18px, 2vw, 28px);
    }
    .bhfb-header .bhfb-below_header_row .main-navigation a {
        padding-block: 10px;
        white-space: nowrap;
    }
}
.bhfb-header .header-item,
.bhfb-header .menu-toggle {
    color: var(--lyli-color-text);
    border-radius: 999px;
    transition: color var(--lyli-transition), background var(--lyli-transition);
}
@media (hover: hover) and (pointer: fine) {
    .bhfb-header .header-item:hover,
    .bhfb-header .menu-toggle:hover { color: var(--lyli-color-primary); background: var(--lyli-color-surface-soft); }
}
/*
 * assets/js/cart-badge.js adds this class only on WooCommerce's own
 * added_to_cart event — never on a plain page load — so it only pulses
 * when the cart count actually changes.
 */
@keyframes lyli-cart-pulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.25); }
}
.cart-count.lyli-cart-pulse { animation: lyli-cart-pulse var(--lyli-motion-slow) var(--lyli-ease-out); }

/* Global section rhythm */
.lyli-pattern {
    box-sizing: border-box;
    width: min(calc(100% - (var(--lyli-gutter) * 2)), var(--lyli-content-width));
    max-width: 100%;
    margin: clamp(36px, 8vw, 118px) auto;
}

.lyli-pattern:first-child { margin-top: clamp(22px, 4vw, 56px); }
.lyli-section-heading { max-width: 700px; margin: 0 auto clamp(22px, 4vw, 52px); }
.lyli-section-heading h2 { font-size: clamp(1.75rem, calc(1.4rem + 1.5vw), 3.25rem); margin-bottom: 10px; }
.lyli-section-heading p { max-width: 52ch; margin-inline: auto; color: var(--lyli-color-muted); font-size: clamp(0.95rem, calc(0.9rem + 0.25vw), 1.05rem); }
.lyli-eyebrow {
    color: var(--lyli-color-primary);
    font-size: 0.76rem;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

/* Buttons */
.wp-block-button__link,
.woocommerce a.button,
.woocommerce button.button,
.woocommerce input.button,
.wc-block-components-button {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 12px 24px;
    border: 1px solid var(--lyli-color-primary);
    border-radius: 999px;
    background: var(--lyli-color-primary);
    color: var(--lyli-color-warm-white);
    text-decoration: none;
    font-weight: 500;
    line-height: 1.2;
    box-shadow: 0 8px 18px rgba(122, 59, 23, 0);
    transition: transform var(--lyli-transition), background var(--lyli-transition), border-color var(--lyli-transition), box-shadow var(--lyli-transition);
}
.wp-block-button__link:hover,
.woocommerce a.button:hover,
.woocommerce button.button:hover,
.woocommerce input.button:hover,
.wc-block-components-button:hover,
.wp-block-button__link:active,
.woocommerce a.button:active,
.woocommerce button.button:active,
.woocommerce input.button:active,
.wc-block-components-button:active {
    background: var(--lyli-color-action-hover);
    border-color: var(--lyli-color-action-hover);
    color: var(--lyli-color-warm-white);
}
@media (hover: hover) and (pointer: fine) {
    .wp-block-button__link:hover,
    .woocommerce a.button:hover,
    .woocommerce button.button:hover,
    .woocommerce input.button:hover,
    .wc-block-components-button:hover {
        transform: translateY(var(--lyli-lift-sm));
        box-shadow: 0 8px 18px rgba(122, 59, 23, 0.16);
    }
}
/*
 * :active must give real feedback on its own — touch devices fire :active
 * without ever matching :hover, so reversing the hover lift back to 0 would
 * be invisible there. Press slightly past the resting position instead, on
 * every pointer type, and speed the transition up so it reads as immediate.
 */
.wp-block-button__link:active,
.woocommerce a.button:active,
.woocommerce button.button:active,
.woocommerce input.button:active,
.wc-block-components-button:active {
    transform: translateY(1px);
    transition-duration: var(--lyli-motion-fast);
}
.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-background):not(.has-text-color) {
    background: transparent;
    color: var(--lyli-color-primary);
    border-color: var(--lyli-color-primary);
}
.wp-block-button.is-style-outline > .wp-block-button__link:hover:not(.has-background):not(.has-text-color),
.wp-block-button.is-style-outline > .wp-block-button__link:active:not(.has-background):not(.has-text-color) {
    background: var(--lyli-color-cream);
    color: var(--lyli-color-primary);
    border-color: var(--lyli-color-primary);
}
/*
 * Motion Language v1, item 2 (arrow-nudge). Scoped to outline buttons only
 * — the solid/primary button already carries lift + shadow feedback, and
 * stacking a third moving element on top would be busier, not clearer.
 * Outline buttons currently only get a background swap on hover, so the
 * nudge adds affordance without competing with anything else.
 */
.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-background):not(.has-text-color)::after {
    content: "→";
    display: inline-block;
    margin-left: 8px;
    transition: transform var(--lyli-motion-base) var(--lyli-ease-standard);
}
@media (hover: hover) and (pointer: fine) {
    .wp-block-button.is-style-outline > .wp-block-button__link:hover:not(.has-background):not(.has-text-color)::after {
        transform: translateX(4px);
    }
}

/* Hero */
.lyli-hero {
    position: relative;
    overflow: hidden;
    padding: clamp(20px, 4vw, 54px);
    border: 1px solid color-mix(in srgb, var(--lyli-color-primary) 12%, transparent);
    border-radius: 28px;
    background: linear-gradient(135deg, var(--lyli-color-warm-white) 0%, var(--lyli-color-cream) 72%, var(--lyli-color-blush) 100%);
    box-shadow: 0 20px 60px rgba(73, 42, 25, 0.07);
}
.lyli-hero::before {
    content: "";
    position: absolute;
    width: 320px;
    height: 320px;
    left: -190px;
    top: -190px;
    border: 46px solid color-mix(in srgb, var(--lyli-color-blush) 78%, transparent);
    border-radius: 50%;
}
/*
 * Browser-measured: this block sits directly inside .lyli-hero, which
 * carries Gutenberg's `is-layout-constrained` class. Two WordPress-core
 * inline rules (block-supports "layout" feature, printed in <head>, not
 * part of this theme) were winning over the plain `.lyli-hero-grid`
 * selector: `body .is-layout-flex { display: flex }` (specificity beats a
 * single class) forced flex instead of grid — meaning the columns fell
 * back to their own inline `style="flex-basis:56%/44%"` — and
 * `.is-layout-constrained > :where(...) { max-width: var(--wp--style--
 * global--content-size) }` capped this block at theme.json's 730px
 * *content* width instead of the 1140px *wide* width .lyli-hero itself
 * gets, making the whole hero text column much narrower than it looked
 * (368px measured at a 1920px viewport) and forcing the H1 to wrap across
 * up to 7 lines. `.lyli-hero .lyli-hero-grid` (two classes) reliably
 * outweighs both — a single class plus a `:where()` (always zero
 * specificity) or a single class plus one type selector.
 */
.lyli-hero .lyli-hero-grid {
    display: grid;
    max-width: none;
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(22px, 5vw, 72px);
}
.lyli-hero-grid > .wp-block-column { min-width: 0; }
/*
 * Reported and browser-confirmed: at wide desktop viewports (content is
 * still capped at --lyli-content-width regardless of screen width, so
 * this isn't about screen size directly) the 15ch cap was narrow enough,
 * relative to the clamped font-size reaching 3.75rem, to wrap the
 * headline across as many as 6 short lines — making the whole hero block
 * unusually tall. Widen the cap so the same text settles into fewer,
 * more natural line breaks.
 */
.lyli-hero h1 { font-size: clamp(1.9rem, calc(1.4rem + 2.4vw), 3.75rem); max-width: 20ch; margin: 10px 0 16px; word-break: normal; overflow-wrap: normal; }
.lyli-hero-copy { max-width: 45ch; color: var(--lyli-color-muted); margin-bottom: 22px; }
/*
 * Load-triggered entrance for the hero text only — never the hero image.
 * The image is a likely LCP (Largest Contentful Paint) candidate
 * (fetchpriority="high" in the markup); fading it in would push back the
 * moment it's painted opaque, working against the Core Web Vitals
 * constraint. Pure CSS, no JS: the existing blanket
 * `prefers-reduced-motion` rule already collapses animation-duration to
 * near-zero, so reduced-motion users see the end state almost immediately
 * — exactly the "show content immediately" behavior required, with no
 * extra rule needed here.
 */
@keyframes lyli-fade-up {
    from { opacity: 0; transform: translateY(14px); }
    to { opacity: 1; transform: translateY(0); }
}
/* Eyebrow stays a deliberate 500ms (slightly faster, first to animate) rather than the 600ms section token — an intentional stagger variation, not a stray magic number. */
.lyli-hero .lyli-eyebrow { animation: lyli-fade-up 500ms var(--lyli-ease-out) both; }
.lyli-hero h1 { animation: lyli-fade-up var(--lyli-motion-section) var(--lyli-ease-out) 80ms both; }
.lyli-hero-copy { animation: lyli-fade-up var(--lyli-motion-section) var(--lyli-ease-out) 160ms both; }
.lyli-hero .wp-block-buttons { animation: lyli-fade-up var(--lyli-motion-section) var(--lyli-ease-out) 160ms both; }
.lyli-hero-visual {
    position: relative;
    overflow: hidden;
    min-height: clamp(240px, 68vw, 280px) !important;
    background: var(--lyli-color-lavender);
    border-radius: 22px;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--lyli-color-primary) 10%, transparent), 0 16px 45px rgba(73, 42, 25, 0.12);
}
/*
 * Scoped to .wp-block-cover only: these rings decorate the abstract
 * placeholder background of the original cover-block hero. Once an owner
 * replaces it with a real photo (wp:image, class wp-block-image instead of
 * wp-block-cover), the rings must not carry over — they would paint on top
 * of the photo instead of behind an empty color field.
 */
.lyli-hero-visual.wp-block-cover::before,
.lyli-hero-visual.wp-block-cover::after {
    content: "";
    position: absolute;
    border-radius: 50%;
    border: 28px solid color-mix(in srgb, var(--lyli-color-primary) 18%, transparent);
}
.lyli-hero-visual.wp-block-cover::before { width: 240px; height: 240px; top: 15%; right: -65px; }
.lyli-hero-visual.wp-block-cover::after { width: 145px; height: 145px; bottom: 8%; left: -35px; border-color: color-mix(in srgb, var(--lyli-color-warm-white) 48%, transparent); }
.lyli-hero-visual .wp-block-cover__inner-container { position: relative; z-index: 2; max-width: 25ch; }
.lyli-yarn-mark { font-family: var(--lyli-font-heading); color: var(--lyli-color-primary); font-size: clamp(3rem, 7vw, 5.5rem); font-weight: 600; letter-spacing: 0.05em; margin: 0; }

/*
 * Editorial content remains native Gutenberg content editable in WP Admin.
 */
.lyli-story-image img,
.lyli-source-gallery img {
    width: 100%;
    object-fit: cover;
}
.lyli-story-image img { aspect-ratio: 4 / 3; border-radius: 20px; }
/*
 * .lyli-hero-visual img is handled separately from the other two: its
 * container carries an intentional min-height (originally sized for the
 * text-only cover-block placeholder, independent of any image), and that
 * min-height can be — and on live production, is — taller than what the
 * image's own 5:4 aspect-ratio would produce at the column's width. A
 * plain `width:100%` + `aspect-ratio` image just renders shorter than the
 * container in that case, exposing the lavender background below it
 * (screenshot-verified: 520px container vs 231px image). Absolutely
 * positioning the image to fill the container's padding box (inset: 0)
 * sidesteps the percentage-height-vs-min-height restriction entirely —
 * unlike a plain in-flow `height: 100%`, an absolutely positioned box with
 * both top and bottom set resolves against the containing block's actual
 * final height, whatever produced it (min-height included).
 */
.lyli-hero-visual img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.lyli-source-gallery { gap: clamp(10px, 2vw, 20px); }
.lyli-source-gallery .wp-block-image { overflow: hidden; border-radius: 14px; }
.lyli-source-gallery img { aspect-ratio: 1; }
.lyli-editorial-faq .wp-block-details {
    margin-block: 12px;
    padding: 18px 20px;
    border: 1px solid var(--lyli-color-border);
    border-radius: 12px;
    background: var(--lyli-color-warm-white);
}
/*
 * CSS-only disclosure indicator. Native <details>/<summary> already handles
 * Enter/Space, screen-reader expanded/collapsed state and the open/closed
 * toggle; this only replaces the browser's default triangle with a chevron
 * and widens the touch target. No JS, no panel-height animation — only the
 * chevron rotates, which is exactly what web.dev/CSS current practice
 * supports without an experimental Chromium-only calc-size()/interpolate-size
 * dependency for animating an intrinsic (auto) height.
 */
.lyli-editorial-faq summary {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding-block: 10px;
    margin-block: -10px;
    cursor: pointer;
    font-weight: 500;
    list-style: none;
    transition: color var(--lyli-transition);
}
.lyli-editorial-faq summary::-webkit-details-marker { display: none; }
.lyli-editorial-faq summary::after {
    content: "";
    flex: none;
    width: 8px;
    height: 8px;
    border: solid currentColor;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
    transition: transform var(--lyli-motion-slow) var(--lyli-ease-out);
}
.lyli-editorial-faq details[open] summary::after { transform: rotate(-135deg); }
.lyli-editorial-faq summary:hover { color: var(--lyli-color-action-hover); }
.lyli-editorial-contact { padding: clamp(28px, 5vw, 58px); border-radius: 24px; background: var(--lyli-color-blush); }

/*
 * Core Gutenberg `wp:latest-posts` block, added directly by the shop owner
 * (not one of the 8 controlled patterns). wp-block-library core CSS is not
 * enqueued on the frontend, so this block had zero styling — its declared
 * 150x150 thumbnail fell back to the theme's generic `img{max-width:100%}`
 * and rendered at nearly the full section width. Style it in the same card
 * language already used for the blog archive (.posts-archive.layout3)
 * below, instead of inventing a new visual pattern.
 */
.lyli-editorial-news .wp-block-latest-posts {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 20px;
    margin: 0;
    padding: 0;
    list-style: none;
}
.lyli-editorial-news .wp-block-latest-posts li {
    padding: 18px;
    border: 1px solid var(--lyli-color-border);
    border-radius: 18px;
    background: var(--lyli-color-warm-white);
    box-shadow: var(--lyli-shadow-card);
}
.lyli-editorial-news .wp-block-latest-posts__featured-image { float: none; margin: 0 0 14px; overflow: hidden; border-radius: 12px; }
.lyli-editorial-news .wp-block-latest-posts__featured-image img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    transition: transform var(--lyli-motion-slow) var(--lyli-ease-out);
}
@media (hover: hover) and (pointer: fine) {
    .lyli-editorial-news .wp-block-latest-posts li:hover .wp-block-latest-posts__featured-image img { transform: scale(1.02); }
}
.lyli-editorial-news .wp-block-latest-posts__post-title { display: block; font-family: var(--lyli-font-heading); font-weight: 600; color: var(--lyli-color-primary); font-size: clamp(1.1rem, 2vw, 1.35rem); text-decoration: none; margin-bottom: 6px; }
.lyli-editorial-news .wp-block-latest-posts__post-date { color: var(--lyli-color-muted); font-size: 0.85rem; }

.blog .posts-archive.layout3 article {
    padding: 18px;
    border: 1px solid var(--lyli-color-border);
    border-radius: 18px;
    background: var(--lyli-color-warm-white);
    box-shadow: var(--lyli-shadow-card);
    transition: border-color var(--lyli-transition), box-shadow var(--lyli-transition);
}
@media (hover: hover) and (pointer: fine) {
    /* Border/shadow only, no lift — category cards already own that language elsewhere on the site. */
    .blog .posts-archive.layout3 article:hover {
        border-color: color-mix(in srgb, var(--lyli-color-primary) 30%, transparent);
        box-shadow: var(--lyli-shadow-card-hover);
    }
}
.blog .posts-archive .post-thumbnail { overflow: hidden; border-radius: 12px; }
.blog .posts-archive .post-thumbnail img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
.blog .posts-archive .entry-title { font-size: clamp(1.25rem, 2vw, 1.6rem); }
.blog .posts-archive .entry-meta { color: var(--lyli-color-muted); font-size: 0.85rem; }

/* Categories */
.lyli-category-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: stretch;
    gap: 12px;
}
.lyli-category-card {
    height: 100%;
    min-width: 0;
    min-height: 158px;
    padding: 16px 14px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    border: 1px solid var(--lyli-color-border);
    border-radius: 18px;
    background: var(--lyli-color-warm-white);
    /*
     * Same two-layer geometry as --lyli-shadow-card-hover, alpha 0 on the
     * first layer, so box-shadow interpolates alpha only. A single-layer
     * resting shadow transitioning to the two-layer hover value has a
     * mismatched shadow count, which browsers cannot interpolate smoothly.
     */
    box-shadow: 0 4px 12px rgba(45, 42, 38, 0), 0 10px 28px rgba(45, 42, 38, 0.045);
    transition: transform var(--lyli-transition), box-shadow var(--lyli-transition), border-color var(--lyli-transition);
}
.lyli-category-card:nth-child(even) { background: var(--lyli-color-cream); }
.lyli-category-card:hover { border-color: color-mix(in srgb, var(--lyli-color-primary) 36%, transparent); box-shadow: var(--lyli-shadow-card-hover); }
@media (hover: hover) and (pointer: fine) {
    .lyli-category-card:hover { transform: translateY(var(--lyli-lift-md)); }
}
.lyli-category-index { margin: 0 0 auto; color: color-mix(in srgb, var(--lyli-color-primary) 42%, transparent); font-family: var(--lyli-font-heading); font-size: clamp(1.75rem, 7vw, 2.6rem); line-height: 1; }
/*
 * Deployed and immediately reverted: line-clamp: 2 (previous version of
 * this rule) truncated real titles mid-word ("Đặt mẫu...", "Hộp quà đ...")
 * — screenshot-reported on production. Losing part of the category name
 * is worse than a few pixels of height difference between cards, so this
 * card's height is left to CSS Grid's default `align-items: stretch`
 * (every card in the grid still stretches to match the tallest sibling in
 * its row; only the *distribution* of the extra space inside a shorter
 * card differs, handled by this card's own `justify-content: space-between`
 * further up).
 */
.lyli-category-card h3 {
    font-size: clamp(1rem, calc(0.9rem + 0.6vw), 1.35rem);
    margin: 20px 0 6px;
    overflow-wrap: anywhere;
}
.lyli-category-card h3 a { color: var(--lyli-color-primary); text-decoration: none; }
.lyli-category-card p:last-child { margin: 0; color: var(--lyli-color-muted); font-size: 0.9rem; line-height: 1.55; }

/* USP, story and CTA */
.lyli-usp-section { padding: clamp(30px, 7vw, 78px); border-radius: 26px; background: var(--lyli-color-cream); border: 1px solid var(--lyli-color-border); }
.lyli-usp-grid { gap: 18px; }
.lyli-info-card { padding: clamp(20px, 3vw, 26px); border-radius: 16px; background: var(--lyli-color-warm-white); }
.lyli-info-card h3 { margin: 12px 0 8px; font-size: 1.4rem; }
.lyli-info-card p:last-child { color: var(--lyli-color-muted); }
.lyli-info-number { font-family: var(--lyli-font-heading); font-size: 1rem; color: var(--lyli-color-primary); }
.lyli-custom-cta {
    padding: clamp(30px, 6vw, 72px);
    border-radius: 24px;
    background: var(--lyli-color-blush);
    color: var(--lyli-color-text);
    border: 1px solid color-mix(in srgb, var(--lyli-color-primary) 14%, transparent);
}
.lyli-custom-cta h2, .lyli-custom-cta .lyli-eyebrow { color: var(--lyli-color-primary); }
.lyli-custom-cta .wp-block-button__link { background: var(--lyli-color-primary); color: var(--lyli-color-warm-white); border-color: var(--lyli-color-primary); }
.lyli-story { padding: clamp(10px, 3vw, 34px); }
.lyli-story .wp-block-columns { gap: clamp(34px, 7vw, 86px); }
.lyli-story-visual {
    min-height: 280px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(22px, 4vw, 32px);
    border-radius: 48% 48% 18px 18px;
    background: radial-gradient(circle at 30% 25%, var(--lyli-color-warm-white) 0 10%, transparent 11%), linear-gradient(145deg, var(--lyli-color-sage), var(--lyli-color-cream));
    border: 1px solid color-mix(in srgb, var(--lyli-color-primary) 12%, transparent);
}
.lyli-story-mark { color: var(--lyli-color-primary); font-family: var(--lyli-font-heading); font-size: clamp(2rem, 4vw, 3.2rem); line-height: 1.18; }
/*
 * Post-Storefront-V2 UX audit UX-006 — was --lyli-color-lavender (#C2C3D2),
 * a cool blue-gray. Root-caused: NOT an accidental default block-editor
 * swatch as originally hypothesized (the homepage's own block JSON carries
 * no per-block color choice at all) — it was a deliberate CSS rule using a
 * real, defined palette token that simply reads as off-brand at this
 * position, the single most conversion-relevant spot on the homepage.
 * Swapped to --lyli-color-blush, the same warm dusty-rose token the
 * page's other CTA block ("Có một ý tưởng...", .lyli-custom-cta) already
 * uses, so the two CTA sections on the same page read as one consistent
 * family instead of two unrelated hues.
 */
.lyli-final-cta { padding: clamp(34px, 8vw, 90px) var(--lyli-gutter); border-radius: 26px; background: var(--lyli-color-blush); border: 1px solid color-mix(in srgb, var(--lyli-color-primary) 14%, transparent); }
.lyli-final-cta h2 { max-width: 19ch; margin-inline: auto; font-size: clamp(1.9rem, calc(1.45rem + 2vw), 3.6rem); }
.lyli-final-cta p { max-width: 48ch; margin-inline: auto; color: var(--lyli-color-muted); }

/*
 * Section-level scroll reveal (assets/js/reveal.js). The `.has-js-reveal`
 * gate class is only added by that script — if it fails to load or
 * execute, none of this ever applies and every element below stays at its
 * normal, fully visible state. Also wrapped in
 * `prefers-reduced-motion: no-preference` as a second, CSS-only guarantee:
 * even if the script somehow added the class for a reduced-motion user,
 * this media query alone keeps content visible without relying on the
 * script's own (already-present) reduced-motion check.
 */
@media (prefers-reduced-motion: no-preference) {
    .has-js-reveal .lyli-category-card,
    .has-js-reveal .lyli-info-card,
    .has-js-reveal .lyli-story-visual,
    .has-js-reveal .lyli-final-cta,
    .has-js-reveal .lyli-editorial-news .wp-block-latest-posts li,
    .has-js-reveal .blog .posts-archive.layout3 article {
        opacity: 0;
        transform: translateY(16px);
        transition: opacity var(--lyli-motion-section) var(--lyli-ease-out), transform var(--lyli-motion-section) var(--lyli-ease-out);
    }
    /*
     * Each revealed-state selector below repeats its hidden-state selector
     * plus .is-revealed, instead of a single low-specificity
     * `.has-js-reveal .is-revealed` catch-all. That catch-all (0,2,0) lost
     * to the blog-archive and latest-posts hidden selectors above, which
     * both carry more classes/an element than that (0,4,1) and (0,3,1) —
     * so their content was added to the DOM as .is-revealed but stayed at
     * opacity: 0 forever. Live-verified broken on both blog archive and
     * homepage latest-posts before this fix.
     */
    .has-js-reveal .lyli-category-card.is-revealed,
    .has-js-reveal .lyli-info-card.is-revealed,
    .has-js-reveal .lyli-story-visual.is-revealed,
    .has-js-reveal .lyli-final-cta.is-revealed,
    .has-js-reveal .lyli-editorial-news .wp-block-latest-posts li.is-revealed,
    .has-js-reveal .blog .posts-archive.layout3 article.is-revealed {
        opacity: 1;
        transform: translateY(0);
    }
    .has-js-reveal .lyli-category-card:nth-child(2) { transition-delay: 60ms; }
    .has-js-reveal .lyli-category-card:nth-child(3) { transition-delay: 120ms; }
    .has-js-reveal .lyli-category-card:nth-child(4) { transition-delay: 180ms; }
    .has-js-reveal .lyli-category-card:nth-child(5) { transition-delay: 240ms; }
    .has-js-reveal .lyli-info-card:nth-child(2) { transition-delay: 80ms; }
    .has-js-reveal .lyli-info-card:nth-child(3) { transition-delay: 160ms; }
    .has-js-reveal .lyli-editorial-news .wp-block-latest-posts li:nth-child(2) { transition-delay: 60ms; }
    .has-js-reveal .lyli-editorial-news .wp-block-latest-posts li:nth-child(3) { transition-delay: 120ms; }
    .has-js-reveal .blog .posts-archive.layout3 article:nth-child(2) { transition-delay: 60ms; }
    .has-js-reveal .blog .posts-archive.layout3 article:nth-child(3) { transition-delay: 120ms; }
}

/* Product loops */
.woocommerce .products ul,
.woocommerce ul.products {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(12px, 2.4vw, 30px);
    margin: 0 0 42px;
}
.woocommerce ul.products::before,
.woocommerce ul.products::after { display: none; }
.woocommerce ul.products li.product,
.woocommerce-page ul.products li.product {
    min-width: 0;
    float: none;
    width: auto !important;
    margin: 0 !important;
    padding: 0 0 16px;
    overflow: hidden;
    border: 0;
    border-radius: 16px;
    background: var(--lyli-color-warm-white);
    box-shadow: 0 8px 28px rgba(45, 42, 38, 0.065);
}
.woocommerce ul.products li.product > a:first-child { display: block; }
.woocommerce ul.products li.product img {
    width: 100%;
    aspect-ratio: 1 / 1.08;
    object-fit: cover;
    margin: 0 0 18px !important;
    background: var(--lyli-color-surface-soft);
    transition: transform var(--lyli-motion-slow) var(--lyli-ease-out);
}
/*
 * Botiga's own woocommerce.css dims this image to opacity 0.7 on
 * .loop-image-wrap:hover (ease opacity 0.3s). This theme's higher-specificity
 * selector below used to strip that transition without cancelling the dim
 * itself, so the two effects fought: an instant opacity snap plus a smooth
 * scale. Neutralize the dim so scale is the only thing that happens.
 */
.woocommerce ul.products li.product .loop-image-wrap:hover img { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
    .woocommerce ul.products li.product:hover img { transform: scale(1.02); }
}
.woocommerce ul.products li.product .woocommerce-loop-product__title,
.woocommerce ul.products li.product .price,
.woocommerce ul.products li.product .star-rating { margin-inline: clamp(10px, 2vw, 18px); }
/*
 * Botiga renders the title as plain text after the product-link anchor
 * closes (woocommerce_before_shop_loop_item_title priority 12), not inside
 * it — only the image/button area is actually clickable. Respond to the
 * real link's hover via the sibling combinator instead of the card, so the
 * title confirms interactivity without implying the whole card is a button.
 */
.woocommerce ul.products li.product > a:first-child:hover ~ .woocommerce-loop-product__title {
    color: var(--lyli-color-action-hover);
}
.woocommerce ul.products li.product .woocommerce-loop-product__title {
    font-family: var(--lyli-font-heading);
    font-weight: 600;
    font-size: clamp(0.95rem, calc(0.85rem + 0.5vw), 1.18rem);
    overflow-wrap: anywhere;
    transition: color var(--lyli-transition);
}
.woocommerce ul.products li.product .price { min-height: 24px; font-size: clamp(0.9rem, calc(0.82rem + 0.3vw), 1rem); }
/*
 * Storefront V2 Batch A — stock-derived metadata line (inc/woocommerce.php
 * render_card_stock_metadata()). Deliberately quiet: smaller and muted
 * relative to title/price, which stay the card's commerce-priority
 * elements per the frozen card contract.
 */
.woocommerce ul.products li.product .lyli-card-metadata {
    margin: 4px clamp(10px, 2vw, 18px) 0;
    color: var(--lyli-color-muted);
    font-size: 0.78rem;
}
.woocommerce ul.products li.product { position: relative; }
/*
 * Storefront V2 Batch A — with shop_product_add_to_cart_layout=layout2
 * (see docs/STOREFRONT-V2-IMPLEMENTATION.md §4.1/§8), Botiga's own CSS no
 * longer hides this button until hover (that behavior is scoped to the
 * .button-layout3 class only, confirmed in Botiga's woocommerce.css) — it
 * renders in normal flow below price already. This rule only restyles it,
 * it does not need to un-hide anything.
 */
.woocommerce ul.products li.product .button { margin: 14px clamp(10px, 2vw, 18px) 0; align-self: flex-start; min-height: 44px; padding: 9px 14px; font-size: 0.82rem; }
/*
 * Add-to-cart inline success feedback (assets/js/cart-badge.js). Anchored
 * to the stable li.product wrapper, not the add_to_cart_button itself —
 * live-verified that on this AJAX flow, WooCommerce/Botiga rewrite the
 * button's own class list (into a remove-from-cart / quantity control)
 * within well under a second of a successful add, which would wipe a class
 * added directly to the button before the confirmation ever became
 * visible. li.product is never touched by that rewrite. Only added by JS
 * on WooCommerce's own added_to_cart event, so this never appears for a
 * redirect/non-AJAX add-to-cart submit (that flow doesn't fire the event).
 */
.woocommerce ul.products li.product.lyli-added-confirm::after {
    content: "✓ Đã thêm vào giỏ";
    position: absolute;
    top: 10px;
    right: 10px;
    padding: 6px 12px;
    border-radius: 999px;
    background: var(--lyli-color-sage);
    color: var(--lyli-color-primary);
    font-size: 0.78rem;
    font-weight: 600;
    box-shadow: var(--lyli-shadow-card);
    z-index: 2;
    opacity: 0;
    /*
     * 1800ms is a deliberate read-time duration for a short Vietnamese
     * confirmation message, not a section-tier entrance — the token tiers
     * (fast/base/slow/section) describe transform/opacity motion length,
     * not how long text needs to stay legible, so this stays a literal
     * value rather than being forced onto an unrelated token.
     */
    animation: lyli-add-confirm-fade 1800ms var(--lyli-ease-out) forwards;
}
@keyframes lyli-add-confirm-fade {
    0% { opacity: 0; }
    15% { opacity: 1; }
    85% { opacity: 1; }
    100% { opacity: 0; }
}
/*
 * Storefront V2 Batch A.2 UX correction — the archive header is a
 * catalog-browsing entry point, not a landing-page hero. A.1's flex
 * `order` reordering machinery is gone: the intro paragraph moved to the
 * native page-description slot (inc/woocommerce.php), which already
 * renders in the right DOM position with no reordering, and the eyebrow's
 * hook already fires in its natural position (before the H1) — nothing
 * left to reorder. Left-aligned instead of centered: a centered block
 * read as a marketing hero and created a jarring realignment into the
 * left/right result-count/sort toolbar immediately below it; left
 * alignment keeps one continuous reading axis into the product grid.
 */
.woocommerce-page-header,
.woocommerce-page-header .container { text-align: left; }
.lyli-shop-intro-eyebrow {
    margin: 0 0 6px;
    color: var(--lyli-color-primary);
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.woocommerce-page-header .page-description,
.woocommerce-page-header .term-description {
    margin: 8px 0 0;
    max-width: 60ch;
    color: var(--lyli-color-muted);
}
.woocommerce-page-header .page-description p,
.woocommerce-page-header .term-description p { margin: 0; }
/*
 * Storefront V2 Batch A — Botiga's native category chips (theme mod
 * shop_archive_header_style_show_categories / _show_sub_categories,
 * inc/plugins/woocommerce/features/wc-page-header.php). Restyled only —
 * markup/behavior stays Botiga's own .categories-wrapper/.category-button
 * output, per the frozen contract's "native primitive first" rule.
 * Batch A.2: the theme mods driving these are now count-gated in PHP
 * (a single-choice chip row isn't real navigation), so this CSS only
 * needs to handle the "2+ choices" case — no single-chip layout to
 * special-case here.
 *
 * The .woocommerce-page-header prefix on the flex/justify rule matches
 * Botiga's own `.woocommerce-page-header .categories-wrapper {
 * justify-content: center }` (styles.css) at equal specificity so this
 * theme's load-order still wins — a bare `.categories-wrapper` rule
 * (one class) loses to Botiga's two-class one outright. Live-verified:
 * dropping this back to flex-start is what keeps the chip row on the
 * same left edge as the H1 above it instead of snapping back to
 * Botiga's centered default.
 */
.woocommerce-page-header .categories-wrapper {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    gap: 8px;
    overflow-x: auto;
    padding-bottom: 4px;
    margin-top: 14px;
    -webkit-overflow-scrolling: touch;
}
/*
 * Post-Storefront-V2 soft-navigation pass — owner feedback described these
 * as "heavy outlined pills, disconnected from the softer handmade
 * character of Lyli Shop." Root cause, live-verified via computed style:
 * the chip's fill (--lyli-color-warm-white, #FFFCF7) was IDENTICAL to the
 * page canvas behind it (--lyli-color-canvas is the same token), so the
 * 1px border was the only visible signal — an outline floating on
 * matching background reads as a harder, more "wireframe/admin-filter"
 * shape than a genuinely filled pill would. Softness comes from giving the
 * chip a real, visibly-different fill (--lyli-color-cream) instead of
 * relying on the border for definition — no new colors, no icons, just a
 * token swap plus removing the now-redundant border.
 */
/*
 * Botiga's own rule for chips inside the archive header is
 * `.woocommerce-page-header .category-button` (two classes) — a bare
 * `.category-button` selector loses to it outright regardless of source
 * order (same specificity trap already documented above for
 * `.categories-wrapper`). Matching that ancestor here, rather than only
 * writing a bare `.category-button` rule, is what makes this apply to
 * Botiga's own shop-root/child chips too, not just the new taxonomy-nav
 * chips rendered outside that header block. Also resets the assorted
 * margin/border-width overrides Botiga's compound selectors apply at that
 * same specificity, since layout here comes from the wrapper's flex `gap`
 * instead.
 */
.category-button,
.woocommerce-page-header .category-button {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    margin: 0;
    padding: 8px 16px;
    border-radius: 999px;
    border: 1px solid transparent;
    background: var(--lyli-color-cream);
    color: var(--lyli-color-primary);
    font-size: 0.82rem;
    font-weight: 500;
    white-space: nowrap;
    text-decoration: none;
    transition: color var(--lyli-transition), border-color var(--lyli-transition), background var(--lyli-transition);
}
/*
 * Owner-reported: hover and current-category state "can become too
 * similar." Root cause, found via computed-style + matched-CSSRule
 * inspection (not guessed): Botiga generates a per-site Customizer
 * stylesheet (`custom-styles.css`, from the theme's own "Button" color
 * setting) containing
 *   .woocommerce-page-header .category-button:hover {
 *     background-color: #212121 !important; color: #fff !important;
 *     border-color: #212121 !important;
 *   }
 * — a near-black fill with `!important`, which silently won over this
 * theme's own (non-`!important`) hover rule regardless of specificity or
 * load order, and read as visually close in darkness/weight to the
 * primary-brown CURRENT-state fill, especially on a quick glance or tap.
 * The only reliable way to override a `!important` rule is another
 * `!important` rule at equal-or-higher specificity — Botiga's own output
 * already set that precedent here, so matching it is the narrowly-scoped
 * fix, not a general license to reach for `!important` elsewhere. Hover
 * now uses a distinct blush surface (never the dark current-state fill),
 * so CURRENT and HOVER can never visually collide again.
 */
@media (hover: hover) and (pointer: fine) {
    .category-button:hover,
    .woocommerce-page-header .category-button:hover {
        border-color: var(--lyli-color-primary) !important;
        background: var(--lyli-color-blush) !important;
        color: var(--lyli-color-primary) !important;
    }
}
/*
 * Current-category state. Reuses the exact filled-circle "you are here"
 * language the site already established for pagination's current-page
 * number (table.woocommerce-pagination .page-numbers.current) rather than
 * inventing a second "selected" visual pattern.
 */
.category-button.is-current,
.category-button[aria-current="page"] {
    background: var(--lyli-color-primary);
    color: var(--lyli-color-warm-white);
}
@media (hover: hover) and (pointer: fine) {
    /*
     * !important for the same reason as the non-current hover rule above:
     * Botiga's Customizer stylesheet's `.category-button:hover` also
     * matches a current chip (it has no knowledge of `.is-current`), so
     * without this a current chip would go near-black on hover instead of
     * staying the current-state brown.
     */
    .category-button.is-current:hover,
    .category-button[aria-current="page"]:hover {
        border-color: var(--lyli-color-primary) !important;
        background: var(--lyli-color-primary) !important;
        color: var(--lyli-color-warm-white) !important;
    }
}

/*
 * Post-Storefront-V2 UX audit UX-008 — Botiga's own pagination controls
 * (`.woocommerce-pagination li .page-numbers`) measured 36×36px live,
 * below the site's own established 44px touch-target convention used
 * everywhere else. Only became reachable for the first time once the
 * catalog crossed 12 products (page 2+ appeared). Matches Botiga's own
 * selector shape so it wins on load order rather than needing
 * `!important`; color/current-state styling is untouched (Botiga's own
 * customizer button-color setting already resolves to the Lyli primary
 * brown here, confirmed live — only size needed fixing).
 */
.woocommerce-pagination li .page-numbers {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    line-height: normal;
}

/*
 * Taxonomy up/sibling navigation (docs/UX-AUDIT-POST-STOREFRONT-V2-2026-08-19.md
 * owner-reported addendum) — see inc/woocommerce/archive.php
 * render_taxonomy_nav() for the full rationale. One compact row: a
 * ghost-style "up" link (deliberately NOT chip-styled — it is exit/
 * ancestor navigation, a different kind of action from lateral sibling
 * browsing, and should read differently) followed by sibling chips
 * (reusing .category-button, so they automatically match every other chip
 * on the site). The whole row scrolls natively on overflow, the same
 * proven no-JS pattern already used for .categories-wrapper — no custom
 * carousel, no new script.
 */
.lyli-taxonomy-nav {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: 10px;
    margin: 14px 0 0;
    overflow-x: auto;
    padding-bottom: 4px;
    -webkit-overflow-scrolling: touch;
}
.lyli-taxonomy-nav-up {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 8px 2px;
    min-height: 44px;
    color: var(--lyli-color-muted);
    font-size: 0.82rem;
    text-decoration: none;
    white-space: nowrap;
    border-right: 1px solid var(--lyli-color-border);
    padding-right: 12px;
    transition: color var(--lyli-transition);
}
@media (hover: hover) and (pointer: fine) {
    .lyli-taxonomy-nav-up:hover { color: var(--lyli-color-primary); }
}
.lyli-taxonomy-nav-siblings {
    flex: 0 0 auto;
    display: flex;
    gap: 8px;
}

/*
 * Soft-catalog-navigation pass (docs/SOFT-CATALOG-NAVIGATION-RESEARCH-2026-08-20.md).
 * Desktop: two rows, ROW 1 = product family (top-level), ROW 2 =
 * .lyli-taxonomy-nav (local collection, unchanged). Mobile: both rows
 * hidden, a single "Danh mục" trigger + native <dialog> panel instead —
 * matches the existing 782px breakpoint sticky-header/sticky-CTA already
 * use, not a new one.
 */
.lyli-catalog-nav { margin-top: 14px; }
.lyli-catalog-row-family {
    display: flex;
    flex-wrap: nowrap;
    gap: 8px;
    overflow-x: auto;
    padding-bottom: 4px;
    -webkit-overflow-scrolling: touch;
}
.lyli-catalog-nav .lyli-taxonomy-nav { margin-top: 10px; }
.lyli-catalog-mobile-trigger,
.lyli-catalog-panel { display: none; }

/*
 * Native <dialog> does not itself prevent the page behind it from
 * scrolling (confirmed live: background scrollY moved while the panel
 * was open). JS toggles this class for the duration the panel is open;
 * it never applies without the panel actually being open, so JS-disabled
 * visitors are unaffected.
 */
body.lyli-catalog-panel-open { overflow: hidden; }

/*
 * Without JS (or without native <dialog> support), the mobile trigger
 * button and panel are non-functional, so the fallback must be the
 * always-working Row 2 (.lyli-taxonomy-nav) — never a dead-end button.
 * catalog-nav.js only adds `has-js-catalog-panel` to <html> once it has
 * confirmed <dialog>.showModal exists; every rule below that swaps Row 2
 * out for the trigger is gated behind that class, matching the same
 * default-visible/JS-adds-the-swap pattern already used by
 * .has-js-reveal above.
 */
@media (max-width: 782px) {
    .has-js-catalog-panel .lyli-catalog-row-family,
    .has-js-catalog-panel .lyli-catalog-nav .lyli-taxonomy-nav { display: none; }
    .lyli-catalog-mobile-trigger { display: none; }
    .has-js-catalog-panel .lyli-catalog-mobile-trigger {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 10px;
        width: 100%;
        min-height: 48px;
        padding: 10px 16px;
        border: 1px solid var(--lyli-color-border);
        border-radius: var(--lyli-radius-md);
        background: var(--lyli-color-cream);
        color: var(--lyli-color-primary);
        font-family: var(--lyli-font-body);
        font-size: 0.9rem;
        font-weight: 500;
        cursor: pointer;
        transition: border-color var(--lyli-transition), background var(--lyli-transition);
    }
    .lyli-catalog-mobile-trigger-label {
        text-transform: uppercase;
        font-size: 0.72rem;
        letter-spacing: 0.06em;
        color: var(--lyli-color-muted);
    }
    .lyli-catalog-mobile-trigger-current {
        flex: 1 1 auto;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        text-align: right;
        font-family: var(--lyli-font-heading);
        font-weight: 600;
    }
    .lyli-catalog-mobile-trigger:focus-visible {
        outline: 3px solid var(--lyli-color-primary);
        outline-offset: 2px;
    }
}

/*
 * Native <dialog> — modal semantics (focus containment, Escape, background
 * inertness) come from the browser, not hand-rolled ARIA. Only ever opened
 * on <=782px (catalog-nav.js gates this the same way the CSS above does),
 * so the desktop rows are the only interface there.
 */
.lyli-catalog-panel {
    box-sizing: border-box;
    width: min(100%, 480px);
    max-height: 82vh;
    margin: auto auto 0;
    padding: 0;
    border: none;
    border-radius: var(--lyli-radius-lg) var(--lyli-radius-lg) 0 0;
    background: var(--lyli-color-warm-white);
    box-shadow: 0 -8px 32px rgba(45, 42, 38, 0.16);
}
.lyli-catalog-panel::backdrop {
    background: rgba(45, 42, 38, 0.35);
}
.lyli-catalog-panel[open] {
    display: block;
    animation: lyli-panel-in var(--lyli-motion-base) var(--lyli-ease-out);
}
@keyframes lyli-panel-in {
    from { transform: translateY(100%); }
    to { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
    .lyli-catalog-panel[open] { animation: none; }
}
.lyli-catalog-panel-inner {
    max-height: 82vh;
    overflow-y: auto;
    padding: 18px 20px calc(20px + env(safe-area-inset-bottom, 0px));
}
.lyli-catalog-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
}
.lyli-catalog-panel-title {
    margin: 0;
    font-family: var(--lyli-font-heading);
    font-size: 1.1rem;
    color: var(--lyli-color-primary);
}
.lyli-catalog-panel-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 999px;
    background: var(--lyli-color-cream);
    color: var(--lyli-color-primary);
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
}
.lyli-catalog-panel-close:focus-visible {
    outline: 3px solid var(--lyli-color-primary);
    outline-offset: 2px;
}
.lyli-catalog-panel-link {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 10px 4px;
    color: var(--lyli-color-text);
    text-decoration: none;
    font-size: 0.94rem;
}
.lyli-catalog-panel-link-all {
    font-weight: 600;
    color: var(--lyli-color-primary);
    border-bottom: 1px solid var(--lyli-color-border);
    margin-bottom: 6px;
}
.lyli-catalog-panel-group { border-top: 1px solid var(--lyli-color-border); padding-top: 4px; margin-top: 4px; }
.lyli-catalog-panel-group:first-of-type { border-top: none; margin-top: 0; }
.lyli-catalog-panel-link-parent {
    font-family: var(--lyli-font-heading);
    font-weight: 600;
    color: var(--lyli-color-primary);
}
.lyli-catalog-panel-children { padding-left: 16px; }
.lyli-catalog-panel-link.is-current {
    color: var(--lyli-color-primary);
    font-weight: 700;
    position: relative;
}
.lyli-catalog-panel-link.is-current::after {
    content: "";
    width: 6px;
    height: 6px;
    margin-left: 8px;
    border-radius: 999px;
    background: var(--lyli-color-primary);
}

/*
 * Botiga's own `a:focus { outline: thin dotted; outline-offset: 5px }`
 * (styles.css) is one element + one pseudo-class (0,1,1) — higher
 * specificity than this theme's bare `:focus-visible` rule (0,1,0), so it
 * silently wins on any plain-anchor chip/link regardless of load order.
 * Live-verified: an unfocused `.category-button` chip fell back to
 * Botiga's thin dotted ring instead of the intended 3px solid one, and on
 * a dark-filled current chip that ring's color (currentColor, i.e. the
 * chip's own white text) was nearly invisible against the dark fill.
 * Scoped to the two link types this pass touches, matching Botiga's
 * specificity band rather than reaching for `!important`.
 */
.category-button:focus-visible,
.lyli-taxonomy-nav-up:focus-visible {
    outline: 3px solid var(--lyli-color-primary);
    outline-offset: 2px;
}

.woocommerce .woocommerce-result-count { margin: 0; color: var(--lyli-color-muted); }
.woocommerce .woocommerce-ordering { max-width: 100%; }
/*
 * Storefront V2 Batch A.2 — the sort <select> had appearance:none and no
 * border from Botiga's own CSS (verified live via getComputedStyle:
 * border 0px none, no background-image) — it read as plain text, not a
 * control. Restores a visible affordance without reintroducing native
 * appearance (which renders inconsistently across browsers/OS): a border,
 * background contrast, and a CSS-drawn chevron via background-image so it
 * still visually says "this opens options" while staying the real native
 * <select>, no JS.
 */
.woocommerce .woocommerce-ordering select {
    width: 100%;
    min-width: 0;
    max-width: 260px;
    padding: 9px 34px 9px 14px;
    border: 1px solid var(--lyli-color-border);
    border-radius: var(--lyli-radius-md);
    background-color: var(--lyli-color-warm-white);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%237A3B17' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 14px center;
}
.woocommerce .woocommerce-ordering select:focus-visible {
    outline: 3px solid var(--lyli-color-primary);
    outline-offset: 2px;
}
/*
 * Storefront V2 Batch A — fixes the result-count/sort row clipping the
 * sort <select>'s own text at narrow widths. Root cause: Botiga wraps both
 * in a Bootstrap-style .row > .col-6 pair (confirmed in styles.css,
 * flex: 0 0 50%), squeezing the sort control into ~50% of the viewport —
 * not enough room for "Sắp xếp mặc định" as one line. Below the existing
 * 600px breakpoint (the same boundary already used elsewhere in this
 * file), stack the two columns instead of splitting them.
 */
@media (max-width: 599px) {
    .woocommerce-sorting-wrapper .col-6 {
        flex: 0 0 100%;
        max-width: 100%;
    }
    .woocommerce-sorting-wrapper .botiga-sorting-right { margin-top: 10px; }
}
.woocommerce-products-header,
.woocommerce .page-title { margin-bottom: clamp(28px, 5vw, 54px); }
.woocommerce .page-title { font-size: clamp(2rem, calc(1.45rem + 2.4vw), 4.3rem); }
/*
 * Screenshot-verified: this banner still used Botiga's unbranded default
 * (background:#FFF, h1 color:#212121) — a stark white band against the
 * warm cream site background above and below it, on both the shop archive
 * and every product-category archive (both use this same wrapper). Same
 * specificity as Botiga's own rules (single class each); this wins because
 * shop-child's stylesheet loads after both botiga-custom-styles and
 * botiga-style-min.
 *
 * Batch A.2: padding tightened further — live audit measured the archive
 * header + toolbar consuming 68% of a 1440x900 first viewport before any
 * product row appeared, the core reported UX problem. This is a catalog
 * entry point, not a campaign hero.
 */
/*
 * Post-Storefront-V2 UX audit UX-007 — at 1366×768 (a common laptop
 * height, not just a narrow 900px-tall viewport), firstProductTop
 * measured 548.7px — only ~29% of the viewport showed any product.
 * Trimmed the header's own padding ceiling modestly (not the fragile
 * post-header margin-collapse system documented below, which A.2 already
 * had to fix once) — a measurable, low-risk reduction in redundant
 * whitespace rather than hiding any content.
 */
.woocommerce-page-header {
    background: var(--lyli-color-warm-white);
    padding-top: clamp(18px, 2.6vw, 26px);
    padding-bottom: clamp(14px, 2vw, 18px);
}
/*
 * UX-007 — Botiga's own breadcrumb carried a 30px margin-bottom, the
 * single largest gap in the header stack (vs. 6–14px between every other
 * header element). Scoped to the archive-header context only — PDP has
 * its own separate breadcrumb placement, untouched.
 */
.woocommerce-page-header .woocommerce-breadcrumb { margin-bottom: 14px; }
.woocommerce-page-header h1 { color: var(--lyli-color-primary); }
/*
 * Storefront V2 Batch A.1/A.2 — the shared .page-title rule above (used
 * by every WooCommerce page-header context) sized the shop archive H1 at
 * ~58px live at 1440px, with 54px margin-bottom — oversized for a compact
 * archive header. Scoped to .woocommerce-page-header specifically (the
 * wrapper Botiga only ever renders for shop/category/tag archives — never
 * cart/checkout/account/single-product), so PDP and other page-title
 * contexts are untouched.
 */
.woocommerce-page-header .page-title {
    margin: 4px 0 0;
    font-size: clamp(1.6rem, calc(1.25rem + 1.4vw), 2.5rem);
}
/*
 * Storefront V2 Batch A.2 — this relationship turned out to be
 * content-dependent, not fixed: with category chips rendering (A.1's
 * state), .content-wrapper's previous sibling is .site (the wrapper
 * containing .woocommerce-page-header), because Botiga's chip function
 * closes and reopens a container div along the way. With chips suppressed
 * (A.2's default, single-category case), that dance never happens and
 * </header> lands directly before .content-wrapper instead — reviving
 * Botiga's own dormant `.woocommerce-page-header + .content-wrapper {
 * margin-top: 100px }` rule (styles.css), live-verified to reintroduce
 * the oversized gap this override exists to prevent. Covering both
 * selectors keeps this correct regardless of which structural case is
 * live, including if the catalog grows past one category later and
 * chips reappear.
 */
.site + .content-wrapper,
.woocommerce-page-header + .content-wrapper {
    margin-top: clamp(20px, 4vw, 32px);
}
.woocommerce-no-products-found { max-width: 760px; margin: 60px auto; text-align: center; }
.woocommerce-no-products-found .woocommerce-info { padding: 32px; border: 1px solid var(--lyli-color-border); border-radius: 16px; background: var(--lyli-color-surface-soft); }
.lyli-search-no-results-hint { margin: 14px 0 0; color: var(--lyli-color-muted); text-align: center; }

/*
 * Post-Storefront-V2 UX audit UX-009 — see inc/content-pages.php for the
 * scoping rationale (policy pages + "Giới thiệu" only, not every WP
 * page). Constrains paragraph/list/heading text to a comfortable reading
 * measure; explicitly does not touch table/gallery/wide-block content, so
 * anything intentionally wider (an embedded table, a full-width image)
 * still uses the full container.
 */
.lyli-readable-content .entry-content > p,
.lyli-readable-content .entry-content > ul,
.lyli-readable-content .entry-content > ol,
.lyli-readable-content .entry-content > h2,
.lyli-readable-content .entry-content > h3,
.lyli-readable-content .entry-content > h4 {
    max-width: 760px;
}

/* Single product */
.single-product div.product { padding-top: clamp(18px, 3vw, 42px); }
.woocommerce div.product div.images,
.woocommerce div.product div.summary { margin-bottom: clamp(42px, 6vw, 78px); }
.woocommerce div.product div.images img { border-radius: 18px; background: var(--lyli-color-surface-soft); }
.woocommerce div.product .product_title { font-size: clamp(1.75rem, calc(1.35rem + 2vw), 3.8rem); }
.woocommerce div.product .summary .price { font-size: 1.55rem; margin: 18px 0; }
.woocommerce div.product .woocommerce-product-details__short-description { color: var(--lyli-color-muted); font-size: 1.03rem; }
.woocommerce div.product form.cart { padding: 20px 0; border-top: 1px solid var(--lyli-color-border); border-bottom: 1px solid var(--lyli-color-border); }
.woocommerce div.product form.cart .variations th,
.woocommerce div.product form.cart .variations td { display: block; width: 100%; text-align: left; }
.woocommerce div.product form.cart .variations select { width: 100%; min-height: 48px; background: var(--lyli-color-warm-white); }
.woocommerce .quantity .qty { min-height: 48px; border-radius: 999px; }
.woocommerce div.product .woocommerce-tabs { margin-top: 20px; }
.woocommerce div.product .woocommerce-tabs ul.tabs { padding: 0; border-bottom: 1px solid var(--lyli-color-border); }
.woocommerce div.product .woocommerce-tabs ul.tabs li { border: 0; background: transparent; }
.woocommerce div.product .woocommerce-tabs .panel { max-width: 760px; padding-top: 24px; }
.lyli-missing-image-product .woocommerce-product-gallery__wrapper { min-height: 440px; display: grid; place-items: center; background: var(--lyli-color-surface-soft); border-radius: 18px; }
/*
 * Storefront V2 Batch B — PDP description recomposition
 * (inc/woocommerce/single-product.php render_recomposed_description_tab()).
 * Deliberately quiet: a heading + top border reusing the same divider
 * language form.cart already uses (border-top/bottom above), not a card —
 * the brief explicitly warns against turning paragraphs into cards/icon
 * soup. Inherits the tab panel's existing 760px reading width, no new
 * width system.
 */
.lyli-pdp-description-intro { margin-bottom: 20px; }
.lyli-pdp-section { margin-top: clamp(28px, 4vw, 40px); padding-top: clamp(20px, 3vw, 28px); border-top: 1px solid var(--lyli-color-border); }
.lyli-pdp-section-title {
    margin: 0 0 12px;
    font-family: var(--lyli-font-heading);
    font-size: 1.15rem;
    color: var(--lyli-color-primary);
}
.lyli-pdp-section-details h2 {
    margin: 0 0 10px;
    font-family: var(--lyli-font-heading);
    font-size: 1.05rem;
    color: var(--lyli-color-primary);
}
.lyli-pdp-section-details h2:not(:first-child) { margin-top: 24px; }
/*
 * Storefront V2 Batch B — sticky mobile add-to-cart proxy
 * (assets/js/pdp-sticky-cta.js). Visually active only at <=782px, the
 * same "mobile" boundary the sticky header already uses for its own
 * sticky/relative switch (verified in that script's own comment) — not a
 * new breakpoint invented for this feature. Hidden by default; JS toggles
 * .lyli-sticky-cta-visible once the real purchase form scrolls out of
 * view. Reuses the existing primary-button and motion-token language
 * rather than a separate visual system.
 */
/*
 * Post-Storefront-V2 UX audit UX-012 — the sticky bar was the one major
 * interactive surface on the site with zero corner radius (every card/
 * button/chip elsewhere uses the brand's rounded language), reading as a
 * utility bar rather than a native Lyli surface. Visual-only change: top
 * corners rounded with the existing --lyli-radius-lg token, border
 * softened to match, shadow left as-is (already correctly subtle).
 * Visibility/proxy/observation logic in pdp-sticky-cta.js is completely
 * untouched.
 */
.lyli-sticky-cta {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 998;
    align-items: center;
    gap: 14px;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
    background: var(--lyli-color-warm-white);
    border-top: 1px solid var(--lyli-color-border);
    border-radius: var(--lyli-radius-lg) var(--lyli-radius-lg) 0 0;
    box-shadow: 0 -8px 24px rgba(45, 42, 38, 0.08);
    transform: translateY(100%);
    transition: transform var(--lyli-motion-base) var(--lyli-ease-standard);
}
@media (max-width: 782px) {
    .lyli-sticky-cta { display: flex; }
    .lyli-sticky-cta.lyli-sticky-cta-visible { transform: translateY(0); }
}
/*
 * Post-Storefront-V2 UX audit UX-013 — Botiga's own back-to-top button
 * (`.back-to-top.display { bottom: 30px; right: 30px }`) collided with
 * two different things on mobile, both live-measured: the footer's
 * "Tài khoản" link (covered ~48px of its tap area) and, on any PDP with
 * the sticky add-to-cart bar visible, the sticky bar itself (near-total
 * bounding-box overlap). Raised on mobile only, clear of both — the
 * sticky CTA's own height plus safe-area inset, applied everywhere on
 * mobile rather than only on PDP, since detecting "is the sticky bar
 * currently visible" would need JS coupling between two independently-
 * built components for no real benefit (an empty gap of exposed page
 * background above the footer on non-PDP mobile pages is a fully
 * acceptable trade for a positioning rule this simple and robust).
 */
@media (max-width: 782px) {
    .back-to-top.display { bottom: calc(88px + env(safe-area-inset-bottom, 0px)); }
}
.lyli-sticky-cta-info { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.lyli-sticky-cta-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.82rem;
    color: var(--lyli-color-muted);
}
.lyli-sticky-cta-price { font-family: var(--lyli-font-heading); font-weight: 600; color: var(--lyli-color-primary); }
.lyli-sticky-cta-button {
    flex: 0 0 auto;
    min-height: 44px;
    padding: 10px 20px;
    border: 0;
    border-radius: var(--lyli-radius-md);
    background: var(--lyli-color-primary);
    color: var(--lyli-color-warm-white);
    font-weight: 600;
    cursor: pointer;
}
.lyli-sticky-cta.lyli-sticky-cta-unavailable .lyli-sticky-cta-button { opacity: 0.7; }
.lyli-sticky-cta-button:focus-visible { outline: 3px solid var(--lyli-color-primary); outline-offset: 2px; }

/* Cart, checkout, account */
.woocommerce-cart .entry-content,
.woocommerce-checkout .entry-content,
.woocommerce-account .entry-content { max-width: 1140px; }
.woocommerce table.shop_table { width: 100%; background: var(--lyli-color-warm-white); border: 1px solid var(--lyli-color-border); border-collapse: separate; }
/*
 * Storefront V2 full-review pass — live-verified at 390px: the cart
 * table's price/subtotal cells narrow enough on mobile that the amount
 * wraps mid-number ("55.0" / "00 đ" on two lines). A price splitting
 * across a line break reads as a real defect, not a design choice —
 * amounts should never wrap regardless of column width.
 */
.woocommerce table.shop_table .amount { white-space: nowrap; }
.woocommerce-cart .cart-collaterals .cart_totals,
.woocommerce-checkout #order_review,
.woocommerce-account .woocommerce-MyAccount-content {
    padding: clamp(22px, 3vw, 36px);
    border: 1px solid var(--lyli-color-border);
    border-radius: 16px;
    background: var(--lyli-color-cream);
    box-shadow: var(--lyli-shadow-card);
}
.woocommerce-checkout .col2-set { display: grid; grid-template-columns: minmax(0, 1fr); gap: 22px; }
.woocommerce-checkout .col2-set .col-1,
.woocommerce-checkout .col2-set .col-2 { float: none; width: auto; }
.woocommerce form .form-row input.input-text,
.woocommerce form .form-row textarea,
.woocommerce form .form-row select { width: 100%; max-width: 100%; min-height: 48px; padding: 10px 12px; }
.woocommerce form .form-row input.input-text,
.woocommerce form .form-row textarea { background: var(--lyli-color-warm-white); }
/*
 * Selects get their own background here (not the shorthand above): the
 * `background` shorthand resets background-image too, which silently
 * dropped the chevron affordance added below at an equal-specificity tie
 * (live-verified: .woocommerce-checkout select.select's chevron never
 * painted until this was split out).
 */
.woocommerce form .form-row select { background-color: var(--lyli-color-warm-white); }
.woocommerce-account .woocommerce-MyAccount-navigation { width: 25%; }
.woocommerce-account .woocommerce-MyAccount-navigation ul { list-style: none; margin: 0; padding: 8px; border: 1px solid var(--lyli-color-border); border-radius: 14px; background: var(--lyli-color-warm-white); }
.woocommerce-account .woocommerce-MyAccount-navigation li a { display: block; padding: 11px 14px; border-radius: 9px; text-decoration: none; }
.woocommerce-account .woocommerce-MyAccount-navigation li.is-active a { background: var(--lyli-color-surface-soft); }

/*
 * Storefront V2 Batch C — payment-method card treatment.
 * Native markup only (.wc_payment_methods > li.wc_payment_method, each
 * containing the real radio input, its label, and a .payment_box
 * description) — no custom-drawn control, no JS-added state class. The
 * selected/unselected visual state is expressed with :has(), reading the
 * real checked radio directly, so it can never drift out of sync with
 * the actual native selection state (including after a checkout AJAX
 * fragment refresh, which re-renders this exact markup server-side).
 * Browsers without :has() simply show the unselected treatment for every
 * row — a quieter page, not a broken one.
 */
.woocommerce-checkout ul.wc_payment_methods { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.woocommerce-checkout ul.wc_payment_methods li.wc_payment_method {
    padding: 14px 16px;
    border: 1px solid var(--lyli-color-border);
    border-radius: var(--lyli-radius-md);
    background: var(--lyli-color-warm-white);
    transition: border-color var(--lyli-transition), background-color var(--lyli-transition);
}
.woocommerce-checkout ul.wc_payment_methods li.wc_payment_method:has(input:checked) {
    border-color: var(--lyli-color-primary);
    background: var(--lyli-color-cream);
}
.woocommerce-checkout ul.wc_payment_methods li.wc_payment_method input[type="radio"] { margin-right: 8px; }
.woocommerce-checkout ul.wc_payment_methods li.wc_payment_method label { font-weight: 600; color: var(--lyli-color-text); cursor: pointer; }
.woocommerce-checkout ul.wc_payment_methods li.wc_payment_method .payment_box {
    margin: 10px 0 0;
    padding: 12px 14px 0;
    border-top: 1px solid var(--lyli-color-border);
    color: var(--lyli-color-muted);
    font-size: 0.92rem;
    background: none;
}
.woocommerce-checkout ul.wc_payment_methods li.wc_payment_method .payment_box::before { display: none; }

/*
 * Storefront V2 Batch C — checkout address selects.
 * Live-verified: billing/shipping country + state are enhanced by
 * WooCommerce's own bundled selectWoo/select2 (`select2-hidden-
 * accessible` class present on the real <select>) — the visible control
 * is `.select2-selection`, not the (visually hidden but still real,
 * still keyboard-operable) native <select> underneath it. Styling the
 * native select here would touch an invisible element and change
 * nothing on screen, so the selectWoo-rendered UI is targeted directly
 * instead; selectWoo itself is left enabled and untouched. The
 * ward/"Phường-Xã" select (billing_city / shipping_city) is native,
 * unenhanced (no select2 class, confirmed live), so it reuses the same
 * bordered + chevron treatment already shipped for the shop-archive sort
 * control (Batch A, §7) rather than inventing a second select style.
 */
.woocommerce-checkout .select2-container--default .select2-selection--single {
    height: 48px;
    padding: 0;
    border: 1px solid var(--lyli-color-border);
    border-radius: var(--lyli-radius-sm);
    background-color: var(--lyli-color-warm-white);
}
.woocommerce-checkout .select2-container--default .select2-selection--single .select2-selection__rendered {
    line-height: 46px;
    padding-left: 12px;
    color: var(--lyli-color-text);
}
.woocommerce-checkout .select2-container--default .select2-selection--single .select2-selection__arrow { height: 46px; }
.woocommerce-checkout .select2-container--default.select2-container--focus .select2-selection--single,
.woocommerce-checkout .select2-container--default.select2-container--open .select2-selection--single {
    border-color: var(--lyli-color-primary);
}
.woocommerce-checkout select.select {
    width: 100%;
    min-height: 48px;
    padding: 10px 34px 10px 12px;
    border: 1px solid var(--lyli-color-border);
    border-radius: var(--lyli-radius-sm);
    background-color: var(--lyli-color-warm-white);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%237A3B17' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    appearance: none;
}

/* Lyli content lives inside Botiga's single semantic footer. */
.bhfb.bhfb-footer { margin-top: clamp(48px, 9vw, 120px); background: var(--lyli-color-cream); color: var(--lyli-color-muted); border-top: 1px solid var(--lyli-color-border); }
.bhfb-footer .bhfb-rows { display: none; }
.lyli-footer-content { width: 100%; }
.lyli-footer-inner { width: min(calc(100% - (var(--lyli-gutter) * 2)), 1140px); margin: auto; display: grid; grid-template-columns: minmax(0, 1fr); gap: 28px; padding: clamp(38px, 8vw, 90px) 0; }
.lyli-footer-logo { display: inline-block; margin-bottom: 14px; color: var(--lyli-color-primary); font-family: var(--lyli-font-heading); font-size: 2rem; font-weight: 600; text-decoration: none; transition: color var(--lyli-transition); }
.bhfb-footer .lyli-footer-content h2 { margin: 4px 0 14px; color: var(--lyli-color-primary); font-size: 1rem; font-family: var(--lyli-font-body); text-transform: uppercase; letter-spacing: .12em; }
.lyli-footer-intro { max-width: 38ch; color: var(--lyli-color-muted); }
.lyli-footer-menu,
.lyli-footer-contact,
.lyli-footer-social { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
.lyli-footer-menu a,
.lyli-footer-contact a,
.lyli-footer-social a { min-height: 44px; display: inline-flex; align-items: center; color: var(--lyli-color-text); text-decoration: none; transition: color var(--lyli-transition); }
.lyli-footer-menu a:hover,
.lyli-footer-contact a:hover,
.lyli-footer-social a:hover { color: var(--lyli-color-primary); }
.lyli-footer-bottom { width: min(calc(100% - (var(--lyli-gutter) * 2)), 1140px); margin: auto; padding: 20px 0; display: flex; flex-direction: column; gap: 12px; border-top: 1px solid var(--lyli-color-border); font-size: .88rem; }
.lyli-footer-bottom a { color: var(--lyli-color-primary); }
/*
 * Post-Storefront-V2 UX audit UX-002 — the 4 policy pages had no link
 * anywhere on the site. The slim copyright bar already carries the
 * "Tài khoản" link, so pairing legal links here (a common real-world
 * footer pattern) avoids a fourth full-width column that would unbalance
 * the existing brand/nav/contact 3-column layout.
 */
.lyli-footer-bottom-links { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; }
.lyli-footer-legal { display: flex; flex-wrap: wrap; gap: 14px; margin: 0; padding: 0; list-style: none; }

/* Mobile-first responsive product and storefront layouts. */
.woocommerce-account .woocommerce-MyAccount-navigation,
.woocommerce-account .woocommerce-MyAccount-content {
    float: none;
    width: 100%;
    min-width: 0;
}

.woocommerce-account .woocommerce-MyAccount-navigation { margin-bottom: 22px; }
.woocommerce table.shop_table_responsive { table-layout: fixed; }
.woocommerce table.shop_table_responsive tr td { overflow-wrap: anywhere; }
.woocommerce-cart table.cart td.actions .coupon { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; width: 100%; }
.woocommerce-cart table.cart td.actions .coupon input,
.woocommerce-cart table.cart td.actions .coupon button { width: 100%; }

@media (min-width: 600px) {
    .lyli-category-card { min-height: 190px; padding: 20px 18px; }
    .lyli-footer-inner { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
    .lyli-footer-brand { grid-column: 1 / -1; }
    .lyli-footer-bottom { flex-direction: row; justify-content: space-between; gap: 20px; }
    .woocommerce-cart table.cart td.actions .coupon { grid-template-columns: minmax(150px, 1fr) auto; max-width: 470px; }
    .woocommerce-cart table.cart td.actions .coupon button { width: auto; }
}

@media (min-width: 700px) {
    .lyli-category-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
    .woocommerce .products ul,
    .woocommerce ul.products { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .lyli-editorial-news .wp-block-latest-posts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 783px) {
    /*
     * backdrop-filter forces the browser to resample everything behind this
     * element on every scroll frame — real compositing cost. Below this
     * breakpoint .bhfb-header switches to position: relative (see the
     * max-width: 782px block), so it never overlaps scrolling content and
     * the blur has nothing to show; it was pure cost with zero visible
     * effect on mobile.
     */
    .bhfb-header { backdrop-filter: blur(14px); }
    /*
     * Hide-on-scroll-down / reveal-on-scroll-up (assets/js/sticky-header.js).
     * Scoped to this breakpoint because it's the one where .bhfb-header is
     * actually position: sticky — below it the header is position: relative
     * and this transform has no sticky context to detach from. The blanket
     * prefers-reduced-motion rule already collapses this transition to near
     * zero duration; the class still toggles, so functionality (space
     * reclaimed on scroll) is preserved and only the animation is removed.
     */
    .bhfb-header { transition: transform var(--lyli-motion-base) var(--lyli-ease-standard); }
    .bhfb-header.lyli-header-hidden { transform: translateY(-100%); }
    .lyli-pattern { margin-block: clamp(52px, 7vw, 86px); }
    /* Same .lyli-hero .lyli-hero-grid specificity as the base rule — a media query doesn't grant extra priority, so a lower-specificity override here would lose to the base rule's grid-template-columns at every width. */
    .lyli-hero .lyli-hero-grid { grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr); align-items: center; }
    .lyli-hero-visual { min-height: clamp(400px, 42vw, 520px) !important; }
    .lyli-story-visual { min-height: 360px; }
    .woocommerce-checkout .col2-set { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 28px; }
    .woocommerce-account .woocommerce-MyAccount-navigation { float: left; width: 25%; margin-bottom: 0; }
    .woocommerce-account .woocommerce-MyAccount-content { float: right; width: 70%; }
}

@media (max-width: 782px) {
    .admin-bar .bhfb-header { top: 46px; }
    .bhfb-header { position: relative; top: auto; }
    .lyli-hero { border-radius: 20px; }
    .lyli-hero .wp-block-buttons { gap: 10px; }
    .lyli-hero .wp-block-button__link { padding-inline: 18px; }
    .lyli-custom-cta .wp-block-columns,
    .lyli-story .wp-block-columns { gap: 22px; }
    .lyli-custom-cta .wp-block-buttons { justify-content: flex-start; }
}

@media (max-width: 420px) {
    body { font-size: 15px; }
    .lyli-pattern { width: min(calc(100% - 28px), var(--lyli-content-width)); }
    /*
     * .lyli-hero::before is a fixed-pixel ring, sized for a desktop-width
     * card. On a narrow phone viewport it's proportionally huge and its
     * visible corner (clipped by .lyli-hero's own overflow:hidden) lands
     * directly over the start of the H1 — browser-verified at 390px wide.
     * Shrink it proportionally so it stays a corner accent, not a smear
     * across the headline.
     */
    .lyli-hero::before { width: 180px; height: 180px; left: -110px; top: -110px; border-width: 26px; }
    .lyli-hero > * { min-width: 0; max-width: 100%; }
    .lyli-hero .wp-block-buttons { display: grid; }
    .lyli-hero .wp-block-button,
    .lyli-hero .wp-block-button__link { width: 100%; }
    .lyli-usp-section,
    .lyli-custom-cta,
    .lyli-final-cta { padding: 28px 18px; border-radius: 18px; }
    .woocommerce ul.products li.product img { margin-bottom: 12px !important; }
    .woocommerce table.shop_table_responsive tr td { padding: 10px; }
}

@media (min-width: 1025px) {
    .bhfb-header .bhfb-row { min-height: 76px; }
    .lyli-pattern { margin-block: clamp(72px, 8vw, 118px); }
    .lyli-category-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
    .lyli-category-card { min-height: 230px; padding: 24px 20px; }
    .woocommerce .products ul,
    .woocommerce ul.products { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .lyli-story-visual { min-height: 430px; }
    .lyli-editorial-news .wp-block-latest-posts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .lyli-footer-inner { grid-template-columns: minmax(260px, 1.7fr) minmax(0, 1fr) minmax(0, 1fr); }
    .lyli-footer-brand { grid-column: auto; }
}

/* ==========================================================================
   Soft catalog navigation (docs/SOFT-CATALOG-NAVIGATION-RESEARCH-2026-08-20.md)
   ========================================================================== */

/*
 * assets/js/catalog-nav.js sets [aria-busy="true"] on #primary while a
 * fetch is in flight — the existing grid stays fully visible and
 * interactive (task brief: "do NOT blank the grid immediately"), just
 * subtly dimmed so the pending state reads without a spinner/skeleton.
 * The dim is itself skipped under reduced motion below (state, not motion,
 * so it's not fully removed — just not eased).
 */
#primary[aria-busy="true"] { opacity: 0.6; transition: opacity var(--lyli-motion-base) var(--lyli-ease-standard); }

/*
 * View Transition API (progressive enhancement — catalog-nav.js only calls
 * document.startViewTransition when it exists and prefers-reduced-motion
 * is not set; every browser without support gets the plain CSS
 * .lyli-catalog-swap-* fallback below instead, same visual result, less
 * smooth interpolation). Named on #primary specifically, not the whole
 * page, so header/footer/sticky elements never animate.
 */
#primary { view-transition-name: lyli-catalog-region; }
::view-transition-old(lyli-catalog-region) {
    animation: lyli-catalog-out var(--lyli-motion-base) var(--lyli-ease-standard) both;
}
::view-transition-new(lyli-catalog-region) {
    animation: lyli-catalog-in var(--lyli-motion-base) var(--lyli-ease-out) both;
}
@keyframes lyli-catalog-out {
    to { opacity: 0; transform: translateX(-10px); }
}
@keyframes lyli-catalog-in {
    from { opacity: 0; transform: translateX(10px); }
    to { opacity: 1; transform: translateX(0); }
}
/*
 * CSS-only fallback for browsers without the View Transition API —
 * catalog-nav.js toggles these classes directly around the DOM swap
 * instead of calling startViewTransition().
 */
.lyli-catalog-swap-out { animation: lyli-catalog-out var(--lyli-motion-base) var(--lyli-ease-standard) both; }
.lyli-catalog-swap-in { animation: lyli-catalog-in var(--lyli-motion-base) var(--lyli-ease-out) both; }

@media (prefers-reduced-motion: reduce) {
    ::view-transition-old(lyli-catalog-region),
    ::view-transition-new(lyli-catalog-region),
    .lyli-catalog-swap-out,
    .lyli-catalog-swap-in {
        animation: none !important;
    }
}

/* Visually-hidden live region for the polite result-count announcement. */
.lyli-catalog-status {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
