/**
 * Spring Curtains — global design system.
 *
 * theme.json owns the tokens; this file is the only place that turns them into
 * geometry. It is written in layers, and the order matters:
 *
 *   0. Foundations      box model, element defaults, motion
 *   1. Focus            the one visible-focus policy for the whole site
 *   2. Controls         every input/select/textarea from one contract
 *   3. Buttons          every action button from the same contract
 *   4. Rhythm           vertical spacing, prose measure, phantom-gap removal
 *   5. Cards & grids    equal-height loops with bottom-aligned actions
 *   6. WooCommerce      catalogue surfaces, folded into the block language
 *   7. Chrome           header, footer, hero, tiles
 *   8. Entrance         the front page's sections, arriving as they are reached
 *
 * Rules of the house:
 *   - No literal colour, radius, duration or control dimension below this
 *     comment. If a value is missing, add a token in theme.json. The only
 *     literals left are structural zeros, resets, and shapes that are not
 *     measurements (a 999px pill, a 50% circle).
 *   - Broad element selectors are wrapped in :where() so a block, a pattern or
 *     a plugin can still win without !important.
 */

/* ==================================================================== 0. Foundations */

/*
 * The single most consequential line in this file. Neither WordPress core nor
 * Twenty Twenty-Four sets a box model, so form controls were still content-box:
 * `width: 100%` plus 1rem of padding made every text input 30.8px wider than
 * its grid column, while `select` (border-box in the UA sheet) stayed correct.
 * That mismatch is what made the contact form look misaligned — the two columns
 * overlapped by 10.8px and the select sat 30.8px short of the textarea below it.
 */
*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

::selection {
    background-color: var( --wp--custom--state--accent );
    color: var( --wp--preset--color--base );
}

/* Controls inherit the brand face instead of the OS UI font. */
:where( input, select, textarea, button ) {
    font-family: inherit;
}

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

/*
 * Vietnamese stacks a tone mark over an already-diacriticked vowel (ế, ộ, ữ),
 * so the display face needs more leading than a Latin-only setting would. One
 * floor, from theme.json, applied wherever that face is used.
 *
 * `.has-heading-font-family` is the class core emits for any block set to the
 * display font, so this also covers blocks the client styles later. It is in
 * :where() so a block that chooses its own leading still wins.
 */
:where( .has-heading-font-family ) {
    line-height: var( --wp--custom--type--heading-line-height );
}

/*
 * No heading ends on a word by itself.
 *
 * The rag is the one thing a display setting cannot hide, and at 36px a single
 * trailing word reads as a mistake: the price page ran "Giá tham khảo theo từng
 * dòng" and then "rèm" alone on a line of its own. `pretty` asks the browser to
 * look ahead over the last few lines and pull a word down rather than leave an
 * orphan, which is exactly the fault and no more — the line breaks above it are
 * left as they were.
 *
 * It lives here and not in theme.json because WordPress has no `textWrap` in
 * its typography schema; `textColumns` and `writingMode` are as close as the
 * allowlist gets, so a heading's wrapping cannot be expressed as a token.
 *
 * In :where(), like every other broad rule in this section, and safe to leave
 * unprefixed: a browser that does not know the value drops the declaration and
 * breaks lines the way it always did.
 */
:where( h1, h2, h3, h4, h5, h6 ) {
    text-wrap: pretty;
}

/* One motion contract, honoured everywhere. */
@media ( prefers-reduced-motion: reduce ) {

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        scroll-behavior: auto !important;
        transition-duration: 0.01ms !important;
    }
}

/* ==================================================================== 1. Focus */

/*
 * The client asked for no focus rings, which had been implemented as a blanket
 * `:focus { outline: none !important }` — that also removed the only indicator
 * keyboard users have, failing WCAG 2.4.7. The split below keeps the request
 * for pointer users (a click never draws a ring) and restores a clear ring for
 * keyboard and assistive-tech users, who are the people the criterion is for.
 */
:focus:not( :focus-visible ) {
    outline: none;
}

:focus-visible {
    outline: var( --wp--custom--state--focus-ring-width ) solid var( --wp--custom--state--accent );
    outline-offset: var( --wp--custom--state--focus-ring-offset );
}

/* On the ink sections the dark green ring is invisible, so it flips to the
   light tint, which clears 8:1 against #1A1A1A. */
.has-ink-background-color :focus-visible,
.spring-footer :focus-visible,
.spring-topbar :focus-visible,
.wp-block-cover :focus-visible,
.spring-lead.is-tone-dark :focus-visible,
.wp-block-navigation__responsive-container.is-menu-open :focus-visible {
    outline-color: var( --wp--custom--state--accent-on-dark );
}

/* ==================================================================== 2. Controls */

/*
 * The control contract. Height comes from min-height rather than padding maths
 * so a longer label, a different font-size or an added border can never change
 * a field's box — which is how the select ended up 1px shorter than the inputs
 * beside it.
 */
:where(
    input:not( [type="submit"], [type="button"], [type="reset"], [type="checkbox"], [type="radio"], [type="file"], [type="range"], [type="image"], [type="hidden"] ),
    select,
    textarea
),
.wp-block-search__input,
.woocommerce form .input-text,
.woocommerce form select,
.woocommerce .quantity input.qty,
.wc-block-components-text-input input {
    background-color: var( --wp--custom--control--background );
    border: var( --wp--custom--control--border-width ) solid var( --wp--custom--line--control );
    border-radius: var( --wp--custom--control--radius );
    color: var( --wp--custom--control--text );
    font-family: inherit;
    font-size: var( --wp--custom--control--font-size );
    line-height: var( --wp--custom--control--line-height );
    min-height: var( --wp--custom--control--height );
    padding: var( --wp--custom--control--padding-block ) var( --wp--custom--control--padding-inline );
    transition:
        border-color var( --wp--custom--motion--duration ) ease,
        box-shadow var( --wp--custom--motion--duration ) ease;
    width: 100%;
}

:where( input, textarea )::placeholder {
    color: var( --wp--custom--control--placeholder );
    opacity: 1;
}

:where( textarea ) {
    line-height: 1.7;
    min-height: var( --wp--custom--control--textarea-min-height );
    resize: vertical;
}

/* Native select and the OS triangle are replaced with the same line chevron the
   accordions use, so both read as one family. The colour is baked into the SVG,
   so it is derived from --wp--preset--color--ink (#1A1A1A). */
:where( select ),
.woocommerce form select,
.woocommerce-ordering select {
    appearance: none;
    background-image: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M1.5 4L6 8l4.5-4' stroke='%231A1A1A' stroke-width='1.5'/%3E%3C/svg%3E" );
    background-position: right var( --wp--custom--control--padding-inline ) center;
    background-repeat: no-repeat;
    background-size: 0.75rem 0.75rem;
    padding-inline-end: calc( var( --wp--custom--control--padding-inline ) * 2 + 0.75rem );
}

/* Hover reads as "this is interactive"; focus-visible adds the accent halo. */
:where( input, select, textarea ):hover:not( :disabled ) {
    border-color: var( --wp--custom--line--strong );
}

:where( input, select, textarea ):focus-visible,
.wp-block-search__input:focus-visible,
.woocommerce form .input-text:focus-visible,
.woocommerce form select:focus-visible {
    border-color: var( --wp--custom--state--accent );
    box-shadow: var( --wp--custom--state--focus-halo );
    /* The halo already marks the field; a second outline on top of a 3px ring
       reads as a doubled border. */
    outline: none;
}

.has-ink-background-color :where( input, select, textarea ):focus-visible,
.spring-lead.is-tone-dark :where( input, select, textarea ):focus-visible {
    box-shadow: var( --wp--custom--state--focus-halo-on-dark );
}

:where( input, select, textarea ):disabled {
    cursor: not-allowed;
    opacity: var( --wp--custom--state--disabled-opacity );
}

/* Checkboxes and radios are the exception to the height contract: they are
   glyphs, not boxes, so they only take the accent and a legible size. */
:where( input[type="checkbox"], input[type="radio"] ) {
    accent-color: var( --wp--custom--state--accent );
    block-size: 1.15rem;
    inline-size: 1.15rem;
    margin: 0;
}

/* Error state, shared by the lead form and WooCommerce validation. */
:where( input, select, textarea )[aria-invalid="true"],
.woocommerce-invalid :where( input, select, textarea ) {
    border-color: var( --wp--preset--color--danger );
}

/*
 * Multi-column form layout: the grid owns the gutters (no margins on the
 * fields), and `margin-block-start: auto` pushes each control to the bottom of
 * its cell, so a label that wraps onto two lines can no longer drag its input
 * out of line with the one next to it.
 */
.spring-lead__grid {
    display: grid;
    gap: var( --wp--custom--control--row-gap ) var( --wp--custom--control--column-gap );
    grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
}

.spring-lead__field {
    display: flex;
    flex-direction: column;
    gap: var( --wp--custom--control--label-gap );
    margin: 0;
    min-width: 0;
}

.spring-lead__field > :where( input, select, textarea ) {
    margin-block-start: auto;
}

.spring-lead__field--full {
    grid-column: 1 / -1;
}

@media ( max-width: 700px ) {

    .spring-lead__grid {
        grid-template-columns: 1fr;
    }
}

/* ==================================================================== 3. Buttons */

/*
 * Every action button on the site, from one contract. min-height plus flex
 * centring means a button's box no longer depends on its font-size or on
 * whether it carries a border — which is what let the outline variation render
 * 15px shorter than the fill button sitting next to it in the same row.
 *
 * Deliberately absent: a bare `button` selector. The navigation toggle, the
 * accordion summaries, the slider arrows and the gallery thumbnails are all
 * buttons that must not look like calls to action.
 */
.wp-element-button,
.wp-block-button__link,
.wp-block-search__button,
.spring-slider__cta,
.spring-lead__submit,
.spring-product-cta__primary,
.spring-product-cta__secondary,
.woocommerce a.button,
.woocommerce button.button,
.woocommerce input.button,
.woocommerce #respond input#submit,
.woocommerce a.button.alt,
.woocommerce button.button.alt {
    align-items: center;
    border-radius: var( --wp--custom--button--radius );
    border-style: solid;
    border-width: var( --wp--custom--button--border-width );
    cursor: pointer;
    display: inline-flex;
    font-family: inherit;
    font-size: var( --wp--custom--button--font-size );
    font-weight: 600;
    gap: var( --wp--custom--button--icon-gap );
    justify-content: center;
    letter-spacing: var( --wp--custom--button--letter-spacing );
    line-height: var( --wp--custom--button--line-height );
    min-height: var( --wp--custom--button--height );
    padding-block: var( --wp--custom--button--padding-block );
    padding-inline: var( --wp--custom--button--padding-inline );
    text-align: center;
    text-decoration: none;
    text-transform: uppercase;
    transition:
        background-color var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease ),
        border-color var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease ),
        color var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease ),
        transform var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease );
    width: auto;
}

/*
 * A button that gives nothing back on press feels broken on a phone, where
 * there is no hover state to have confirmed the target first and the tap may
 * be followed by a page load the visitor has to wait out. The scale is small
 * enough to be felt rather than seen, and it is the press itself — not a state
 * the button then holds — so it costs nothing on the way back.
 *
 * Not `scale(0)`-style theatre and not a shadow: these buttons sit on
 * photographs as often as on paper, and a transform is the one thing that reads
 * the same on both.
 */
.wp-element-button:active,
.wp-block-button__link:active,
.wp-block-search__button:active,
.spring-slider__cta:active,
.spring-lead__submit:active,
.spring-product-cta__primary:active,
.spring-product-cta__secondary:active,
.woocommerce a.button:active,
.woocommerce button.button:active,
.woocommerce input.button:active,
.woocommerce #respond input#submit:active {
    transform: scale( 0.97 );
}

/* Primary skin. theme.json already colours core buttons; these are the ones
   core does not know about. */
.spring-slider__cta,
.spring-lead__submit,
.spring-product-cta__primary,
.woocommerce a.button,
.woocommerce button.button,
.woocommerce input.button,
.woocommerce #respond input#submit {
    background-color: var( --wp--custom--state--accent );
    border-color: var( --wp--custom--state--accent );
    color: var( --wp--preset--color--base );
}

.spring-slider__cta:hover,
.spring-slider__cta:focus-visible,
.spring-lead__submit:hover,
.spring-lead__submit:focus-visible,
.spring-product-cta__primary:hover,
.spring-product-cta__primary:focus-visible,
.woocommerce a.button:hover,
.woocommerce button.button:hover,
.woocommerce input.button:hover,
.woocommerce #respond input#submit:hover {
    background-color: var( --wp--custom--state--hover-surface );
    border-color: var( --wp--custom--state--hover-surface );
    color: var( --wp--custom--state--hover-text );
}

/* Secondary skin: an outlined button that is the same box as the filled one. */
.spring-product-cta__secondary,
.spring-slider__cta--ghost {
    background-color: transparent;
    border-color: var( --wp--custom--line--control );
    color: var( --wp--preset--color--ink );
}

.spring-product-cta__secondary:hover,
.spring-product-cta__secondary:focus-visible {
    background-color: var( --wp--custom--state--hover-surface );
    border-color: var( --wp--custom--state--hover-surface );
    color: var( --wp--custom--state--hover-text );
}

