/* ==========================================================================
   Equinova storefront — shared stylesheet.

   Everything both pages need: the reset, the chrome (announcement bar, header, drawer, search
   overlay, footer, toast), the modal shell, skeletons, and the touch-device rules. Page-specific
   rules live in home.css and cart.css.

   The tokens are no longer here. Palette, roles, type scale and layer scale moved to tokens.css
   when the admin console came onto the same design system, because two front ends keeping their
   own copies of the same names is exactly how they drift apart. layout.html links tokens.css
   ahead of this file; nothing below works without it.
   ========================================================================== */

*{box-sizing:border-box;}

/* Makes the hidden attribute mean what it says.

   It is only a user-agent default, so the weakest class setting a display beats it and the element
   stays on screen with `hidden` sitting in the markup doing nothing. That is not a theoretical
   ordering problem: .account-loading was display:grid, so the account page's skeleton went on
   loading forever in front of a profile that had already arrived, and every reader of that code saw
   `el("accountLoading").hidden = true` and reasonably concluded it was handled.

   One rule here rather than a pairing per element. The shop had five of those — .totals-row,
   .trending-panel, .auth-error, .field-help, .bundle-notice — each added the day its own element
   was caught, which is a pattern that only ever finds the bugs somebody has already tripped over.
   The admin console reached the same conclusion in its own sheet after the drawer's dimming sheet
   swallowed every tap on a phone. Marked important because that is what it takes to outrank a
   class, and because "hidden" should not be a suggestion. */
[hidden]{ display:none !important; }

/* scroll-padding keeps anchor targets clear of the sticky header. */
html{ scroll-behavior:smooth; scroll-padding-top:var(--header-h); }

/*
   Body scroll lock, applied while a drawer, overlay or modal is open.

   position:fixed rather than overflow:hidden, because Safari on iOS ignores overflow:hidden on the
   body and scrolls the page behind the overlay anyway. The trade is that fixing the body throws
   away the scroll position, so EquinovaChrome records it, pins the body at the negative offset,
   and scrolls back to the exact pixel on close.
*/
body.scroll-locked{ position:fixed; left:0; right:0; width:100%; overflow:hidden; }

/*
   The page fills the window, and the footer sits on the bottom edge of it.

   Every storefront page is a column of announcement, header, main and footer, so the column is
   declared once here rather than nine times. Without it a short page — /account before anybody has
   signed in, /orders/{number} while the order is still being fetched, a policy page of three
   paragraphs — leaves the footer stranded halfway up the screen with the page background under it,
   which reads as a page that failed to finish loading.

   dvh with vh behind it, the pair the drawer already uses. vh is measured against the tallest the
   viewport ever gets, so on a phone that still has its address bar showing, 100vh is taller than
   the screen and a page with nothing to scroll arrives scrollable.
*/
body{
  margin:0; background:var(--surface-page); color:var(--text-primary); font-family:var(--sans);
  -webkit-font-smoothing:antialiased; line-height:1.5;
  min-height:100vh; min-height:100dvh;
  display:flex; flex-direction:column;
}

/*
   The slack goes to the page's own region rather than into a margin above the footer.

   Attached to <main> and not to the footer on purpose. The admin console has a shell of its own and
   never loads this sheet, but the live preview does, and it renders the footer fragment on its own
   with no page around it — pushing that to the foot of the frame would leave a hole above the one
   thing the owner asked to look at. A preview of the footer has no <main>, so it grows nothing.

   width is not belt and braces. Every page's <main> is also the .wrap, which centres itself with
   margin:0 auto, and a flex item with an auto margin across the container is not stretched to fill
   it — it is shrunk to its contents and then centred. Without this line the catalogue collapses to
   the width of one card. Said here rather than in .wrap, because .wrap is used inside the header
   and the footer as well, where it is not a flex item and has nothing to fill.
*/
body > main{ flex:1 0 auto; width:100%; }
img{max-width:100%; display:block;}
a{color:inherit; text-decoration:none;}
button{font-family:var(--sans); cursor:pointer; border:none; background:none;}
h1,h2,h3,h4{font-family:var(--serif); color:var(--text-heading); margin:0; font-weight:600;}
:focus-visible{outline:2px solid var(--focus-ring); outline-offset:3px;}

.wrap{max-width:1260px; margin:0 auto; padding:0 32px;}
@media(max-width:640px){ .wrap{padding:0 20px;} }

.eyebrow{font-size:var(--type-xs); letter-spacing:0.2em; text-transform:uppercase; font-weight:600; color:var(--text-accent);}

.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

@media (prefers-reduced-motion: reduce){
  *{animation-duration:0.001ms !important; transition-duration:0.001ms !important;}
  html{scroll-behavior:auto;}
}

/* ============ Announcement bar ============ */
/* First thing on the page, so it is what clears the notch. The side insets matter in landscape,
   where the notch eats into one edge of the viewport. */
.announce{
  background:var(--surface-inverse-deep); color:var(--text-on-inverse); font-size:var(--type-sm); text-align:center;
  padding:calc(10px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right))
          10px calc(16px + env(safe-area-inset-left));
  letter-spacing:0.02em; overflow:hidden;
  border-bottom:1px solid var(--border-accent-on-inverse-soft);
}
.announce span{opacity:0.9;}
.announce b{color:var(--text-accent-on-inverse); font-weight:600;}
/* The bar also held a link-styled button — the Fit Quiz call to action — between the text and the
   currency note. It is withdrawn with the quiz, and the bar is two centred spans now. */
@media(max-width:480px){
  .announce{
    font-size:var(--type-xs);
    padding:calc(9px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right))
            9px calc(12px + env(safe-area-inset-left));
  }
}

/* ---- The offers strip -------------------------------------------------------------------------
   The running promo codes, between the announcement bar and the sale band.

   Forest rather than forest-deep, so it reads as the second line of the same dark band at the top
   of the page rather than as a third separate thing. The code itself is the gold-on-dark role,
   which is the only place in the shop that colour is used for a string a shopper has to retype.

   Fixed height. A marquee that changes height when the list changes length would shift the whole
   page down as it loaded, which is the layout shift this shop measures itself on. */
.offers-bar{
  background:var(--surface-inverse); color:var(--text-on-inverse);
  border-bottom:1px solid var(--border-accent-on-inverse-soft);
  font-size:var(--type-sm); overflow:hidden;
}
.offers-bar-link{
  display:flex; align-items:center; justify-content:center; gap:16px; min-width:0;
  height:38px; padding:0 calc(16px + env(safe-area-inset-right)) 0 calc(16px + env(safe-area-inset-left));
  color:inherit; text-decoration:none;
}
.offers-bar-link:hover .offers-bar-cta{ color:var(--text-accent-on-inverse-bright); }
.offers-bar-single{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.offers-bar-cta{
  flex:none; font-size:var(--type-xs); letter-spacing:0.06em; text-transform:uppercase;
  color:var(--text-accent-on-inverse); text-decoration:underline; text-underline-offset:3px;
}

/* The window the list travels across. No fade at the edges: the only way to soften them is a mask
   built from a literal colour, and a raw colour in this file is the thing raw-colours.mjs exists
   to find. The band is the same dark as the strip, so a hard edge reads as the edge of the band. */
.offers-marquee{ flex:1 1 auto; min-width:0; overflow:hidden; }
.offers-marquee-track{
  display:flex; width:max-content; animation:offers-scroll 28s linear infinite;
}
/* Half the track is one copy of the list, so a shift of exactly -50% lands the second copy where
   the first began and the loop has no seam. */
@keyframes offers-scroll{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }
.offers-bar-link:hover .offers-marquee-track{ animation-play-state:paused; }
.offers-marquee-run{ display:flex; }
.offers-marquee-item{
  display:inline-flex; align-items:center; gap:8px; padding:0 22px; white-space:nowrap;
}
.offers-marquee-code{
  font-weight:700; letter-spacing:0.08em; color:var(--text-accent-on-inverse);
}
.offers-marquee-offer{ opacity:0.85; }

/* Nothing moves. The list wraps onto the line it is given and is read rather than watched — the
   offers are information, and information must not depend on an animation to be reachable. */
@media(prefers-reduced-motion:reduce){
  .offers-marquee-track{ animation:none; }
}
@media(max-width:480px){
  .offers-bar-link{ height:34px; gap:10px; font-size:var(--type-xs); }
  .offers-bar-cta{ display:none; }
  .offers-marquee-item{ padding:0 14px; }
}

/* The panel the strip opens — #offersModal in fragments/layout.html. Here beside the strip rather
   than in catalogue.css with the /offers page, because the panel is on every page in the shop and
   a rule the whole site depends on cannot live in one page's sheet. */
.offers-panel{ width:520px; }
.offer-rows{ list-style:none; margin:0 0 18px; padding:0; display:grid; gap:12px; }
.offer-row{
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  padding:16px 18px; border:1px solid var(--border); border-radius:var(--radius-md);
  background:var(--surface-sunken);
}
.offer-row-text{ min-width:0; }
.offer-row-code{
  font-weight:700; letter-spacing:0.08em; font-size:var(--type-md); color:var(--text-accent);
  margin:0;
}
.offer-row-offer{ font-size:var(--type-sm); color:var(--text-primary); margin:2px 0 0; }
.offer-row-label,
.offer-row-conditions{ font-size:var(--type-xs); color:var(--text-muted); margin:2px 0 0; }
/* Never shrinks under a long condition line: the button is the reason the panel exists. */
.offer-row-use{ flex:none; }

@media(max-width:480px){
  .offer-row{ flex-direction:column; align-items:stretch; gap:12px; }
}

/* ---- The sale band ----------------------------------------------------------------------------
   Under the announcement bar and above the header, present only while a sale is running.

   Gold rather than the sale red used on the prices. The red is right on a price, where it is a
   word-sized mark against a card; the same colour eighty pixels wide across the top of every page
   in the shop is a warning banner, and this is not a warning. The accent roles already flip
   correctly for dark — in light it is brass carrying forest, in dark the lifted gold carrying
   near-black — which is also why nothing here names a colour.

   The whole band is one link, so it has no separate hit target to miss. */
.sale-band{
  display:flex; align-items:center; justify-content:center; flex-wrap:wrap; gap:6px 12px;
  background:var(--surface-accent); color:var(--text-on-accent);
  font-size:var(--type-sm); text-align:center;
  padding:9px calc(16px + env(safe-area-inset-right)) 9px calc(16px + env(safe-area-inset-left));
  letter-spacing:0.01em;
}
.sale-band:hover{ background:var(--surface-accent-bright); }
.sale-band-flag{
  font-size:var(--type-xs); font-weight:700; letter-spacing:0.06em; text-transform:uppercase;
  border:1px solid var(--text-on-accent); border-radius:999px; padding:1px 9px;
}
.sale-band-text{ font-weight:600; }
/* The nudge, not the news. It goes first on a phone, where the sentence itself needs the width. */
.sale-band-cta{ text-decoration:underline; text-underline-offset:3px; }
@media(max-width:480px){
  .sale-band{ font-size:var(--type-xs); gap:4px 8px; }
  .sale-band-cta{ display:none; }
}

/* ============ Header ============ */
/*
   The frosted bar. Its tint is a role rather than a colour because a blur is only convincing when
   the pane is the same hue as what is behind it — the light value over a dark page reads as milky
   grey plastic, which is what a naive dark mode does to every piece of frosted glass it owns.
*/
header.site{
  position:sticky; top:0; z-index:var(--layer-sticky); background:var(--surface-header);
  backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px); border-bottom:1px solid var(--border);
  transition:background .25s ease, box-shadow .25s ease;
}
header.site.scrolled{
  background:var(--surface-header-scrolled); box-shadow:var(--shadow-header);
}
/*
   ROW ONE — the visitor's own controls.

   It used to be everything: logo, six links, a currency pill, search, theme, account, bag and a
   gold quiz button, spread by justify-content:space-between under a 1260px cap. The link list was
   the only flexible thing in it, so it was the thing that got squeezed, and "Winter Rugs" and
   "Tack & Leather" wrapped onto two lines while the short labels did not — ragged baselines, from
   labels the owner types into the admin and can lengthen at any time.

   Stopping the wrap on its own only moved the fault: the links would have overlapped the controls
   somewhere between 920 and 1100px instead. They needed a row, so they have one.
*/
.nav{
  display:flex; align-items:center; gap:16px;
  padding:12px calc(32px + env(safe-area-inset-right)) 12px calc(32px + env(safe-area-inset-left));
  max-width:1260px; margin:0 auto;
}
.logo{
  font-family:var(--serif); font-weight:700; font-size:var(--type-lg); letter-spacing:0.25em;
  text-transform:uppercase; color:var(--text-heading); white-space:nowrap; flex-shrink:0;
}

