/* Little Gems Boutiques storefront.
   Mobile-first and dependency-free — no Bootstrap, no jQuery, no JavaScript at all.
   Traffic is mostly phones, and the whole point of leaving Shopify was fewer things
   to maintain.

   Direction: "Grosgrain" — a proper British children's outfitters.

   It comes from the catalogue rather than from a mood board. The shop sells Pex suits,
   Spanish layette sets, broderie anglaise, smocked bows and frilly bum tights, mostly to
   grandmothers buying an occasion outfit. That world is haberdashery: ribbon, ticket
   tags, mounted photographs, navy and cream. So the page is bound top and bottom by a
   band of navy, trimmed with a cherry grosgrain ribbon drawn in CSS, and set in Caslon.

   Two previous directions are worth recording, because both failed the same way. The
   first ("Soft Boutique") was a cream ground, Fraunces and muted clay; the second was a
   white ground, Raleway/Lato and a saturated clay. Each was diagnosed as "bland" and
   answered with more contrast, but neither had anything to do with what the shop
   actually sells — they were generic boutique styling, and clay twice over. The fix was
   subject matter, not saturation.

   Structure is still driven entirely by the custom properties in :root, so the token
   names below are unchanged from the previous direction even where their values have
   moved a long way. That is deliberate: it keeps the 900 lines of component CSS under
   this block working, and it means the next direction is again mostly a :root edit. */

:root {
    /* Self-hosted — see css/fonts.css. Fallbacks are chosen close in width so the swap
       when the webfont lands shifts the layout as little as possible. */
    /* ---- spacing scale

       Everything that positions or separates uses one of these eleven steps. The file
       previously carried FORTY distinct rem values across padding, margin and gap — .28,
       .35, .45, .55, .65, .85, 1.05, 1.15, 1.7, 2.2, 2.4, 2.8 and so on — which is not a
       system, it is forty separate judgement calls, and it is the main reason components
       never quite lined up with each other.

       Named for the pixel value at a 16px root, so --sp-12 is 12px and the arithmetic is
       doable in your head while reading a rule. A 4px grid throughout, with 2px kept for
       optical nudges (badge eyelets, underline offsets). */
    --sp-2:  .125rem;
    --sp-4:  .25rem;
    --sp-8:  .5rem;
    --sp-12: .75rem;
    --sp-16: 1rem;
    --sp-20: 1.25rem;
    --sp-24: 1.5rem;
    --sp-32: 2rem;
    --sp-40: 2.5rem;
    --sp-48: 3rem;
    --sp-64: 4rem;

    --sans: 'Archivo', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
    --display: 'Libre Caslon Text', Georgia, 'Times New Roman', Times, serif;

    /* ---- palette

       Navy and cherry are the ribbon; paper and cream are the card it is pinned to; ink
       and rule do the quiet work. There is no third hue anywhere.

       The field is WHITE, and that is a decision about the stock rather than a taste for
       minimalism. Measured across all 936 hero images: 24% of the catalogue is
       predominantly pale or ivory — broderie anglaise, layette sets, christening wear —
       and 24% of the photographs carry significant white padding or a white studio sweep,
       because the importer composites every shot onto a square white canvas. A cream
       ground fought both at once: it tinted ivory garments warm so they read muddier than
       they are, and it made that padded canvas show as a brighter square inside the frame.
       On white the padding disappears into the page and the whites photograph true.

       The warm cream that used to be the ground is not gone, it is demoted to --warm and
       now tints the alternating bands. Warmth survives as punctuation instead of field. */
    --paper:       #ffffff;
    /* The old cream ground, deepened two steps. At its original #fbf6ef it separated
       from a white field by only 1.08:1 and the alternating bands all but vanished; this
       reads as a band at 1.13:1 while keeping muted text at 5.07 and cherry at 5.93. */
    --warm:        #f7f0e5;
    --navy:        #1e2b45;
    --navy-deep:   #16203a;
    --cherry:      #b21f2d;
    --cherry-deep: #8e1822;
    --cream:       #f4ede3;

    /* ---- the grosgrain ribbon

       The one bold thing on the page, and the reason it is drawn rather than shipped as
       an image: grosgrain is a ribbed weave, and a repeating gradient of a highlight
       and a shadow on a 4px period is exactly that rib. The inset shadows are the
       selvedge — the denser woven edge a real ribbon has top and bottom.

       Used in three places only: the base of the masthead, the top of the footer, and
       the short marker that opens a section. A device that appears everywhere stops
       being a device. */
    --ribbon-weave:
        repeating-linear-gradient(90deg,
            rgba(255, 255, 255, .13) 0 1px,
            rgba(0, 0, 0, .13) 1px 2px,
            transparent 2px 3px);
    --ribbon-edge:
        inset 0 1px 0 rgba(255, 255, 255, .22),
        inset 0 -1px 0 rgba(0, 0, 0, .30);

    /* ---- semantic tokens (names kept from the previous direction) */
    --bg: var(--paper);
    --ink: #2a2f3a;
    --muted: #6b6560;
    /* Neutral, not the old warm tan. On a cream ground #ddd2c3 was invisible enough to
       be a hairline; on white it reads dirty. And the rule now does structural work: with
       the frame and the page both white, this 1px border is the only thing defining a
       product card, so it is set stronger than a decorative hairline would be — 1.48:1
       against white. Measured off the rendered page at #e2dfda it came out as a single
       #e5e5e5 pixel, present but too faint to bound a card shot on a white sweep. */
    --line: #d8d4cd;
    --outbg: rgba(30, 43, 69, .82);

    /* The footer is a navy field; the masthead is not, any more.
    
       Both were navy at first, which gave the identity its spine but put too much weight
       at the top of the page: measured on the real homepage, 42% of the first screen sat
       below 35% lightness — a near-black band and a scrimmed hero stacked on top of each
       other — above photography whose median lightness is 0.71. The goods this shop sells
       are pale and chalky, and the page opened with a tonal cliff before a single product
       was visible.

       Demoting the masthead to paper takes that to 28%, and brings the header's own mean
       lightness to 0.76 against the catalogue's 0.71. Navy is not lost, it is just no
       longer MASS: it still holds the footer, the promo strip, every heading, the wordmark
       and every button. The thin navy promo strip above keeps the colour present at the
       very top, and the cherry ribbon still divides header from content. */
    --hd-bg: var(--paper);
    --ft-bg: var(--warm);

    /* The announcement bar and the footer take the warm band, not navy.

       Navy as a FIELD was the answer to a problem that no longer exists: it was
       introduced to stop a cream ground plus a serif reading as the same generic warm
       boutique the two earlier directions landed on. The white field settles that on its
       own, and once it did, the two navy slabs were left bookending an airy page with
       nothing between them to bridge the contrast.

       So navy keeps one register — type and primary action: every heading, the wordmark,
       the buttons, the card hover. Cherry keeps the other — ribbon and ticket. One
       meaning each, which is what the palette was short of. */
    --bar-bg: var(--warm);
    --bar-ink: var(--ink);

    /* Still navy: the page-builder's "dark" promo strip is a deliberate full-bleed band
       an editor opts into, which is a different thing from site chrome. */
    --promo-bg: var(--navy-deep);
    --promo-ink: var(--cream);

    /* Image frames must be white: the importer pads every photo onto a square white
       canvas, so any other frame colour would show that padding as a visible square.
       Now that the page is white too, the padding simply vanishes into the page — the
       constraint stops needing to be designed around at all. The frame's 1px rule is
       what gives the grid its edge, which matters here because the photography is not
       consistent: flat-lays, fur rugs, on-model shots and studio sweeps all sit in the
       same grid, and the border supplies the uniform boundary they don't. */
    --frame-bg: #ffffff;

    /* The tint on alternating bands.

       This was briefly a dusty rose, chosen because pink is 28% of every saturated
       garment pixel and had no place elsewhere in the palette. That reasoning held while
       the field was cream; against white a rose band reads markedly pinker and starts to
       look like a brand colour rather than a warm note, which would misdescribe a shop
       that also sells boys' clothing and Christmas. So the band takes the old ground
       instead — the same warmth, now used sparingly. */
    --surface: var(--warm);

    /* Square. Printed matter — tickets, labels, mounted plates — has cut edges. */
    --radius: 0;
    --badge-r: 0;
    --chip-r: 0;
    --chip-bg: #ffffff;

    /* Caslon wants no tracking and no capitals. It was cut for setting English text and
       it has a real lowercase; spacing it out or shouting it wastes the face. The
       previous direction uppercased the wordmark, every h1, the hero, option labels,
       badges, filter legends and field labels — that much all-caps is the commonest
       tell of a template, and none of it survived. */
    --brand-size: 1.6rem;
    --brand-weight: 700;
    --brand-track: 0;
    --brand-transform: none;

    --h1: 1.875rem;
    --h1-weight: 400;
    --h1-track: 0;
    --h1-transform: none;

    --title-weight: 400;
    --ratio: 1 / 1;
    --img-fit: cover;
    --gap: var(--sp-32) var(--sp-24);
    --shell: 1240px;
}