/*
 * A ghost button on a photograph inverts on hover: it fills with the light and
 * the label goes dark. Two components need it — the slider's own anchor, and
 * the consultation cover's outline button, which is a core button block.
 *
 * The cover's button used to carry its colours as inline styles, so it had no
 * hover at all: inline beats any stylesheet rule, and the one hover rule that
 * could have reached it excludes buttons with `has-text-color`. Declaring both
 * states here instead means neither needs !important.
 */
.spring-slider__cta--ghost,
.spring-ghost-on-dark > .wp-block-button__link {
    background-color: transparent;
    border-color: var( --wp--custom--on-dark--muted );
    color: var( --wp--preset--color--base );
}

.spring-slider__cta--ghost:hover,
.spring-slider__cta--ghost:focus-visible {
    background-color: var( --wp--preset--color--base );
    border-color: var( --wp--preset--color--base );
    color: var( --wp--preset--color--ink );
}

/*
 * The core-button variant needs the same weight as the Twenty Twenty-Four fix
 * below it. The parent theme hovers outline buttons at (0,5,0) with
 * `contrast-2 -> contrast -> transparent`; this palette has neither slug, so
 * that rule resolves to a transparent fill and white text — no visible change
 * on a dark cover. Only border-color got through until this matched its
 * weight, because TT4's rule does not set one.
 */
:root .wp-block-button.is-style-outline.spring-ghost-on-dark > .wp-block-button__link:hover,
:root .wp-block-button.is-style-outline.spring-ghost-on-dark > .wp-block-button__link:focus-visible {
    background-color: var( --wp--preset--color--base );
    border-color: var( --wp--preset--color--base );
    color: var( --wp--preset--color--ink );
}

/*
 * Twenty Twenty-Four hovers outline buttons with
 * var(--wp--preset--color--contrast-2, var(--wp--preset--color--contrast, transparent)).
 * This palette has neither slug, so the chain fell through to transparent while
 * the text turned base — near-white lettering on a white page. The :root prefix
 * outranks the parent rule; buttons that set their own colour are left alone,
 * as are the on-dark ghosts above, which invert to light instead of to ink.
 */
:root .wp-block-button.is-style-outline:not( .spring-ghost-on-dark ) > .wp-block-button__link:not( .has-text-color, .has-background ):hover,
:root .wp-block-button.is-style-outline:not( .spring-ghost-on-dark ) > .wp-block-button__link:not( .has-text-color, .has-background ):focus,
:root .wp-block-button:not( .spring-ghost-on-dark ) .wp-block-button__link.is-style-outline:not( .has-text-color, .has-background ):hover,
:root .wp-block-button:not( .spring-ghost-on-dark ) .wp-block-button__link.is-style-outline:not( .has-text-color, .has-background ):focus {
    background-color: var( --wp--custom--state--hover-surface );
    border-color: var( --wp--custom--state--hover-surface );
    color: var( --wp--custom--state--hover-text );
}

/* A row of buttons is a row of controls: one gutter, from the scale. The core
   block's own gutter is set through blockGap in theme.json, because the layout
   rule .wp-block-buttons.is-layout-flex outranks any plain class selector. */
.spring-product-cta,
.spring-slider__actions,
.spring-lead__actions {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: var( --wp--preset--spacing--10 );
    /* No margin reset here: these rows are <p> elements whose block margin is
       what separates them from the field or copy above, and each component sets
       that distance itself. */
    margin-inline: 0;
}

/* Search puts a control and a button in one row, so they must share a height. */
.wp-block-search__inside-wrapper {
    align-items: stretch;
    gap: var( --wp--preset--spacing--05 );
}

.wp-block-search__button {
    flex: none;
    margin: 0;
}

.wp-block-search.wp-block-search__button-inside .wp-block-search__inside-wrapper {
    gap: 0;
}

.wp-block-search.wp-block-search__button-inside .wp-block-search__input {
    border: 0;
    min-height: calc( var( --wp--custom--control--height ) - var( --wp--custom--control--border-width ) * 2 );
}

/* Pagination: the same control geometry, at a compact width. */
.wp-block-query-pagination .wp-block-query-pagination-numbers .page-numbers,
.wp-block-query-pagination > a,
.woocommerce nav.woocommerce-pagination ul li a,
.woocommerce nav.woocommerce-pagination ul li span {
    align-items: center;
    border: var( --wp--custom--control--border-width ) solid var( --wp--custom--line--base );
    border-radius: var( --wp--custom--control--radius );
    display: inline-flex;
    justify-content: center;
    min-height: var( --wp--custom--control--height );
    min-width: var( --wp--custom--control--height );
    padding-inline: var( --wp--preset--spacing--05 );
    text-decoration: none;
    transition:
        background-color var( --wp--custom--motion--duration ) ease,
        border-color var( --wp--custom--motion--duration ) ease,
        color var( --wp--custom--motion--duration ) ease;
}

.wp-block-query-pagination .page-numbers:hover,
.wp-block-query-pagination > a:hover,
.woocommerce nav.woocommerce-pagination ul li a:hover {
    border-color: var( --wp--custom--line--strong );
}

.wp-block-query-pagination .page-numbers.current,
.woocommerce nav.woocommerce-pagination ul li span.current {
    background-color: var( --wp--custom--state--hover-surface );
    border-color: var( --wp--custom--state--hover-surface );
    color: var( --wp--custom--state--hover-text );
}

/*
 * WooCommerce prints its pager as a sibling of the product grid with no spacing
 * of its own, so the page numbers sat flush against the last row of cards. The
 * block templates give core's pager `spacing|40`; this matches it, so both
 * pagers stand off their list by the same distance.
 */
.woocommerce nav.woocommerce-pagination {
    margin-block-start: var( --wp--preset--spacing--40 );
}

.woocommerce nav.woocommerce-pagination ul,
.woocommerce nav.woocommerce-pagination ul li {
    border: 0;
    margin: 0;
}

.woocommerce nav.woocommerce-pagination ul {
    display: flex;
    gap: var( --wp--preset--spacing--05 );
    justify-content: center;
}

/* ==================================================================== 4. Rhythm */

/*
 * Core gives every top-level section a block-gap margin, which showed up as an
 * empty white strip under the sticky header and above the dark footer. Both
 * sections carry their own padding, so the gap is pure dead space.
 */
.wp-site-blocks > main,
.wp-site-blocks > footer {
    margin-block-start: 0;
}

/* Flow rhythm inside long-form content, from the scale rather than per block.
   Full-width section groups are excluded: they carry their own block padding
   from the section contract below, and the extra margin was stacking a third
   gap on top of two already-generous pads. */
/* The two :not()s are wrapped in :where() so they filter what matches without
   adding weight. Unwrapped they made this selector (0,3,0) — heavier than the
   heading-rhythm rules further down, which then silently lost to it. */
.wp-block-post-content > * + *:where( :not( .spring-section ):not( .alignfull ) ),
.spring-prose > * + * {
    margin-block-start: var( --wp--preset--spacing--20 );
}

/*
 * -------------------------------------------------- Section contract
 *
 * Every full-width band on a composed page is a `.spring-section`, and this is
 * the only place its block padding is set. Before this, all ten homepage
 * patterns each wrote `spacing|50` top and bottom inline, so a junction between
 * two neighbours cost 120 + 28 + 120 = 268px at 1440 — read as the page falling
 * apart rather than as breathing room.
 *
 * The pads are deliberately not symmetrical with the old value: 72px a side
 * gives a 144px junction, which is generous for a 1320px editorial field.
 */
.spring-section {
    padding-block: var( --wp--preset--spacing--40 );
}

/*
 * Core's constrained-layout block gap puts another 28px between every child of
 * the post content, on top of the two pads above. Sections own their own
 * spacing, so the gap is zeroed here. The class is doubled to clear core's
 * `.wp-container-xxx > * + *`, which is generated inline in the document head
 * and would otherwise win on source order.
 */
.wp-block-post-content > .spring-section.spring-section,
.wp-block-post-content > .spring-slider.spring-slider {
    margin-block-start: 0;
}

/*
 * The same gap appears one level up. Templates that compose their page in the
 * template itself rather than in post content — every archive, the blog index
 * and the single post — put their bands directly in <main>, which is also a
 * constrained layout, so core inserted 28px between each one. On the single
 * post that showed as a white strip between the title band and the full-bleed
 * featured image, which are meant to touch.
 */
main.wp-block-group > * {
    margin-block-start: 0;
}

/*
 * Two neighbours on the same ground have no colour boundary to separate them,
 * so the junction is drawn with a hairline instead of paid for with more empty
 * space. It is inset to the wide measure rather than bled full-width, so it
 * reads as a rule between two sections and not as a page-wide divider.
 */
.spring-section:not( [class*="-background-color"] ) + .spring-section:not( [class*="-background-color"] ) {
    position: relative;
}

:where( .spring-section:not( [class*="-background-color"] ) + .spring-section:not( [class*="-background-color"] ) )::before {
    background-color: var( --wp--custom--line--soft );
    block-size: 1px;
    content: "";
    inline-size: min( 100% - 2 * var( --wp--preset--spacing--20 ), var( --wp--style--global--wide-size ) );
    inset-block-start: 0;
    inset-inline-start: 50%;
    position: absolute;
    transform: translateX( -50% );
}

/*
 * The band directly under the hero is part of the opening statement, not the
 * next thought: it shares the hero's ink ground and drops the junction to zero,
 * so the slider and its proof numbers read as one block. Keyed on the slider so
 * it can only ever fire on the front page — the same patterns are reused on
 * Giới thiệu and Dịch vụ, where they must stay light.
 */
.spring-slider + .spring-section {
    background-color: var( --wp--preset--color--ink );
    color: var( --wp--custom--on-dark--text );
    padding-block: var( --wp--preset--spacing--30 );
}

/* That plinth carries no background *class*, so the hairline rule above would
   otherwise mistake it and the section after it for two neighbours on white.
   The rule it overrides is wrapped in :where() precisely so this can win. */
.spring-slider + .spring-section + .spring-section::before {
    content: none;
}

.spring-prose p,
.spring-prose li {
    line-height: 1.85;
}

/*
 * A prose group can run the full wide field so its tables and figures have room
 * to breathe, but the running text must not: at 1320px a paragraph is about 155
 * characters. Text takes the measure, data keeps the width.
 *
 * The column is measured in rem, not ch. A ch is a fraction of the element's
 * own font size, so `68ch` on a 52px h2 resolves to 2121px — no constraint at
 * all — while the 17px paragraph below it stopped at 781px. The headings ran
 * the full container and the body sat narrower, leaving two different left
 * edges in the same block. One absolute width keeps the column square.
 */
.spring-prose > :where( p, h2, h3, h4, ul, ol ) {
    max-width: var( --wp--custom--measure--column );
}

/*
 * A heading inside a text column is not a section head, and it must not be
 * sized like one. Both were xx-large, so an article's title and the headings
 * inside it came out at exactly 52px — you could not tell the title of the
 * piece from a subheading, and three of the four wrapped to two lines in a
 * 768px column. Full-width section heads keep 52px; only headings inside a
 * prose column step down.
 */
.spring-prose > h2 {
    font-size: var( --wp--preset--font-size--x-large );
}

.spring-prose > h3 {
    font-size: var( --wp--preset--font-size--large );
}

.spring-prose > h4 {
    font-size: var( --wp--preset--font-size--medium-large );
}

/*
 * A heading belongs to what follows it, so it takes more air above than below.
 * Every block was separated by the same 28px, which left each heading floating
 * equidistant between the section it closed and the one it opened.
 */
/* The class is doubled to clear core's generated `.wp-container-xxx > * + *`
   block-gap rule, which is inline in the document head at the same weight. */
.spring-prose.spring-prose > * + :where( h2, h3, h4 ) {
    margin-block-start: var( --wp--preset--spacing--30 );
}

.spring-prose.spring-prose > :where( h2, h3, h4 ) + * {
    margin-block-start: var( --wp--preset--spacing--10 );
}

/* An article ended straight into the consultation CTA with no route back to
   the index. The rule marks the end of the piece before that hand-off. */
.spring-post-back {
    border-block-start: 1px solid var( --wp--custom--line--base );
    max-width: var( --wp--custom--measure--column );
    padding-block-start: var( --wp--preset--spacing--20 );
}

/* A wide image inside a prose column is a breath between sections of text, so
   it takes the full field rather than the 48rem measure the copy sits in. */
.spring-about__image {
    margin-block-start: var( --wp--preset--spacing--40 );
}

.spring-about__image img {
    aspect-ratio: 16 / 9;
    object-fit: cover;
    width: 100%;
}

/* -------------------------------------------------- Price table */

/*
 * Core's stripe style paints alternate rows #F0F0F0 — a cold grey with no place
 * in a warm sand palette, and the only filled-row device on a site that
 * separates everything else with hairlines. Rules instead, matching the FAQ
 * accordion and the section junctions.
 */
.spring-price-table {
    margin-block-end: var( --wp--preset--spacing--30 );
}

.spring-price-table table {
    border-collapse: collapse;
    width: 100%;
}

/* Not :where() here: core ships `.wp-block-table td{border:1px solid}` at 0,1,1,
   which would keep painting the column separators through a 0,1,0 rule. */