/* ============ The official mark ============

   Two treatments were supplied, brass and silver. Brass belongs to the light theme — it is the
   same accent the shop already uses, and it is the only one of the two that reads on cream at
   all. Silver belongs to the dark theme.

   BOTH ARE IN THE HTML AND THIS FILE PICKS ONE. The theme is settled by the inline script in the
   head before the first paint, so :not([data-theme]) and [data-theme="dark"] are both decidable
   by the time anything is drawn. Swapping a src from a script would paint the wrong logo first
   and correct it after, which is the fault the theme script exists to prevent.

   The supplied files were 819x1024 mockups with the backdrop baked in, so what these rules point
   at are transparent crops recovered from them — see backend/tools/brand-assets.py. Nothing was
   traced or redrawn.

   Two placements only, the header and the footer. The drawer keeps its typeset wordmark: it is a
   dialog, and a mark repeated inside one is the duplicate branding the review asked us to avoid.
*/
/* A column since the tagline joined the mark, centred on it the way the printed lock-up is. The
   line is the wider of the two, so this is the mark that moves: it sits over the middle of the
   line rather than above its first letter. */
.logo-link{ display:inline-flex; flex-direction:column; align-items:center; gap:5px; }
.brand-dark{ display:none; }
[data-theme="dark"] .brand-light{ display:none; }
[data-theme="dark"] .brand-dark{ display:block; }

/* Height-driven, so the row's rhythm sets the size and the aspect ratio follows from the
   intrinsic width/height on the element. */
.brand-mark{ height:38px; width:auto; }

/* The lock-up's line, carried into the header under the mark. It is the smallest step on the scale
   and no smaller: the tracking is what makes it read as part of the mark, and a size invented
   below --type-xs to buy the tracking back would be a size nothing else in the shop uses. It goes
   away with the search field at 768, where the row is a toggle, the mark and three controls. */
.brand-tagline{
  font-family:var(--serif); font-size:var(--type-xs); font-weight:600; line-height:1;
  letter-spacing:0.16em; text-transform:uppercase; color:var(--text-muted); white-space:nowrap;
}

.footer-logo{ display:block; }
.brand-lockup{ width:190px; height:auto; max-width:100%; }

/* The auto margins centre the field in whatever is left between the logo and the controls. */
.header-search{ position:relative; display:flex; align-items:center; flex:1; min-width:0; max-width:520px; margin:0 auto; }
.header-search-icon{
  position:absolute; left:15px; display:flex; pointer-events:none; color:var(--text-muted);
}
.header-search-input{
  width:100%; height:40px; padding:0 16px 0 42px; border-radius:999px;
  border:1px solid var(--border); background:var(--surface-sunken); color:var(--text-primary);
  font-family:inherit; font-size:var(--type-sm); outline:none;
  transition:border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
.header-search-input::placeholder{ color:var(--text-muted); }
.header-search-input:focus{
  background:var(--surface-raised); border-color:var(--surface-accent); box-shadow:0 0 0 3px var(--glow-accent);
}
/* The type=search clear button, which Chrome draws in its own grey regardless of the theme. */
.header-search-input::-webkit-search-cancel-button{ -webkit-appearance:none; appearance:none; }

/*
   What the overlay used to offer, moved to where the typing now happens. z-index is a small local
   number on purpose: header.site is a stacking context of its own, so this only has to beat the
   category row below it and can never reach the scale in the layer block.
*/
.trending-panel{
  position:absolute; top:calc(100% + 10px); left:0; right:0; z-index:2;
  display:flex; flex-wrap:wrap; align-items:center; gap:8px;
  background:var(--surface-raised); border:1px solid var(--border); border-radius:var(--radius-md);
  box-shadow:var(--shadow-drawer); padding:14px 16px;
}
.trending-panel .eyebrow{ color:var(--text-muted); }
.trending-panel .trend-pill{
  background:var(--surface-sunken); border:1px solid var(--border); color:var(--text-secondary);
}
.trending-panel .trend-pill:hover{ border-color:var(--surface-accent); color:var(--text-primary); }

.nav-right{display:flex; align-items:center; gap:10px; flex-shrink:0;}
/*
   The field is the search control from 768px up; the icon and its overlay take over below that.

   Scoped to the row rather than left as a bare .search-icon-btn, because the button also wears
   .icon-btn, whose display:flex is written further down this file at the same specificity — so
   the bare version lost on source order and put a second search control in the desktop header.
*/
.nav-right .search-icon-btn{ display:none; }

/*
   ROW TWO — what the shop sells.

   Full width and never wrapping. The labels are admin rows, so there is no number of them this
   layout can promise to fit; rather than drop the ones that do not, the strip scrolls sideways and
   fades at whichever edge has more behind it. Nothing the owner chose to show is hidden, no cap
   has to be explained to him, and the drawer carries the same list for anyone who would rather
   read it as one.
*/
.nav-categories{ border-top:1px solid var(--border); }
.nav-categories-inner{
  display:flex; align-items:center; gap:20px; max-width:1260px; margin:0 auto;
  padding:0 calc(32px + env(safe-area-inset-right)) 0 calc(32px + env(safe-area-inset-left));
}
.nav-links{
  --fade-left:0px; --fade-right:0px;
  display:flex; gap:32px; list-style:none; margin:0; padding:0;
  flex:1; min-width:0; overflow-x:auto; scrollbar-width:none; scroll-behavior:smooth;
  /* A mask only reads the alpha channel, so the stops need "opaque" rather than any particular
     colour — currentColor is the way to say that without naming one. Both fades are 0 until the
     script finds something behind an edge, and two stops at the same position is a hard edge, so
     the strip is unmasked until then. */
  -webkit-mask-image:linear-gradient(to right, transparent 0, currentColor var(--fade-left),
    currentColor calc(100% - var(--fade-right)), transparent 100%);
  mask-image:linear-gradient(to right, transparent 0, currentColor var(--fade-left),
    currentColor calc(100% - var(--fade-right)), transparent 100%);
}
.nav-links::-webkit-scrollbar{ display:none; }
.nav-links li{ flex-shrink:0; }
.nav-links a{
  display:block; font-size:var(--type-sm); font-weight:500; letter-spacing:0.01em;
  position:relative; padding:11px 0; white-space:nowrap;
}
/* A one-pixel rule, not words, so this is one of the jobs gold keeps. */
.nav-links a::after{ content:''; position:absolute; left:0; bottom:6px; width:0; height:1px; background:var(--surface-accent); transition:width .3s ease; }
.nav-links a:hover::after{width:100%;}
.icon-btn{
  width:38px; height:38px; display:flex; align-items:center; justify-content:center; flex-shrink:0;
  border-radius:50%; position:relative; color:var(--text-heading); transition:background .2s ease, color .2s ease;
}
/* The tinted disc is the hover feedback. The icon itself used to turn gold, which on that disc
   measures 2.12:1 — the least visible the control ever was, at the moment of pointing at it. */
.icon-btn:hover{background:var(--surface-hover); color:var(--text-heading);}
.icon-btn.current{background:var(--surface-hover);}
.icon-btn svg, .trust-icon svg, .modal-close svg, .search-close svg{ display:block; }
/* Both icons are in the markup and CSS shows the one that suits the theme in force. Choosing in
   stylesheet rather than in script means the control is already right at the first paint, the same
   way the theme itself is, instead of correcting itself once a script has read the stored choice. */
.theme-toggle .theme-icon-to-light{ display:none; }
[data-theme="dark"] .theme-toggle .theme-icon-to-dark{ display:none; }
[data-theme="dark"] .theme-toggle .theme-icon-to-light{ display:block; }
.cart-badge{
  position:absolute; top:1px; right:1px; background:var(--surface-accent); color:var(--text-on-accent); font-size:var(--type-xs);
  font-weight:700; width:16px; height:16px; border-radius:50%; display:flex; align-items:center; justify-content:center;
}
/* A count we could not verify is shown as unknown rather than as a stale number. */
.cart-badge.unknown{ background:var(--danger); color:var(--on-danger); }
/* .btn-find-fit was here: the gold pill at the end of the category row that opened the Fit
   Finder. The control is withdrawn with the quiz, so the row is the category links alone. */
/* Still the drawer's, where the header's row two is not on screen. */
.price-note{
  background:var(--surface-sunken);
  border:1px solid var(--border); color:var(--text-heading); font-size:var(--type-sm); font-weight:600;
  padding:8px 14px; border-radius:999px; white-space:nowrap;
}
/* Quiet, and separated by a rule rather than by a border, so it reads as a note and not a button. */
.announce-currency{
  margin-left:14px; padding-left:14px; border-left:1px solid var(--border-accent-on-inverse-soft);
  opacity:0.75; white-space:nowrap;
}
@media(max-width:640px){ .announce-currency{ display:none; } }
.mobile-toggle{display:none;}

/*
   One breakpoint for the whole header. Above it: two rows, a search field, no hamburger. Below it:
   one row, the search icon and its overlay, and the drawer carrying the links, the currency note
   and the quiz. The links used to leave at 920px because they were fighting the controls for the
   same row; with a row of their own they hold on until the field has to go too.
*/
@media(max-width:768px){
  .nav{
    padding:11px calc(18px + env(safe-area-inset-right)) 11px calc(18px + env(safe-area-inset-left));
    gap:10px;
  }
  .nav-categories{ display:none; }
  .header-search{ display:none; }
  .nav-right .search-icon-btn{ display:flex; }
  .mobile-toggle{ display:flex; }
  .nav-right{ gap:8px; margin-left:auto; }
  .logo{ font-size:var(--type-md); letter-spacing:0.18em; }
  /* The drawer toggle, the mark and three controls have to share a phone's width. */
  .brand-mark{ height:30px; }
  .brand-tagline{ display:none; }
}

/* ============ The chrome on a short screen ============

   Four full-width bands stack above the page before a word of it is read: the announcement strip,
   the offers strip while a code is running, the sale band while a sale is on, and the header's two
   rows. Each is defensible on its own and together they came to about 200 pixels — a third of the
   viewport on a 1366x768 laptop, and a quarter of a 1080p one once the browser has taken its
   toolbars off the top. That is the complaint this block answers, and it is a height complaint
   rather than a width one, so it is answered with a height query.

   Nothing is removed and nothing moves. Every band keeps its contents, its wording and its place;
   the padding around them is spent at the rate a short screen can afford instead of the rate a
   large monitor can. 1000px rather than something tighter because a 1080p laptop at full zoom is
   about 950px of viewport, and that is the screen the complaint came from; an external monitor is
   taller than this and keeps the roomier version.

   Row one is the part worth explaining. Its height is not set by its padding but by the tallest
   thing in it, and that was the logo lock-up: a 38px mark, a 5px gap and the strapline under it
   came to 56px in a row whose search field is 40px. Bringing the lock-up to 44px hands the job of
   setting the height back to the field, which is why a small change to the mark buys more than it
   looks like it should. The strapline stays.

   These heights are safe to change because nothing else measures them. --header-h in tokens.css is
   what fragment links offset against, and storefront-chrome.js keeps it on a ResizeObserver over
   the header, so a header that changes height republishes its own offset. */
@media (min-width:769px) and (max-height:1000px){
  .announce{
    font-size:var(--type-xs);
    padding:calc(5px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right))
            5px calc(16px + env(safe-area-inset-left));
  }
  .offers-bar-link{ height:30px; }
  .sale-band{
    font-size:var(--type-xs);
    padding:5px calc(16px + env(safe-area-inset-right)) 5px calc(16px + env(safe-area-inset-left));
  }
  .nav{
    padding:7px calc(32px + env(safe-area-inset-right)) 7px calc(32px + env(safe-area-inset-left));
  }
  .logo-link{ gap:3px; }
  .brand-mark{ height:28px; }
  .nav-links a{ padding:7px 0; }
  /* The underline was 6px up from the bottom of an 11px pad. At 7px that puts it through the
     descenders, so it moves with the padding that was holding it clear. */
  .nav-links a::after{ bottom:3px; }
}