/* ---------------------------------------------------------------- accent

   --accent is cherry, and it means MONEY: the price, the sale ticket, the ribbon and the
   trim. Never decoration, never a heading.

   It used also to mean focus, current page, selected filter, selected photograph, low
   stock and out of stock — eight jobs for one hue, two of which were error states. A
   shopper cannot learn a colour that means eight things, and a keyboard shopper tabbing
   the size selector met the same red that says the size does not exist.

   So position and selection moved to navy, which is already this site's word for "you
   can act on this": the buttons, the wordmark, every heading, the card's hover border.
   Cherry keeps money, plus "only N left" — the single genuine alarm on the shop. */

:root {
    --accent: var(--cherry);
    --accent-dark: var(--cherry-deep);
    --sale: var(--cherry);

    /* Focus is navy, not cherry. Named separately from --navy so the ring can be
       retuned later without dragging every button along with it. */
    --focus: var(--navy);
}

/* The ribbon itself. 6px is the height a real grosgrain trim reads at on screen: any
   thinner and the rib disappears, any thicker and it becomes a banner. */
.ribbon,
.masthead::after,
.footer::before {
    content: "";
    display: block;
    height: 7px;
    background-color: var(--cherry);
    background-image: var(--ribbon-weave);
    box-shadow: var(--ribbon-edge);
}

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

body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: var(--sans);
    font-size: 16px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

/* One visible focus style for everything, rather than each component inventing its own.

   Two-tone, and the inner ring is not decoration. A single-colour ring cannot serve both
   grounds this site has: cherry on the navy Add to basket button measured 2.10:1 against
   WCAG's 3:1 floor for focus indicators, and plain navy on that same button is 1.00:1 —
   invisible. The paper ring fills the offset gap, so whatever sits underneath, there is
   always a light edge against a dark one. 14.13:1 on paper, on the warm band, and on the
   navy button alike.

   Declared as box-shadow rather than a second outline because an element gets only one
   outline, and outline is the half that must survive: it follows border-radius and is
   never clipped by an ancestor's overflow. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 3px;
    box-shadow: 0 0 0 3px var(--paper);
}

/* ---------------------------------------------------------------- motion

   One rule governs every animation in this file: motion answers something the shopper
   did, and shows what changed. Nothing plays on scroll, nothing plays on load, and
   nothing plays on its own. A shop that moves while you read it is a shop that is
   pleased with itself.

   Durations sit between 120ms and 300ms. Below 100ms a change reads as a glitch rather
   than a movement; above 350ms the interface starts to feel like it is thinking.

   Everything below is switched off wholesale by the block that follows. */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
    }
}

/* Lets a height or block-size transition finish at `auto` rather than at a guessed pixel
   value. Two things below rely on it — the mobile drawer and the FAQ answer — and they
   were shipped by browsers at different times, so it is declared once here on its own
   rather than inside either feature's @supports block. Without it both simply snap open,
   which is what they did before any of this. */
@supports (interpolate-size: allow-keywords) {
    :root { interpolate-size: allow-keywords; }
}

.shell { max-width: var(--shell); margin: 0 auto; padding: 0 var(--sp-20); }

/* ------------------------------------------------------------- promo bar */

.promo {
    background: var(--bar-bg);
    color: var(--bar-ink);
    border-bottom: 1px solid var(--line);
    text-align: center;
    font-size: .78rem;
    letter-spacing: .03em;
    padding: var(--sp-8) var(--sp-16);
}

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

.masthead {
    position: sticky;
    top: 0;
    z-index: 20;
    background: var(--hd-bg);
    color: var(--navy);
}

.masthead__bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-8) var(--sp-16);
    padding: var(--sp-16) 0;
}

.masthead__brand {
    font-family: var(--display);
    font-size: var(--brand-size);
    font-weight: var(--brand-weight);
    letter-spacing: var(--brand-track);
    text-transform: var(--brand-transform);
    white-space: nowrap;
}

/* Full cherry now the ground is paper — the lifted tint existed only to survive navy. */
.masthead__brand span { color: var(--cherry); }

/* min-width:0 is load-bearing: flex items default to min-width:auto, which would make the
   input's intrinsic width (~20 characters) a floor the header cannot shrink below and push
   the document wider than a narrow phone. The wrap on .masthead__bar is a second guard. */
.masthead__search {
    margin-left: auto;
    display: flex;
    gap: var(--sp-8);
    flex: 1 1 auto;
    min-width: 0;
    /* Widened from 300px. With 948 products the search is a primary way in, not a
       utility tucked in the corner. */
    max-width: 380px;
}

.masthead__search input {
    flex: 1;
    min-width: 0;
    padding: var(--sp-8) var(--sp-12);
    border: 1px solid var(--line);
    border-radius: var(--chip-r);
    font: inherit;
    background: #fff;
    color: var(--ink);
}

.masthead__search input::placeholder { color: var(--muted); }

/* ==========================================================================
   Predictive search

   The input gains a leading magnifier and a submit affordance; the panel below is the
   listbox. All of it is progressive — with js/search.js absent the panel stays hidden
   and the form is an ordinary GET to /Search.
   ========================================================================== */

.search { position: relative; display: flex; align-items: center; width: 100%; }

.search__icon {
    position: absolute;
    left: var(--sp-12);
    width: 18px;
    height: 18px;
    color: var(--muted);
    pointer-events: none;
}

/* Room for the icon on the left and the submit on the right. */
.masthead__search input {
    padding-left: calc(var(--sp-16) + 18px);
    padding-right: 42px;
}

.search__go {
    position: absolute;
    right: 1px;
    display: grid;
    place-items: center;
    width: 38px;
    height: calc(100% - 2px);
    border: 0;
    background: none;
    color: var(--muted);
    font: inherit;
    font-size: 1.05rem;
    cursor: pointer;
}

.search__go:hover { color: var(--navy); }

.search__panel {
    position: absolute;
    top: calc(100% + var(--sp-8));
    left: 0;
    right: 0;
    z-index: 40;
    max-height: min(70vh, 460px);
    overflow-y: auto;
    background: #fff;
    border: 1px solid var(--line);
    /* The one shadow on the site. An overlay has to read as floating above the page, and
       a border alone does not say that; everything else here stays flat. */
    box-shadow: 0 12px 28px rgba(30, 43, 69, .13);
    padding: var(--sp-8) 0;
    text-align: left;

    /* The shadow says this floats above the page; appearing instantly contradicts it.
       Four pixels and 120ms is enough to say the panel came from the input below. On
       the animation rather than a transition because the panel is toggled with the
       hidden attribute, and there is no from-state to transition out of. */
    animation: panel-in .12s ease-out;
}

@keyframes panel-in {
    from { opacity: 0; transform: translateY(-4px); }
}

.search__label {
    margin: var(--sp-8) 0 var(--sp-4);
    padding: 0 var(--sp-12);
    font-size: .75rem;
    font-weight: 600;
    color: var(--muted);
}

.search__row {
    display: flex;
    align-items: center;
    gap: var(--sp-12);
    width: 100%;
    padding: var(--sp-8) var(--sp-12);
    border: 0;
    background: none;
    font: inherit;
    text-align: left;
    color: var(--ink);
    cursor: pointer;
}

/* Hover and keyboard highlight are the same state deliberately: a shopper arrowing down
   and a shopper pointing should never see two different "current" rows. */
.search__row:hover,
.search__row[aria-selected="true"],
.search__all[aria-selected="true"] { background: var(--warm); }