.wp-block-table.spring-price-table :where( th, td ) {
    border: 0;
    border-block-end: 1px solid var( --wp--custom--line--base );
    /* Tighter than the page's usual rhythm on purpose: the job here is scanning
       down a column of prices, and fourteen airy rows make that a scroll. */
    padding: var( --wp--preset--spacing--10 ) var( --wp--preset--spacing--20 ) var( --wp--preset--spacing--10 ) 0;
    text-align: start;
    vertical-align: baseline;
}

.wp-block-table.spring-price-table thead th {
    border-block-end-color: var( --wp--custom--line--strong );
    color: var( --wp--preset--color--muted );
    font-family: var( --wp--preset--font-family--body );
    font-size: var( --wp--preset--font-size--x-small );
    font-weight: 600;
    letter-spacing: 0.14em;
    padding-block-end: var( --wp--preset--spacing--10 );
    text-transform: uppercase;
}

.wp-block-table.spring-price-table tbody tr:last-child :where( th, td ) {
    border-block-end: 0;
}

/*
 * The middle column is the reason anyone opens this page, so it carries the
 * weight the row name used to share equally. Tabular figures line the digits up
 * down the column, which is what makes a price list comparable at a glance —
 * proportional numerals put every thousands separator in a different place.
 */
.wp-block-table.spring-price-table tbody td:nth-child( 2 ) {
    font-feature-settings: "tnum";
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    white-space: nowrap;
}

.wp-block-table.spring-price-table tbody td:nth-child( 3 ) {
    color: var( --wp--preset--color--muted );
    font-size: var( --wp--preset--font-size--small );
}

.spring-price-table figcaption {
    max-width: var( --wp--custom--measure--prose );
}

@media ( max-width: 767px ) {

    /* The figure already scrolls; keep the columns from collapsing to shreds. */
    .spring-price-table table {
        min-width: 34rem;
    }
}

/* Optical line lengths, so every intro block is measured the same way. */
.spring-section-head {
    max-width: var( --wp--custom--measure--head );
}

/* A panel, not a paragraph: it holds a display heading as well as body copy, so
   it is measured in rem. A ch measure here is sized by the body font and would
   wrap the heading to the width of the text beneath it. */
.spring-lead__intro {
    max-width: var( --wp--custom--measure--panel );
}

.spring-term-intro,
.woocommerce div.product .woocommerce-product-details__short-description {
    max-width: var( --wp--custom--measure--intro );
}

/* ==================================================================== 5. Cards & grids */

/*
 * Editorial and project cards. The grid stretches every card to the row height,
 * so the card itself becomes a flex column: the excerpt takes the slack and any
 * action lands on the baseline of the row instead of floating under short copy.
 */
.spring-card {
    display: flex;
    flex-direction: column;
    height: 100%;
}

.spring-card > * {
    margin-block: 0;
}

.spring-card > * + * {
    margin-block-start: var( --wp--preset--spacing--05 );
}

/* The image is the card's lid: it never inherits the flow gap. Its bottom
   margin is set with the rest of the loop proximity rules further down. */
.spring-card > .wp-block-post-featured-image {
    overflow: hidden;
}

.spring-card > .wp-block-post-excerpt {
    margin-block-start: var( --wp--preset--spacing--05 );
}

/* Two-line titles keep the price and the action of neighbouring cards aligned;
   a third line would push one card's footer out of the row. */
.spring-card .wp-block-post-title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
}

.spring-card .wp-block-post-featured-image img {
    display: block;
    object-fit: cover;
    transition: transform var( --wp--custom--motion--duration-slow ) var( --wp--custom--motion--ease );
    width: 100%;
}

.spring-card:hover .wp-block-post-featured-image img {
    transform: scale( 1.04 );
}

/* Category and project tiles: an image with a caption underneath. Kept separate
   from .spring-card because the tile's crop is portrait and fixed. */
.spring-tile {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
}

.spring-tile figure,
.spring-tile .wp-block-image {
    margin: 0;
}

.spring-tile img {
    aspect-ratio: 4 / 5;
    display: block;
    object-fit: cover;
    transition: transform var( --wp--custom--motion--duration-slow ) var( --wp--custom--motion--ease );
    width: 100%;
}

.spring-tile:hover img {
    transform: scale( 1.06 );
}

@media ( prefers-reduced-motion: reduce ) {

    .spring-tile img,
    .spring-card .wp-block-post-featured-image img {
        transition: none;
    }

    .spring-tile:hover img,
    .spring-card:hover .wp-block-post-featured-image img {
        transform: none;
    }
}

/*
 * Loop proximity: the gap between two cards always beats the gap inside one.
 *
 * A caption in a loop sits between two photographs, and the eye assigns it to
 * whichever one is nearer. Measured on the category grid at 390px, a tile put
 * 24px between an image and its own title and 20px between that title's count
 * line and the next image — the label was marginally closer to the photograph
 * it did not describe, so the column read as an unlabelled stack.
 *
 * The fix is ratio rather than size, and it has two halves. The captions bind
 * to their image at one step of the scale (that half lives in the patterns, and
 * in the featured-image rule below). The rows are pushed apart at three, here.
 *
 * Row gap only. The column gap is the measure the grid was designed on and is
 * untouched, so a row of tiles keeps its rhythm; it is the vertical seam
 * between rows that has to out-measure a caption, and that seam exists at every
 * width — a five-column grid of ten tiles has one too. Below 600px the loop is
 * a single column, the seam is the only thing separating one card from the
 * next, and it takes the wider step.
 */
.spring-section .spring-tile-grid,
.spring-section .wp-block-post-template.is-layout-grid {
    row-gap: var( --wp--preset--spacing--30 );
}

/* The image is the card's lid, so it binds to the title beneath it, not to the
   card above. Same step as .spring-card's own flow gap. */
.spring-card > .wp-block-post-featured-image {
    margin-block-end: var( --wp--preset--spacing--05 );
}

@media ( max-width: 599px ) {

    /*
     * The tile grid is an index, not a loop. Core's responsive grid keeps a
     * 200px minimum track, so on a phone the ten curtain types collapse to a
     * single column and the section becomes a ten-screen scroll between the
     * heading that introduces it and the next one. Two tracks is what the
     * editor shows at its own canvas width and what the section was drawn as:
     * a name and its crop are still legible at ~150px, and the whole index
     * fits in five rows.
     *
     * Two classes, so this beats the .wp-container-… rule the grid layout
     * support prints for the block's own columnCount/minimumColumnWidth pair.
     */
    .spring-section .spring-tile-grid {
        grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
    }

    /*
     * Row gap stays at the grid's own step here. The wider step below exists
     * because a single column puts nothing but the seam between two cards; a
     * two-track row has the column gap doing that work as well, and pulling
     * the rows further apart would break the block into floating pairs.
     */
    .spring-section .wp-block-post-template.is-layout-grid {
        row-gap: var( --wp--preset--spacing--40 );
    }
}

/*
 * Columns stack at 781px in core, and the same proximity problem arrives with
 * them: a service column is an image with a heading and a paragraph under it,
 * and the 32px that separated two columns was only half a step clear of the
 * 20px that held a heading to its own photograph.
 *
 * Only sets of three or more, which is what a column set built as a list of
 * peers looks like. A two-column set on this site is a pair — one photograph
 * beside the story it illustrates — and pulling a pair apart is the opposite of
 * what proximity asks for, so those keep the tighter step.
 */
@media ( max-width: 781px ) {

    .spring-section .wp-block-columns:has( > .wp-block-column:nth-child( 3 ) ) {
        row-gap: var( --wp--preset--spacing--40 );
    }
}

/* ==================================================================== 6. WooCommerce */

/*
 * The store runs in catalogue mode by design (the brief leaves online ordering
 * unchecked), so cart, checkout and account are redirected away in
 * Spring_Woo_Catalog. The form and table primitives below are still defined:
 * they cost nothing, and they mean that re-enabling purchasing inherits the
 * design system instead of arriving as unstyled WooCommerce defaults.
 */

.woocommerce-products-header,
.woocommerce-result-count,
.woocommerce-ordering {
    display: none;
}

/* -------------------------------------------------- Category filter */

/*
 * The shop archive listed all thirty products across three pages with no way to
 * reach the ten categories, even though every one of them has a description and
 * a thumbnail and is linked from the homepage and the footer. This is that
 * missing navigation. It sits on both the shop root and the category archives,
 * so switching category never means going back to the menu.
 *
 * The pills borrow the button contract wholesale — same radius, border width,
 * tracking and ink-on-hover inversion — rather than inventing a control style.
 */
.spring-filter-band {
    padding-block: var( --wp--preset--spacing--20 );
}

.spring-filter__list {
    display: flex;
    flex-wrap: wrap;
    gap: var( --wp--preset--spacing--05 );
    list-style: none;
    margin: 0;
    padding: 0;
}

.spring-filter__item {
    align-items: baseline;
    border: var( --wp--custom--button--border-width ) solid var( --wp--custom--line--base );
    border-radius: var( --wp--custom--button--radius );
    color: var( --wp--preset--color--ink );
    display: inline-flex;
    font-size: var( --wp--preset--font-size--x-small );
    gap: 0.5em;
    letter-spacing: 0.06em;
    padding: 0.7em 1.1em;
    transition:
        background-color var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease ),
        border-color var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease ),
        color var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease );
}

.spring-filter__count {
    color: var( --wp--preset--color--muted );
    font-variant-numeric: tabular-nums;
}

.spring-filter__item:hover,
.spring-filter__item:focus-visible {
    background-color: var( --wp--custom--state--hover-surface );
    border-color: var( --wp--custom--state--hover-surface );
    color: var( --wp--custom--state--hover-text );
}

/* The page you are on is stated, not merely tinted: colour alone would not
   survive a monochrome display or a colour-vision deficiency. */
.spring-filter__item[aria-current="page"] {
    background-color: var( --wp--custom--state--hover-surface );
    border-color: var( --wp--custom--state--hover-surface );
    color: var( --wp--custom--state--hover-text );
    cursor: default;
}

.spring-filter__item:hover .spring-filter__count,
.spring-filter__item:focus-visible .spring-filter__count,
.spring-filter__item[aria-current="page"] .spring-filter__count {
    color: var( --wp--custom--on-dark--faint );
}

/*
 * Wrapped, eleven pills take five rows on a phone and push the first product
 * most of a screen down. Below the tablet breakpoint they become one scrolling
 * strip instead: the part-visible pill at the trailing edge is the affordance,
 * and the products stay where a thumb expects them.
 */
@media ( max-width: 699px ) {

    .spring-filter__list {
        flex-wrap: nowrap;
        overflow-x: auto;
        overscroll-behavior-inline: contain;
        scrollbar-width: none;
        scroll-snap-type: inline proximity;
    }

    .spring-filter__list::-webkit-scrollbar {
        display: none;
    }

    .spring-filter__list > li {
        flex: 0 0 auto;
        scroll-snap-align: start;
    }
}

/* -------------------------------------------------- Product loop */

.wc-block-grid,
.woocommerce ul.products,
ul.products {
    display: grid;
    gap: var( --wp--preset--spacing--30 ) var( --wp--preset--spacing--20 );
    grid-template-columns: repeat( 3, minmax( 0, 1fr ) );
    list-style: none;
    margin: 0;
    padding: 0;
}

.woocommerce ul.products::before,
.woocommerce ul.products::after,
ul.products::before,
ul.products::after {
    content: none;
}

/* WooCommerce still emits floats and percentage widths for its own float grid;
   the grid above supersedes them, so they are unwound here. The card becomes a
   flex column so the price and the "view detail" link align across a row. */
ul.products li.product {
    display: flex;
    flex-direction: column;
    float: none !important;
    margin: 0 !important;
    text-align: start;
    width: 100% !important;
}

ul.products li.product > * {
    margin-block: 0;
}

ul.products li.product a {
    text-decoration: none;
}

ul.products li.product a img {
    aspect-ratio: 4 / 5;
    margin: 0 0 var( --wp--preset--spacing--05 );
    object-fit: cover;
    transition: opacity var( --wp--custom--motion--duration ) ease;
    width: 100%;
}

ul.products li.product:hover a img {
    opacity: 0.88;
}

