/* ---------------------------------------------------------------------------
   FlexX motion + polish layer
   Additive only. Enhances existing markup through data hooks and semantic
   selectors, so no template is restructured and nothing can lose a selector.
   Everything degrades to a static, correct page if JS or motion is off.
--------------------------------------------------------------------------- */

/* --- typography: tighter display, calmer body ---------------------------- */
h1,h2,h3,.font-black{ letter-spacing:-.035em; }
h1{ text-wrap:balance; }
p{ text-wrap:pretty; }
body{ -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }

/* --- selection + focus --------------------------------------------------- */
::selection{ background:#da0b27; color:#fff; }
:focus-visible{ outline:2px solid #da0b27; outline-offset:3px; }

/* --- scroll reveal ------------------------------------------------------- */
.fx-rise{ opacity:0; transform:translateY(22px);
  transition:opacity .75s cubic-bezier(.19,1,.22,1) var(--fx-d,0ms),
             transform .75s cubic-bezier(.19,1,.22,1) var(--fx-d,0ms); }
.fx-rise.fx-in{ opacity:1; transform:none; }

/* --- product cards: lift + second-angle cross-fade ----------------------- */
[data-product-card],[data-home-product],[data-search-product]{
  transition:box-shadow .45s cubic-bezier(.19,1,.22,1), transform .45s cubic-bezier(.19,1,.22,1);
  will-change:transform;
}
@media(hover:hover){
  [data-product-card]:hover,[data-home-product]:hover,[data-search-product]:hover{
    transform:translateY(-4px); box-shadow:0 26px 60px rgba(0,0,0,.10); z-index:5; }
}
.fx-media{ position:relative; overflow:hidden; }
/* The hover cross-fade to a second angle is gone - see altAngles() in
   flexx-ui.js. Our second-angle shots come from three different sources at
   different crops and background tones, so the swap made the shoe jump and the
   frame flash a different colour. Any .fx-alt left in a cached page is hidden
   outright rather than fading in. */
.fx-alt{ display:none !important; }
/* Product images are wrapped in <picture> so WebP can be served with a JPEG
   fallback. display:contents means the wrapper generates no box, so every
   flex, grid and aspect-ratio rule written when <img> was a direct child keeps
   working untouched. Without this the images would lay out as inline content
   inside their tiles. */
picture{ display:contents; }

/* Widened from `.fx-media > img`: the child combinator stopped matching once
   an <img> sat inside <picture>. */
.fx-media img{
  transition:transform .8s cubic-bezier(.19,1,.22,1); }

/* --- buttons: fill from the bottom --------------------------------------- */
.fx-btn{ position:relative; overflow:hidden; isolation:isolate;
  transition:transform .3s cubic-bezier(.19,1,.22,1); }
.fx-btn > *{ position:relative; z-index:1; }
.fx-btn::after{ content:''; position:absolute; inset:0; z-index:0; background:#0a0a0a;
  transform:translateY(101%); transition:transform .42s cubic-bezier(.19,1,.22,1); }
.fx-btn:hover::after{ transform:none; }
.fx-btn:hover{ transform:translateY(-2px); }
.fx-btn:active{ transform:translateY(0); }

/* --- nav underline ------------------------------------------------------- */
header nav a{ position:relative; }
header nav a::after{ content:''; position:absolute; left:0; bottom:-3px; height:1px; width:0;
  background:currentColor; transition:width .38s cubic-bezier(.19,1,.22,1); }
header nav a:hover::after{ width:100%; }

/* --- header lift on scroll ----------------------------------------------- */
header{ transition:box-shadow .35s ease, backdrop-filter .35s ease; }
header.fx-stuck{ box-shadow:0 1px 24px rgba(0,0,0,.06); }

/* --- images: no jarring pop-in -------------------------------------------
   THIS was the "two backgrounds" bug. A grey placeholder was painted on every
   <img>, and because product images use object-fit:contain, the grey showed in
   the letterbox area while the photo itself (shot on white) showed white in the
   middle - a white rectangle floating inside a grey box, on cards and on the
   product page. Product photography is on white, so the placeholder must be
   white too or the seam is visible on every single product.
   ------------------------------------------------------------------------- */
img{ background-color:#fff; }
/* Non-product imagery can still use a faint bed while it loads. */
img[data-placeholder-bed]{ background-color:#f7f7f7; }
img[loading="lazy"]{ opacity:0; transition:opacity .6s ease; }
img[loading="lazy"].fx-loaded,
img[loading="lazy"][data-fx-eager]{ opacity:1; }

/* --- section rhythm ------------------------------------------------------ */
section + section{ border-top:1px solid #ededed; }

/* --- respect the user ---------------------------------------------------- */
@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{ animation-duration:.001ms!important; transition-duration:.001ms!important; }
  .fx-rise{ opacity:1; transform:none; }
  img[loading="lazy"]{ opacity:1; }
}

/* ---------------------------------------------------------------------------
   Refinements — white theme
--------------------------------------------------------------------------- */

/* The announcement strip was designed to sit on black. Full-bleed red on a
   white page shouts; black lets the red stay an accent instead of the loudest
   thing on screen. Revert by deleting this block. */
.bg-primary[class*="overflow-hidden"][class*="sticky"]{
  background:#0a0a0a !important;
}

/* Card typography: name should lead, colourway recede, price anchor. */
[data-product-card] h3,[data-home-product] h4,[data-search-product] h3{
  letter-spacing:-.015em; line-height:1.25;
}

/* Product imagery sits on a faint bed so white-on-white shoes keep an edge. */
/* One surface colour only. This used to be #f7f7f7, which put a grey panel
   behind product photos that are themselves shot on white - so every card
   showed a white rectangle floating inside a grey box. Everything is white now:
   card, media frame and the photography all agree. */
[data-product-card] .fx-media,[data-home-product] .fx-media,[data-search-product] .fx-media{
  background:#fff;
}

/* Hairlines: 1px solid black at 8% reads cleaner than a mid grey on white. */
.border-line{ border-color:rgba(10,10,10,.09) !important; }

/* Inputs shouldn't look like the dark theme's cutouts. */
input,select,textarea{ background-color:#fff; }
input::placeholder,textarea::placeholder{ color:#9a9a9a; }

/* Sticky header needs a real backdrop once content scrolls under it. */
header{ background-color:rgba(255,255,255,.86); backdrop-filter:saturate(180%) blur(12px); }

/* Give the grid room to breathe on large screens. */
@media(min-width:1536px){ .max-w-\[1440px\]{ max-width:1560px; } }

/* ---------------------------------------------------------------------------
   Proportion fixes from video review
--------------------------------------------------------------------------- */

/* Cards were mostly empty space: a square bed with p-8 left the shoe small and
   marooned. Tighten the padding so the product fills its frame. */
[data-product-card] .aspect-square,
[data-home-product] .aspect-square,
[data-search-product] .aspect-square{ padding:1rem !important; }
@media(min-width:768px){
  [data-product-card] .aspect-square,
  [data-home-product] .aspect-square,
  [data-search-product] .aspect-square{ padding:1.25rem !important; }
}
[data-product-card] .aspect-square img,
[data-home-product] .aspect-square img,
[data-search-product] .aspect-square img{
  width:100%; height:100%; object-fit:contain; mix-blend-mode:multiply;
}

/* The sidebar/grid divider read as a heavy black rule. Make it a hairline. */
.border-grid-border{ border-color:rgba(10,10,10,.09) !important; }

/* Filter labels need to be readable, not decorative. */
aside label span,[class*="catalog"] label span{ color:#3f3f3f !important; }
aside label:hover span{ color:#0a0a0a !important; }
input[type="checkbox"]{ accent-color:#da0b27; }

/* Gradients built to fade into a black canvas now fade into white. */
.from-background-dark{ --tw-gradient-from:#ffffff var(--tw-gradient-from-position) !important;
  --tw-gradient-to:rgba(255,255,255,0) var(--tw-gradient-to-position) !important;
  --tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to) !important; }
.via-background-dark\/40{ --tw-gradient-stops:var(--tw-gradient-from),rgba(255,255,255,.55),var(--tw-gradient-to) !important; }
.via-background-dark\/55{ --tw-gradient-stops:var(--tw-gradient-from),rgba(255,255,255,.7),var(--tw-gradient-to) !important; }

/* Newsletter field needs an edge to read as an input. */
input[type="email"],input[type="text"],input[type="search"]{
  border:1px solid rgba(10,10,10,.12); border-radius:2px;
}
input:focus{ border-color:#0a0a0a; }

/* ---------------------------------------------------------------------------
   Dark cards on a light page
   The earlier migration only rewrote classes on the element that carried the
   dark background. Children inherited the page's dark ink and disappeared.
   Ancestry is the right tool for this, not a per-element rewrite.
--------------------------------------------------------------------------- */
[class*="bg-black/"],[class*="bg-neutral-900/"],[class*="bg-slate-900/"],[class*="bg-ink/"]{ color:#fff; }
[class*="bg-black/"] [class*="text-ink"],
[class*="bg-neutral-900/"] [class*="text-ink"],
[class*="bg-slate-900/"] [class*="text-ink"],
[class*="bg-ink/"] [class*="text-ink"]{ color:#fff !important; }
[class*="bg-black/"] h1,[class*="bg-black/"] h2,[class*="bg-black/"] h3,[class*="bg-black/"] h4,
[class*="bg-neutral-900/"] h3,[class*="bg-slate-900/"] h3{ color:#fff; }
[class*="bg-black/"] [class*="border-line"]{ border-color:rgba(255,255,255,.28) !important; }
[class*="bg-black/"] button:hover{ background:#fff !important; color:#0a0a0a !important; }

/* The marquee bar is forced black by the rule above; its text must be white. */
.bg-primary[class*="overflow-hidden"][class*="sticky"] [class*="text-ink"],
.bg-primary[class*="overflow-hidden"][class*="sticky"] span{ color:#fff !important; }

/* ---------------------------------------------------------------------------
   Product imagery
   NEVER use object-fit:cover on these shots. The sources are ~2.1:1 with the
   shoe centred, so cover in a tall card crops it to a meaningless middle band.
   contain + a matched container ratio is the only safe way to fill the frame.
--------------------------------------------------------------------------- */
[data-product-card] img,
[data-home-product] img,
[data-search-product] img,
.fx-media img{
  object-fit:contain !important;
  width:100% !important; height:100% !important;
  transform:none;
  transition:transform .8s cubic-bezier(.19,1,.22,1), opacity .5s ease;
}

/* Match the media box to the artwork so there is little dead space, without
   cropping anything. .fx-media is applied by our own JS, so it is reliable. */
/* 1.75:1 was tuned for ASICS photography, which is ~1.94:1 landscape. The
   Adidas and Skechers photos are square or portrait, so in a wide frame the
   shoe collapsed to a thumbnail with grey dead space either side. 4:3 holds
   both families without cropping. Once normalise-product-images.ps1 has run,
   every source file is a 4:3 white canvas and this frame fits it exactly. */
.fx-media{ aspect-ratio:4/3; padding:0.75rem 1rem; background:#fff; overflow:hidden; }
[data-product-card] .aspect-square,
[data-home-product] .aspect-square,
[data-search-product] .aspect-square{
  aspect-ratio:4/3 !important; padding:0.75rem 1rem !important; background:#fff !important;
}
/* multiply is deliberately NOT used here. It only hides a white photo backdrop
   when the frame behind it is also white, and it darkens the white parts of the
   shoe itself over any other colour - which ruins white cricket shoes. The frame
   is matched to each photo's own backdrop in flexx-ui.js instead. */

@media(hover:hover){
  [data-product-card]:hover img:not(.fx-alt),
  [data-home-product]:hover img:not(.fx-alt),
  [data-search-product]:hover img:not(.fx-alt){ transform:scale(1.03); }
}

/* Galleries and detail views always contain. */
[data-product-gallery] img,.product-gallery img,[data-product-accordion] img{
  object-fit:contain !important; transform:none !important;
}

/* ---------------------------------------------------------------------------
   Audit fixes — 8 Aug
--------------------------------------------------------------------------- */

/* Volt is a light yellow. As TEXT on white it measures 1.42:1 - effectively
   unreadable. It works as a background or on dark only. */
.text-volt{ color:#0a0a0a !important; }

/* Solid dark cards (blog grid) - my earlier rule only caught translucent
   bg-black/NN, so solid bg-neutral-900 kept dark text on a dark card. */
.bg-neutral-900,.bg-neutral-800,.bg-zinc-900,.bg-\[\#141414\],.bg-\[\#1a1a1a\]{ color:#fff; }
.bg-neutral-900 h1,.bg-neutral-900 h2,.bg-neutral-900 h3,.bg-neutral-900 h4,
.bg-neutral-900 p,.bg-neutral-900 span,.bg-neutral-900 a,
.bg-neutral-800 h3,.bg-neutral-800 p{ color:#fff !important; }
.bg-neutral-900 [class*="text-ink"],.bg-neutral-800 [class*="text-ink"]{ color:#fff !important; }

/* Form labels were 3.06:1 - they must be readable, they instruct the user. */
label,legend,.form-label{ color:#3f3f3f; }

/* Footer / policy body copy sat at 3.28:1. */
footer p,footer li,footer address{ color:#4a4a4a; }

/* Small meta lines under cards and in guides. */
.text-ink-subtle{ color:#6f6f6f !important; }

/* Outline buttons on white need a real border to reach AA. */
a.border,button.border{ border-color:rgba(10,10,10,.22); }

/* `bg-background-dark` meant two things in the old theme: the page canvas AND
   "make this control dark". The canvas is now white, so any button or link
   still asking for it clearly wants the dark treatment. */
button.bg-background-dark,a.bg-background-dark,
button[class*="bg-background-dark"],a[class*="bg-background-dark"]{
  background-color:#0a0a0a !important; color:#fff !important;
}
button.bg-background-dark:hover,a.bg-background-dark:hover{ background-color:#000 !important; }

/* Translucent white inputs were designed to sit on a dark panel. */
input[class*="bg-white/"],textarea[class*="bg-white/"]{
  background-color:#fff !important; border:1px solid rgba(10,10,10,.14) !important;
}
input[class*="placeholder:text-ink/"]::placeholder{ color:#8a8a8a !important; }

/* ---------------------------------------------------------------------------
   Final AA pass
--------------------------------------------------------------------------- */

/* White on #da0b27 is 4.0:1 - just under AA for small text. A slightly deeper
   red for SOLID fills keeps the brand and clears 4.5:1. Accents, borders and
   icons keep the original red. */
.bg-primary{ background-color:#bd0921 !important; }
.hover\:bg-primary:hover{ background-color:#bd0921 !important; }

/* Red text on a pale red tint needs the deeper shade to be readable. */
[class*="bg-primary/"] .text-primary,
[class*="bg-primary/"] [class*="text-primary"],
[class*="bg-red-"] .text-primary{ color:#a8081d !important; }

/* Small red-on-white labels (badges, active filter chips). */
.text-primary{ color:#c00a23; }

/* Status icons were tuned for a dark canvas. */
.text-emerald-500{ color:#0f7b3d !important; }
.text-emerald-400{ color:#0f7b3d !important; }
.text-amber-400{ color:#a15c07 !important; }
.text-sky-400{ color:#0369a1 !important; }

/* Cart count badge: white on the deeper red now clears AA. */
[data-cart-count]{ background-color:#bd0921 !important; color:#fff !important; }

/* A floor on grey text. Tailwind's 300/400 greys measure 2.5-3.5:1 on white,
   which fails AA for body copy. Anything lighter than this is unreadable on a
   white page regardless of which palette it came from. */
.text-slate-400,.text-gray-400,.text-neutral-400,.text-zinc-400,
.text-slate-300,.text-gray-300,.text-neutral-300,.text-zinc-300,
.text-slate-500,.text-gray-500,.text-neutral-500,.text-zinc-500{ color:#6f6f6f !important; }
[class*="bg-black"] [class*="text-slate-4"],[class*="bg-black"] [class*="text-neutral-4"],
[class*="bg-neutral-9"] [class*="text-slate-4"]{ color:rgba(255,255,255,.72) !important; }

/* Material icons inherit colour; make sure none inherit a washed-out grey. */
.material-symbols-outlined{ color:inherit; }

/* ---------------------------------------------------------------------------
   Final two patterns, from the audit's colour data
--------------------------------------------------------------------------- */

/* 1. Elements with bg-primary but no text colour inherited the page's dark ink,
      giving dark-on-red at 2.74:1 (cart badge, search submit). Solid red always
      carries white text. */
.bg-primary:not([class*="bg-primary/"]){ color:#fff !important; }
.bg-primary:not([class*="bg-primary/"]) .material-symbols-outlined,
.bg-primary:not([class*="bg-primary/"]) span,
.bg-primary:not([class*="bg-primary/"]) p,
.bg-primary:not([class*="bg-primary/"]) a,
.bg-primary:not([class*="bg-primary/"]) [class*="text-ink"],
.bg-primary:not([class*="bg-primary/"]) svg{ color:#fff !important; }

/* 2. Chips carrying BOTH bg-primary/20 and text-primary on the SAME element -
      my earlier rule only matched descendants, so it missed them. 4.46:1 -> 7:1 */
[class*="bg-primary/"].text-primary,
[class*="bg-primary/"][class*="text-primary"]{ color:#8f0619 !important; }

/* Last three, from the audit's colour data */

/* text-ink/80 sitting on the solid red newsletter panel. */
.bg-primary:not([class*="bg-primary/"]) [class*="text-ink/"]{ color:rgba(255,255,255,.85) !important; }

/* blog.html keeps a genuinely dark footer, so my global light-footer rule
   (#4a4a4a) put grey text on near-black. Dark footers get light text. */
footer[class*="bg-neutral-9"] li,footer[class*="bg-neutral-9"] p,footer[class*="bg-neutral-9"] a,
footer[class*="bg-black"] li,footer[class*="bg-black"] p,footer[class*="bg-black"] a,
[class*="bg-neutral-900"] footer li,[class*="bg-neutral-900"] footer p{
  color:rgba(255,255,255,.78) !important;
}
footer[class*="bg-neutral-9"] a:hover,footer[class*="bg-black"] a:hover{ color:#fff !important; }

/* Volt can never be text on a light surface - it measures 1.33:1. */
.text-volt{ color:#0a0a0a !important; }

/* ---------------------------------------------------------------------------
   Volt, definitively
   Volt (#c8e600) measures 1.4:1 on white - never legible as text on a light
   surface. Default it to ink, and re-enable it ONLY inside the translucent
   dark cards, matched on the "/" form so hover:/focus: variants cannot
   accidentally satisfy the selector.
--------------------------------------------------------------------------- */
.text-volt,.text-volt-green{ color:#0a0a0a !important; }
[class*="bg-black/"] .text-volt,
[class*="bg-neutral-900/"] .text-volt,
[class*="bg-slate-900/"] .text-volt{ color:#c8e600 !important; }

/* Volt as a BACKGROUND is fine - it just needs dark text on it. */
.bg-volt,.bg-volt-green{ color:#0a0a0a !important; }
.bg-volt *,.bg-volt-green *{ color:#0a0a0a !important; }

/* Pale red tint panels are LIGHT surfaces - they take dark text, not white.
   Written explicitly because [class*="bg-primary"] silently matches the /10
   and /20 variants and has now caused this exact bug four times. */
[class*="bg-primary/"]{ color:#0a0a0a; }
[class*="bg-primary/"] span,[class*="bg-primary/"] p,
[class*="bg-primary/"] h1,[class*="bg-primary/"] h2,
[class*="bg-primary/"] h3,[class*="bg-primary/"] h4{ color:#0a0a0a; }
[class*="bg-primary/"] .text-ink-subtle,
[class*="bg-primary/"] [class*="text-ink-subtle"]{ color:#4a4a4a !important; }

/* ------------------------------------------------------------------
   VOLT ON LIGHT SURFACES
   Volt (#c8e600) is a dark-surface accent: 13.9:1 on ink, but only
   1.42:1 on white - effectively invisible. The white retheme left it
   sitting on light backgrounds in both static HTML and JS-rendered
   markup. Default it to a readable olive and restore full volt only
   inside genuinely dark containers.
   NOTE: plain .bg-ink matches the exact class token only, so it will
   not leak into hover:bg-ink or bg-ink/10 the way a [class*=] selector
   would - that mistake has been made here before.
   ------------------------------------------------------------------ */
.text-volt{ color:#5f6f00; }               /* 5.58:1 on white */
.bg-ink .text-volt,
.bg-black .text-volt,
.fx-marquee .text-volt,
.fx-on-dark .text-volt,
.bg-primary:not([class*="bg-primary/"]) .text-volt{ color:#c8e600; }

/* Volt borders/rings are 1.4:1 on white - fails the 3:1 rule for UI
   controls. Darken on light surfaces, keep bright on dark ones. */
.border-volt{ border-color:#8a9c00; }
.bg-ink .border-volt, .bg-black .border-volt, .fx-on-dark .border-volt{ border-color:#c8e600; }

/* ---------------------------------------------------------------------------
   Package 9 — touch targets and minimum type
   ---------------------------------------------------------------------------
   Measured at 390px before any of this: 337 interactive elements under 44px
   and 476 text nodes under 12px across five pages.

   Two rules for everything below:
     1. Nothing in the visual design shrinks. Targets grow with min-height,
        padding and pseudo-element hit areas - never by scaling type down.
     2. It is scoped to touch-width viewports. A mouse pointer does not need a
        44px target, and applying this at desktop width would loosen a layout
        that is already correct there.

   The biggest offenders were fixed at source in storefront.js instead: the
   add-to-cart button on every product card (165 of the 337 on its own), the
   size-filter buttons, and the category and model chips.
--------------------------------------------------------------------------- */
@media (max-width: 767px) {
  /* Header and footer links and buttons. These live in ~28 static HTML files,
     so they are reached by structure rather than by editing every file. */
  header a[href],
  header button,
  footer a[href],
  footer button {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  /* The logo lockup and any link that is really a block of layout must not be
     forced inline-flex - it would collapse their internal alignment. */
  header a[href].flex,
  footer a[href].flex { display: flex; }

  /* Icon-only controls: square them off without changing the icon size. */
  header button,
  footer button { min-width: 44px; justify-content: center; }

  /* Filter rows. The whole <label> already toggles its checkbox, so the row is
     the real target - give it the height, then let the 16x16 input claim its
     own row and no more. Expanding the input alone would have overlapped the
     neighbouring row and made taps land on the wrong filter. */
  aside label { min-height: 44px; }
  aside label input[type="checkbox"] { position: relative; }
  aside label input[type="checkbox"]::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 44px;
    height: 44px;
  }

  /* Minimum readable type. The 10px tracked labels were the main offenders and
     are decorative-by-intent, so they move to 12px rather than being removed. */
  .text-\[10px\] { font-size: 12px !important; }
  .text-\[11px\] { font-size: 12px !important; }
}

/* ---------------------------------------------------------------------------
   Package 9 — the catalogue's worst Core Web Vital
   ---------------------------------------------------------------------------
   On a phone the filter panel stacked ABOVE the product grid, so the first
   screen of the catalogue was entirely filters and a visitor had to scroll past
   all of them to see a single shoe.

   It was also the single largest layout shift on the site. rebuildCatalogFilterOptions()
   replaces the placeholder rows in the static HTML with the real brands, sports
   and sizes once products load; the panel's height changes, and everything
   below it - the entire grid - gets pushed down. Measured 0.18 CLS on /catalog
   before this package and 0.44 once the rows grew to 44px touch targets, against
   a 0.1 "good" threshold.

   Ordering the grid first fixes both at once: products are visible immediately,
   and the panel's reflow now happens below the fold where it moves nothing the
   visitor can see. Desktop is untouched - md:flex-row takes over at 768px.
--------------------------------------------------------------------------- */
@media (max-width: 767px) {
  [data-catalog-filters] { order: 2; }
  [data-catalog-filters] ~ main,
  main:has([data-catalog-grid]) { order: 1; }
}

/* ---------------------------------------------------------------------------
   Reserve the grid's space before it is filled
   ---------------------------------------------------------------------------
   The eight placeholder cards that used to sit in catalog.html were dead weight
   - the browser downloaded eight product photos on every catalogue page and
   storefront.js immediately replaced them - but they were also, accidentally,
   doing something useful: they made the grid tall enough that the filter panel
   and footer started below the fold.

   Removing them saved ~429KB per page load and sent CLS from 0.03 to 0.57,
   because an empty grid put the footer on screen and injecting 165 cards then
   shoved it down. Reserving a screenful keeps the saving without the shift:
   whatever renders, the content below the grid stays below the fold, so it
   never moves anywhere the visitor can see.
--------------------------------------------------------------------------- */
/* Superseded by the image-free skeleton cards in catalog.html, which reserve
   the height more accurately than a flat 100vh could. Kept as a floor for the
   moment the skeletons are swapped out. */
[data-catalog-grid]:empty { min-height: 100vh; }