.search__thumb {
    flex: 0 0 44px;
    width: 44px;
    height: 44px;
    background: var(--frame-bg);
    border: 1px solid var(--line);
    overflow: hidden;
}

.search__thumb img { width: 100%; height: 100%; object-fit: cover; }

.search__row-body { display: grid; gap: 2px; min-width: 0; }

.search__row-title {
    font-size: .9rem;
    line-height: 1.3;
    /* Titles carry Shopify's size suffixes and run long; one line keeps the rows even. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search__row-meta {
    font-size: .82rem;
    font-weight: 600;
    color: var(--accent);
    font-variant-numeric: tabular-nums lining-nums;
}

.search__row--collection { justify-content: space-between; }
.search__row--collection .search__row-meta { font-weight: 400; color: var(--muted); }
.search__row--term { color: var(--ink); font-size: .9rem; }

.search__oos {
    margin-left: var(--sp-8);
    color: var(--muted);
    font-weight: 400;
}

/* Pinned to the bottom of the panel. With six products and a collection the list
   overflows its max-height, and this is the escape hatch to the full results — scrolling
   it out of sight is exactly when a shopper needs it. */
.search__all {
    position: sticky;
    bottom: 0;
    display: block;
    margin-top: var(--sp-8);
    padding: var(--sp-12);
    border-top: 1px solid var(--line);
    background: #fff;
    font-size: .85rem;
    font-weight: 500;
    color: var(--navy);
    cursor: pointer;
}

.search__empty { padding: var(--sp-16) var(--sp-12); }
.search__empty-title { margin: 0 0 var(--sp-4); font-size: .92rem; font-weight: 600; color: var(--ink); }
.search__empty-hint { margin: 0 0 var(--sp-8); font-size: .85rem; color: var(--muted); }
.search__empty-link { font-size: .85rem; color: var(--accent); border-bottom: 1px solid currentColor; }

/* Wider than the input on desktop, anchored to its right edge. The catalogue's titles
   carry Shopify's size suffixes — "Alber Flower Frilly Top and Sunglasses Bow Shorts
   14YR" — and at the input's own 380px nearly every row ellipsed. */
@media (min-width: 860px) {
    .search__panel {
        left: auto;
        right: 0;
        width: 440px;
        max-width: calc(100vw - var(--sp-32));
    }
}

@media (max-width: 859px) {
    /* Full-width panel and 44px rows on a phone. */
    .search__row { padding-block: var(--sp-12); }
    .search__panel { max-height: 60vh; }
}

/* Horizontally scrollable: 11 collections will not fit on a phone. */
.masthead__nav {
    display: flex;
    gap: var(--sp-24);
    overflow-x: auto;
    padding: 0 0 var(--sp-12);
    scrollbar-width: none;
}
.masthead__nav::-webkit-scrollbar { display: none; }

.masthead__nav a {
    color: var(--muted);
    font-size: .9rem;
    white-space: nowrap;
    padding-bottom: var(--sp-4);
    border-bottom: 2px solid transparent;
}

.masthead__nav a:hover { color: var(--navy); }
.masthead__nav a[aria-current="page"] { color: var(--navy); border-bottom-color: var(--cherry); }

/* ------------------------------------------------------------------ type */

.crumbs { font-size: .78rem; color: var(--muted); padding: var(--sp-16) 0 var(--sp-4); }
.crumbs a:hover { color: var(--ink); }

/* Caslon set as Caslon: regular weight, sentence case, no tracking, and a tight-ish
   leading because the face has a modest x-height and headings are short. */
h1 {
    font-family: var(--display);
    font-size: var(--h1);
    font-weight: var(--h1-weight);
    letter-spacing: var(--h1-track);
    text-transform: var(--h1-transform);
    line-height: 1.18;
    color: var(--navy);
    margin: 0 0 var(--sp-4);
}

h2 {
    font-family: var(--display);
    font-size: 1.5rem;
    font-weight: var(--h1-weight);
    line-height: 1.22;
    color: var(--navy);
    margin: var(--sp-32) 0 var(--sp-16);
}

.muted { color: var(--muted); }
.small { font-size: .875rem; }

.page-head { padding: var(--sp-4) 0 var(--sp-20); }
.page-head p { margin: 0; color: var(--muted); font-size: .9rem; max-width: 56ch; }

/* ------------------------------------------------------------------ grid */

.grid {
    display: grid;
    gap: var(--gap);
    grid-template-columns: repeat(2, minmax(0, 1fr));
    padding-bottom: var(--sp-32);
}

@media (min-width: 620px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 980px) { .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

.card { display: block; }

.card__frame {
    position: relative;
    background: var(--frame-bg);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    aspect-ratio: var(--ratio);
    transition: border-color .15s ease;
}

/* No hover swap and no zoom. The second shot is often a detail crop or a different
   colourway rather than an alternate view of the same garment, so swapping looked like a
   glitch; and scaling on hover pushed the image past the frame, cropping it. The
   photography would have to be shot for those effects to earn them. */
.card__frame img {
    width: 100%;
    height: 100%;
    object-fit: var(--img-fit);
}

.card__frame--empty { display: grid; place-items: center; color: var(--muted); font-size: .8rem; }

.card__title {
    margin: var(--sp-12) 0 var(--sp-4);
    font-size: .9rem;
    font-weight: var(--title-weight);
    line-height: 1.35;
    color: var(--ink);
    text-decoration: underline 1px transparent;
    text-underline-offset: 3px;
    transition: text-decoration-color .15s ease;
}

/* Hover darkens the mount and underlines the name — the gesture of picking a mounted
   photograph off the card, rather than turning the title a different colour. */
.card:hover .card__frame { border-color: var(--navy); }
.card:hover .card__title { text-decoration-color: var(--navy); }

/* Price carries the accent. It is the one number a shopper is scanning for, and giving
   it the only hue on an otherwise neutral card is what stops the grid reading flat. */
.card__price {
    font-size: .95rem;
    font-weight: 600;
    color: var(--accent);
    /* Prices sit in a column down the grid, so the figures are lined and tabular. */
    font-variant-numeric: tabular-nums lining-nums;
}

.card__was { color: var(--muted); text-decoration: line-through; margin-left: var(--sp-4); font-size: .82rem; font-weight: 400; }

.badge {
    position: absolute;
    top: var(--sp-8);
    left: 0;
    padding: var(--sp-4) var(--sp-8) var(--sp-4) var(--sp-16);
    border-radius: var(--badge-r);
    font-size: .75rem;
    font-weight: 500;
    background: var(--sale);
    color: #fff;
    /* A swing ticket, not a chip: the notch is the pointed end of a garment tag and the
       dot is its punched eyelet. This is the ONLY place the ticket shape is used — a
       grid of 948 products with a tag on every card would be noise, so the shape is
       reserved for the two states that genuinely are a tag tied to the garment. */
    clip-path: polygon(0 50%, .5rem 0, 100% 0, 100% 100%, .5rem 100%);
    /* A fine light halo, so the ticket reads as a shape against any photograph. Measured
       across all 936 hero images, the cherry ticket falls below 1.6:1 against whatever is
       behind it on a fifth of the catalogue — the mid greys and putties of knitted sets
       especially — and simply disappears into the cloth. drop-shadow rather than
       box-shadow because the clip-path would crop a box-shadow away; a filter is applied
       after clipping and so traces the notched silhouette. */
    filter: drop-shadow(0 0 1.5px rgba(255, 255, 255, .95));
}

.badge::before {
    content: "";
    position: absolute;
    left: var(--sp-8);
    top: 50%;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--paper);
    transform: translateY(-50%);
}

.badge--out { background: var(--outbg); }

/* ------------------------------------------------------- listing toolbar */

.toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-8);
    align-items: center;
    justify-content: space-between;
    padding: 0 0 var(--sp-16);
    border-bottom: 1px solid var(--line);
    margin-bottom: var(--sp-24);
}

.toolbar form { display: flex; gap: var(--sp-8); align-items: center; }

.toolbar select {
    padding: var(--sp-8) var(--sp-8);
    border: 1px solid var(--line);
    border-radius: var(--chip-r);
    background: var(--chip-bg);
    color: var(--ink);
    font: inherit;
    font-size: .85rem;
}

/* ----------------------------------------------------------- collections */

.tiles {
    display: grid;
    gap: var(--sp-16);
    grid-template-columns: repeat(2, minmax(0, 1fr));
    padding-bottom: var(--sp-32);
}