ul.products li.product .woocommerce-loop-product__title {
    /* Two lines keep the price row level across the grid. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    font-family: var( --wp--preset--font-family--heading );
    font-size: var( --wp--preset--font-size--large ) !important;
    font-weight: 400;
    line-height: 1.35;
    margin: 0 0 var( --wp--preset--spacing--05 ) !important;
    overflow: hidden;
    padding: 0 !important;
}

ul.products li.product .price {
    color: var( --wp--preset--color--ink ) !important;
    display: block;
    font-size: var( --wp--custom--control--font-size ) !important;
    margin: 0 !important;
}

/* The catalogue CTA is the card's footer, so it is pinned to the bottom. */
ul.products li.product .spring-loop-cta {
    margin-block-start: auto;
    padding-block-start: var( --wp--preset--spacing--05 );
}

ul.products li.product .onsale,
.woocommerce span.onsale {
    display: none;
}

/*
 * Two columns, and they hold all the way down. A catalogue is browsed by
 * comparison — the customer is picking a curtain type by how it hangs — and a
 * single column shows one photograph at a time, so the phone gets the same
 * side-by-side reading as the category index above. The base gap carries: with
 * two tracks the column gutter separates a pair, so the row seam does not have
 * to take the wider step a stacked list needed.
 */
@media ( max-width: 1023px ) {

    .woocommerce ul.products,
    ul.products {
        grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
    }
}

/*
 * The loop title is sized for a third of the content width. At half a phone it
 * is set beside its own crop, so it steps down to the body scale and keeps the
 * two-line clamp — a curtain name is three or four words and reads in two
 * lines at this measure, where at the large size it would have been cut.
 */
@media ( max-width: 599px ) {

    ul.products li.product .woocommerce-loop-product__title {
        font-size: var( --wp--preset--font-size--medium ) !important;
    }
}

/* -------------------------------------------------- Single product */

/*
 * The gallery runs 952px tall (a 4/5 crop plus its thumbnail strip) while the
 * summary is 352px, so 628px of the most important column on the site was
 * empty and the quote button scrolled out of reach almost immediately. Pinning
 * the summary keeps the price and the three ways to get a quote on screen for
 * the whole length of the gallery. Only from the breakpoint where the two sit
 * side by side; stacked, there is nothing to stick to.
 */
/*
 * WooCommerce lays the top of a product page out with floats inside a plain
 * block wrapper, which makes the wrapper — gallery, summary, tabs and related
 * products, all 1971px of it — the containing block for anything sticky. The
 * pinned summary therefore kept travelling long after its own row had ended and
 * printed itself over the description and the related grid.
 *
 * A grid fixes it at the root: a grid item's sticky containing block is its own
 * grid area, so the summary can only move within row one and comes to rest when
 * the gallery does. Everything other than those two spans the full width, so an
 * element this rule does not know about lands on its own row rather than in the
 * gallery column.
 */
@media ( min-width: 1024px ) {

    .spring-product-top {
        align-items: start;
        column-gap: var( --wp--preset--spacing--30 );
        display: grid;
        grid-template-columns: minmax( 0, 1fr ) minmax( 0, 1fr );
    }

    /* Woo's own 48% floats are what the grid replaces. */
    .spring-product-top > .woocommerce-product-gallery,
    .spring-product-top > .summary.entry-summary {
        float: none !important;
        margin: 0 !important;
        width: auto !important;
    }

    /* Bounded by .spring-product-top, so it comes to rest with the gallery. */
    .spring-product-top > .summary.entry-summary {
        position: sticky;
        top: var( --wp--custom--header--sticky-offset );
    }
}

/*
 * The description ran the full 1320px of the wide field, about 155 characters a
 * line — roughly double what stays readable. The prose measure already exists
 * for exactly this and was simply never applied to the WooCommerce tabs.
 */
.woocommerce div.product .woocommerce-Tabs-panel > p,
.woocommerce div.product .woocommerce-Tabs-panel > ul,
.woocommerce div.product .woocommerce-Tabs-panel > ol {
    max-width: var( --wp--custom--measure--prose );
}

/* -------------------------------------------------- Key facts list */

/*
 * A label-and-value list: the short answers people want before they act. Used
 * for a product's category, warranty and pricing unit, and on the contact page
 * for the details that are not themselves actions.
 *
 * On the product page the first row is also the only route back to the
 * category — WooCommerce's breadcrumb is off in catalogue mode, which left the
 * product page with no way back to its category or the shop.
 */
.spring-facts,
.spring-product-facts {
    border-block-start: 1px solid var( --wp--custom--line--base );
    display: grid;
    gap: var( --wp--preset--spacing--05 ) var( --wp--preset--spacing--20 );
    grid-template-columns: auto 1fr;
    margin-block-start: var( --wp--preset--spacing--20 );
    padding-block-start: var( --wp--preset--spacing--15 );
}

.spring-facts dt,
.spring-product-facts dt {
    color: var( --wp--preset--color--muted );
    font-size: var( --wp--preset--font-size--x-small );
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.spring-facts dd,
.spring-product-facts dd {
    font-size: var( --wp--preset--font-size--small );
    margin: 0;
    overflow-wrap: anywhere;
}

.woocommerce div.product .product_title {
    font-family: var( --wp--preset--font-family--heading );
    font-size: var( --wp--preset--font-size--x-large );
    font-weight: 400;
    line-height: var( --wp--custom--type--heading-line-height );
    margin-block-end: var( --wp--preset--spacing--05 );
}

.woocommerce div.product p.price,
.woocommerce div.product span.price {
    color: var( --wp--preset--color--ink );
    font-size: var( --wp--preset--font-size--large );
    margin-block-end: var( --wp--preset--spacing--15 );
}

.woocommerce div.product .woocommerce-product-details__short-description {
    color: var( --wp--preset--color--muted );
    line-height: 1.75;
}

/*
 * Nothing had ever set this, so the tab bar inherited a div's zero and sat
 * directly on the gallery — the labels read as a caption belonging to the last
 * photograph rather than as the heading of a new section. WooCommerce gives the
 * related grid below 5em of its own, so the tabs were the one seam on the page
 * with no gap at all.
 *
 * 40 and not 30: the bar keeps the tighter 30 between itself and its own panel,
 * so the break above has to be the larger of the two or the labels group upward
 * into the gallery instead of downward onto the prose they title.
 *
 * Padding, not margin. Below 1024px the top of the page is Woo's two floats
 * again and this element carries `clear: both`, so its position is set by
 * clearance — which is measured to the float's bottom edge and swallows whatever
 * top margin it finds on the way. A margin of 48px was arriving as 33px of
 * actual gap. Padding is inside the box, so clearance cannot spend it, and with
 * no background or border here the two are otherwise indistinguishable.
 */
.woocommerce div.product .woocommerce-tabs {
    padding-block-start: var( --wp--preset--spacing--40 );
}

.woocommerce div.product .woocommerce-tabs ul.tabs {
    border-bottom: var( --wp--custom--control--border-width ) solid var( --wp--custom--line--base );
    display: flex;
    flex-wrap: wrap;
    gap: var( --wp--preset--spacing--30 );
    margin: 0 0 var( --wp--preset--spacing--30 );
    padding: 0;
}

.woocommerce div.product .woocommerce-tabs ul.tabs::before,
.woocommerce div.product .woocommerce-tabs ul.tabs li::before,
.woocommerce div.product .woocommerce-tabs ul.tabs li::after {
    border: 0;
    content: none;
}

.woocommerce div.product .woocommerce-tabs ul.tabs li {
    background: transparent;
    border: 0;
    border-radius: 0;
    margin: 0;
    padding: 0 0 var( --wp--preset--spacing--05 );
}

.woocommerce div.product .woocommerce-tabs ul.tabs li a {
    color: var( --wp--preset--color--muted );
    font-size: var( --wp--preset--font-size--x-small );
    font-weight: 600;
    letter-spacing: 0.12em;
    padding: 0;
    text-transform: uppercase;
}

.woocommerce div.product .woocommerce-tabs ul.tabs li.active a {
    color: var( --wp--preset--color--ink );
}

.woocommerce div.product .woocommerce-tabs ul.tabs li.active {
    border-bottom: 2px solid var( --wp--custom--state--accent );
}

.woocommerce div.product .woocommerce-tabs .panel h2 {
    display: none;
}

.woocommerce .related.products > h2,
.woocommerce .up-sells > h2 {
    font-family: var( --wp--preset--font-family--heading );
    font-size: var( --wp--preset--font-size--x-large );
    font-weight: 400;
    margin-block-end: var( --wp--preset--spacing--30 );
}

/*
 * The gallery is a square window, whatever shape the photograph inside it is.
 *
 * FlexSlider measures the slide it is showing and writes that onto
 * .flex-viewport as an inline height, so the frame was only ever as tall as the
 * current picture: 507px for the two portrait shots and 253px for a landscape
 * one. Clicking that third thumbnail collapsed the frame by 254px and dragged
 * the tabs and everything below them up the page. Pinning the ratio makes the
 * height a function of the column's width alone — the same for every slide, and
 * settled before the images arrive rather than after.
 *
 * Square, and not the 4/5 the product grid and the tiles are cut to: at the
 * width this column gets on a desktop, 4/5 stood the photograph 796px tall and
 * pushed the price and the three ways to ask for a quote most of the way off the
 * first screen. The grid can afford the taller crop because its cells are a
 * third of this wide; here the frame has to leave room for the column beside it.
 *
 * The placeholder is in here too. A product with no photographs yet gets Woo's
 * square stand-in under a class of its own, so it sat at 1/1 while every real
 * gallery sat at 4/5 — meaning the page would have changed height the moment the
 * client uploaded the actual pictures, which is the shift this rule exists to
 * prevent. Framing the stand-in the same way makes the empty state the right
 * shape in advance.
 *
 * :not(.zoomImg) is a leftover guard rather than a live one — the hover zoom is
 * switched off in functions.php — but it costs nothing and keeps this rule
 * correct if that is ever turned back on: that copy is meant to overflow the
 * frame and be clipped by it, and cropping it to 4/5 as well would leave nothing
 * to pan over.
 */
.woocommerce div.product div.images .woocommerce-product-gallery__image img:not( .zoomImg ),
.woocommerce div.product div.images .woocommerce-product-gallery__image--placeholder img {
    /* 1/1 and 5/5 are the same ratio; normalised to match the rest of the file. */
    aspect-ratio: 1 / 1;
    display: block;
    height: auto;
    object-fit: cover;
    width: 100%;
}

/*
 * The frame says it can be dragged, but only once there is something to drag
 * to: product-gallery.js adds .is-draggable after it finds a slider with more
 * than one photograph in it, so a single-image product keeps an ordinary cursor.
 */
.woocommerce div.product div.images .woocommerce-product-gallery__wrapper.is-draggable {
    cursor: grab;
}

.woocommerce div.product div.images .woocommerce-product-gallery__wrapper.is-dragging {
    cursor: grabbing;
    /* A drag across a photograph would otherwise start selecting the page. */
    user-select: none;
}

/* Product gallery: keep the thumbnail strip on an even grid. */
.woocommerce div.product div.images .flex-control-thumbs {
    display: grid;
    gap: var( --wp--preset--spacing--05 );
    grid-template-columns: repeat( 4, minmax( 0, 1fr ) );
    margin: var( --wp--preset--spacing--05 ) 0 0;
}

.woocommerce div.product div.images .flex-control-thumbs li {
    float: none;
    margin: 0;
    width: 100%;
}

.woocommerce div.product div.images .flex-control-thumbs img {
    aspect-ratio: 1 / 1;
    object-fit: cover;
    opacity: 0.75;
    transition: opacity var( --wp--custom--motion--duration ) ease;
    width: 100%;
}

.woocommerce div.product div.images .flex-control-thumbs img:hover,
.woocommerce div.product div.images .flex-control-thumbs img.flex-active {
    opacity: 1;
}

/* -------------------------------------------------- Shared primitives */

/* Tables (attributes, order details, cart) share the hairline contract. */
.woocommerce table.shop_attributes,
.woocommerce table.shop_table {
    border: 0;
    border-collapse: collapse;
    width: 100%;
}

.woocommerce table.shop_attributes th,
.woocommerce table.shop_attributes td,
.woocommerce table.shop_table th,
.woocommerce table.shop_table td {
    border: 0;
    border-bottom: var( --wp--custom--control--border-width ) solid var( --wp--custom--line--base );
    font-size: var( --wp--preset--font-size--small );
    font-style: normal;
    padding: var( --wp--preset--spacing--10 ) var( --wp--preset--spacing--05 );
    text-align: start;
}

/* Labels across every WooCommerce form read like the lead form's labels. */
.woocommerce form .form-row label,
.woocommerce-form-row label {
    display: block;
    font-size: var( --wp--preset--font-size--x-small );
    font-weight: 500;
    letter-spacing: 0.04em;
    margin-block-end: var( --wp--custom--control--label-gap );
    text-transform: uppercase;
}

.woocommerce form .form-row {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
}

/* WooCommerce's own two-column form rows, rebuilt on grid so the fields cannot
   drift out of alignment the way float/percentage rows do. */
.woocommerce form .form-row-wide {
    grid-column: 1 / -1;
}

.woocommerce-billing-fields__field-wrapper,
.woocommerce-shipping-fields__field-wrapper,
.woocommerce-address-fields__field-wrapper,
.woocommerce form.login .form-row-wrapper,
.woocommerce form.register .form-row-wrapper {
    display: grid;
    gap: var( --wp--custom--control--row-gap ) var( --wp--custom--control--column-gap );
    grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
}

@media ( max-width: 700px ) {

    .woocommerce-billing-fields__field-wrapper,
    .woocommerce-shipping-fields__field-wrapper,
    .woocommerce-address-fields__field-wrapper {
        grid-template-columns: 1fr;
    }
}

/* Notices use the same left-rule treatment as the lead form's notice. */
.woocommerce-message,
.woocommerce-info,
.woocommerce-error,
.woocommerce-notice {
    background-color: var( --wp--preset--color--sand-2 );
    border: 0;
    border-inline-start: 3px solid var( --wp--custom--state--accent );
    border-radius: 0;
    font-size: var( --wp--preset--font-size--small );
    list-style: none;
    margin: 0 0 var( --wp--preset--spacing--20 );
    padding: var( --wp--preset--spacing--10 ) var( --wp--preset--spacing--15 );
}

.woocommerce-error {
    border-inline-start-color: var( --wp--preset--color--danger );
}

.woocommerce-message::before,
.woocommerce-info::before,
.woocommerce-error::before {
    content: none;
}

/* Quantity field: a control, so it takes the control height. */
.woocommerce .quantity input.qty {
    max-width: 5.5rem;
    text-align: center;
}

/* My Account navigation, styled as a plain list of links rather than buttons. */
.woocommerce-MyAccount-navigation ul {
    display: flex;
    flex-wrap: wrap;
    gap: var( --wp--preset--spacing--20 );
    list-style: none;
    margin: 0 0 var( --wp--preset--spacing--30 );
    padding: 0;
}

.woocommerce-MyAccount-navigation li a {
    font-size: var( --wp--preset--font-size--x-small );
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.woocommerce-MyAccount-navigation li.is-active a {
    color: var( --wp--custom--state--accent );
}

/* ==================================================================== 7. Chrome */

/* -------------------------------------------------- Dark surfaces */

/* The brand green is dark, so on the ink-coloured sections and the footer the
   accent has to fall back to the light tint or it sinks into the background. */
.has-ink-background-color .is-style-spring-eyebrow,
.wp-block-cover .is-style-spring-eyebrow,
.wp-site-blocks > footer .wp-block-heading {
    color: var( --wp--custom--state--accent-on-dark );
}

.spring-footer hr.wp-block-separator {
    background-color: var( --wp--custom--on-dark--line );
    border: 0;
    color: var( --wp--custom--on-dark--line );
}

/*
 * Clears Spring Core's floating contact pills.
 *
 * From 782px the plugin parks a column of pills in the bottom-right corner. It
 * is fixed, so the only thing it can hide for good is whatever sits in that
 * corner at the very end of the document — here the credit line and the last
 * rows of the showroom column. The plugin cannot pad the body for this the way
 * it does on a phone: that padding would land outside this full-bleed band and
 * open a strip of white under it. So it publishes the footprint and the last
 * row of the footer absorbs it, which keeps the extra height inside the dark
 * panel. Unset below 782px and with the plugin inactive, where the fallback
 * leaves the footer exactly as it was.
 */
.spring-footer > .wp-block-group:last-child {
    padding-block-end: var( --spring-sticky-clearance, 0 );
}

/* -------------------------------------------------- Header */

/*
 * The assembly is 98px tall: a 32px strip of fine print over a 66px bar. Both
 * are stated as heights rather than block padding because three other things
 * measure against them — the offset the sticky product summary parks at, the
 * top padding the home hero needs to clear the bar, and the negative margin
 * that pulls that hero up behind it.
 *
 * They are aliased here rather than read from theme.json at every use so the
 * phone bar can be shorter and the fine-print strip can drop to zero without
 * every consumer having to know about the breakpoint. The values themselves
 * still come from theme.json; this is only where they turn into geometry.
 */
:root {
    --spring-topbar-height: var( --wp--custom--header--topbar-height );
    --spring-bar-height: var( --wp--custom--header--bar-height );
    --spring-logo-height: var( --wp--custom--header--logo-height );
    --spring-header-height: calc( var( --spring-topbar-height ) + var( --spring-bar-height ) );
}

/*
 * The header group carries position:sticky, but its containing block was the
 * template-part wrapper, which is only as tall as the header itself — so it
 * never stuck to anything. Dropping the wrapper's box makes .wp-site-blocks the
 * containing block; the top bar keeps scrolling away because it stays a sibling
 * in the flow rather than part of the sticky element. The :not() keeps this off
 * the Site Editor canvas, where a template part with no box would leave the
 * block toolbar and selection outline with nothing to attach to.
 */
body:not( .editor-styles-wrapper ) header.wp-block-template-part {
    display: contents;
}

/* -------------------------- The strip of fine print */

/*
 * Grid rather than flex: the row has to sit centred inside a fixed height, and
 * a flex parent would collapse the constrained-layout child to max-content
 * instead of letting it keep its wide max-width and its auto margins.
 *
 * The colours are set here and not on the block, because the home page turns
 * the strip transparent and core writes its background class with !important.
 */
.spring-topbar {
    align-content: center;
    background-color: var( --wp--preset--color--ink );
    color: var( --wp--custom--on-dark--text );
    display: grid;
    min-height: var( --spring-topbar-height );
}

/*
 * Both rows are grid items now, and core's constrained layout gives them auto
 * inline margins — which in a grid stop an item stretching and shrink it to its
 * content. Restoring the width is what keeps the wordmark on the page's left
 * margin instead of floating in from it.
 */
.spring-topbar > .alignwide,
.spring-header__bar {
    width: 100%;
}

/*
 * Weight 500, not 400. At 13px with antialiasing on, thin white type over a
 * photograph renders grey however light the colour is — the strip measured
 * 9.2:1 and still read faint. The weight is what makes it legible; the colour
 * only had one step left to give.
 */
.spring-topbar__note {
    color: var(--spring-line-control );
    font-size: var( --wp--preset--font-size--x-small );
    font-weight: 500;
    letter-spacing: 0.06em;
    margin-block: 0;
}

/* The strip sets its own text colour, so its links have to follow it instead of
   the global ink link colour, which rendered the hotline invisible. The hotline
   is the only thing here anyone acts on, so it carries the only weight. */
.spring-topbar a {
    color: inherit;
    font-weight: 600;
}

.spring-topbar a:hover,
.spring-topbar a:focus-visible {
    color: var( --wp--custom--state--accent-on-dark );
}

/* -------------------------- The bar */

.spring-header {
    background-color: var( --wp--custom--surface--header-panel );
    border-bottom: var( --wp--custom--control--border-width ) solid var( --wp--custom--line--base );
    display: grid;
    transition:
        background-color var( --wp--custom--motion--duration ) ease,
        border-color var( --wp--custom--motion--duration ) ease;
    /* The height lives here and not on the row inside, so that the 1px rule at
       the bottom is part of the 66px rather than a 67th pixel — the hero is
       pulled up by exactly 98px and any rounding shows as a seam of photograph
       above the bar. */
    min-height: var( --spring-bar-height );
    /* Above the plugin's fixed contact bar (z-index 999): the header is a sticky
       stacking context, so the navigation overlay inside it can only paint over
       the bar if the header itself does. */
    z-index: 1000;
}

/* The logo is a stacked lockup, so it is sized by height: the bar is a fixed
   66px and the wordmark has to sit inside it whatever the client uploads. */
.spring-header .wp-block-site-logo,
.spring-header .wp-block-site-logo img {
    height: auto;
    max-height: var( --spring-logo-height );
    transition: filter var( --wp--custom--motion--duration ) ease;
    width: auto;
}

/* The bar is 66px, so the site-wide 48px button would leave 9px of air above
   and below it. The compact box is the one the button contract already carries
   for exactly this case. */
.spring-header-cta .wp-block-button__link {
    min-height: var( --wp--custom--header--cta-height );
    padding-inline: var( --wp--custom--button--padding-inline-compact );
    white-space: nowrap;
}

/* -------------------------- The menu
 *
 * Every top-level item is a full-height cell rather than a line of text. That
 * buys two things: the hit area becomes the cell instead of the six words
 * inside it, and the bar's own bottom edge becomes the rail that the
 * current-page marker parks on.
 *
 * The cell rules are scoped away from the open overlay, where the same list is
 * a stacked dialog and a 66px row would be nonsense.
 */

.spring-header .wp-block-navigation__container {
    gap: var( --wp--custom--header--item-gap );
}

/*
 * `stretch`, and the padding is on the children rather than here. The row was
 * already the full 65px, but centring left the <a> at its own 26px, so 40px of
 * every cell — and the 8.8px of gutter down each side — lit the marker on hover
 * and then did nothing under the pointer. Hover was advertising a target that
 * only the middle third of could accept a click. The link has to *be* the cell,
 * not sit in the middle of one, so the padding moves inward to the things that
 * are actually clickable and both children fill the height.
 */
.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li {
    align-items: stretch;
    display: flex;
    height: calc( var( --spring-bar-height ) - var( --wp--custom--control--border-width ) );
    position: relative;
}

.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li > .wp-block-navigation-item__content {
    align-items: center;
    display: flex;
    padding-inline: var( --wp--custom--header--item-padding-inline );
}

/*
 * The chevron is a sibling <button>, not part of the link, so it has to be
 * stretched on its own or the trailing gutter of every submenu item stays dead.
 * The link gives up its trailing padding and the button carries it instead, so
 * the pair tile the cell edge to edge with no seam between them.
 */
.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li.has-child > .wp-block-navigation-item__content {
    padding-inline-end: 0;
}

.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li > .wp-block-navigation__submenu-icon {
    align-items: center;
    /* Core sets align-self:center on this button, which beats the row's
       stretch — so the stretch has to be asked for by name. It also gives the
       button a definite `height: 0.6em`, and a flex item only stretches while
       its cross size is auto, so that has to be handed back as well. */
    align-self: stretch;
    display: flex;
    height: auto;
    /* The 0.25em core puts between label and chevron was margin, so it read as a
       3.8px dead column running the full height of the cell — the one gap left
       once both children were stretched. Same gap, moved inside the button, so
       it belongs to the target instead of separating two of them. */
    margin-inline-start: 0;
    padding-inline: 0.25em var( --wp--custom--header--item-padding-inline );
    width: auto;
}

/*
 * Core sizes this glyph with `width/height: inherit`, which only ever meant
 * "however tall the button happens to be". That was fine while the button hugged
 * the glyph; now that it is 65px of hit area, inherit would scale the chevron to
 * match. Pinning it here is what keeps the two apart. 0.6em is the size it
 * already rendered at, and in em it still follows the bar's type.
 */
.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li > .wp-block-navigation__submenu-icon svg {
    height: 0.6em;
    width: 0.6em;
}

/*
 * The ring goes inside the cell now that the cell is the target. At the shared
 * +2px offset it was drawn from a box the full height of the bar, so it cleared
 * the bar top by 2px and crossed the rail at the bottom — a ring around the
 * furniture rather than around the link. Negative by exactly the ring's own
 * width tucks it just within the 65px and it reads as one item again.
 */
.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li > .wp-block-navigation-item__content:focus-visible,
.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li > .wp-block-navigation__submenu-icon:focus-visible {
    outline-offset: calc( -1 * var( --wp--custom--state--focus-ring-width ) );
}

/*
 * The marker. It runs out from the label's leading edge along the rail rather
 * than fading in on the spot — a runner arriving at a position, which is the
 * one piece of character the bar is allowed. Hover shows it faint; the page you
 * are actually on shows it solid.
 */
.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li::after {
    background-color: var( --wp--custom--state--accent );
    bottom: 0;
    content: "";
    height: var( --wp--custom--header--marker-height );
    inset-inline: var( --wp--custom--header--item-padding-inline );
    opacity: 0;
    position: absolute;
    transform: scaleX( 0 );
    transform-origin: left;
    transition:
        opacity var( --wp--custom--motion--duration ) ease,
        transform var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease );
}

.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li:hover::after,
.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li:focus-within::after {
    opacity: 0.4;
    transform: scaleX( 1 );
}

.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li.spring-is-current::after,
.spring-header .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li.spring-is-ancestor::after {
    opacity: 1;
    transform: scaleX( 1 );
}

/*
 * The colour belongs to the row, not to the link inside it.
 *
 * A submenu's chevron is a <button> sitting beside the link rather than inside
 * it, so every rule aimed at .wp-block-navigation-item__content missed it and
 * it kept the inherited ink — black on a photograph. Core already resolves both
 * children from their parent (`color: inherit` on the link at three classes,
 * `currentColor` on the chevron), so stating the colour once on the item feeds
 * the pair and cannot drift again.
 *
 * This is also what makes the idle colour take effect at all: core's rule on
 * the link is heavier than the two-class rule that used to sit here, so the bar
 * was quietly rendering full ink instead of the softer idle it was written for.
 */
.spring-header .wp-block-navigation__container > li {
    color: var( --wp--preset--color--ink-soft );
    transition: color var( --wp--custom--motion--duration ) ease;
}

/* :focus-within so a tab into the item, or into its open panel, lights the row
   the same way the pointer does. */
.spring-header .wp-block-navigation__container > li:hover,
.spring-header .wp-block-navigation__container > li:focus-within {
    color: var( --wp--preset--color--ink );
}

.spring-header .wp-block-navigation__container > li.spring-is-current,
.spring-header .wp-block-navigation__container > li.spring-is-ancestor {
    color: var( --wp--custom--state--accent );
}

/*
 * The one place the row cannot carry the colour. Inside an open overlay core
 * states `color: inherit !important` on every .wp-block-navigation-item, which
 * no amount of specificity reaches — so there the link takes it back. The
 * chevron is display:none in the overlay, so nothing else needs feeding.
 *
 * The overlay is an ink panel (see "The mobile menu" below), so these are the
 * on-dark pair rather than the ink pair the bar uses.
 */
.spring-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content {
    color: var( --wp--custom--on-dark--text );
}

.spring-header .wp-block-navigation__responsive-container.is-menu-open li.spring-is-current > .wp-block-navigation-item__content,
.spring-header .wp-block-navigation__responsive-container.is-menu-open li.spring-is-ancestor > .wp-block-navigation-item__content {
    color: var( --wp--custom--state--accent-on-dark );
}

/*
 * Global styles underline navigation links on hover
 * (`:root :where(.wp-block-navigation a:where(:not(.wp-element-button)):hover)`).
 * In this bar the current page is already marked by a rule on the rail, so a
 * second line appearing under whatever the pointer touches said the same thing
 * in a different place and made the two impossible to tell apart.
 */
.spring-header .wp-block-navigation-item__content:hover,
.spring-header .wp-block-navigation-item__content:focus,
.spring-header .wp-block-navigation-item__content:focus-visible {
    text-decoration: none;
}

/*
 * The chevron turns over while its menu is down, whether that came from a
 * hover, a click on the toggle, or a tab into the list.
 *
 * On the glyph, not on the button around it, for two reasons. Core already
 * rotates the glyph at [aria-expanded="true"], so turning the button as well
 * nested one 180 inside the other and the pair composed to 360 — the chevron sat
 * still on exactly the state it most needed to report. Landing both rules on the
 * same element leaves one winner. And the button is now a full-height hit area
 * with padding down its trailing edge, so its box is no longer centred on the
 * glyph: rotating it would swing the chevron sideways as well as over.
 */
.spring-header .wp-block-navigation__submenu-icon svg {
    transition: transform var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease );
}

.spring-header .wp-block-navigation__submenu-icon[aria-expanded="true"] svg,
.spring-header .has-child:hover > .wp-block-navigation__submenu-icon svg,
.spring-header .has-child:focus-within > .wp-block-navigation__submenu-icon svg {
    transform: rotate( 180deg );
}

/* -------------------------- The product menu
 *
 * This is the one place on the site where all ten curtain types are listed
 * together, so it is a two-column index rather than a ten-item drop: five rows
 * to read instead of a column the height of the hero. The last row is the shop
 * itself, ruled off from the types above it because it is a different kind of
 * destination, not the eleventh type.
 *
 * Four classes because core sets the panel's display at three
 * (.wp-block-navigation .has-child .wp-block-navigation__submenu-container) and
 * a tie would leave the column count depending on stylesheet order.
 */
.spring-header .wp-block-navigation .has-child > .wp-block-navigation__submenu-container {
    background-color: var( --wp--preset--color--base );
    border: var( --wp--custom--control--border-width ) solid var( --wp--custom--line--base );
    box-shadow: var( --wp--preset--shadow--soft );
    column-gap: var( --wp--preset--spacing--20 );
    display: grid;
    /* Two equal tracks sized to the longest name in the list: fr tracks under a
       max-content box settle on the widest item's contribution, which is why the
       names below must not be allowed to wrap. */
    grid-template-columns: repeat( 2, minmax( max-content, 1fr ) );
    padding: var( --wp--preset--spacing--15 );
    top: 100%;
    transform: translateY( calc( -1 * var( --wp--preset--spacing--05 ) ) );
    transition:
        opacity var( --wp--custom--motion--duration ) ease,
        transform var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease );
    width: max-content;
}