/* ============ Mobile drawer ============ */
.mnav{
  position:fixed; top:0; left:0; width:300px; max-width:84vw; background:var(--surface-page); z-index:var(--layer-drawer);
  /* dvh, so the drawer is not left short or overflowing as the browser's chrome slides away. */
  height:100%; height:100dvh;
  transform:translateX(-100%); transition:transform .3s ease;
  padding:calc(22px + env(safe-area-inset-top)) 24px calc(24px + env(safe-area-inset-bottom))
          calc(24px + env(safe-area-inset-left));
  box-shadow:var(--shadow-drawer);
  display:flex; flex-direction:column; overflow-y:auto;
}
.mnav.open{ transform:translateX(0); }
.mnav-head{ display:flex; align-items:center; justify-content:space-between; }
.mnav-logo{ font-size:var(--type-md); }
.mnav ul{ list-style:none; padding:0; margin:22px 0 0; display:flex; flex-direction:column; gap:6px; }
.mnav a{ font-size:var(--type-md); font-weight:600; color:var(--text-heading); display:flex; align-items:center; min-height:44px; }
.mnav-note{ margin-top:18px; align-self:flex-start; }
/* The label sits where the drawer's other words sit and the disc sits at the far edge, so the row
   reads as a setting rather than as the header's button dropped into a list. */
.mnav-theme{
  margin-top:14px; width:100%; min-height:44px; display:flex; align-items:center;
  justify-content:space-between; gap:12px; font-size:var(--type-sm); color:var(--text-heading);
}
.mnav-theme .icon-btn{ margin-right:-8px; }
/* .mnav-quiz was the drawer's Launch Fit Quiz button, pinned to the bottom by margin-top:auto.
   It is withdrawn with the quiz; the account link is the drawer's last item now. */

/* ============ Buttons ============

   The filled role and the outlined role are a pair. They are meant to be used side by side — the
   product page's buy row does it, and so does the home hero — so they have to be the same size,
   and the only way to be sure of that is for both of them to count the same frame.

   They did not. `button{}` above sets border:none, .btn-primary never took it back, and .btn-outline
   drew 1.5px. Under box-sizing:border-box that border is inside the height, so the outlined button
   stood 33px of frame against the filled one's 32 and Buy Now sat a pixel taller than Add to Bag on
   every product page. The 15px-against-16px padding was somebody trying to pay for a border by
   hand and getting the arithmetic half a pixel wrong, which is the sort of correction that stops
   being right the moment anybody changes the stroke.

   So: one padding, one border width, and the filled button's border is transparent. Nothing has to
   be kept in step by hand, and a third role added later only has to copy the two declarations.

   The same pair, the same row, a second time: both roles are inline-flex and only the filled one
   said where its label went along the main axis. A button that is exactly as wide as its label does
   not care, which is why this survived — but the buy row gives each button a share of the line
   rather than its own width, and Buy Now spent that share by pushing its label against the left
   edge while Add to Bag beside it stayed centred. justify-content, not text-align: text-align
   places lines of text inside a box, and the label of a flex container is not a line of text in it,
   it is a flex item. backend/tools/half-a-flex.mjs looks for the next one of these. */
.btn-primary{
  background:var(--surface-action); color:var(--text-on-action);
  border:1.5px solid transparent; padding:15px 30px; border-radius:999px;
  font-size:var(--type-sm); font-weight:600; letter-spacing:0.01em; transition:transform .2s ease, background .2s ease;
  display:inline-flex; align-items:center; justify-content:center; gap:8px; position:relative;
}
.btn-primary:hover{ background:var(--surface-action-hover); }
.btn-primary[disabled]{ opacity:.45; pointer-events:none; }
.btn-outline{
  background:transparent; border:1.5px solid var(--surface-action); color:var(--text-heading); padding:15px 30px;
  border-radius:999px; font-size:var(--type-sm); font-weight:600; transition:all .25s ease;
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
}
.btn-outline:hover{ background:var(--surface-action); color:var(--text-on-action); }
.btn-brass{
  background:var(--surface-accent); color:var(--text-on-accent); padding:15px 28px; border-radius:999px;
  font-weight:700; font-size:var(--type-sm); transition:transform .2s ease, box-shadow .2s ease;
}
.btn-brass:hover{ box-shadow:var(--shadow-accent); }
.btn-ghost{ color:var(--text-muted); font-size:var(--type-sm); font-weight:600; }
.btn-ghost:hover{ color:var(--text-heading); }

/*
   The tertiary control: a button with no fill and no border, used where an action belongs on the
   page but must not compete with the one the page is for. The danger variant is the same shape in
   the failure colour — the account page's Delete address and Remove horse, and the bag's Remove.

   Three files had a version of this and none of them had the shape itself: the class only ever
   existed as a descendant of the announcement bar, .auth-switch and .address-actions, each leaning
   on the bare button reset for everything else. The shape is here now, deliberately without a
   colour or a weight of its own so that the ones that override those still win. (The announcement
   bar's was the Fit Quiz call to action, and went with the quiz.)
*/
.link-btn{ background:none; border:none; font-family:inherit; font-size:var(--type-sm); }
.link-btn.danger{ color:var(--danger); }
.link-btn:hover{ text-decoration:underline; text-underline-offset:3px; }

/* ============ Product grid ============
   Shared by the home page, every category page and the search results page. It lived in home.css
   while the home page was the only place a product grid appeared. */
.prod-grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:24px; }
@media(max-width:920px){ .prod-grid{ grid-template-columns:1fr 1fr; } }
@media(max-width:520px){ .prod-grid{ grid-template-columns:1fr 1fr; gap:14px; } }
.prod-card{
  background:var(--surface-raised); border:1px solid var(--border); border-radius:var(--radius-lg); overflow:hidden;
  transition:box-shadow .3s ease; transform-style:preserve-3d;
}
.prod-card:hover{ box-shadow:var(--shadow); }
.prod-media{ position:relative; aspect-ratio:4/5; overflow:hidden; background:var(--surface-sunken); }
.prod-media img{ width:100%; height:100%; object-fit:cover; transition:transform .5s ease; cursor:pointer; }
.prod-card:hover .prod-media img{ transform:scale(1.05); }
.quick-add{
  position:absolute; left:14px; right:14px; bottom:-52px; background:var(--surface-action); color:var(--text-on-action);
  padding:12px; border-radius:999px; font-size:var(--type-sm); font-weight:600; transition:bottom .3s ease;
  text-align:center;
}
.prod-card:hover .quick-add, .prod-card:focus-within .quick-add{ bottom:14px; }
.prod-info{ padding:18px 18px 20px; }
@media(max-width:520px){ .prod-info{ padding:14px; } }
.prod-info h4{ font-family:var(--serif); font-size:var(--type-md); color:var(--text-heading); }
/*
   This used to turn gold on hover. Gold on a card measures 2.46:1, so the hover state was the
   least readable the title ever got — and now that accent text is the heading colour, a colour
   swap here would say nothing at all. An underline is the feedback instead: it is not a colour,
   so it cannot fail a contrast check, and it survives both themes unchanged.
*/
.prod-info h4 a:hover{ text-decoration:underline; text-underline-offset:3px; }
/* The photograph and the title are links to the product's own page. On the home page a plain
   click still opens the quick view; the href is there so the card can be opened in a new tab or
   its address copied. On a server-rendered page the link is all there is. */