@media (min-width: 700px) { .tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.tile {
    display: block;
    padding: var(--sp-20) var(--sp-20);
    border: 1px solid var(--line);
    border-left: 4px solid var(--navy);
    border-radius: var(--radius);
    background: #fff;
    transition: border-left-color .15s ease;
}

/* The card's hover already darkens its border to navy; these agree with it rather than
   inventing a second language for the same gesture. */
.tile:hover { border-left-color: var(--navy); }
.tile strong { display: block; font-family: var(--display); font-size: 1.15rem; font-weight: 400; color: var(--navy); }
.tile span { color: var(--muted); font-size: .84rem; }

/* -------------------------------------------------------- product detail */

.product { display: grid; gap: var(--sp-24); padding: var(--sp-16) 0 var(--sp-40); }

@media (min-width: 820px) {
    .product { grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); gap: var(--sp-48); align-items: start; }
}

.gallery__main {
    background: var(--frame-bg);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    aspect-ratio: var(--ratio);
}

.gallery__main img {
    width: 100%;
    height: 100%;
    object-fit: var(--img-fit);
    /* Every photograph sits on the same square white canvas, so a hard src swap can be
       genuinely hard to see — particularly on a phone, where the new file decodes and
       pops in. The fade is what makes the tap legible as a change of picture. */
    transition: opacity .15s ease;
}

.gallery__main img.is-swapping { opacity: 0; }

.gallery__thumbs { display: flex; gap: var(--sp-8); overflow-x: auto; padding-top: var(--sp-8); scrollbar-width: thin; }

.gallery__thumbs button {
    flex: 0 0 68px;
    height: 68px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--frame-bg);
    cursor: pointer;
}

.gallery__thumbs button[aria-current="true"] { border-color: var(--navy); border-width: 2px; }
.gallery__thumbs img { width: 100%; height: 100%; object-fit: var(--img-fit); }

/* Changing size rewrites the price, the stock line and the SKU. When two sizes cost the
   same, nothing visibly happens and the shopper cannot tell the control worked. A wash
   of the warm band behind the figures confirms the shop heard them — and is applied only
   when the text genuinely differs, so it never fires for a change that changed nothing. */
@keyframes noticed {
    from { background-color: var(--cream); }
    to   { background-color: transparent; }
}

.price.is-updated,
.stock.is-updated { animation: noticed .4s ease-out; }

.price { font-size: 1.6rem; font-weight: 600; margin: var(--sp-12) 0; color: var(--accent); font-variant-numeric: tabular-nums lining-nums; }
.price s { color: var(--muted); font-weight: 400; font-size: 1rem; margin-left: var(--sp-8); }

.options { display: grid; gap: var(--sp-16); margin: var(--sp-20) 0; }
.option label { display: block; font-size: .85rem; font-weight: 600; margin-bottom: var(--sp-8); }

.option select {
    width: 100%;
    padding: var(--sp-12) var(--sp-12);
    border: 1px solid var(--line);
    border-radius: var(--chip-r);
    background: var(--bg);
    color: var(--ink);
    font: inherit;
}

/* Stock state used a green and an amber, which were the only two hues on the site
   outside navy and cherry. Neither earned that: "in stock" is the unremarkable case and
   reads fine as plain text.

   Of the two remaining states only ONE is an alarm. "Only 2 left" is urgent and keeps
   cherry — it is now the single warning on the whole shop, which is what makes it read
   as one. "Sold out" is not a warning, it is a fact: nobody is being cautioned, they are
   being told. It takes the muted grey that every other piece of context on the page
   uses, and stops competing with the price directly above it. */
.stock { font-size: .9rem; margin: var(--sp-8) 0; color: var(--muted); }
.stock--in { color: var(--muted); }
.stock--low { color: var(--accent); font-weight: 500; }
.stock--out { color: var(--muted); font-weight: 500; }

.btn {
    display: inline-block;
    width: 100%;
    padding: var(--sp-12) var(--sp-20);
    border: 0;
    border-radius: var(--chip-r);
    background: var(--navy);
    color: var(--cream);
    font: inherit;
    font-weight: 500;
    font-size: .95rem;
    letter-spacing: .01em;
    text-align: center;
    cursor: pointer;
    transition: background-color .15s ease;
}

/* Navy, not cherry. Cherry is spoken for — it means ribbon and ticket, and a shop where
   the accent is also every button has no accent left for the price. */
.btn:hover { background: var(--navy-deep); }

/* Secondary action: same size and shape, outlined rather than filled. Applying a filter
   and adding to basket are not the same kind of act, and two solid navy buttons on one
   screen leaves the page with no primary at all. */
.btn--secondary {
    background: none;
    color: var(--navy);
    border: 1px solid var(--navy);
}

.btn--secondary:hover { background: var(--navy); color: var(--cream); }
.btn[disabled] { background: #b9b1a6; color: #f4ede3; cursor: not-allowed; }

/* ---- buying information

   Delivery, returns and payment, on the page and above the description. Left as a plain
   list rather than three icon cards: it is text a shopper reads once, and boxing it would
   make it compete with the Add to basket button directly above. */
.buying {
    list-style: none;
    margin: var(--sp-24) 0 0;
    padding: var(--sp-16) 0 0;
    border-top: 1px solid var(--line);
    font-size: .88rem;
    color: var(--muted);
}

.buying li { margin-bottom: var(--sp-8); }
.buying li:last-child { margin-bottom: 0; }
.buying strong { display: block; color: var(--navy); font-weight: 600; }

.description { margin-top: var(--sp-32); padding-top: var(--sp-20); border-top: 1px solid var(--line); font-size: .92rem; }
.description img { border-radius: var(--radius); margin: var(--sp-8) 0; }
.description :is(h1, h2, h3) { font-size: 1.05rem; font-family: var(--sans); margin: var(--sp-16) 0 var(--sp-8); }

.meta { margin-top: var(--sp-20); font-size: .84rem; color: var(--muted); }
.meta a { color: var(--accent-dark); text-decoration: underline; }

/* --------------------------------------------------------------- pager */

.pager { display: flex; gap: var(--sp-8); align-items: center; justify-content: center; flex-wrap: wrap; padding: var(--sp-8) 0 var(--sp-40); }

.pager a, .pager span {
    min-width: 2.3rem;
    padding: var(--sp-8) var(--sp-8);
    border: 1px solid var(--line);
    border-radius: var(--chip-r);
    text-align: center;
    font-size: .88rem;
}

.pager a:hover { border-color: var(--navy); }
.pager [aria-current="page"] { background: var(--navy); border-color: var(--navy); color: #fff; }
.pager .gap { border: 0; min-width: 1rem; }

/* --------------------------------------------------------------- footer */

/* Footer closes the page the way the masthead opens it: a navy field trimmed with the
   ribbon. ::before puts the ribbon at the top edge — see the .ribbon rule up in the
   identity block, which all three uses share. */
.footer {
    margin-top: var(--sp-48);
    background: var(--ft-bg);
    padding: 0 0 var(--sp-24);
    font-size: .88rem;
    color: var(--muted);
}

.footer > .shell { padding-top: var(--sp-40); }

.footer__cols { display: grid; gap: var(--sp-24); grid-template-columns: 1fr; }

@media (min-width: 700px) { .footer__cols { grid-template-columns: repeat(3, 1fr); } }

.footer__cols strong {
    color: var(--navy);
    display: block;
    margin-bottom: var(--sp-8);
    font-family: var(--display);
    font-weight: 400;
    font-size: 1.1rem;
}

.footer__cols p { margin: 0 0 var(--sp-4); }
.footer__cols a { display: block; padding: var(--sp-2) 0; }
.footer__cols a:hover { color: var(--cherry); }

.footer__legal {
    margin-top: var(--sp-32);
    padding-top: var(--sp-16);
    border-top: 1px solid var(--line);
    font-size: .78rem;
    color: var(--muted);
}

/* ---------------------------------------------------------------- states */

.empty { padding: var(--sp-48) var(--sp-16); text-align: center; color: var(--muted); }

.notice { padding: var(--sp-48) var(--sp-16) var(--sp-64); text-align: center; }
.notice h1 { font-size: 2rem; }
.notice .btn { width: auto; margin-top: var(--sp-16); padding-inline: var(--sp-32); }

.skip { position: absolute; left: -9999px; }

.skip:focus {
    left: var(--sp-16);
    top: var(--sp-16);
    z-index: 30;
    background: var(--bg);
    padding: var(--sp-8) var(--sp-16);
    border-radius: var(--radius);
}

/* ==========================================================================
   Sections

   Everything below renders a composable section from the page builder. Each one is a
   full-bleed band that applies .shell internally, which is why section-composed pages
   set ViewData["FullBleed"] and drop the wrapper.
   ========================================================================== */

.section { padding: var(--sp-48) 0; }
.section--surface { background: var(--surface); }

/* The ribbon marker. A short length of the same grosgrain opens each band — it says
   "a new section starts here" and it is the brand device, which is why there is no
   tracked-out caps label above the heading doing the same job in words. */
.section__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-16);
    margin-bottom: var(--sp-24);
}