/*
 * Core right-aligns submenus inside a right-justified navigation, which reached
 * this panel back past the whole menu — "Sản phẩm" is the second item from the
 * left, so the panel ended up belonging to nothing. Six classes because core
 * states its own version at five
 * (.wp-block-navigation.items-justified-right .wp-block-navigation__container
 * .has-child .wp-block-navigation__submenu-container).
 */
.spring-header .wp-block-navigation.items-justified-right .wp-block-navigation__container .has-child > .wp-block-navigation__submenu-container {
    inset-inline: 0 auto;
}

/* Core already flips visibility and opacity on these three; the panel only has
   to stop hanging above its rest position. */
.spring-header .wp-block-navigation .has-child:hover > .wp-block-navigation__submenu-container,
.spring-header .wp-block-navigation .has-child:focus-within > .wp-block-navigation__submenu-container,
.spring-header .wp-block-navigation__submenu-icon[aria-expanded="true"] + .wp-block-navigation__submenu-container {
    transform: none;
}

/* Core paints submenu links pure black whenever the navigation has no text
   colour of its own, which is a shade the palette does not contain. */
.spring-header .wp-block-navigation .has-child > .wp-block-navigation__submenu-container .wp-block-navigation-item__content {
    border-radius: var( --wp--custom--control--radius );
    color: var( --wp--preset--color--ink-soft );
    display: block;
    padding: var( --wp--preset--spacing--05 ) var( --wp--preset--spacing--10 );
    white-space: nowrap;
    width: 100%;
}