.prod-media .prod-link{ display:block; height:100%; }

/* ---- Every photograph of one product, on the card --------------------------------------------
   The card used to show the first photograph and stop, with the rest of the gallery sitting in
   the API response unread.

   The two arrows are pinned to the sides of the photograph and the cue to its top corner, which
   are the three places nothing else on the tile wants: the badge has the top left, and Add to
   Cart slides up across the bottom on hover.

   They do not wait for hover. A control that appears on hover does not exist on a phone, and this
   grid is mostly read on phones — so the buttons are on the tile from the moment it renders. That
   is also why the wrapper takes no pointer events: it covers the whole photograph, and without
   that the product link underneath it would be unclickable everywhere except two circles. */
.card-shot{ display:none; }
.card-shot.is-shown{ display:block; }

.card-gallery{
  position:absolute; inset:0; padding:0 8px; pointer-events:none;
  display:flex; align-items:center; justify-content:space-between;
}
.card-gallery-nav{
  pointer-events:auto;
  width:34px; height:34px;
  display:flex; align-items:center; justify-content:center;
  border-radius:50%; border:1px solid var(--border);
  background:var(--surface-raised); color:var(--text-heading);
  box-shadow:var(--shadow-header);
}
.card-gallery-nav:hover{ background:var(--surface-hover); }
.card-gallery-count{
  position:absolute; top:10px; right:10px;
  border:1px solid var(--border); border-radius:999px;
  background:var(--surface-raised); color:var(--text-heading);
  padding:3px 9px; font-size:var(--type-xs); font-weight:700;
}
.prod-meta{ display:flex; justify-content:space-between; align-items:center; margin-top:8px; gap:8px; }
.prod-price{ font-weight:700; font-size:var(--type-sm); }

/* ---- A reduced price, on a card ---------------------------------------------------------------
   Three pieces where an unreduced card has one, so the row is packed from the left instead of
   pushed to its two ends: was, now, and how much off. The old price is quiet and the new one is
   the colour, which is the order the eye should take them in.

   .prod-price-was is an <s>, and the line through it comes from the element rather than from
   here. A border-bottom would look the same and mean nothing. */
.prod-meta.is-on-sale{ justify-content:flex-start; flex-wrap:wrap; gap:6px 8px; }
.prod-price.is-sale{ color:var(--sale-ink); }
.prod-price-was{ font-size:var(--type-sm); color:var(--text-muted); }
.prod-price-off{
  font-size:var(--type-xs); font-weight:700; letter-spacing:0.02em;
  color:var(--sale-ink); background:var(--sale-surface);
  border:1px solid var(--sale-border); border-radius:999px; padding:2px 8px;
}

/* The flag on the photograph. Same shape and inset as the merchandising badge it sits above,
   because two pills of different sizes in the same corner read as a mistake. */
.sale-badge{
  position:absolute; top:14px; left:14px; z-index:var(--layer-furniture);
  background:var(--sale-badge); color:var(--on-sale-badge);
  font-size:var(--type-xs); font-weight:700; padding:5px 12px; border-radius:999px;
  letter-spacing:0.03em;
}
/* Both on one card: the sale flag keeps the corner and the other badge steps under it. */
.sale-badge ~ .best-seller-badge:not(.oos){ top:48px; }

/* ---- A rating, wherever one is shown ----------------------------------------------------------
   Two rows of five stars, one over the other, and the top row clipped to a percentage. That is
   how a half star is drawn without a half-star glyph: 4.3 of 5 is a row 86% wide, which is honest
   in a way that rounding to four or to five is not.

   Both rows are hidden from screen readers — a reader makes "star star star star star star star
   star star star" of them — and the sentence beside them is read instead.

   Nothing here appears for a product nobody has reviewed. The markup is simply not rendered; see
   ProductResponse for why an empty five-star row is worse than no stars at all. */
.stars{ position:relative; display:inline-block; white-space:nowrap; line-height:1; }
.stars-track{ color:var(--text-disabled); letter-spacing:1px; }
.stars-fill{
  position:absolute; left:0; top:0; overflow:hidden; white-space:nowrap;
  color:var(--surface-accent); letter-spacing:1px;
}
.card-rating{
  display:flex; align-items:center; gap:6px; margin:6px 0 0; font-size:var(--type-sm);
}
.rating-count{ color:var(--text-muted); font-size:var(--type-xs); }
/* ---- The colour dots on a product card ----
   One of three colour controls in the shop, and the smallest: a row of round dots under the price
   that change which colour the card is showing. The other two are the catalogue's colour filter,
   further down this file, and the product page's colour picker in product.css.

   All three were called .swatch. Two of them are on the home page at the same time, so this rule's
   width, height and border-radius were landing on the filter's chips and squashing an 80-pixel
   pill into an 18-pixel ellipse. The names now say which is which, which is what keeps this
   18px off the other two — not an override further down, which is only ever a bet on source
   order that the next person to reorder the file loses. */
.card-swatches{ display:flex; gap:7px; margin-top:12px; align-items:center; flex-wrap:wrap; }
.card-swatch{
  width:18px; height:18px; border-radius:50%; border:2px solid transparent; position:relative;
  padding:0; transition:border-color .2s ease, transform .15s ease;
}
.card-swatch.active{ border-color:var(--surface-selected); transform:scale(1.12); }
.card-swatch-label{ font-size:var(--type-xs); color:var(--text-muted); margin-left:6px; }
.best-seller-badge{
  position:absolute; top:14px; left:14px; z-index:var(--layer-furniture); background:var(--surface-accent); color:var(--text-on-accent);
  font-size:var(--type-xs); font-weight:700; padding:5px 12px; border-radius:999px; letter-spacing:0.03em;
}
.best-seller-badge.oos{ left:auto; right:14px; background:var(--danger); color:var(--on-danger); }
.no-results{ text-align:center; padding:50px 20px; color:var(--text-muted); font-size:var(--type-sm); grid-column:1/-1; }
.no-results-actions{ display:flex; gap:16px; justify-content:center; align-items:center; flex-wrap:wrap; margin-top:18px; }

.filter-pill{
  display:inline-flex; align-items:center; min-height:38px;
  border:1.5px solid var(--border); background:var(--surface-raised); color:var(--text-heading); border-radius:999px;
  padding:9px 18px; font-size:var(--type-sm); font-weight:600; transition:all .2s ease;
}
.filter-pill:hover{ border-color:var(--border-accent); }
.filter-pill.active{ background:var(--surface-selected); color:var(--text-on-selected); border-color:var(--surface-selected); }
/* Underlined, so losing the gold costs it nothing: it still reads as the one thing here to click. */
.filter-clear{ color:var(--text-accent); font-size:var(--type-sm); font-weight:600; text-decoration:underline; text-underline-offset:3px; }
.filter-clear:hover{ color:var(--text-primary); }

/* ============ Filter panel ============
   Sort, size, price and stock.

   These rules were in catalogue.css, which the home page does not load. The panel is one Thymeleaf
   fragment rendered on three pages, so on the home page the same markup arrived with no styling at
   all and fell back to raw browser controls — the sort box and the tick box the owner called
   cheap. Loading the whole of catalogue.css on the home page would have fixed the symptom and
   dragged in breadcrumb and pager rules for furniture the home page does not have. The fragment is
   shared, so its rules belong in the shared sheet, the same way the product grid and the pills
   above already moved here when the category pages started rendering them. */
.filter-panel{
  background:var(--surface-raised); border:1px solid var(--border); border-radius:var(--radius-lg);
  padding:18px 20px; margin:0 0 16px;
}
.filter-fields{ display:flex; flex-wrap:wrap; align-items:flex-end; gap:18px; }
.filter-field{ display:flex; flex-direction:column; gap:6px; }
.filter-field label,
.filter-field legend{ font-size:var(--type-xs); letter-spacing:.12em; text-transform:uppercase; font-weight:700; color:var(--text-muted); }
.filter-field select,
.filter-field input[type="text"]{
  min-height:44px; border:1.5px solid var(--border); border-radius:10px; background:var(--surface-raised);
  padding:0 12px; font-size:var(--type-sm); color:var(--text-heading); font-family:inherit;
  transition:border-color .2s ease;
}
.filter-field select:hover,
.filter-field input[type="text"]:hover{ border-color:var(--border-accent); }
.filter-field select{ min-width:172px; }
.filter-price-row{ display:flex; align-items:center; gap:8px; }
.filter-price-row input{ width:96px; }
.filter-price-row span{ color:var(--text-muted); }

/*
   The chevron.

   A select cannot carry a pseudo-element of its own, so the arrow belongs to the field around it —
   which is why appearance:none is written in the same :has() block rather than on the select
   directly. A browser that does not understand :has() throws all three rules out together and
   keeps the operating system's own chevron, rather than accepting the appearance reset and leaving
   a box with no arrow at all.

   Drawn from two borders instead of a background image, so it reads a role and needs no second
   copy of itself for the dark theme.
*/
.filter-field:has(select){ position:relative; }
.filter-field:has(select) select{ appearance:none; -webkit-appearance:none; padding-right:38px; }
.filter-field:has(select)::after{
  content:''; position:absolute; right:16px; bottom:20px; width:8px; height:8px; pointer-events:none;
  border-right:1.5px solid var(--text-muted); border-bottom:1.5px solid var(--text-muted);
  transform:rotate(45deg);
}

/* The checkbox sits beside its label rather than under it, which is the one field here that
   reads as a sentence rather than as a setting. */
.filter-check{ flex-direction:row; align-items:center; gap:9px; min-height:44px; }
.filter-check label{ text-transform:none; letter-spacing:0; font-size:var(--type-sm); color:var(--text-heading); font-weight:500; }
/*
   It was a native tick box with accent-color on it, which paints the fill and nothing else: the
   box itself stayed whatever grey the operating system draws, next to fields with a 1.5px border
   and a 10px radius. It is the same box as those fields now, at the size a tick box should be.
*/
.filter-check input[type="checkbox"]{
  appearance:none; -webkit-appearance:none; margin:0; flex-shrink:0;
  width:20px; height:20px; border:1.5px solid var(--border); border-radius:5px;
  background:var(--surface-raised); display:grid; place-content:center;
  transition:background .15s ease, border-color .15s ease;
}
.filter-check input[type="checkbox"]:hover{ border-color:var(--border-accent); }
.filter-check input[type="checkbox"]:checked{ background:var(--surface-selected); border-color:var(--surface-selected); }
/* Two sides of a box, rotated. Same reason as the chevron: a drawn tick reads a role, an image
   would need one file per theme. */
.filter-check input[type="checkbox"]::before{
  content:''; width:10px; height:5px; margin-top:-3px;
  border-left:2px solid var(--text-on-selected); border-bottom:2px solid var(--text-on-selected);
  transform:rotate(-45deg) scale(0); transition:transform .15s ease;
}
.filter-check input[type="checkbox"]:checked::before{ transform:rotate(-45deg) scale(1); }