.section__head { position: relative; padding-top: var(--sp-24); }

.section__head::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 72px;
    height: 7px;
    background-color: var(--cherry);
    background-image: var(--ribbon-weave);
    box-shadow: var(--ribbon-edge);
}
.section__head h2 { margin: 0; }

/* Left-aligned throughout. A shop's printed matter — labels, dockets, price lists — is
   ranged left; centring every band was the previous direction's habit, not a decision. */
.section__head--center { justify-content: space-between; text-align: left; }
.section__more {
    font-size: .88rem;
    color: var(--accent);
    border-bottom: 1px solid currentColor;
    white-space: nowrap;
}

/* ------------------------------------------------------------------- hero */

.hero {
    position: relative;
    display: grid;
    place-items: center;
    background-color: var(--surface);
    background-image: var(--hero-image);
    background-size: cover;
    background-position: center;
    color: var(--cream);
    text-align: left;
}

/* Scrim, not a filter on the image: text has to stay legible over photography we do not
   control, and the hero we recovered is only 786px wide so it is already soft. */
.hero::after {
    content: "";
    position: absolute;
    inset: 0;
    /* Directional, not a flat wash over the whole photograph.

       A uniform scrim has to choose between legible text and a visible picture, and at the
       strength the text needs it was darkening the entire hero — which is most of the dark
       mass this design was trying to shed. Measured on the real homepage, cream subheading
       over the flat .35 scrim came out at 2.96:1, under the 4.5:1 AA needs at that size;
       lightening the scrim to fix the mass made it 2.38:1. Neither was acceptable.

       So the scrim is now a gradient anchored to whichever side the text sits on: heavy
       enough behind the words to clear AA, clearing to almost nothing across the rest of
       the image. The admin's overlay setting still drives it — the stops are multiples of
       --hero-overlay rather than fixed values, so turning it up or down still works. Alpha
       above 1 simply clamps, which is why the strong stop can be a 2.2x multiple. */
    background: var(--hero-scrim);
}

.hero {
    --hero-scrim: linear-gradient(90deg,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * 2.2)) 0%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * 2.2)) 38%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * 1.1)) 62%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * .25)) 100%);
}

/* Centred text needs cover across the middle, so the gradient is symmetric instead. */
.hero--center {
    --hero-scrim: linear-gradient(90deg,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * .5)) 0%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * 2.2)) 22%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * 2.2)) 78%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * .5)) 100%);
}

.hero--right {
    --hero-scrim: linear-gradient(270deg,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * 2.2)) 0%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * 2.2)) 38%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * 1.1)) 62%,
        rgba(22, 32, 58, calc(var(--hero-overlay, .35) * .25)) 100%);
}

.hero--plain { color: var(--ink); }
.hero--plain::after { display: none; }

/* Sized against the viewport rather than in fixed pixels, so the hero gives way on a
   phone instead of pushing the products off the bottom of the first screen. At 390x844
   the medium hero was 340px on top of a ~140px header — the grid did not begin until
   well past the fold. The clamp keeps the desktop figure as the ceiling and lets small
   screens take the vh floor, which is one rule adapting to the space rather than a
   breakpoint guessing at it. */
/* Raised so the banner can carry advertising imagery rather than just sit behind a
   heading. The vh floor still protects the phone — at 844px tall, "large" resolves to
   439px, which leaves the trust bar and the first row of products reachable without a
   deliberate scroll, so this does not undo the above-the-fold work.

   Worth knowing before turning these up: the recovered hero photograph is 786x518 and is
   already upscaled about 1.9x at desktop width, which is why the original seed capped it
   at "medium". Artwork for a full-height banner wants to be roughly 2400x1000. */
.hero--small  { min-height: clamp(180px, 28vh, 260px); }
.hero--medium { min-height: clamp(240px, 38vh, 420px); }
.hero--large  { min-height: clamp(320px, 52vh, 600px); }
.hero--full   { min-height: clamp(420px, 82vh, 900px); }

.hero__inner { position: relative; z-index: 1; padding: clamp(var(--sp-32), 6vw, var(--sp-48)) var(--sp-20); width: 100%; }
.hero--left  .hero__inner { text-align: left; }
.hero--right .hero__inner { text-align: right; }

.hero__heading {
    font-family: var(--display);
    font-size: clamp(2rem, 5.2vw, 3.4rem);
    font-weight: 400;
    line-height: 1.1;
    /* h1 sets navy for the rest of the site; over photography the hero takes the cream
       it inherits from .hero instead. Without this the heading is navy on a dark scrim. */
    color: inherit;
    letter-spacing: 0;
    text-transform: none;
    margin: 0;
    text-wrap: balance;
}

.hero__sub {
    margin: var(--sp-16) 0 0;
    max-width: 42ch;
    font-size: 1.05rem;
    /* Was 88% cream. The transparency cost roughly a fifth of the contrast against the
       photograph for no visual gain — the hierarchy is already carried by size. */
    color: var(--cream);
}

.hero--center .hero__inner { text-align: center; }
.hero--center .hero__sub { margin-inline: auto; }

/* Paper button on photography. Navy would sink into the scrim it is sitting on. */
.hero .btn { background: var(--paper); color: var(--navy); }
.hero .btn:hover { background: var(--cream); }
.hero--plain .btn { background: var(--navy); color: var(--cream); }

.btn--inline { width: auto; display: inline-block; margin-top: var(--sp-24); padding-inline: var(--sp-32); }

/* ------------------------------------------------------------ promo strip */

.promo-strip { padding: var(--sp-12) 0; font-size: .9rem; }
/* A full-width band of flat cherry competed with the trim for the same meaning. Given
   the weave it stops being a coloured slab and becomes what it should have been: a broad
   length of the same ribbon, run across the page. */
.promo-strip--accent {
    background-color: var(--cherry);
    background-image: var(--ribbon-weave);
    box-shadow: var(--ribbon-edge);
    color: #fff;
}
.promo-strip--dark    { background: var(--promo-bg); color: var(--promo-ink); }
.promo-strip--surface { background: var(--surface); color: var(--ink); }

.promo-strip__inner { display: flex; gap: var(--sp-16); align-items: center; justify-content: center; flex-wrap: wrap; }
.promo-strip a { border-bottom: 1px solid currentColor; font-weight: 700; }

/* -------------------------------------------------------------- trust bar */

.trust { border-block: 1px solid var(--line); padding: var(--sp-24) 0; }

.trust__grid {
    display: grid;
    gap: var(--sp-20);
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 800px) { .trust__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

.trust__item { display: flex; gap: var(--sp-12); align-items: center; }
.trust__icon { color: var(--navy); flex: 0 0 28px; }
.trust__icon svg { width: 28px; height: 28px; }
.trust__item strong { display: block; font-size: .9rem; font-weight: 600; color: var(--ink); }
.trust__item span { color: var(--muted); font-size: .8rem; }

/* --------------------------------------------------------- feature columns */

.feature-cols {
    display: grid;
    gap: var(--sp-20);
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 800px) {
    .feature-cols { grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); }
}

.feature-col { display: block; position: relative; }

.feature-col__frame {
    display: block;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    background: var(--frame-bg);
}

/* No zoom on hover. .card__frame rejects that effect a thousand lines above, for a
   reason that applies just as well here: the importer composites every shot onto a
   square white canvas, so scaling pushes the garment past its frame and crops it. The
   label swap below is the part that reads as a button; the zoom was only movement. */