.spring-header .wp-block-navigation .has-child > .wp-block-navigation__submenu-container .wp-block-navigation-item__content:hover,
.spring-header .wp-block-navigation .has-child > .wp-block-navigation__submenu-container .wp-block-navigation-item__content:focus-visible {
    background-color: var( --wp--preset--color--sand-2 );
    color: var( --wp--preset--color--ink );
}

.spring-header .wp-block-navigation .has-child > .wp-block-navigation__submenu-container li.spring-is-current > .wp-block-navigation-item__content {
    color: var( --wp--custom--state--accent );
    font-weight: 600;
}

/*
 * The shop row. :last-child rather than a URL match, because what makes this
 * row different is its position in the list — if the client adds an eleventh
 * curtain type above it, it is still the row that means "all of them".
 */
.spring-header .wp-block-navigation .has-child > .wp-block-navigation__submenu-container > li:last-child {
    border-block-start: var( --wp--custom--control--border-width ) solid var( --wp--custom--line--soft );
    grid-column: 1 / -1;
    margin-block-start: var( --wp--preset--spacing--05 );
    padding-block-start: var( --wp--preset--spacing--05 );
}

.spring-header .wp-block-navigation .has-child > .wp-block-navigation__submenu-container > li:last-child .wp-block-navigation-item__content {
    color: var( --wp--custom--state--accent );
    font-weight: 600;
}

.spring-header .wp-block-navigation__submenu-container > li:last-child .wp-block-navigation-item__label::after {
    content: "\2192";
    display: inline-block;
    margin-inline-start: 0.5em;
    transition: transform var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease );
}

.spring-header .wp-block-navigation__submenu-container > li:last-child:hover .wp-block-navigation-item__label::after {
    transform: translateX( 0.25em );
}

/* -------------------------- The sheer state
 *
 * On the home page the header is drawn over the hero photograph rather than
 * above it: the assembly comes off the flow by exactly its own height and the
 * slider runs up behind it. What holds the type legible is a scrim — light
 * falling off from the top of the frame — and not a blur, because the thing
 * being obscured is a photograph of a room, which is the entire argument the
 * page is making.
 *
 * header.js adds .is-drawn once the hero has passed under the bar, at which
 * point the panel closes and everything returns to its ordinary colours.
 */
.spring-overlay-header .wp-site-blocks > main {
    margin-block-start: calc( -1 * var( --spring-header-height ) );
}

.spring-overlay-header .spring-topbar {
    position: relative;
    /*
     * The slider is later in the flow, so without this it paints over a strip
     * that has no background of its own to sit on.
     *
     * One higher than the bar, and that one step is the whole point. The strip
     * and the bar are siblings — `display: contents` on the template part left
     * them with no shared box — so at equal z-index document order decides, and
     * the bar wins. The bar is a stacking context, which takes its scrim with
     * it: a pseudo reaching 32px up over the strip was painting 0.72 of black
     * across the fine print rather than behind it, and the line measured 107 of
     * 255 where it should have measured white. Ordering the strip above the bar
     * puts the scrim back under the words it was drawn to make legible.
     */
    z-index: 1001;
}

/*
 * Except while the overlay is open. That panel is fixed and full-screen but
 * lives inside the bar, so it cannot climb out of the bar's stacking context —
 * and between 782px and 1179px the strip is still on screen, which would leave
 * a band of fine print floating over the panel. Core sets this class for the
 * length of the modal; dropping back to a tie hands document order the decision
 * again, and document order gives it to the bar.
 *
 * Unprefixed, because core puts the class on <html> here and has moved it
 * before — and the strip is a descendant of either root, but never of the
 * overlay container that also carries it, so there is nothing to disambiguate.
 */
.has-modal-open .spring-topbar {
    z-index: 1000;
}

.spring-overlay-header .spring-header::before {
    background-image: var( --wp--custom--surface--header-scrim );
    /* Reaches up over the fine-print strip so that is legible too, and keeps
       reaching once the bar has stuck to the top and the strip has gone. */
    bottom: calc( -1 * var( --wp--custom--header--scrim-depth ) );
    content: "";
    inset-inline: 0;
    opacity: 0;
    position: absolute;
    top: calc( -1 * var( --spring-topbar-height ) );
    transition: opacity var( --wp--custom--motion--duration ) ease;
    z-index: -1;
		pointer-events: none;
}

.spring-overlay-header .spring-header:not( .is-drawn )::before {
    opacity: 1;
}

.spring-overlay-header .spring-topbar:not( .is-drawn ),
.spring-overlay-header .spring-header:not( .is-drawn ) {
    background-color: transparent;
    border-bottom-color: transparent;
}

/*
 * Every colour below is scoped to the closed bar, not merely to the sheer
 * header. The mobile overlay is an opaque light panel living inside that same
 * header, so without the second :not() the white these rules need on a
 * photograph followed the links into the panel — most visibly on the current
 * item, whose rule is specific enough to have beaten any reset placed after it.
 */

/* Without a bar edge to park on, the marker needs a rail of its own — which is
   the one line the sheer header draws, and only under the menu. An inset shadow
   rather than a border: a border here is a 67th pixel of bar, and the hero is
   pulled up by exactly 98. */
.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container {
    box-shadow: inset 0 calc( -1 * var( --wp--custom--control--border-width ) ) 0 var( --wp--custom--on-dark--line );
}

/*
 * `> li >` and not a descendant selector. The product panel hangs inside a
 * top-level item, so a descendant selector painted its links white too — and
 * the panel is an opaque white card, which left ten curtain types invisible on
 * the one page the treatment applies to. Only the items sitting on the
 * photograph may take the photograph's colours.
 */
.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li {
    color: var( --wp--custom--on-dark--text );
}

.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li:hover,
.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li:focus-within,
.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li.spring-is-current,
.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li.spring-is-ancestor {
    color: var( --wp--preset--color--base );
}

.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-navigation__responsive-container:not( .is-menu-open ) .wp-block-navigation__container > li::after {
    background-color: var( --wp--custom--state--accent-on-dark );
}

.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-navigation__responsive-container-open {
    color: var( --wp--preset--color--base );
}

/* A single-colour wordmark on a photograph. The lockup is one ink, so flattening
   it to white keeps its shape and loses nothing but the green. */
.spring-overlay-header .spring-header:not( .is-drawn ) .wp-block-site-logo img {
    filter: brightness( 0 ) invert( 1 );
}

/* Over a photograph the filled button is a block of paint in the corner; an
   outline lets the room through and still reads as the one thing to press. It
   fills in on hover, and fills in for good once the panel has closed. */
.spring-overlay-header .spring-header:not( .is-drawn ) .spring-header-cta .wp-block-button__link {
    background-color: transparent;
    border-color: var( --wp--custom--on-dark--muted );
    color: var( --wp--preset--color--base );
}

.spring-overlay-header .spring-header:not( .is-drawn ) .spring-header-cta .wp-block-button__link:hover,
.spring-overlay-header .spring-header:not( .is-drawn ) .spring-header-cta .wp-block-button__link:focus-visible {
    background-color: var( --wp--preset--color--base );
    border-color: var( --wp--preset--color--base );
    color: var( --wp--preset--color--ink );
}

/* -------------------------------------------------- Section helpers */

/*
 * Every Spring quote begins with someone measuring a window, so each figure
 * sits under its own tick rule: the four columns read as marks on a tape rather
 * than as four unrelated numbers floating on the ground.
 */
.spring-stat {
    border-block-start: 1px solid var( --wp--custom--line--base );
    padding-block-start: var( --wp--preset--spacing--15 );
}

.spring-stat__figure {
    font-family: var( --wp--preset--font-family--heading );
    font-size: var( --wp--preset--font-size--xx-large );
    font-variant-numeric: lining-nums tabular-nums;
    line-height: 1;
}

/*
 * Sentence case, not caps. These labels run to three lines of Vietnamese, and
 * at 13px with 0.08em tracking the diacritics collided with the line above —
 * the least readable text on the page for the sake of a uniform look.
 */
.spring-stat__label {
    color: var( --wp--preset--color--muted );
    font-size: var( --wp--preset--font-size--small );
    line-height: 1.6;
    max-width: 30ch;
}

/* On the ink plinth under the hero, both tiers and the tick rule invert. */
.spring-slider + .spring-section .spring-stat {
    border-block-start-color: var( --wp--custom--on-dark--line );
}

.spring-slider + .spring-section .spring-stat__figure {
    color: var( --wp--preset--color--sand-2 );
}

.spring-slider + .spring-section .spring-stat__label {
    color: var( --wp--custom--on-dark--muted );
}

/*
 * The three reasons to choose Spring were numbered 01/02/03, which read as a
 * sequence and collided with the process band lower down, where the numbers are
 * real steps. They are not an order — they are the three phases of the job, so
 * they are labelled as such and share the stats band's tick rule.
 */