.filter-actions{ display:flex; align-items:center; gap:16px; margin-left:auto; }
.filter-actions .btn-brass{ min-height:44px; padding:0 26px; }
.filter-note{ flex-basis:100%; margin:0; font-size:var(--type-sm); color:var(--text-muted); }

/* ============ The colour filter ============
   The one control here where the word is not the whole answer, so it is chips with the colour on
   them rather than a dropdown. Radios underneath, so it submits with the rest of the form.

   Named for the filter it belongs to. It used to be .swatch and .swatch-row, the same two names
   the product card's colour dots use higher up this file, and the home page renders both — so the
   card's `width:18px; height:18px; border-radius:50%` was landing on these chips and crushing
   them. A chip has no width and no height of its own on purpose: it is as wide as the colour's
   name and as tall as min-height and its padding make it. There is nothing to override, because
   after the rename there is nothing reaching in. */
.filter-colours{ border:0; margin:0; padding:0; min-width:0; }
.filter-colours legend{ padding:0; margin:0 0 6px; }
/* Wraps onto as many lines as it needs. Sideways scrolling inside a form the shopper is filling in
   hides options behind an edge with nothing to say they are there. */
.filter-colour-row{ display:flex; flex-wrap:wrap; gap:8px; }
.filter-colour-chip{
  display:inline-flex; align-items:center; gap:8px; min-height:38px; padding:7px 14px;
  border:1.5px solid var(--border); border-radius:999px; background:var(--surface-raised);
  color:var(--text-heading); font-size:var(--type-sm); font-weight:600; cursor:pointer;
  transition:border-color .2s ease, background .2s ease, color .2s ease;
}
.filter-colour-chip:hover{ border-color:var(--border-accent); }
/* Off the screen rather than display:none, which would take it out of the tab order and off the
   list a screen reader reads. The chip around it is the visible control. */
.filter-colour-chip input[type="radio"]{ position:absolute; width:1px; height:1px; opacity:0; margin:0; }

/*
   Selected twice over, on purpose.

   The class is server-rendered, so a page arrives with the right chip filled in even where :has()
   is not understood. The :has() rule is what fills it in the instant the shopper clicks, before
   Apply reloads anything — without it, tapping a colour looks like tapping a dead chip, because
   the radio itself is off the screen.
*/
.filter-colour-chip.is-selected,
.filter-colour-chip:has(input:checked){
  background:var(--surface-selected); color:var(--text-on-selected); border-color:var(--surface-selected);
}
.filter-colour-chip:has(input:focus-visible){ outline:2px solid var(--focus-ring-strong); outline-offset:2px; }
.filter-colour-dot{
  width:14px; height:14px; border-radius:50%; flex-shrink:0;
  /* Set from the product's own hex in a style attribute. This is the colour the thing is, not a
     decision about the theme, so it is the same in both and has no role token to come from. */
  background:var(--swatch); box-shadow:0 0 0 1px var(--border) inset;
}

/* ============ The panel on a phone ============
   Six controls stacked is most of a phone screen, and a shopper who has to scroll past the filters
   to reach the shop is a shopper who has been shown a form instead of a catalogue. Below this the
   panel is a disclosure, closed until asked for. */
.filter-toggle{
  display:flex; align-items:center; gap:10px; min-height:44px; cursor:pointer;
  font-size:var(--type-sm); font-weight:700; color:var(--text-heading);
  letter-spacing:.08em; text-transform:uppercase; list-style:none;
}
.filter-toggle::-webkit-details-marker{ display:none; }
/* The same drawn chevron as the selects, turned down when the drawer is open. */
.filter-toggle::after{
  content:''; width:8px; height:8px; margin-bottom:3px;
  border-right:1.5px solid var(--text-muted); border-bottom:1.5px solid var(--text-muted);
  transform:rotate(45deg); transition:transform .2s ease;
}
.filter-drawer[open] > .filter-toggle::after{ transform:rotate(-135deg); margin-bottom:-3px; }
.filter-drawer[open] > .filter-toggle{ margin-bottom:16px; }
.filter-count{
  display:inline-grid; place-content:center; min-width:22px; height:22px; padding:0 6px;
  border-radius:999px; background:var(--surface-selected); color:var(--text-on-selected);
  font-size:var(--type-xs); letter-spacing:0;
}

/*
   Above this the disclosure is not a disclosure: the summary goes and the fields are simply there.

   Written twice because browsers hide a closed <details> two different ways. The older way is a
   display rule on the children, which the author rule below outranks; the newer one hides a
   ::details-content pseudo-element, which it does not, and which older browsers ignore harmlessly.
   If a browser somehow honoured neither, the panel would open on a click rather than break.
*/
@media (min-width:861px){
  .filter-drawer > .filter-toggle{ display:none; }
  .filter-drawer > .filter-fields{ display:flex; }
  .filter-drawer::details-content{ content-visibility:visible; block-size:auto; }
}

@media (max-width:860px){
  /* Full width, one per line, and the Apply button no longer pushed to the far right by itself. */
  .filter-fields{ flex-direction:column; align-items:stretch; gap:16px; }
  .filter-field select,
  .filter-price-row input{ width:100%; }
  .filter-actions{ margin-left:0; }
  .filter-actions .btn-brass{ flex:1; }
}

/* Every control has a visible focus ring. The brass on white is not enough on its own for
   somebody moving through the form with a keyboard. */
.filter-panel :is(select, input, button, a, summary):focus-visible{
  outline:2px solid var(--focus-ring-strong); outline-offset:2px;
}

/* ============ Live search overlay ============ */
.search-overlay{
  position:fixed; top:0; left:0; right:0; z-index:var(--layer-search); background:var(--surface-glass-dark);
  backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
  border-bottom:1px solid var(--border-accent-on-inverse); padding:32px 0 26px;
  transform:translateY(-100%); transition:transform .35s cubic-bezier(.2,.8,.2,1);
}
.search-overlay.open{ transform:translateY(0); }
.search-input-row{ display:flex; align-items:center; gap:14px; max-width:720px; margin:0 auto; padding:0 32px; }
.search-input{
  flex:1; min-width:0; background:var(--surface-well-on-inverse); border:1.5px solid var(--surface-accent); border-radius:999px;
  padding:14px 22px; color:var(--text-on-inverse); font-size:var(--type-md); outline:none; box-shadow:0 0 20px var(--glow-accent-soft);
}
.search-input::placeholder{ color:var(--text-on-inverse-faint); }
.search-close{ color:var(--text-on-inverse); flex-shrink:0; display:flex; }
.search-close:hover{ background:var(--surface-hover-on-inverse); color:var(--text-accent-on-inverse-bright); }
.trending{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; max-width:720px; margin:18px auto 0; padding:0 32px; }
.trending .eyebrow{ color:var(--text-on-inverse-faint); }
.trend-pill{
  background:var(--surface-well-on-inverse); border:1px solid var(--border-on-inverse); color:var(--text-on-inverse-muted);
  padding:7px 14px; border-radius:999px; font-size:var(--type-sm); transition:all .2s ease;
}
.trend-pill:hover{ border-color:var(--surface-accent); color:var(--text-accent-on-inverse-bright); }
@media(max-width:640px){ .search-input-row, .trending{ padding:0 20px; } }

/* ============ Footer ============
   Qualified by a class, the way header.site above is, and for the reason that one is.

   This was a bare `footer{}`, which is not "the footer" — it is every <footer> element anybody
   ever writes. The account page's order cards put their two buttons in one, and each card grew a
   near-black 68-pixel slab with unreadable text in it, in both themes. The fix is not a reset in
   account.css taking the background and the padding back off: an undo leaves the trap armed for
   the next component that reaches for the element the HTML spec provides for exactly this. */
footer.site{ background:var(--surface-inverse-deep); color:var(--text-on-inverse-muted); padding:68px 0 30px; }
.footer-top{
  display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:40px;
  padding-bottom:40px; border-bottom:1px solid var(--border-on-inverse-faint);
}
@media(max-width:820px){ .footer-top{grid-template-columns:1fr 1fr;} }
@media(max-width:520px){ .footer-top{grid-template-columns:1fr;} }
.footer-brand .logo{ color:var(--text-on-inverse-strong); }
.footer-brand p{ font-size:var(--type-sm); color:var(--text-on-inverse-soft); max-width:280px; margin-top:12px; }
.newsletter{ margin-top:20px; display:flex; max-width:320px; }
.newsletter input{
  flex:1; min-width:0; padding:13px 15px; border-radius:999px 0 0 999px;
  border:1px solid var(--border-on-inverse); background:var(--surface-well-on-inverse);
  color:var(--text-on-inverse); font-size:var(--type-md);
}
.newsletter input::placeholder{ color:var(--text-on-inverse-faint); }
.newsletter button{ background:var(--surface-accent); color:var(--text-on-accent); padding:0 18px; border-radius:0 999px 999px 0; font-weight:700; font-size:var(--type-sm); }
.newsletter-note{ margin-top:8px; max-width:320px; font-size:var(--type-sm); color:var(--text-on-inverse-faint); }
.footer-col h5{ font-family:var(--sans); font-size:var(--type-sm); letter-spacing:0.12em; text-transform:uppercase; color:var(--text-accent-on-inverse); margin-bottom:16px; font-weight:700; }
.footer-col ul{ list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:10px; }
.footer-col a{ font-size:var(--type-sm); color:var(--text-on-inverse-muted); transition:color .2s ease; }
.footer-col a:hover{ color:var(--text-accent-on-inverse); }
.footer-bottom{ display:flex; justify-content:space-between; padding-top:22px; font-size:var(--type-sm); color:var(--text-on-inverse-faint); flex-wrap:wrap; gap:10px; }