.feature-col__frame img { width: 100%; height: 100%; object-fit: cover; }

/* Overlaid rather than beneath: these tiles are the primary navigation on a phone, and a
   label under the image reads as a caption instead of a button. */
.feature-col__label {
    position: absolute;
    left: 50%;
    bottom: var(--sp-20);
    transform: translateX(-50%);
    background: var(--paper);
    color: var(--navy);
    padding: var(--sp-8) var(--sp-24);
    font-family: var(--display);
    font-size: 1.05rem;
    font-weight: 400;
    white-space: nowrap;
    transition: background-color .15s ease, color .15s ease;
}

.feature-col:hover .feature-col__label { background: var(--navy); color: var(--cream); }

/* ------------------------------------------------------------- feature row */

.feature-row { display: grid; gap: var(--sp-32); align-items: center; }

@media (min-width: 820px) {
    .feature-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-48); }
    .feature-row--right .feature-row__media { order: 2; }
}

.feature-row__media img { width: 100%; height: auto; }
.feature-row__body h2 { margin-top: 0; }

/* --------------------------------------------------------------- rich text */

/* Archivo at 16px runs to about 62 characters here. Serif body can take a longer line;
   a sans cannot, and the previous 68ch was over the comfortable limit. */
.rich-text { max-width: 62ch; }
.rich-text--center { margin-inline: auto; text-align: center; }
.rich-text--right { margin-left: auto; text-align: right; }

.rte { font-size: .95rem; line-height: 1.7; }
.rte :is(p, ul, ol) { margin: 0 0 var(--sp-16); }
.rte a { color: var(--accent); text-decoration: underline; }
.rte img { max-width: 100%; height: auto; }

/* ----------------------------------------------------------------- quotes */

.quotes { display: grid; gap: var(--sp-24); grid-template-columns: 1fr; }

@media (min-width: 780px) { .quotes { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* Set in Caslon's italic, which is a separate cut rather than a slant — it reads as
   quoted speech on its own, so the oversized curly quote mark the previous direction
   leaned on is gone. A short ribbon opens each one instead. */
/* No ribbon marker here, though an earlier pass had one. With a marker on the section
   head as well, three reviews put four lengths of ribbon in a single viewport and the
   device stopped meaning anything. The italic is doing the work already. */
.quote { margin: 0; text-align: left; }

.quote blockquote {
    margin: 0;
    font-family: var(--display);
    font-style: italic;
    font-size: 1.15rem;
    line-height: 1.55;
    color: var(--navy);
    max-width: 34ch;
}

.quote blockquote p { margin: 0; }
.quote figcaption { margin-top: var(--sp-16); font-size: .85rem; font-weight: 500; color: var(--muted); }

/* ------------------------------------------------------------- newsletter */

.newsletter { text-align: left; max-width: 46ch; }
.newsletter h2 { margin-top: 0; }
.newsletter p { color: var(--muted); font-size: .94rem; margin: 0; }

.newsletter__form { display: flex; gap: var(--sp-8); margin-top: var(--sp-20); flex-wrap: wrap; }
.newsletter__form .btn { width: auto; flex: 0 0 auto; padding-inline: var(--sp-24); }

.newsletter__form input {
    flex: 1 1 240px;
    min-width: 0;
    padding: var(--sp-12) var(--sp-16);
    border: 1px solid var(--line);
    border-radius: var(--chip-r);
    background: #fff;
    color: var(--ink);
    font: inherit;
}

.newsletter__form input:focus { outline: 2px solid var(--focus); outline-offset: 3px; box-shadow: 0 0 0 3px var(--paper); }
.newsletter__form .btn { margin-top: 0; }

/* ----------------------------------------------------------------- misc */

.flash {
    margin: var(--sp-16) 0 0;
    padding: var(--sp-12) var(--sp-16);
    background: var(--surface);
    border-left: 3px solid var(--accent);
    font-size: .9rem;
}

.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* .grid carries its own bottom padding for template pages that end on a grid. Inside a
   section that padding stacks with the section's own, leaving a conspicuous hole between
   bands — so it is removed there. */
.section .grid { padding-bottom: 0; }
.section:first-child { padding-top: var(--sp-32); }

/* ==========================================================================
   Faceted listing
   ========================================================================== */

.listing { display: grid; gap: var(--sp-24); align-items: start; }

@media (min-width: 900px) {
    .listing { grid-template-columns: 232px minmax(0, 1fr); gap: var(--sp-40); }
    /* Sticky so the filters stay reachable down a 31-page collection. */
    .listing__aside { position: sticky; top: 122px; }
}

.filters { border: 1px solid var(--line); }

.filters__toggle {
    padding: var(--sp-12) var(--sp-16);
    font-family: var(--display);
    font-size: 1.05rem;
    font-weight: 400;
    color: var(--navy);
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: var(--sp-8);
}

.filters__toggle::-webkit-details-marker { display: none; }
.filters__toggle::after { content: "+"; margin-left: auto; font-size: 1.1rem; line-height: 1; }
.filters[open] .filters__toggle::after { content: "\2212"; }

.filters__count {
    background: var(--accent);
    color: #fff;
    font-size: .62rem;
    padding: var(--sp-2) var(--sp-8);
    letter-spacing: .06em;
}

.filters__body { padding: 0 var(--sp-16) var(--sp-16); }
.filters__group { border: 0; padding: 0; margin: 0 0 var(--sp-20); }

.filters__group legend {
    padding: 0;
    margin-bottom: var(--sp-8);
    font-size: .85rem;
    font-weight: 600;
    color: var(--ink);
}

.filters__sizes { display: flex; flex-wrap: wrap; gap: var(--sp-8); }

/* The checkbox itself is visually hidden but still focusable, so the chip is keyboard
   reachable and screen readers announce a real checkbox rather than a styled div. */
.chip input { position: absolute; opacity: 0; width: 0; height: 0; }

.chip {
    display: inline-flex;
    border: 1px solid var(--line);
    padding: var(--sp-4) var(--sp-12);
    font-size: .8rem;
    cursor: pointer;
    user-select: none;
}

.chip:hover { border-color: var(--navy); }
.chip:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 3px; box-shadow: 0 0 0 3px var(--paper); }
/* Navy, not cherry. A filter you have switched on is a thing you did, not a warning —
   and filled cherry pills sat two rows above a cherry "Out of stock" line. */
.chip--on, .chip:has(input:checked) { background: var(--navy); border-color: var(--navy); color: #fff; }

/* ---- applied filters

   Deliberately NOT styled like .chip. A .chip is a control you toggle on and off; these
   are a statement of the current query, and each one is a link that removes itself. They
   read as navy outlines with a cross rather than filled cherry pills, so a shopper cannot
   mistake the two rows for the same thing. */
.applied {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-8) var(--sp-12);
    margin: 0 0 var(--sp-20);
}

.applied__label { font-size: .85rem; color: var(--muted); }

.applied__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-8);
    list-style: none;
    margin: 0;
    padding: 0;
}

.applied__chip {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-8);
    border: 1px solid var(--navy);
    color: var(--navy);
    /* 44px of touch target on a phone without a 44px-tall pill on desktop. */
    padding: var(--sp-8) var(--sp-12);
    min-height: 36px;
    font-size: .85rem;
    line-height: 1.2;
    transition: background-color .15s ease, color .15s ease;
}

.applied__chip:hover { background: var(--navy); color: var(--cream); }
.applied__x { font-size: 1.1em; line-height: 1; }
.applied__clear { font-size: .85rem; color: var(--accent); border-bottom: 1px solid currentColor; }

@media (max-width: 640px) {
    .applied__chip { min-height: 44px; }
}

.filters__range { display: flex; align-items: center; gap: var(--sp-8); }

.filters__range input {
    width: 100%;
    min-width: 0;
    padding: var(--sp-8) var(--sp-8);
    border: 1px solid var(--line);
    background: #fff;
    color: var(--ink);
    font: inherit;
    font-size: .85rem;
}

.check { display: flex; align-items: center; gap: var(--sp-8); font-size: .85rem; padding: var(--sp-2) 0; cursor: pointer; }
.check input { accent-color: var(--accent); width: 16px; height: 16px; }

.filters__actions { display: flex; align-items: center; gap: var(--sp-12); }
.filters__actions .btn { width: auto; flex: 1; padding: var(--sp-8) var(--sp-16); }
.filters__clear { font-size: .8rem; color: var(--muted); border-bottom: 1px solid currentColor; }