.spring-phase-col {
    border-block-start: 1px solid var( --wp--custom--line--base );
    padding-block-start: var( --wp--preset--spacing--15 );
}

.spring-phase {
    color: var( --wp--custom--state--accent );
    font-size: var( --wp--preset--font-size--x-small );
    font-weight: 600;
    letter-spacing: 0.14em;
    margin-block-end: var( --wp--preset--spacing--15 );
    text-transform: uppercase;
}

.spring-step__number {
    color: var( --wp--custom--state--accent );
    font-family: var( --wp--preset--font-family--heading );
    font-size: var( --wp--preset--font-size--x-large );
    line-height: 1;
}

/* -------------------------------------------------- FAQ */

/*
 * The questions used to be a single 860px column inside the 1320px field, which
 * left 460px of the page empty beside them. Splitting the section into a
 * heading rail and a list uses the width the rest of the page already uses, and
 * puts the hotline next to the questions — where someone who did not find their
 * answer is actually looking.
 */
.spring-faq {
    align-items: start;
    display: grid;
    gap: var( --wp--preset--spacing--30 ) var( --wp--preset--spacing--50 );
    grid-template-columns: minmax( 0, 0.85fr ) minmax( 0, 1.15fr );
}

.spring-faq__rail .wp-block-heading {
    text-wrap: balance;
}

.spring-faq__help {
    margin-block-start: var( --wp--preset--spacing--20 );
    max-width: 34ch;
}

.spring-faq__help a {
    color: var( --wp--custom--state--accent );
    font-weight: 500;
}

@media ( max-width: 1023px ) {
    .spring-faq {
        grid-template-columns: minmax( 0, 1fr );
    }
}

/* -------------------------------------------------- Accordions */

/* Line chevron instead of the browser's solid triangle marker. */
.wp-block-details > summary {
    align-items: center;
    cursor: pointer;
    display: flex;
    gap: var( --wp--preset--spacing--15 );
    justify-content: space-between;
    list-style: none;
}

.wp-block-details > summary::-webkit-details-marker {
    display: none;
}

.wp-block-details > summary::after {
    border-block-end: 1.5px solid currentColor;
    border-inline-end: 1.5px solid currentColor;
    content: "";
    flex: none;
    height: 0.48em;
    margin-block-start: -0.24em;
    transform: rotate( 45deg );
    transition: transform var( --wp--custom--motion--duration ) ease;
    width: 0.48em;
}

.wp-block-details[open] > summary::after {
    margin-block-start: 0.16em;
    transform: rotate( -135deg );
}

/* -------------------------------------------------- Mobile header */

@media ( max-width: 781px ) {

    /* The strip duplicates what the floating contact bar already offers, so on
       a phone the whole assembly is the 56px bar and nothing else. Redeclaring
       the two aliases is what tells the home hero how far to run up. */
    :root {
        --spring-topbar-height: 0px;
        --spring-bar-height: var( --wp--custom--header--bar-height-compact );
        --spring-logo-height: var( --wp--custom--header--logo-height-compact );
    }

    .spring-topbar {
        display: none;
    }

    /* Same for the header button: the bar carries "Nhận báo giá" on mobile.
       Specificity has to beat the block layout rule .wp-block-buttons.is-layout-flex. */
    .spring-header .wp-block-buttons.spring-header-cta {
        display: none;
    }
}

/* -------------------------------------------------- The mobile menu
 *
 * The overlay is the most-used surface on this site and was the least designed
 * one: nineteen rows of body text on white, no wordmark, no way to call, and a
 * white panel opening over a white page with no edge to tell you it had opened
 * at all. It is rebuilt here as a panel of its own.
 *
 * Ink, and not the bar's white. The site already owns this surface — the
 * projects band and the footer are cut from it — so the panel costs no new
 * token and cannot drift from the rest of the design. It also settles the
 * white-on-white problem outright: the menu is plainly a different plane. And
 * it is the right image for the object: a menu is a curtain drawn across the
 * page, and a room goes dark when you draw one.
 *
 * The list is set in Playfair, which appears nowhere else in the navigation.
 * The bar cannot afford it — eight items in a display face do not fit a 66px
 * row — but the panel has one column and all the room in the world, and the
 * display face is what makes the menu read as a page of this site rather than
 * as system furniture.
 *
 * Structure follows what the menu actually holds: one destination that is a
 * catalogue of fabrics, and six that are pages. The catalogue gets photographs,
 * because a curtain is chosen by looking; "Rèm tổ ong" next to "Rèm cầu vồng"
 * is two names for two things nobody can picture. The pages get type, because
 * they are pages. That contrast is the design; a hairline marks the seam.
 */
@media ( max-width: 1179px ) {

    /* ---- The panel */

    .spring-header .wp-block-navigation__responsive-container.is-menu-open {
        /*
         * The height of the wordmark-and-close line at the top of the panel.
         * Two things read it: the padding that clears it, and the wordmark
         * itself, which is lifted back up through that padding.
         */
        --spring-menu-head: 3rem;

        /*
         * !important answers !important. Core paints the open overlay
         * `background-color: inherit !important` for any navigation without a
         * background of its own, and this one inherits from the white bar it
         * lives in. Setting `backgroundColor` on the block instead would paint
         * the desktop <nav> as well, which is a bar, not a panel.
         */
        background-color: var( --wp--preset--color--ink ) !important;
        color: var( --wp--custom--on-dark--text );
        /*
         * Block padding only. The swatch strip runs to both screen edges, and
         * a negative margin against a padded scroll container is what put the
         * whole panel into a horizontal scroll — the rows carry the inline
         * padding instead, so the strip has nothing to escape from.
         */
        overflow-x: clip;
        padding: var( --wp--preset--spacing--20 ) 0;
    }

    /*
     * Drawn, not faded. The panel comes across from the closing edge the way a
     * single-pull curtain does, and the rows settle behind it a beat later.
     * Two gestures and no more: the draw, and the settle.
     */
    @media ( prefers-reduced-motion: no-preference ) {

        .spring-header .wp-block-navigation__responsive-container.is-menu-open {
            animation: spring-menu-draw 0.36s var( --wp--custom--motion--ease ) both;
        }

        .spring-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container > li,
        .spring-header .wp-block-navigation__responsive-container.is-menu-open .spring-menu__cta {
            animation: spring-menu-settle 0.32s var( --wp--custom--motion--ease ) both;
        }

        /*
         * Nine steps of about 26ms, which covers the eight menu rows, the
         * hotline and the button. Anything past the eighth row shares the last
         * beat rather than arriving after the reader has started looking.
         */
        .spring-header .is-menu-open .wp-block-navigation__container > li:nth-child( 1 ) { animation-delay: 0.10s; }
        .spring-header .is-menu-open .wp-block-navigation__container > li:nth-child( 2 ) { animation-delay: 0.13s; }
        .spring-header .is-menu-open .wp-block-navigation__container > li:nth-child( 3 ) { animation-delay: 0.16s; }
        .spring-header .is-menu-open .wp-block-navigation__container > li:nth-child( 4 ) { animation-delay: 0.18s; }
        .spring-header .is-menu-open .wp-block-navigation__container > li:nth-child( 5 ) { animation-delay: 0.21s; }
        .spring-header .is-menu-open .wp-block-navigation__container > li:nth-child( 6 ) { animation-delay: 0.23s; }
        .spring-header .is-menu-open .wp-block-navigation__container > li:nth-child( 7 ) { animation-delay: 0.26s; }
        .spring-header .is-menu-open .wp-block-navigation__container > li:nth-child( n + 8 ) { animation-delay: 0.28s; }
        .spring-header .is-menu-open .spring-menu__cta { animation-delay: 0.31s; }
    }

    /* ---- The top line: wordmark on the left, close on the right */

    /*
     * The wordmark is the ninth item in the list — core renders every allowed
     * block inside the <ul> — and it is lifted out of the flow to sit on the
     * close button's line. Without it the panel opens onto a bare X in a corner
     * and you lose the one thing that says where you are.
     */
    .spring-header .is-menu-open .wp-block-navigation__container > li:has( > .spring-menu__brand ) {
        align-items: center;
        display: flex;
        height: var( --spring-menu-head );
        /* The list is the containing block, and it starts one head-band below
           the top of the panel; this is that band, given back. */
        inset-block-start: calc( -1 * var( --spring-menu-head ) );
        /* Zero, because the list already carries the panel's inline padding:
           the wordmark is measured from the same margin as the rows under it. */
        inset-inline-start: 0;
        padding: 0;
        position: absolute;
    }

    .spring-header .is-menu-open .spring-menu__brand img {
        /* One ink on ink is invisible; the lockup is a single colour, so
           flattening it to white keeps its shape and loses only the green. */
        filter: brightness( 0 ) invert( 1 );
        max-height: 2rem;
        width: auto;
    }

    /* A 24px glyph in a 48px target, on the same band as the wordmark. The
       close is the only way out of a modal and it was a thumb-sized miss. */
    .spring-header .is-menu-open .wp-block-navigation__responsive-container-close {
        color: var( --wp--custom--on-dark--text );
        display: grid;
        height: var( --spring-menu-head );
        inset-inline-end: var( --wp--preset--spacing--20 );
        min-width: 2.75rem;
        place-items: center;
    }

    /*
     * The panel is taller than the menu on every phone, and the slack belonged
     * at the bottom rather than under the button: the hotline and the quote
     * button are pinned to the foot of the panel, which is where the floating
     * contact bar they stand in for sits when the menu is closed. Four links in
     * the chain because core wraps the dialog twice before the list.
     */
    .spring-header .is-menu-open .wp-block-navigation__responsive-close,
    .spring-header .is-menu-open .wp-block-navigation__responsive-dialog {
        display: flex;
        flex: 1;
        flex-direction: column;
    }

    .spring-header .is-menu-open .wp-block-navigation__responsive-container-content,
    .spring-header .wp-block-navigation .is-menu-open .wp-block-navigation__container {
        flex: 1;
    }

    /*
     * The overlay runs all the way to 1179px, which is a tablet and a small
     * laptop as well as a phone. Left to itself the panel stretched the whole
     * way: eight menu rows down the far left of a 900px void and a quote button
     * a metre wide. The panel is a column of a fixed measure, and the ink around
     * it is the curtain it is drawn from.
     */
    .spring-header .is-menu-open .wp-block-navigation__responsive-dialog {
        max-width: 26rem;
    }

    /* ---- The list */

    /*
     * The bar is right-justified and core hands that justification straight to
     * the overlay, which stacked the items against the right edge while their
     * submenu stayed left — two menus rather than one. A stacked list reads
     * from the left, so every level starts there.
     */
    .spring-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
        align-items: stretch;
        flex-direction: column;
        /* Clears the wordmark and close line above. */
        padding-block-start: var( --spring-menu-head );
        padding-inline: var( --wp--preset--spacing--20 );
        width: 100%;
    }

    /*
     * Core hands each item the bar's justification
     * (`align-items: var(--navigation-layout-justification-setting)`), which is
     * flex-end here — so every row shrank to its own words and sat against the
     * right edge. A stacked row is the full width of the panel.
     */
    .spring-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item {
        align-items: stretch;
    }

    .spring-header .wp-block-navigation .is-menu-open .wp-block-navigation__container {
        align-items: stretch;
        border-block-end: 0;
        gap: 0;
        width: 100%;
    }

    /*
     * Every row is capped at the panel, and none of them may claim a minimum.
     * Without both, the swatch strip's 1067px of chips set the width of the
     * flex line it sits on and dragged all eight rows out to it — the panel
     * scrolled sideways and the menu ran off the screen. Capped, the strip is
     * the only thing that overflows, which is what it is built to do.
     */
    .spring-header .is-menu-open .wp-block-navigation__container > li {
        max-width: 100%;
        min-width: 0;
    }

    /* The submenu parent is a column here, holding its own label above its
       swatches, and it centred that label over them. */
    .spring-header .wp-block-navigation .is-menu-open .wp-block-navigation-item.has-child {
        align-items: stretch;
        /*
         * A flex item will not shrink below its own min-content width, and the
         * strip inside this one is ten 96px chips that refuse to shrink — so
         * min-content was 1067px and the row grew to it, taking the panel with
         * it. Zero lets the row be the width of the panel and hands the
         * overflow to the strip, which is built to scroll.
         */
        min-width: 0;
    }

    /*
     * The display face, and the reason the panel exists. 22px is the theme's
     * `large`, which is as far as Playfair can go before a Vietnamese row like
     * "Giới thiệu" needs a second line on a 320px phone.
     */
    .spring-header .is-menu-open .wp-block-navigation__container > li > .wp-block-navigation-item__content {
        align-items: center;
        display: flex;
        font-family: var( --wp--preset--font-family--heading );
        font-size: var( --wp--preset--font-size--large );
        font-weight: 400;
        line-height: var( --wp--custom--type--heading-line-height );
        /* A row measured 41px, which is under the 44px a finger needs. The
           control height is what the rest of the site hits on a phone. */
        min-height: var( --wp--custom--control--height );
        padding-block: var( --wp--preset--spacing--05 );
        padding-inline: 0;
        width: 100%;
    }

    /* The chevron opens nothing here — the swatches are already open — and a
       control that does nothing is worse than no control. */
    .spring-header .is-menu-open .wp-block-navigation__submenu-icon {
        display: none;
    }

    /*
     * "Sản phẩm" is the catalogue as well as the heading over the strip, and
     * with the chevron gone and the duplicate "Xem tất cả sản phẩm" row stood
     * down it was the only row that looked like a label rather than a link.
     * The arrow is the same one the desktop submenu ends on.
     */
    .spring-header .is-menu-open .wp-block-navigation-item.has-child > .wp-block-navigation-item__content .wp-block-navigation-item__label::after {
        content: "\2192";
        display: inline-block;
        /* Playfair has no arrow of its own; the body face draws every other
           one on the site. */
        font-family: var( --wp--preset--font-family--body );
        font-size: var( --wp--preset--font-size--medium );
        margin-inline-start: 0.45em;
        transition: transform var( --wp--custom--motion--duration ) var( --wp--custom--motion--ease );
    }

    .spring-header .is-menu-open .wp-block-navigation-item.has-child > .wp-block-navigation-item__content:hover .wp-block-navigation-item__label::after {
        transform: translateX( 0.25em );
    }

    /* The seam between the catalogue and the pages. Placed by structure and not
       by count: it is the row after the submenu, wherever that ends up. */
    .spring-header .is-menu-open .wp-block-navigation__container > li.has-child + li {
        border-block-start: var( --wp--custom--control--border-width ) solid var( --wp--custom--on-dark--line );
        margin-block-start: var( --wp--preset--spacing--15 );
        padding-block-start: var( --wp--preset--spacing--10 );
    }

    /* ---- The swatch strip */

    /*
     * Ten curtain types as a swiped strip rather than ten more rows. It costs
     * 150px instead of 750px, which is what puts "Bảng giá" and "Liên hệ" on
     * the first screen instead of below ten fabrics nobody scrolled past — and
     * it is how the trade actually works, since a consultant hands you a bound
     * card of swatches rather than a list of names.
     *
     * spring_curtains_nav_swatch() in functions.php gives each item the
     * photograph of its own category as --spring-swatch.
     */
    .spring-header .wp-block-navigation .is-menu-open .has-child > .wp-block-navigation__submenu-container {
        border: none;
        box-shadow: none;
        display: flex;
        /* Core stacks every submenu in the overlay; this one is a strip. */
        flex-direction: row;
        gap: 0.625rem;
        /* See the row above: the strip is the scroller, not the panel. */
        min-width: 0;
        /*
         * Snapping aligns a chip to the scrollport edge, which is inside the
         * gutter — so the strip loaded already scrolled 20px and the first
         * swatch sat half off the screen. This is the gutter, told to the
         * snap engine.
         */
        scroll-padding-inline-start: var( --wp--preset--spacing--20 );
        /* Runs to both screen edges, so the part-shown chip on the right is the
           cue that there is more to swipe. */
        margin-inline: calc( -1 * var( --wp--preset--spacing--20 ) );
        overflow-x: auto;
        overscroll-behavior-inline: contain;
        padding: 0 var( --wp--preset--spacing--20 );
        scroll-snap-type: inline proximity;
        scrollbar-width: none;
        transform: none;
        width: auto;
    }

    .spring-header .is-menu-open .wp-block-navigation__submenu-container::-webkit-scrollbar {
        display: none;
    }

    .spring-header .is-menu-open .wp-block-navigation__submenu-container > li.spring-swatch {
        flex: 0 0 6rem;
        /*
         * A flex item will not go below its own min-content width, and three of
         * the ten labels measured just over the chip — so those three chips
         * were 97, 100 and 108px, and their photographs stood a few pixels
         * taller than the rest of the strip. Every swatch is the same swatch.
         */
        min-width: 0;
        scroll-snap-align: start;
    }

    /*
     * The eleventh row is "Xem tất cả sản phẩm", which points at the same page
     * as the "Sản phẩm" heading directly above the strip. On the bar the two
     * are far apart and the row earns its place; here they would be two taps
     * on one line, so the heading keeps the job and the row stands down.
     */
    .spring-header .is-menu-open .wp-block-navigation__submenu-container > li:not( .spring-swatch ) {
        display: none;
    }

    .spring-header .wp-block-navigation .is-menu-open .wp-block-navigation__submenu-container .wp-block-navigation-item__content {
        color: var( --wp--custom--on-dark--muted );
        display: block;
        font-size: var( --wp--preset--font-size--x-small );
        line-height: 1.35;
        opacity: 1;
        padding: 0;
    }

    .spring-header .is-menu-open .spring-swatch .wp-block-navigation-item__content::before {
        aspect-ratio: 4 / 5;
        background-color: var( --wp--preset--color--ink-soft );
        background-image: var( --spring-swatch );
        background-position: center;
        background-size: cover;
        content: "";
        display: block;
        margin-block-end: var( --wp--preset--spacing--05 );
    }

    /* Two lines keep the chips level across the strip; the names run from
       "Rèm vải" to "Rèm phòng tắm". */
    .spring-header .is-menu-open .spring-swatch .wp-block-navigation-item__label {
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        overflow: hidden;
    }

    .spring-header .is-menu-open .spring-swatch .wp-block-navigation-item__content:hover::before,
    .spring-header .is-menu-open .spring-swatch .wp-block-navigation-item__content:focus-visible::before {
        outline: var( --wp--custom--state--focus-ring-width ) solid var( --wp--custom--state--accent-on-dark );
        outline-offset: 2px;
    }

    /* ---- The foot: what the phone header drops */

    /*
     * The bar sheds the fine-print strip and the quote button on a phone, and
     * the open panel covers the floating contact bar underneath it — so until
     * now the most-used surface on a site whose whole job is phone calls
     * offered no way to make one. Both come back here, inside the dialog and so
     * inside its focus trap.
     */
    .spring-header .is-menu-open .wp-block-navigation__container > li.spring-menu__tel {
        border-block-start: var( --wp--custom--control--border-width ) solid var( --wp--custom--on-dark--line );
        /* Takes the panel's slack, which pins this row and the button under it
           to the foot. On a panel with no slack it collapses to nothing, so the
           clear space below the last page link is padding rather than margin. */
        margin-block-start: auto;
        padding-block-start: var( --wp--preset--spacing--15 );
    }

    .spring-header .is-menu-open .spring-menu__tel > .wp-block-navigation-item__content {
        font-variant-numeric: lining-nums tabular-nums;
        letter-spacing: 0.02em;
    }

    .spring-header .is-menu-open .spring-menu__cta {
        margin-block-start: var( --wp--preset--spacing--10 );
    }

    .spring-header .is-menu-open .spring-menu__cta .wp-block-button,
    .spring-header .is-menu-open .spring-menu__cta .wp-block-button__link {
        width: 100%;
    }

    /*
     * The one place the button contract inverts, for the same reason the stats
     * band inverts on the ink plinth: the brand's moss green is a dark on a
     * darker, and the primary action on this panel has to be the loudest thing
     * on it.
     */
    .spring-header .is-menu-open .spring-menu__cta .wp-block-button__link {
        background-color: var( --wp--custom--on-dark--text );
        border-color: var( --wp--custom--on-dark--text );
        color: var( --wp--preset--color--ink );
    }

    .spring-header .is-menu-open .spring-menu__cta .wp-block-button__link:hover,
    .spring-header .is-menu-open .spring-menu__cta .wp-block-button__link:focus-visible {
        background-color: var( --wp--custom--state--accent-on-dark );
        border-color: var( --wp--custom--state--accent-on-dark );
        color: var( --wp--preset--color--ink );
    }
}