/* ============ Overlay + modal shell ============ */
.overlay{
  position:fixed; inset:0; background:var(--surface-scrim); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
  z-index:var(--layer-scrim); opacity:0; visibility:hidden; transition:opacity .3s ease;
}
.overlay.open{ opacity:1; visibility:visible; }
.modal{
  position:fixed; inset:0; margin:auto; z-index:var(--layer-modal); display:flex; align-items:center; justify-content:center;
  opacity:0; visibility:hidden; transition:opacity .3s ease;
  padding:calc(20px + env(safe-area-inset-top)) calc(20px + env(safe-area-inset-right))
          calc(20px + env(safe-area-inset-bottom)) calc(20px + env(safe-area-inset-left));
}
.modal.open{ opacity:1; visibility:visible; }
.modal-panel{
  width:600px; max-width:92vw; background:var(--surface-page); border-radius:var(--radius-lg);
  /* dvh: vh is the tallest the viewport ever gets, so a panel measured in it grew taller than the
     screen the moment the address bar or the keyboard appeared, and its foot became unreachable. */
  max-height:88vh; max-height:88dvh;
  box-shadow:var(--shadow-modal); display:flex; flex-direction:column; overflow:hidden;
  transform:scale(0.94) translateY(14px); transition:transform .35s cubic-bezier(.2,.8,.2,1);
  border:1px solid var(--border-accent); position:relative;
}
.modal.open .modal-panel{ transform:scale(1) translateY(0); }
.modal-head{ display:flex; align-items:center; justify-content:space-between; padding:22px 26px; border-bottom:1px solid var(--border); background:var(--surface-inverse); }
.modal-head h3{ font-size:var(--type-lg); color:var(--text-on-inverse-strong); }
.modal-close{
  color:var(--text-accent-on-inverse); display:flex; align-items:center; justify-content:center;
  width:38px; height:38px; border-radius:50%; flex-shrink:0;
}
.modal-body{ padding:24px 26px 28px; overflow-y:auto; }
.modal-body h4{ font-size:var(--type-lg); margin-bottom:6px; }
.modal-body .sub{ font-size:var(--type-sm); color:var(--text-muted); margin:0 0 18px; }
.modal-body input,
.modal-body select{ padding:12px; border:1px solid var(--border); border-radius:10px; font:inherit; font-size:var(--type-md); width:100%; background:var(--surface-field); color:var(--text-primary); }
.modal-body input[readonly],
.modal-body input:disabled{ background:var(--surface-sunken); color:var(--text-muted); }
.modal-nav{ display:flex; justify-content:space-between; margin-top:24px; }

/* ============ Writing a review ============
   fragments/review-form :: reviewFormModal, which the product page opens from under the reviews
   and the account page opens from a delivered order. It lived in product.css until the second
   page existed; a rule two pages depend on in one page's sheet is how the two come to disagree. */
.review-form-panel{ width:520px; }
/* Which product this is about. Set by the account page, where the form is opened from a list of
   several things; the product page leaves it hidden, since the page itself is the answer. */
.review-form-product{
  margin:0 0 4px; font-size:var(--type-sm); font-weight:700; color:var(--text-heading);
}
.review-form-panel .review-field{
  display:block; margin:16px 0 6px; font-size:var(--type-xs); text-transform:uppercase;
  letter-spacing:0.08em; font-weight:700; color:var(--text-heading);
}
.review-form-panel input[type=text],
.review-form-panel textarea{
  width:100%; padding:11px 13px; border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--surface-field); color:var(--text-primary); font-family:inherit;
  font-size:var(--type-sm);
}
.review-form-panel textarea{ resize:vertical; line-height:1.7; }
.review-form-panel .field-hint{ margin:6px 0 0; font-size:var(--type-xs); color:var(--text-muted); }

/* A radio group, not a row of clickable divs: the arrow keys work, the form cannot be sent
   without a choice, and the label is what is clicked. */
.review-stars-field{ border:none; margin:0; padding:0; }
.review-stars-field legend{
  font-size:var(--type-xs); text-transform:uppercase; letter-spacing:0.08em; font-weight:700;
  color:var(--text-heading); padding:0;
}
.review-stars-row{ display:flex; gap:8px; margin-top:8px; }
.review-stars-row input{ position:absolute; opacity:0; width:1px; height:1px; }
.review-stars-row label{
  min-width:44px; min-height:40px; display:inline-flex; align-items:center; justify-content:center;
  border:1.5px solid var(--border); border-radius:999px; cursor:pointer; font-weight:700;
  font-size:var(--type-sm); color:var(--text-heading); background:var(--surface-raised);
}
.review-stars-row input:checked + label{
  background:var(--surface-selected); color:var(--text-on-selected); border-color:var(--surface-selected);
}
.review-stars-row input:focus-visible + label{ outline:2px solid var(--focus-ring); outline-offset:2px; }

.review-form-error{ margin:14px 0 0; color:var(--danger); font-size:var(--type-sm); font-weight:600; }
.review-form-actions{ display:flex; gap:10px; margin-top:20px; flex-wrap:wrap; }

@media(max-width:600px){
  .review-form-panel{ width:auto; }
}
/* A star is a target for a thumb here, not a pointer. */
@media (pointer: coarse){
  .review-stars-row label{ min-height:48px; min-width:48px; }
}

/* ============ Measurement chart ============
   fragments/sizeguide :: chart, which the product page shows in a modal and /size-guide shows a
   column of. It lived in product.css until the second page existed; a rule two pages depend on
   in one page's sheet is how the two come to disagree about what fits.

   The chart is the one thing in this shop that genuinely cannot be made narrow. Four measurements
   in two units is nine columns, and below its minimum width the answer is to scroll it rather
   than to shrink the type or wrap a figure onto two lines — neither of which leaves a number a
   shopper can read against a tape measure. */

.table-scroll{ overflow-x:auto; -webkit-overflow-scrolling:touch; }

.size-guide-table{
  width:100%; border-collapse:collapse; font-size:var(--type-sm); min-width:420px;
  background:var(--surface-raised); border:1px solid var(--border); border-radius:var(--radius-md);
  overflow:hidden;
}
.size-guide-table th,
.size-guide-table td{
  text-align:left; padding:10px 14px; border-bottom:1px solid var(--border); white-space:nowrap;
}
.size-guide-table thead th{
  background:var(--surface-sunken); font-size:var(--type-xs); text-transform:uppercase;
  letter-spacing:0.06em; color:var(--text-heading); font-weight:700;
}
.size-guide-table tbody th{ font-weight:700; color:var(--text-heading); }
.size-guide-table tbody tr:last-child th,
.size-guide-table tbody tr:last-child td{ border-bottom:none; }
/* A measurement nobody has given the shop. Quieter than a figure, so a half-filled chart reads as
   half-filled rather than as a chart with something wrong with it. */
.size-guide-table .not-given{ color:var(--text-muted); }

.size-guide-how{ margin-top:18px; }
.size-guide-how h4{
  font-size:var(--type-xs); text-transform:uppercase; letter-spacing:0.08em;
  color:var(--text-heading); margin:0 0 6px;
}
.size-guide-how p{ margin:0; color:var(--text-secondary); font-size:var(--type-sm); line-height:1.7; }

/* ============ Confirmation ============
   The panel behind EquinovaChrome.confirmAction, which is what the shop asks instead of the
   browser's confirm(). Narrower than the rest, because a question is two lines and a form is not.

   Compounded onto .modal-panel rather than left to follow it in the file. Both are one class, so
   a plain .modal-confirm would only beat the 600px above by being further down the sheet, and a
   width decided by source order is a width that changes when somebody reorders the sheet. */
.modal-panel.modal-confirm{ width:440px; }
.confirm-message{ font-size:var(--type-md); line-height:1.6; color:var(--text-primary); margin:0; }
.confirm-actions{ display:flex; justify-content:flex-end; gap:10px; margin-top:24px; }

/* The third role the note over .btn-primary said a later one would need: the same padding and the
   same border width, so it stands exactly as tall as the outlined Cancel beside it rather than a
   pixel over. Every colour is a token that already exists in both themes.

   Deliberately the console's name for the console's role, drawn from the console's two tokens. The
   .swatch lesson does not apply: that was two components sharing a name inside one cascade, and
   admin.css is never loaded beside this sheet. The reward is that "this cannot be undone" looks
   and reads the same on both sides of the shop, and the contrast tool's existing pair for
   --on-danger on --danger covers this button too, in both themes, without a line being added. */
.btn-destroy{
  background:var(--danger); color:var(--on-danger);
  border:1.5px solid transparent; padding:15px 30px; border-radius:999px;
  font-size:var(--type-sm); font-weight:600; letter-spacing:0.01em;
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  transition:background .2s ease;
}
.btn-destroy:hover{ background:var(--danger-hover); }

/* Two pill buttons and a 10px gap need about 250px, and a 92vw panel on a 320px phone leaves
   242px inside its padding. Rather than let them wrap raggedly they stack and fill the width.

   Reversed, so the destructive answer is at the top and Cancel is the one under the thumb. That
   is the order a phone's own action sheet uses, and the safer of the two to hit by accident is
   the one in the easiest place to hit. */
@media(max-width:420px){
  .confirm-actions{ flex-direction:column-reverse; }
  .confirm-actions button{ width:100%; }
}

/* ============ Fit Finder quiz ============
   What filled the modal shell above: the step dots, the answer cards, the result rows and the
   bundle summary. The quiz is withdrawn from the storefront, and these went with it rather than
   being left behind — they were carried by the shared sheet, so every page in the shop was
   downloading them for a modal that is no longer in the markup. storefront-quiz.js still writes
   this structure, so restoring the quiz means restoring this block with it. */

/* Form layout shared by the checkout, sign-in and saved-address forms. */
.field-stack{ display:grid; gap:12px; }
.field-pair{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
/* Two columns inside a 92vw modal leaves about 140px a field. "Saskatchewan" does not fit, and
   neither does a postal code at the 16px a phone has to render it at, so both were cut off. */
@media(max-width:480px){ .field-pair{ grid-template-columns:1fr; } }
/* The -4px is measured against the stack's 12px gap: eight pixels under the field it belongs to,
   closer to it than the next field is. That only holds while the help sits in the stack, and a help
   line inside a wrapper has no gap to pull against — which put the password rules hard against the
   bottom of the input in the sign-in modal, the one form that wraps its password row. The wrapper
   itself is fixed rather than this rule, so every form keeps the same spacing; see #authPasswordRow. */
.field-help{ font-size:var(--type-sm); color:var(--text-muted); line-height:1.5; margin:-4px 0 0; }
/* Said under the field that caused it, in the same place .field-help would say something helpful,
   because a message about one box belongs beside that box and not in a summary above the form. */
.field-error{ font-size:var(--type-sm); color:var(--danger); line-height:1.5; margin:-4px 0 0; }
input.is-invalid{ border-color:var(--danger-border); }

/* ============ Checkout ============
   All of this was in cart.css, which only the bag page loads. It came across when the checkout
   modal joined the shared layout, because the product page's Buy Now button opens the same form
   and a second copy of it in a second sheet is two things to keep in step. */

/* The notice at the top of the checkout form, before anything has happened.
   Quiet on purpose. It is a note about the state of the build, not a warning about the shop, and
   it used to be the loudest thing on the screen — which read as a fault and is how a shopper
   decides to leave. Same weight as the reassurance line it sits near. */
.test-mode-note{
  display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
  font-size:var(--type-sm); line-height:1.5; color:var(--text-muted); margin:0 0 14px;
}
.test-mode-tag{
  flex:none; font-size:var(--type-xs); font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--warning); background:var(--warning-surface-quiet); border:1px solid var(--warning-border-quiet);
  border-radius:999px; padding:2px 8px;
}