/* ==========================================================================
   Mobile navigation drawer

   Checkbox-driven rather than scripted: the storefront ships no JavaScript, and a nav that
   depends on a script is a nav that breaks before hydration on a slow phone.
   ========================================================================== */

.nav-toggle { position: absolute; opacity: 0; width: 0; height: 0; }

.nav-button {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-8);
    padding: var(--sp-8) var(--sp-2);
    font-size: .9rem;
    font-weight: 500;
    cursor: pointer;
    color: var(--navy);
}

.nav-button svg { width: 20px; height: 20px; }
.nav-toggle:focus-visible ~ .masthead__bar .nav-button { outline: 2px solid var(--focus); outline-offset: 3px; box-shadow: 0 0 0 3px var(--paper); }

@media (min-width: 860px) {
    .nav-button { display: none; }
    .masthead__nav { display: flex !important; }
}

@media (max-width: 859px) {
    /* Opens rather than cuts, and still without a line of JavaScript.

       Height, not max-height: a max-height needs a pixel guess, and a guess either clips
       a longer menu or spends half the animation crossing empty space. interpolate-size
       is what lets a height transition end at auto instead, and where a browser lacks it
       the drawer simply snaps open exactly as it did before — no clipping either way.

       visibility carries the tab order. Height alone would leave eleven invisible links
       focusable, so a keyboard shopper on a phone would tab into a menu that is not
       there. Transitioned rather than set outright because visibility flips to visible
       at once on the way in, and waits for the close to finish on the way out, which is
       precisely the behaviour wanted at both ends. */
    .masthead__nav {
        display: flex;
        flex-direction: column;
        gap: 0;
        overflow: hidden;
        height: 0;
        visibility: hidden;
        padding-bottom: 0;
        border-top: 1px solid transparent;
        transition: height .2s ease, visibility .2s, padding-bottom .2s ease, border-top-color .2s ease;
    }

    .nav-toggle:checked ~ .masthead__nav {
        height: auto;
        visibility: visible;
        padding-bottom: var(--sp-8);
        border-top-color: var(--line);
    }

    .masthead__nav a {
        padding: var(--sp-12) var(--sp-4);
        border-bottom: 1px solid var(--line);
        font-size: .95rem;
    }

    .masthead__nav a[aria-current="page"] { color: var(--navy); font-weight: 600; }
}

/* ==========================================================================
   Content pages: FAQ and contact
   ========================================================================== */

.faq { max-width: 76ch; }

.faq__item { border-bottom: 1px solid var(--line); }

.faq__item summary {
    padding: var(--sp-16) 0;
    cursor: pointer;
    font-weight: 700;
    list-style: none;
    display: flex;
    align-items: center;
    gap: var(--sp-16);
}

.faq__item summary::-webkit-details-marker { display: none; }

/* The marker is drawn rather than typed, because a "+" cannot be rotated into a "−" —
   four-fold symmetry means it can only ever become "×". Two crossed bars can: the
   vertical one collapses to nothing and leaves the horizontal behind. */
.faq__item summary::after {
    content: "";
    margin-left: auto;
    width: 13px;
    height: 13px;
    flex: 0 0 13px;
    background:
        linear-gradient(var(--navy) 0 0) center / 100% 1.5px no-repeat,
        linear-gradient(var(--navy) 0 0) center / 1.5px 100% no-repeat;
    transition: background-size .22s ease;
}

.faq__item[open] summary::after { background-size: 100% 1.5px, 1.5px 0; }

.faq__item .rte { padding: 0 0 var(--sp-16); }

/* Unfolds instead of snapping. Delivery and returns is where a shopper goes when they
   are already slightly anxious about ordering, and a panel that opens reads calmer than
   one that appears.

   Progressive enhancement, deliberately: ::details-content is the only way to animate a
   <details> without scripting it, and where the browser does not have it the answer
   simply appears exactly as it does today. interpolate-size is what allows a height
   animation to end at auto rather than at a guessed pixel value. */
@supports selector(::details-content) {
    .faq__item::details-content {
        block-size: 0;
        overflow: hidden;
        transition: block-size .22s ease, content-visibility .22s allow-discrete;
    }

    .faq__item[open]::details-content { block-size: auto; }
}

.contact { max-width: 46rem; }
.contact__form { display: grid; gap: var(--sp-16); margin-top: var(--sp-24); }

.field { display: grid; gap: var(--sp-4); }
.field label { font-size: .88rem; font-weight: 600; }

.field :is(input, textarea) {
    padding: var(--sp-12) var(--sp-12);
    border: 1px solid var(--line);
    border-radius: var(--chip-r);
    background: #fff;
    color: var(--ink);
    font: inherit;
}

/* Plain :focus, not :focus-visible — a text field should show its ring when clicked as
   well as when tabbed to, because the caret alone is a thin thing to find on a phone. */
.field :is(input, textarea):focus { outline: 2px solid var(--focus); outline-offset: 3px; box-shadow: 0 0 0 3px var(--paper); }

/* Honeypot. Off-screen rather than display:none — some bots skip hidden fields, and the
   point is to tempt them into filling it in. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ==========================================================================
   Basket
   ========================================================================== */

.basket { position: relative; display: inline-flex; padding: var(--sp-4); }
.basket svg { width: 24px; height: 24px; }

.basket__count {
    position: absolute;
    top: -2px;
    right: -4px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: 9px;
    background: var(--accent);
    color: #fff;
    font-size: .65rem;
    font-weight: 700;
    line-height: 17px;
    text-align: center;
}

/* Adding to the basket is a full page load, so the confirmation is never in doubt — but
   the count in the masthead is where the eye goes, and on arrival it is simply a
   different number. One beat ties the action to its result.

   transform-origin is the badge's own centre rather than the bag's, so it grows in place
   instead of drifting across the icon. The class is applied only on the redirect that
   follows an add, never on an ordinary visit to the basket. */
@keyframes landed {
    40% { transform: scale(1.28); }
}

.basket__count--added {
    animation: landed .3s ease-out;
    transform-origin: center;
}

.buy { margin-top: var(--sp-20); }
.buy__row { display: flex; gap: var(--sp-8); align-items: stretch; }

.buy__row input {
    width: 72px;
    padding: var(--sp-12) var(--sp-8);
    border: 1px solid var(--line);
    background: #fff;
    color: var(--ink);
    font: inherit;
    text-align: center;
}

.buy__row .btn { flex: 1; width: auto; }

.cart { display: grid; gap: var(--sp-32); padding-bottom: var(--sp-48); }

@media (min-width: 900px) { .cart { grid-template-columns: minmax(0, 1fr) 300px; gap: var(--sp-48); align-items: start; } }

.cart-line {
    display: grid;
    grid-template-columns: 84px minmax(0, 1fr) auto;
    gap: var(--sp-16);
    align-items: center;
    padding: var(--sp-16) 0;
    border-bottom: 1px solid var(--line);
}

@media (min-width: 700px) {
    .cart-line { grid-template-columns: 84px minmax(0, 1fr) auto auto auto; }
}

.cart-line__frame { display: block; background: var(--frame-bg); aspect-ratio: 1/1; overflow: hidden; }
.cart-line__frame img { width: 100%; height: 100%; object-fit: cover; }
.cart-line__body { display: grid; gap: var(--sp-2); }
.cart-line__price { color: var(--accent); font-weight: 700; font-size: .9rem; }
.cart-line__warn { color: var(--sale); font-size: .8rem; }

.cart-line__qty { display: flex; gap: var(--sp-4); align-items: center; }
.cart-line__qty input { width: 58px; padding: var(--sp-8); border: 1px solid var(--line); font: inherit; text-align: center; }
.cart-line__apply { border: 0; background: none; font: inherit; font-size: .75rem; color: var(--muted); text-decoration: underline; cursor: pointer; }
.cart-line__total { font-weight: 700; white-space: nowrap; }
.cart-line__remove { border: 0; background: none; font-size: 1.4rem; line-height: 1; color: var(--muted); cursor: pointer; padding: var(--sp-4) var(--sp-8); }
.cart-line__remove:hover { color: var(--sale); }