@keyframes spring-menu-draw {

    from {
        transform: translateX( 100% );
    }

    to {
        transform: none;
    }
}

@keyframes spring-menu-settle {

    from {
        opacity: 0;
        transform: translateY( 0.5rem );
    }

    to {
        opacity: 1;
        transform: none;
    }
}

/*
 * The three panel-only pieces are not part of a bar wide enough to carry the
 * menu itself.
 *
 * The chain is long on purpose. The bar's own cell rule up in "The menu" —
 * `.spring-header .wp-block-navigation__responsive-container:not(.is-menu-open)
 * .wp-block-navigation__container > li` — is four classes and a type, and it
 * sets `display: flex` on every row; anything shorter than that lost, and the
 * wordmark and the hotline turned up in the desktop bar.
 */
@media ( min-width: 1180px ) {

    .spring-header .wp-block-navigation .wp-block-navigation__responsive-container .wp-block-navigation__container > li:has( > .spring-menu__brand ),
    .spring-header .wp-block-navigation .wp-block-navigation__responsive-container .wp-block-navigation__container > li.spring-menu__tel,
    .spring-header .wp-block-buttons.spring-menu__cta {
        display: none;
    }
}

/*
 * Core switches the navigation to its overlay at 600px, but the eight top-level
 * items plus the wordmark and the header button only fit on one row above a
 * certain width, so the overlay has to cover everything below it.
 *
 * Measured, not guessed: the row needs 984px, the bar adds 56px of gutter and
 * the scrollbar about 15px, so it survives to roughly 1055px. 1180 is that
 * floor plus enough slack that the wordmark and the menu are not touching, and
 * enough that a longer label added later does not silently wrap the bar.
 */
@media ( min-width: 600px ) and ( max-width: 1179px ) {

    .spring-header .wp-block-navigation__responsive-container-open:not( .always-shown ) {
        display: flex;
    }

    .spring-header .wp-block-navigation__responsive-container:not( .hidden-by-default ):not( .is-menu-open ) {
        display: none;
    }
}

/* ==================================================================== 8. Entrance */

/*
 * Sections arrive rather than appear: opacity and a 14px lift carry the
 * movement, and the items inside a grid add a 3px blur so they read as coming
 * into focus rather than switching on. Nothing loops, nothing repeats on the
 * way back up — a reveal the reader has already watched is a reveal they would
 * start to notice, and this one is meant to go unnoticed.
 *
 * Three gates, and any one of them failing leaves the page exactly as it was
 * before this section existed:
 *
 * - `.spring-reveal` on <html>, written by an inline script in the head. Only a
 *   running script can set it, so scripting off means nothing is ever hidden.
 * - That script does not set it when the visitor has asked for reduced motion.
 *   The media query at the end of this section is the second half of the same
 *   promise, for a preference flipped after the page had loaded.
 * - reveal.js removes the class outright if it cannot finish its work.
 *
 * The hidden state is stated twice on purpose. Before the script has marked
 * anything, every row of a section holds it — a structural selector, so it
 * applies at first paint and no row can flash in and back out. Once the script
 * has run, `.spring-reveal-ready` retires that half and the marks the script
 * actually made take over. Both live in one selector list so the two halves
 * cannot drift apart.
 */
.spring-reveal:not( .spring-reveal-ready ) .entry-content > .spring-section > *,
.spring-reveal [data-spring-reveal]:not( .is-revealed ) {
    filter: blur( var( --spring-reveal-blur, 0px ) );
    opacity: 0;
    transform: translate3d( 0, 14px, 0 );
}

/*
 * The transition is carried only by marked elements, so the hand-off from the
 * structural rule above to the script's own marks — both inside one task, before
 * a frame is painted — moves nothing.
 *
 * Landing on the properties' own initial values rather than on `blur(0)` and
 * `translate3d(0,0,0)` is deliberate: a filter or transform that stays declared
 * makes the element a containing block for anything fixed or sticky inside it,
 * and holds it on its own compositor layer for the rest of the session. Both
 * interpolate from a real value to `none`, so the transition still runs.
 */
.spring-reveal [data-spring-reveal] {
    /*
     * The script says which member of its run this is and nothing else; the
     * step, and the index the sequence stops counting at, are design and belong
     * here. Ten tiles staggered all the way out would still be arriving after
     * the reader had finished reading them, so the tail of a long run lands
     * together.
     */
    --spring-reveal-delay: calc( min( var( --spring-reveal-index, 0 ), 7 ) * var( --wp--custom--motion--stagger ) );

    transition:
        filter var( --wp--custom--motion--duration-reveal ) var( --wp--custom--motion--ease ) var( --spring-reveal-delay, 0ms ),
        opacity var( --wp--custom--motion--duration-reveal ) var( --wp--custom--motion--ease ) var( --spring-reveal-delay, 0ms ),
        transform var( --wp--custom--motion--duration-reveal ) var( --wp--custom--motion--ease ) var( --spring-reveal-delay, 0ms );
}

/*
 * A member of a run — one of four statistics, one of ten tiles. Only these take
 * the blur: they are small enough that it reads as coming into focus, where the
 * same blur across a full-width row would read as a smear, and would ask the
 * compositor for a surface that size on every frame of the arrival.
 */
.spring-reveal [data-spring-reveal="run"] {
    --spring-reveal-blur: 3px;
}

/*
 * The other half of the reduced-motion promise. The global contract in section
 * 0 collapses every duration to nothing, which would leave these elements
 * hidden until the observer reached them — the point here is that they were
 * never hidden at all.
 */
@media ( prefers-reduced-motion: reduce ) {

    .spring-reveal:not( .spring-reveal-ready ) .entry-content > .spring-section > *,
    .spring-reveal [data-spring-reveal]:not( .is-revealed ) {
        filter: none;
        opacity: 1;
        transform: none;
    }
}