/* What a Buy Now is buying, named at the top of the form. The bag page leaves this empty: the bag
   is on the page behind the modal, and repeating it there would be noise. */
.checkout-summary{
  border:1px solid var(--border-accent); border-radius:var(--radius-md); background:var(--surface-sunken);
  padding:14px 16px; margin:0 0 18px;
}
.checkout-summary-label{
  font-size:var(--type-xs); font-weight:700; letter-spacing:.05em; text-transform:uppercase;
  color:var(--text-muted); margin:0 0 8px;
}
.checkout-summary-line{
  display:flex; align-items:baseline; justify-content:space-between; gap:16px;
}
.checkout-summary-name{
  font-family:var(--serif); font-size:var(--type-md); color:var(--text-heading); line-height:1.35;
}
.checkout-summary-price{
  flex:none; font-size:var(--type-md); font-weight:700; color:var(--text-heading);
  font-variant-numeric:tabular-nums;
}
.checkout-summary-meta{ font-size:var(--type-sm); color:var(--text-secondary); margin:6px 0 0; }
/* The reassurance that is the whole point of keeping the two apart: pressing this button does not
   spend their money on the things they were still thinking about. */
.checkout-summary-note{ font-size:var(--type-xs); color:var(--text-muted); margin:8px 0 0; }

/* ---- Promo code ----
   One set of styles for two places: the bag's summary column and the checkout modal. They look
   alike because they do the same thing, and a shopper who found the field on the bag should not
   have to find a different-looking one on the form. Here rather than in cart.css because the modal
   is in every page's chrome and cart.css is only on the bag. */
.co-promo{ margin-bottom:18px; }
.promo-label{ display:block; font-size:var(--type-xs); letter-spacing:.08em;
  text-transform:uppercase; color:var(--text-subtle); margin-bottom:6px; }
.promo-entry{ display:flex; gap:8px; }
.promo-entry input{
  flex:1; min-width:0; padding:10px 12px; font-size:var(--type-sm);
  text-transform:uppercase; letter-spacing:.06em;
  background:var(--surface-field); color:var(--text-primary);
  border:1px solid var(--border); border-radius:var(--radius-sm);
}
/* The field upper-cases what is typed, which would upper-case the placeholder with it. */
.promo-entry input::placeholder{ text-transform:none; letter-spacing:normal; color:var(--text-muted); }
.promo-entry .btn-secondary{ flex:none; padding-left:18px; padding-right:18px; }
/* Under the field, because that is where somebody realises they do not have a code. */
.promo-see-all{ margin:8px 0 0; font-size:var(--type-xs); }
.promo-see-all a{ color:var(--text-accent); text-decoration:underline; text-underline-offset:3px; }
/* The codes that are on, above the field that adds another. A bag may carry more than one since
   V54, so this is a list; a row with no code beside it is a sale the shop is running itself. */
.promo-applied-list{ list-style:none; margin:0 0 12px; padding:0; display:grid; gap:8px; }
.promo-applied{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.promo-applied.is-automatic .promo-applied-note{ color:var(--text-primary); }
.promo-applied-code{
  font-weight:700; letter-spacing:.12em; font-size:var(--type-sm); color:var(--text-heading);
  border:1px dashed var(--border-accent); border-radius:var(--radius-sm); padding:6px 12px;
}
.promo-applied-note{ flex:1; min-width:0; font-size:var(--type-sm); color:var(--text-secondary); }
.promo-remove{
  background:none; border:0; padding:4px; cursor:pointer;
  font-size:var(--type-sm); color:var(--text-subtle);
  text-decoration:underline; text-underline-offset:3px;
}
.promo-remove:hover{ color:var(--text-primary); }
.promo-remove[disabled]{ cursor:default; color:var(--text-disabled); text-decoration:none; }

/*
   The banner on the finished order, which is a different matter and stays loud: an order exists,
   it took no money, and nobody must ship against it.

   It used to carry its own copy of the warning banner — the same #fdf3e3, the same #c9922f, the
   same #5c4108, written out again because the one in this sheet was in another file and therefore
   invisible. It now takes .warning-banner in the markup and only says how it differs, which is the
   padding and the type size.
*/
.sim-banner{
  border-radius:var(--radius-sm); font-size:var(--type-sm); line-height:1.55;
  padding:12px 14px; margin:0 0 16px; display:block;
}
.sim-banner strong{ display:block; font-size:var(--type-sm); margin-bottom:2px; }

/* The saved-address picker. Present only for a signed-in shopper who has one; a guest sees the
   plain form exactly as before, whichever button opened it. */
.saved-address-label{
  display:block; font-size:var(--type-xs); font-weight:700; letter-spacing:.05em; text-transform:uppercase;
  color:var(--text-muted); margin-bottom:6px;
}
.saved-address-select{
  padding:12px; border:1px solid var(--border-accent); border-radius:10px; font:inherit; font-size:var(--type-md);
  width:100%; background:var(--surface-field); color:var(--text-primary); margin-bottom:16px;
}

/* Tester-only control. Styled to look like scaffolding rather than part of the shop, because
   anyone seeing it should know at once that this is not the live checkout. This is the
   information role — a neutral notice that is neither a warning nor a failure. */
/* The payment provider's card field.
   The box is ours; everything inside it is an iframe from their origin, which is what stops a card
   number ever reaching this server. So there is a border and a label here and nothing else: the
   inputs are styled by the provider's SDK and this sheet cannot reach into them. The minimum height
   holds the row open while the SDK is still loading, so the button below does not jump. */
.card-field{ display:grid; gap:8px; }
.card-field label{
  font-size:var(--type-sm); font-weight:700; letter-spacing:0.04em; text-transform:uppercase;
  color:var(--text-muted);
}
.card-field #squareCard{
  min-height:56px; border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--surface); padding:4px 8px;
}

.dev-only{
  border:1px dashed var(--info-border); border-radius:var(--radius-sm); background:var(--info-surface);
  padding:12px 14px; display:grid; gap:8px;
}
.dev-only label{ font-size:var(--type-sm); font-weight:700; letter-spacing:0.04em; text-transform:uppercase; color:var(--info); }
.dev-only p{ font-size:var(--type-xs); color:var(--info-muted); margin:0; }

/* The tax lines as charged, so the arithmetic can be checked rather than taken on trust. */
.receipt{ margin:16px 0 0; display:grid; gap:2px; }
.receipt div{ display:flex; justify-content:space-between; gap:16px; font-size:var(--type-sm); color:var(--text-secondary); padding:6px 0; }
.receipt dd{ margin:0; font-variant-numeric:tabular-nums; }
.receipt .receipt-total{ font-size:var(--type-md); font-weight:700; color:var(--text-heading); border-top:1px solid var(--border); margin-top:6px; padding-top:12px; }

/* The account offer on the confirmation screen. Styled as an aside, not as a step: the order is
   already done and nothing here is required. */
.account-offer{
  border:1px dashed var(--border-accent); border-radius:var(--radius-md); background:var(--surface-sunken);
  padding:16px; margin:18px 0 0; text-align:left;
}
.account-offer h5{ font-family:var(--serif); font-size:var(--type-md); color:var(--text-heading); margin:0 0 6px; }
.account-offer p{ font-size:var(--type-sm); color:var(--text-secondary); line-height:1.6; margin:0 0 14px; overflow-wrap:anywhere; }
/* The one field and its button on a line, stacking when there is not room for both. The button
   keeps its own width: a wrapped label in a 92vw panel is how this block reads as broken. */
.account-offer-form{ display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin:0 0 10px; }
.account-offer-form input{ flex:1 1 200px; min-width:0; }
.account-offer-form .btn-ghost{ flex:0 0 auto; }
/* Both sit tighter than the paragraph above them, because they belong to the field, not to the
   offer. .auth-error is the shop's one error shape and is defined under Signing in. */
.account-offer .field-help{ margin:0 0 10px; }
.account-offer .auth-error{ margin:0; }
.account-saved{
  font-size:var(--type-sm); color:var(--text-heading); margin:18px 0 0; padding:12px 14px; text-align:left;
  background:var(--surface-sunken); border:1px solid var(--border-accent); border-radius:var(--radius-md);
}
.account-saved a{ color:var(--text-accent); text-decoration:underline; text-underline-offset:3px; }

/* ============ Signing in ============ */

/* Narrower than the checkout panel: two fields do not need six hundred pixels, and a wide box
   for a short form reads as an unfinished one. */
.auth-panel{ width:440px; }
.auth-intro{ font-size:var(--type-sm); color:var(--text-secondary); line-height:1.6; margin:0 0 18px; }
/* The error role, defined once here and used by every failure message in the shop. */
.auth-error{
  background:var(--danger-surface); border:1px solid var(--danger-border); border-radius:var(--radius-sm); color:var(--danger);
  font-size:var(--type-sm); line-height:1.5; padding:10px 12px; margin:0;
}
/* The Google block, above the form. Absent from the markup entirely when Google sign-in is not
   configured, so none of this ever applies to a shop that does not offer it. */
.auth-google{ margin:0 0 4px; }
/* Centred because Google's button is drawn at a measured pixel width, so it cannot be stretched to
   the panel by the usual means and would otherwise sit against the left edge on a wide modal. The
   minimum height is the button's, reserved before it is drawn: without it the form beneath jumps
   down the moment Google's script finishes loading. */
.auth-google-button{ display:flex; justify-content:center; min-height:44px; }
.auth-google-error{
  font-size:var(--type-sm); color:var(--danger); line-height:1.5; margin:10px 0 0; text-align:center;
}

/* The password field and its rules, spaced like every other field in the shop.

   The wrapper exists so that "forgot your password" can hide the input and its `required` together
   — see the markup in fragments/layout.html. But a wrapper is also a box, and inside it the two
   children lost the stack's 12px gap: .field-help pulls itself up 4px expecting that gap, so with
   nothing to pull against the rules ended up against the bottom of the input, tighter here than the
   identical sentence on the account and reset-password pages, where both are stack children.

   display:contents gives the box away and leaves the children in the stack, so they are spaced by
   the same gap and the same rule as everywhere else rather than by a second figure kept in step by
   hand. Hiding still works: [hidden] is display:none !important, which outranks this. */
#authPasswordRow{ display:contents; }

/* A rule with the word sitting in a gap in it. The two lines are drawn by the pseudo-elements
   rather than by a border on the paragraph, which is what lets the "or" interrupt them. */