.cart__summary { border: 1px solid var(--line); padding: var(--sp-20); }
.cart__summary h2 { margin-top: 0; }
.cart__row { display: flex; justify-content: space-between; gap: var(--sp-16); padding: var(--sp-8) 0; }
.cart__summary .btn { margin: var(--sp-16) 0 var(--sp-12); }

/* ==========================================================================
   Saved, compare and journal
   ========================================================================== */

.save-row { display: flex; gap: var(--sp-12); margin-top: var(--sp-12); flex-wrap: wrap; }

.save-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-8);
    border: 1px solid var(--line);
    background: #fff;
    color: var(--ink);
    font: inherit;
    font-size: .82rem;
    padding: var(--sp-8) var(--sp-12);
    cursor: pointer;
}

/* Saved is a selection, and follows .chip--on rather than inventing a second colour for
   the same idea: a thing you have switched on. */
.save-btn:hover { border-color: var(--navy); }
.save-btn--on { background: var(--navy); border-color: var(--navy); color: #fff; }
.save-btn svg { width: 16px; height: 16px; }

/* Horizontal scroll rather than reflow: a comparison that stacks is no longer a comparison. */
.compare { overflow-x: auto; padding-bottom: var(--sp-32); }
.compare__table { border-collapse: collapse; min-width: 100%; }
.compare__table th, .compare__table td { border: 1px solid var(--line); padding: var(--sp-12); vertical-align: top; text-align: left; min-width: 170px; }
.compare__table thead th img { width: 100%; height: auto; margin-bottom: var(--sp-8); }
.compare__table tbody th { background: var(--surface); font-size: .85rem; font-weight: 600; color: var(--ink); min-width: 110px; }
.compare__price { color: var(--accent); font-weight: 700; }

.journal { display: grid; gap: var(--sp-32); padding-bottom: var(--sp-48); }
@media (min-width: 760px) { .journal { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1060px) { .journal { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.journal__item h2 { margin: var(--sp-12) 0 var(--sp-4); font-size: 1.1rem; }
.journal__frame { display: block; aspect-ratio: 3 / 2; overflow: hidden; background: var(--frame-bg); }
.journal__frame img { width: 100%; height: 100%; object-fit: cover; }
.journal__item p { font-size: .92rem; color: var(--muted); }
.journal__more { color: var(--accent); border-bottom: 1px solid currentColor; font-size: .85rem; }

.post { max-width: 72ch; margin: 0 auto; padding-bottom: var(--sp-48); }
.post__hero { width: 100%; height: auto; margin: var(--sp-16) 0 var(--sp-24); }
.post__body { font-size: 1rem; }

.promo a { border-bottom: 1px solid currentColor; }
.footer__address { white-space: pre-line; }

/* ---- free-delivery meter

   A real <progress>, not a styled div: it announces its value to assistive tech for free,
   so the bar is never the only carrier of the information. The label above it states the
   remaining spend in words, which is what a shopper actually needs. */
.ship-meter { margin: var(--sp-12) 0 var(--sp-16); }

.ship-meter label {
    display: block;
    font-size: .85rem;
    color: var(--ink);
    margin-bottom: var(--sp-8);
}

.ship-meter progress {
    display: block;
    width: 100%;
    height: 6px;
    border: 0;
    background: var(--line);
    appearance: none;
}

.ship-meter progress::-webkit-progress-bar { background: var(--line); }
.ship-meter progress::-webkit-progress-value { background: var(--navy); }
.ship-meter progress::-moz-progress-bar { background: var(--navy); }

.ship-meter__done {
    margin: var(--sp-12) 0 var(--sp-16);
    font-size: .85rem;
    font-weight: 500;
    color: var(--navy);
}

.cart__row--total {
    border-top: 1px solid var(--line);
    margin-top: var(--sp-8);
    padding-top: var(--sp-12);
    font-weight: 600;
    font-size: 1.05rem;
    color: var(--navy);
}

.cart__code { display: flex; gap: var(--sp-8); margin: var(--sp-12) 0; }
.cart__code input { flex: 1; min-width: 0; padding: var(--sp-8) var(--sp-8); border: 1px solid var(--line); font: inherit; text-transform: uppercase; }
/* Uppercase is for the code the shopper types, not for the hint telling them what to
   type — the rule was shouting the placeholder as well. */
.cart__code input::placeholder { text-transform: none; }
.cart__code button { border: 1px solid var(--navy); background: none; padding: var(--sp-8) var(--sp-16); font: inherit; font-size: .85rem; cursor: pointer; }
.cart__code button:hover { background: var(--navy); color: var(--cream); }
.cart__row--discount { color: var(--accent); font-weight: 700; }
.cart__row--discount span { display: inline-flex; align-items: center; gap: var(--sp-4); }
.cart__remove-code { display: inline; }
.cart__remove-code button { border: 0; background: none; color: var(--muted); font-size: 1.05rem; line-height: 1; cursor: pointer; padding: 0 var(--sp-4); }
.cart__code-problem { color: var(--sale); font-size: .82rem; margin: var(--sp-4) 0 0; }

.track { max-width: 46rem; padding-bottom: var(--sp-48); }
.track__form { display: grid; gap: var(--sp-16); max-width: 22rem; }
.track__form .btn { width: auto; }
.track__result { margin-top: var(--sp-32); padding-top: var(--sp-24); border-top: 1px solid var(--line); }
/* "Dispatched" is good news. In cherry it read as an alert, which is the opposite of
   what someone checking on their parcel wants to see. */
.track__status { font-size: 1.05rem; font-weight: 700; color: var(--navy); }
.track__lines { width: 100%; border-collapse: collapse; margin: var(--sp-20) 0; font-size: .92rem; }
.track__lines td { padding: var(--sp-8) 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.track__lines td:last-child { text-align: right; white-space: nowrap; }
.track__lines span { display: block; }

.rail { margin-top: var(--sp-40); padding-top: var(--sp-32); border-top: 1px solid var(--line); }
.rail .section__head { margin-bottom: var(--sp-20); }
.rail .grid { padding-bottom: 0; }

.notify { margin-top: var(--sp-16); padding: var(--sp-16) var(--sp-16); background: var(--surface); }
.notify label { display: block; font-size: .82rem; font-weight: 700; margin-bottom: var(--sp-8); }
.notify__row { display: flex; gap: var(--sp-8); }
.notify__row input { flex: 1; min-width: 0; padding: var(--sp-8) var(--sp-8); border: 1px solid var(--line); background: #fff; font: inherit; }
.notify__row button { border: 1px solid var(--navy); background: none; padding: var(--sp-8) var(--sp-16); font: inherit; font-size: .85rem; cursor: pointer; white-space: nowrap; }
.notify__row button:hover { background: var(--navy); color: var(--cream); }

/* ---- the preview lock

   One card on the warm band, centred. It borrows the wordmark, the ribbon and the type of
   the shop proper so a client who has been sent the password recognises where they are
   before they read a word, but it carries no navigation and names no product: everything
   this page could show is the thing it exists to withhold. */
.gate {
    display: grid;
    place-items: center;
    min-height: 100vh;
    padding: var(--sp-24);
    background: var(--warm);
}

.gate__card {
    width: 100%;
    max-width: 26rem;
    padding: var(--sp-40) var(--sp-32) var(--sp-32);
    border: 1px solid var(--line);
    background: var(--paper);
    text-align: center;
}

.gate__brand {
    margin: 0;
    font-family: var(--display);
    font-size: 1.6rem;
    color: var(--navy);
}

.gate__brand span { color: var(--cherry); }

/* The ribbon sits under the wordmark exactly as it sits under the masthead, and is the
   only decoration on the page. */
.gate .ribbon { margin: var(--sp-20) calc(var(--sp-32) * -1) var(--sp-32); }

.gate h1 {
    margin: 0 0 var(--sp-8);
    font-family: var(--display);
    font-size: 1.35rem;
    font-weight: 400;
    color: var(--navy);
}

.gate__blurb { margin: 0 0 var(--sp-24); color: var(--muted); font-size: .92rem; }

.gate__error {
    margin: 0 0 var(--sp-16);
    padding: var(--sp-8) var(--sp-12);
    border-left: 3px solid var(--cherry);
    background: var(--cream);
    color: var(--cherry-deep);
    font-size: .9rem;
    text-align: left;
}

.gate .field { text-align: left; margin-bottom: var(--sp-16); }