.auth-divider{
  display:flex; align-items:center; gap:12px;
  font-size:var(--type-xs); color:var(--text-muted); text-transform:uppercase;
  letter-spacing:.08em; margin:18px 0;
}
.auth-divider::before, .auth-divider::after{
  content:""; flex:1; height:1px; background:var(--border);
}

/* The way out for somebody who cannot get in. Quieter than the switch link below it — this is
   the third thing a shopper wants from this modal, not the second, and it sits above the
   register/sign-in swap so that the two never read as one pair of choices. */
.auth-forgot{ font-size:var(--type-sm); margin:14px 0 0; text-align:center; }
.auth-forgot .link-btn{
  background:none; border:none; color:var(--text-muted); text-decoration:underline;
  text-underline-offset:3px; font-size:var(--type-sm); font-family:inherit;
}
.auth-forgot .link-btn:hover{ color:var(--text-accent); }

.auth-switch{ font-size:var(--type-sm); color:var(--text-muted); margin:18px 0 0; text-align:center; }
.auth-switch .link-btn{
  background:none; border:none; color:var(--text-accent); text-decoration:underline;
  text-underline-offset:3px; font-size:var(--type-sm); font-family:inherit;
}

/* The account control in the header. The label is hidden on a narrow bar, where the icon and its
   accessible name carry it — the same treatment the bag and search controls get. */
.account-btn{ width:auto; gap:7px; padding:0 12px; border-radius:999px; }
.account-btn .account-label{ font-size:var(--type-sm); font-weight:600; white-space:nowrap; }
.account-btn.signed-in{ background:var(--surface-hover); }
@media(max-width:900px){
  .account-btn{ width:38px; padding:0; }
  .account-btn .account-label{
    position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap;
  }
}
.mnav-account{
  margin-top:14px; font-size:var(--type-sm); color:var(--text-heading); text-decoration:underline;
  text-underline-offset:4px; overflow-wrap:anywhere;
}

/*
   The modal shell on a small screen.

   These used to live in home.css, which the bag page does not load — so the checkout modal, the
   only modal a shopper has to complete anything in, was the one modal that never got them. It kept
   the desktop padding and put its close button out of reach behind a scrolled panel.

   They are shared rules, so they belong in the shared stylesheet. The panel variants that only
   exist on the home page — the quick view and the lightbox — keep their own overrides there.
*/
@media (max-width:768px){
  .modal{
    padding:calc(12px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right))
            calc(12px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left));
  }
  .modal-panel{
    width:92vw; max-width:92vw; max-height:85vh; max-height:85dvh;
    overflow-y:auto; margin:auto;
  }
  .modal-body{ padding:16px; }
  /* The close control stays reachable at the top of the panel while the body scrolls under it.
     There used to be a second rule here pinning the button itself to the viewport at z-index 100,
     added when the header was covering it and the cause had not been found. It never did what it
     looked like it did: .modal-panel is transformed, which makes it the containing block for a
     fixed child, so the button was pinned to the panel and its z-index was sealed inside the
     panel's own stacking context, where the header could never have seen it. The sticky head is
     what was holding the button in place, and it still is. */
  .modal-head{ position:sticky; top:0; z-index:6; }
}

/* ============ Toast ============ */
/* Clear of the home indicator: 28px from the bottom put the pill straight under the bar on a
   modern iPhone, where it could be neither read nor tapped. */
.toast{
  position:fixed; bottom:calc(28px + env(safe-area-inset-bottom));
  left:50%; transform:translateX(-50%) translateY(20px); background:var(--surface-action);
  color:var(--text-on-action); padding:14px 24px; border-radius:999px; font-size:var(--type-sm); font-weight:600; z-index:var(--layer-toast);
  opacity:0; visibility:hidden; transition:all .3s ease; box-shadow:var(--shadow); border:1px solid var(--border-accent-on-inverse);
  max-width:calc(100vw - 32px); text-align:center;
}
.toast.show{ opacity:1; visibility:visible; transform:translateX(-50%) translateY(0); }
.toast-action{
  color:var(--text-accent-on-inverse-bright); font-weight:700; font-size:var(--type-sm); margin-left:14px;
  text-decoration:underline; text-underline-offset:3px;
}
/* A toast carrying a reference stops being a one-line pill, so it drops the pill shape. */
.toast.with-ref{ border-radius:var(--radius-md); }

/* The request reference, wherever a failure is reported. Monospaced because it will be read
   aloud or typed into a support email, and those are the characters that get confused. */
.ref-copy{
  display:inline-flex; align-items:center; gap:6px; margin-left:12px;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size:var(--type-sm); font-weight:600; letter-spacing:.3px;
  background:var(--surface-well-on-inverse-solid); color:inherit; border:1px solid currentColor;
  border-radius:999px; padding:5px 12px; opacity:.9;
}
.ref-copy:hover{ opacity:1; }
.load-error .ref-copy{ background:var(--warning-well); margin-left:0; }

/* ============ Loading skeletons ============ */
.skeleton{
  position:relative; overflow:hidden; background:var(--surface-sunken); border-radius:var(--radius-sm);
}
/*
   The travelling highlight. It is a token because 70% white sweeping across a near-black card is
   a strobe, not a shimmer — the one place where "it is just a gradient" stops being true.
*/
.skeleton::after{
  content:''; position:absolute; inset:0; transform:translateX(-100%);
  background:linear-gradient(90deg, transparent, var(--shimmer), transparent);
  animation:shimmer 1.3s ease-in-out infinite;
}
@keyframes shimmer{ to{ transform:translateX(100%); } }
.skeleton-line{ height:11px; margin:9px 0; }
.skeleton-thumb{ width:88px; height:106px; flex-shrink:0; }
/* The stand-in for a row of text buttons, which is how a receipt ends. Given a height rather than
   left as a line because a .btn-ghost is 20px tall, and 44 under pointer:coarse — so a line
   reserving 11 would leave most of the jump in place on the devices that feel it. */
.skeleton-btn{ width:150px; height:20px; margin:20px 0 0; }
/* And for a dropdown. flex, because the rows it stands in are flex rows and the real control is
   flex:1 1 180px — which is also what makes it wrap under its label at 360 rather than beside it. */
.skeleton-field{ flex:1 1 180px; height:40px; }
.skeleton-card{ pointer-events:none; }
.w70{ width:70%; } .w60{ width:60%; } .w40{ width:40%; } .w30{ width:30%; }
/*
   The one that is not a line width. Several of the cards a skeleton stands in for are flex rows —
   the address card, the horse card, the order's own heading — and a wrapper inside one of those is
   sized by its contents. Its contents are lines measured as a percentage of it, so there is nothing
   for the percentage to be a percentage of, and every line comes out at nothing wide. The cart's
   skeleton never hit this because .cart-item-info is flex:1 and therefore already has a width.
*/
.w100{ width:100%; }

/* ============ Failed requests ============
   A request that fails says so on the page and offers a way to try again. Nothing that a
   shopper is waiting on is allowed to fail into a console warning.

   This is also the one definition of the warning banner. The identical thing was written out
   again as .sim-banner — then in cart.css, now in the checkout section above — and a third time
   in admin.css, each with the same three literal colours typed by hand, so a change to one of
   them was a change to a third of the shop. The shape lives here; the two variants that differ
   only in their padding say so. */
.load-error,
.warning-banner{
  display:flex; flex-wrap:wrap; align-items:center; gap:14px;
  background:var(--warning-surface); border:1px solid var(--warning-border); border-left:4px solid var(--warning-border);
  border-radius:var(--radius-md); color:var(--warning); font-size:var(--type-sm); line-height:1.5; padding:16px 18px;
}
.load-error p{ margin:0; flex:1; min-width:180px; }
.load-error button{
  background:var(--surface-action); color:var(--text-on-action); border-radius:999px; padding:10px 20px;
  font-size:var(--type-sm); font-weight:600; flex-shrink:0;
}
.load-error.span-grid{ grid-column:1/-1; }
/* The checkout's failure line is a grid row in .field-stack whether or not anything failed, and
   an empty row still costs its two gaps — twelve pixels of nothing above the Place order button
   on every checkout that goes well. */
#checkoutError:empty{ display:none; }
.load-error.compact{ padding:11px 14px; font-size:var(--type-sm); margin:0 0 16px; }
.load-error.compact button{ padding:8px 16px; font-size:var(--type-sm); }

/* ============================================================
   TOUCH DEVICES
   Two rules, applied everywhere: no interactive control smaller than 44x44, and no form
   control under 16px, because Safari force-zooms anything smaller and never zooms back.

   That second one now reads a token, so it is only true for as long as --type-md is 16px or more.
   TypeScaleIntegrationTest holds it there.
   ============================================================ */
@media (pointer:coarse){
  input, select, textarea, button, .search-input, .newsletter input{ font-size:var(--type-md); }

  .icon-btn,
  .modal-close,
  /* The card's photograph arrows. 34 pixels is comfortable under a mouse and small enough not to
     crowd a tile; a thumb needs the full target, and a thumb is what this grid mostly gets. */
  .card-gallery-nav,
  .search-close{ width:44px; height:44px; }

  .trend-pill,
  .newsletter button,
  .load-error button,
  /* Which is Edit and Delete on the account page. The bag's Remove asks for this at every
     width, because it stands in a row of 44-pixel circles either way. */
  .link-btn,
  /* "Continue shopping" on the finished order, which came across with the checkout modal. */
  .checkout-result a,
  .ref-copy{ min-height:44px; }

  .newsletter input{ min-height:44px; }

  a.logo{ display:inline-flex; align-items:center; min-height:44px; }

  /* Split off from a.logo above, which used to share this declaration, so that the button role can
     be given the axis it was missing and the logo can be left alone. Nothing moves today: .btn-ghost
     is nowhere stretched, so its box is its label either way. It gets it because it is one of the
     four button roles and .btn-outline has just shown what a role that names only one axis costs
     the day somebody puts it in a row that shares out the width. */
  .btn-ghost{ display:inline-flex; align-items:center; justify-content:center; min-height:44px; }

  /* Which is why the shapes reserved for them have to grow with them. The field's 44 is set in
     account.css, on the selects it stands in for. */
  .skeleton-btn,
  .skeleton-field{ min-height:44px; }

  /* min-width matters here: "FAQs" is only 33px of text.

     This is the one place in the shop that wants the missing axis missing, and it is the reason
     half-a-flex.mjs has an exemption list rather than a rule with no exceptions. The links sit in
     a column against a common left edge. "FAQs" in a 44-pixel box centred would stand five pixels
     to the right of "Shipping & Returns" above it, so the padding added for a thumb would show up
     as a ragged edge on a desktop that never needed it. flex-start is the answer here. */
  .footer-col a{ display:inline-flex; align-items:center; min-height:44px; min-width:44px; }
  .footer-col ul{ gap:2px; }
}
