/* ==========================================================================
   Equinova home page — sections that exist only here.
   Shared chrome, tokens and the modal shell come from storefront.css.
   ========================================================================== */

a, button{ cursor:pointer; }

/* ============ Launch curtain ============
   A one-time homepage ceremony controlled by Shop settings: ivory silk on a brass rail, with the
   shop's name on it in forest. Its colours are the --curtain-* roles, which are the one group in
   tokens.css besides the photo scrims that does not follow the theme — the invitation is the same
   invitation whichever way somebody reads the shop.

   The fabric is made entirely from gradients and moves only with transforms and opacity, keeping
   the reveal on the compositor instead of repainting a full-screen image on every frame. */
body.launch-curtain-active{ overflow:hidden; }

/* One screen, never scrolled. The ceremony is the shop's first impression and it has to arrive
   whole: on a large monitor a stage that ran past the fold would put the countdown button below
   it, and asking for a scroll to find the only way in is not a welcome.

   The spacing and the mark are capped in vh so the lock-up shrinks with the window; the type is
   not, because a size derived from the viewport is a size off the scale. It steps down instead, in
   the two short-window blocks at the end of this section.

   overflow-y stays auto as the floor under all of that — a window shorter than the last block
   plans for must not bury the button — but with the stage fitted no scrollbar is ever drawn. The
   child centres with auto margins rather than place-items, which is what keeps the top of the
   stage reachable if that floor is ever reached. Transparent, so the panels sliding away reveal
   the shop itself. */
.launch-curtain{
  position:fixed; inset:0; z-index:var(--layer-modal);
  display:flex;
  overflow-x:hidden; overflow-y:auto; isolation:isolate;
  background:transparent;
}

/* Fixed, not absolute: the fabric has to cover the viewport however far the stage has scrolled.

   Light and shadow laid over one flat silk, rather than a gradient between four creams: the folds
   have to be visible on a cream that is already a hair off white, and two alphas do that where two
   more near-identical solids only wash out. The run goes from the outer edge of the window to the
   middle, and it is the same run on both panels because the right one is mirrored — so the silk is
   deepest at the two edges of the screen and brightest where the panels meet, which is where the
   lettering is. The stops are uneven on purpose: evenly spaced folds read as corrugation. */
.launch-curtain-panel{
  position:fixed; top:0; bottom:0; width:51%;
  background:
    linear-gradient(90deg,
      var(--curtain-shadow-deep) 0%,
      var(--curtain-shadow) 7%,
      transparent 15%,
      var(--curtain-sheen) 21%,
      transparent 27%,
      var(--curtain-shadow) 34%,
      transparent 43%,
      var(--curtain-sheen) 53%,
      transparent 61%,
      var(--curtain-shadow) 71%,
      transparent 81%,
      var(--curtain-sheen) 93%,
      var(--curtain-sheen) 100%),
    var(--curtain-silk);
  /* Inset from the left, which the mirroring turns into the outer edge of each panel. */
  box-shadow:inset 44px 0 76px -26px var(--curtain-shadow-deep);
  will-change:transform;
  transition:transform 4s cubic-bezier(.65, 0, .35, 1);
}
/* The hem. Silk hangs heavier at the bottom and catches the light at the top, and without this the
   panels read as two flat sheets of paper. */
.launch-curtain-panel::after{
  content:""; position:absolute; inset:0;
  background:linear-gradient(180deg,
    var(--curtain-sheen) 0%,
    transparent 14%,
    transparent 68%,
    var(--curtain-shadow) 88%,
    var(--curtain-shadow-deep) 100%);
}
.launch-curtain-left{ left:0; }
.launch-curtain-right{ right:0; transform:scaleX(-1); }

/* The rail, drawn as an arc rather than as a pelmet: a brass line that rises towards the middle of
   the window, which is what a curtain hung on a cord does. The box is a third wider than the
   window so the ends of the arc leave the screen instead of curving back into it, and the height
   is capped in vh so a short window is not given a rail deep enough to eat the stage under it. */
.launch-curtain-valance{
  position:fixed; z-index:3; top:clamp(10px, 2.2vh, 26px); left:-16%; width:132%;
  height:min(clamp(44px, 6.5vw, 86px), 10vh);
  border-top:2px solid var(--curtain-rail);
  border-radius:50% 50% 0 0 / 100% 100% 0 0;
  background:linear-gradient(180deg, var(--curtain-sheen) 0%, transparent 62%);
  transform-origin:top;
  transition:transform 2.4s cubic-bezier(.65, 0, .35, 1);
}

.launch-curtain-stage{
  position:relative; z-index:4;
  display:flex; flex-direction:column; align-items:center;
  width:min(680px, calc(100% - 40px));
  margin:auto;
  /* Top clears the rail, which is itself capped at 9vh. */
  padding:clamp(62px, 11vh, 104px) 24px clamp(20px, 4vh, 40px);
  text-align:center; color:var(--text-on-curtain);
  transition:opacity .7s ease, transform .7s ease;
}
/* The vh term is the one that usually wins on a laptop: the mark is the tallest single thing here
   and the first that has to give way when the window is short rather than narrow. */
.launch-curtain-mark{
  display:block; width:min(132px, 26vw, 15vh); height:auto;
  margin:0 0 clamp(10px, 2vh, 22px);
  filter:drop-shadow(0 10px 20px var(--curtain-shadow));
}
/*
   The name is set here rather than taken from the supplied lock-up, which is the mark, the
   wordmark and the line baked into one rose-coloured image. On ivory the name is forest and the
   line is antique gold between two rules, and none of that can be recoloured inside a PNG. The
   mark above is the artwork and is still the file; this is type, as it is in the drawer.
*/
.launch-curtain-wordmark{
  margin:0 0 clamp(8px, 1.4vh, 14px);
  font-family:var(--serif); font-size:var(--type-hero); font-weight:500; line-height:1;
  letter-spacing:.14em; text-transform:uppercase;
  color:var(--text-on-curtain);
}
/* The two rules are the gold at full strength, where it is a line and not a word. The line itself
   is the antique tone, because eleven pixels of brass on cream measures 2.34:1. */
.launch-curtain-tagline{
  display:flex; align-items:center; gap:clamp(10px, 2vw, 18px);
  margin:0 0 clamp(14px, 2.6vh, 26px);
  color:var(--text-on-curtain-accent);
  font-size:var(--type-xs); font-weight:600; line-height:1.2;
  letter-spacing:.2em; text-transform:uppercase;
}
.launch-curtain-tagline::before,
.launch-curtain-tagline::after{
  content:""; height:1px; width:clamp(22px, 6vw, 62px); background:var(--curtain-rail);
}
/* Twenty-one characters, not sixteen: the title is two lines of a wide measure under the lock-up
   rather than three of a narrow one, and a line costs more height here than width. */
.launch-curtain-stage h1{
  max-width:21ch; margin:0;
  color:var(--text-on-curtain);
  font-size:var(--type-hero); line-height:1.06;
}
.launch-curtain-message{
  max-width:54ch; margin:clamp(10px, 2vh, 18px) 0 0;
  color:var(--text-on-curtain-muted);
  font-size:var(--type-md); line-height:1.65;
}
.launch-countdown{
  display:flex; flex-direction:column; align-items:center; gap:3px;
  min-height:clamp(56px, 9vh, 78px);
  margin:clamp(14px, 3.2vh, 38px) 0 clamp(12px, 2.4vh, 20px);
}
#launchCountdownValue{
  color:var(--text-on-curtain);
  font-variant-numeric:tabular-nums;
  font-size:var(--type-hero); font-weight:600; line-height:1;
  letter-spacing:.06em;
}
.launch-countdown-label{
  color:var(--text-on-curtain-muted);
  font-size:var(--type-xs); letter-spacing:.15em; text-transform:uppercase;
}
.launch-curtain-button{
  display:inline-flex; align-items:center; gap:10px;
  min-height:50px; padding:13px 26px;
  border:1px solid var(--curtain-rail); border-radius:999px;
  background:linear-gradient(115deg, var(--curtain-rail-bright), var(--curtain-rail));
  color:var(--text-on-curtain);
  box-shadow:0 14px 28px -16px var(--curtain-shadow-deep);
  font:inherit; font-weight:700; letter-spacing:.02em;
  transition:transform .2s ease, filter .2s ease, box-shadow .2s ease;
}
.launch-curtain-button:hover{ transform:translateY(-2px); filter:brightness(1.06); }
.launch-curtain-button:focus-visible{ outline:3px solid var(--focus-ring-strong); outline-offset:4px; }
.launch-curtain-button:disabled{
  cursor:default; transform:none; box-shadow:none; filter:none;
  border-color:var(--curtain-shadow);
  background:var(--curtain-silk-shade); color:var(--text-on-curtain-muted);
}

.launch-curtain.is-opening .launch-curtain-stage{
  opacity:0; transform:scale(.96);
}
.launch-curtain.is-opening .launch-curtain-left{ transform:translateX(-102%); }
.launch-curtain.is-opening .launch-curtain-right{ transform:translateX(102%) scaleX(-1); }
.launch-curtain.is-opening .launch-curtain-valance{ transform:translateY(-105%) scaleY(.7); }
.launch-curtain.is-opened{ visibility:hidden; pointer-events:none; }

@media (max-width:600px){
  .launch-curtain-stage{ padding-top:74px; }
  .launch-curtain-mark{ width:min(104px, 30vw); margin-bottom:14px; }
  /* Down a step rather than to a size of its own: 48px wraps the title to five lines at 360. */
  .launch-curtain-wordmark{ font-size:var(--type-2xl); letter-spacing:.1em; }
  .launch-curtain-stage h1{ font-size:var(--type-2xl); }
  #launchCountdownValue{ font-size:var(--type-2xl); }
  .launch-curtain-message{ max-width:36ch; font-size:var(--type-sm); }
  .launch-countdown{ margin:20px 0 16px; }
  /* The tracking and the rules come in so the line stays a line: wrapped, it puts the two rules
     halfway down a two-line block and stops reading as a rule through a name. */
  .launch-curtain-tagline{ gap:8px; letter-spacing:.12em; }
  .launch-curtain-tagline::before,
  .launch-curtain-tagline::after{ width:clamp(14px, 4vw, 30px); }
  /* And the button keeps its label on one line at 360, which eight pixels of padding buys. */
  .launch-curtain-button{ gap:8px; padding:12px 18px; }
}

/* Short windows, and after the narrow one on purpose: where both apply it is the height that is
   scarce, so these have to be the rules that win.

   A laptop with the console open, a phone on its side, a browser sharing the screen with
   something else. The title widens as it shrinks — 16ch of 36px is five lines of a headline that
   fits in two at 22 — because lines cost more height here than type size does. */
@media (max-height:760px){
  .launch-curtain-stage{ padding:clamp(52px, 9vh, 80px) 20px clamp(16px, 3vh, 28px); }
  .launch-curtain-mark{ width:min(132px, 24vw, 12vh); margin-bottom:clamp(8px, 1.6vh, 16px); }
  .launch-curtain-wordmark{ font-size:var(--type-2xl); margin-bottom:clamp(6px, 1.2vh, 12px); }
  .launch-curtain-tagline{ margin-bottom:clamp(10px, 2vh, 18px); }
  .launch-curtain-stage h1{ max-width:22ch; font-size:var(--type-2xl); }
  #launchCountdownValue{ font-size:var(--type-2xl); }
  .launch-curtain-message{ max-width:48ch; font-size:var(--type-sm); margin-top:10px; }
  .launch-countdown{ min-height:0; margin:14px 0 12px; }
}

/* Shorter still. Everything stays: the button is the only way out of the ceremony, and a stage
   that drops the invitation to keep it in view has thrown away the thing it was inviting anyone
   to. Down the scale again instead. */
@media (max-height:560px){
  .launch-curtain-stage{ padding:clamp(44px, 12vh, 64px) 16px 14px; }
  .launch-curtain-mark{ width:min(96px, 18vw, 9vh); margin-bottom:6px; }
  .launch-curtain-wordmark{ font-size:var(--type-xl); margin-bottom:6px; }
  .launch-curtain-tagline{ margin-bottom:8px; }
  .launch-curtain-stage h1{ max-width:28ch; font-size:var(--type-xl); }
  #launchCountdownValue{ font-size:var(--type-xl); }
  .launch-curtain-message{ max-width:52ch; margin-top:8px; }
  .launch-countdown{ margin:10px 0; }
  .launch-curtain-button{ min-height:42px; padding:10px 20px; }
}

@media (prefers-reduced-motion:reduce){
  .launch-curtain-panel,
  .launch-curtain-valance,
  .launch-curtain-stage{ transition-duration:.01ms; }
  .launch-curtain-button{ transition:none; }
}

/* ============ Promotion carousel ============
   The first thing on the page: one product per slide, the shop's words beside its photograph.

   The words sit BESIDE the picture rather than on top of it. Text over a photograph needs a scrim
   to stay legible, the scrim has to be dark enough for the worst photograph the owner will ever
   upload, and nothing here can know what that is — so the contrast either fails on a light image or
   the scrim is heavy enough to bury every image. Two columns has neither problem, themes without a
   second thought, and stacks on a phone into exactly the order the slide is read in.

   The track is one row of full-width slides, moved by translating it. There are no cloned slides;
   see fragments/promo-carousel.html for why. */

/* --promo-control-pad is the room around the control row, and it is a variable because the arrows
   are positioned against this element and have to know it. See .promo-controls and .promo-nav. */
.promo{
  position:relative; background:var(--surface-sunken); border-bottom:1px solid var(--border);
  --promo-control-pad:28px;
}

/* The carousel is the one band on the page that is not inside the 1260px column, because a banner
   slide is the shop's front window and has to reach both edges of it. The column is not lost — it
   moves onto the product slide, which needs it, and onto the arrows, which have to stay beside the
   words rather than out at the edges of a wide monitor.

   --promo-column is that column expressed as an inset from the page edge: nothing on a screen
   narrower than the column, and the even margin either side once the screen is wider. */
.promo-wrap{ position:relative; max-width:none; padding:0; --promo-column:max(0px, calc((100% - 1260px) / 2)); }

/* The window the track moves behind. Nothing outside it is visible or reachable — the inactive
   slides are inert as well, so this is belt and braces for the picture rather than the only guard.

   It also clips vertically, which is what lets the band be the height of the slide that is showing
   rather than of the tallest one. storefront-promo.js sets the height; the transition is here so
   the band and the slide move together, on the same curve and over the same half second. Without a
   script there is no inline height, the window is as tall as the track, and the carousel is the
   first slide sitting still — which is the state this file has always degraded to. */
.promo-viewport{ overflow:hidden; transition:height .5s cubic-bezier(.2,.8,.2,1); }
@media (prefers-reduced-motion: reduce){ .promo-viewport{ transition:none; } }

/* Every slide is the height of the tallest, which is the flex default and is why there is no rule
   here any more.

   There was one: it set align-items:flex-start once the script was running, so each slide kept its
   own height and the band resized to whatever was showing. That is defensible when every slide is
   the same kind of thing. This carousel mixes a banner — as tall as the screen allows — with product
   slides a third of that, and it advances by itself every six seconds, so the band was shrinking and
   growing by a couple of hundred pixels on a timer, taking the whole page below it along. It read as
   the layout breaking rather than as a carousel moving.

   The cost is empty surface above and below a product slide, which is the reason it was changed in
   the first place, and it is paid for rather than accepted: the product photograph is stretched to
   the band, so the room the banner asks for is room the other slides use. See .promo-figure in the
   min-width:921px block, and fitHeight in storefront-promo.js for the stacked case, where the
   banner is the short slide and the band goes back to following whichever one is showing. */

.promo-track{
  display:flex; flex-wrap:nowrap;
  transition:transform .5s cubic-bezier(.2,.8,.2,1);
}
/* A carousel that slides is the clearest case of motion that is decoration: the slide still
   changes, it just changes at once. */
@media (prefers-reduced-motion: reduce){ .promo-track{ transition:none; } }

.promo-slide{
  flex:0 0 100%; min-width:100%;
  display:grid; grid-template-columns:1fr 1fr; align-items:center; gap:44px;
  padding:56px 0;
}
.promo-copy{ display:flex; flex-direction:column; align-items:flex-start; gap:16px; }
.promo-headline{ font-size:var(--type-2xl); line-height:1.12; color:var(--text-heading); margin:0; }
.promo-subtext{ font-size:var(--type-md); color:var(--text-secondary); margin:0; max-width:46ch; }
.promo-price{
  display:flex; align-items:center; gap:12px; margin:0;
  font-size:var(--type-lg); font-weight:600; color:var(--text-primary);
}
/* A reduced price on a slide: was, now, how much off, in that order, exactly as the card in
   storefront.css and the product page in product.css state it. The carousel is the first thing on
   the page and the loudest place the shop advertises, so this is the one line that must not be a
   fourth idea of what a sale looks like.

   Wrapping, unlike the row above: the three parts plus a sold-out pill do not fit beside a
   headline on a phone, and the price breaking to two lines is better than the slide scrolling. */
.promo-price.is-on-sale{ align-items:baseline; flex-wrap:wrap; gap:8px 12px; }
.promo-price.is-on-sale .price-now{ color:var(--sale-ink); }
.promo-price .price-was{ font-size:var(--type-md); font-weight:600; color:var(--text-muted); }
.promo-price .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:3px 10px;
}
/* Said in words beside the price rather than by grey-ing the button, because a shopper may still
   want to open a sold-out product to see when it is back. */
.promo-oos{
  font-size:var(--type-xs); font-weight:700; letter-spacing:0.1em; text-transform:uppercase;
  color:var(--text-on-inverse); background:var(--surface-inverse);
  padding:4px 10px; border-radius:999px;
}
.promo-cta{ margin-top:4px; }

/* Positioned because the Sale flag is pinned inside it, the same way .prod-media carries the one
   on a catalogue card. */
.promo-figure{ position:relative; border-radius:var(--radius-lg); overflow:hidden; box-shadow:var(--shadow); }
/* A fixed aspect rather than the image's own, so slides of different photographs are the same
   height and the page does not jump as the carousel moves between them.

   The 3 / 2 applies while the slides are stacked. Side by side the figure is stretched to the band
   and its height is therefore a real number, which is what aspect-ratio steps aside for; the crop
   is then whatever the band is, and object-fit keeps it centred either way. */
.promo-figure img{ display:block; width:100%; height:100%; aspect-ratio:3 / 2; object-fit:cover; }

/* ---- Banner slides ----
   Artwork with its words already in it, so there is no copy column to sit beside.

   NOTHING IS CROPPED, AND NOTHING NEEDS SCROLLING

   A product slide crops its photograph to 3 / 2 without losing anything: the picture is a rug, and
   a rug is still a rug an inch narrower. A banner is a composition — a logo, a headline, a button
   drawn where the designer put them — and cropping it cuts words in half. So the image keeps its
   own shape, whatever that is: width and height are both auto, and the two maximums below decide
   how big it is allowed to get.

     max-width  the width of the page, so it never overflows sideways.
     max-height the screen minus the chrome above it, so the whole banner is in view on arrival.
                The second is the one that matters: a 2:1 banner across a wide monitor is 900px
                tall, and without this the shop opens on the top two thirds of a picture with the
                rest below the fold — which is exactly the thing a banner cannot survive.

   svh, not vh: on a phone vh is the viewport with the browser's toolbars retracted, so a banner
   sized to it is taller than what is actually on screen when the page loads. The vh line before it
   is the fallback for anything that does not know svh.

   EDGE TO EDGE, NOT INSIDE THE COLUMN

   A product slide lives in the 1260px column because it is a headline and a paragraph, and text
   set across a whole monitor is unreadable. A banner is the opposite: it is the shop's front
   window, and a window with 200px of empty surface either side of it looks like a picture somebody
   forgot to finish. So the track runs the full width of the page and the *product* slide puts the
   column back with its own padding — see .promo-slide below. Breaking the banner out of the wrap
   instead would not work: .promo-viewport clips, which is what keeps the next slide off screen.

   HOW MUCH HEIGHT IT MAY TAKE

   Everything above it — announcement bar, header, nav — plus the dots below it, so the banner ends
   where the fold is and the first thing under it is a hint that there is more. The dots are only
   drawn when there are two or more slides, hence the two figures: a shop whose carousel is one
   banner should not reserve room for controls that do not exist.

   Two classes on the selector below, so it beats the plain .promo-slide rules in the media queries
   further down whatever order they end up in — one of those sets side padding, and a banner that
   quietly gains 88px of it is back in the column this section exists to escape. */
.promo-slide.promo-slide-banner{
  display:flex; align-items:center; justify-content:center;
  padding:0;
  --promo-chrome:216px;
}
.promo[data-promo-count="1"] .promo-slide-banner{ --promo-chrome:144px; }

.promo-banner{ display:flex; justify-content:center; width:100%; }
.promo-banner a{ display:block; max-width:100%; }
.promo-banner img{
  display:block; width:100%; height:auto;
  /* The constants are the guess; --promo-room is the measurement storefront-promo.js takes once it
     can see the header, and it wins where there is a script to take it. */
  max-height:calc(100vh - var(--promo-chrome));
  max-height:var(--promo-room, calc(100svh - var(--promo-chrome)));
  /* Only bites when the height limit does. The box is then wider than the picture's own shape
     allows, and contain keeps the shape and centres it rather than stretching it to fill. Without
     this the one case that matters — a short window — is the one that distorts the artwork. */
  object-fit:contain;
  transition:transform .4s cubic-bezier(.2,.8,.2,1);
}
/* The only affordance a banner has, since it carries no button of its own. */
.promo-banner a:hover img{ transform:scale(1.008); }
@media (prefers-reduced-motion: reduce){ .promo-banner img{ transition:none; } }

@media (max-width: 920px){
  /* Less above it on a narrow screen, and the picture is the reason for the page. */
  .promo-slide-banner{ --promo-chrome:176px; }
  .promo[data-promo-count="1"] .promo-slide-banner{ --promo-chrome:112px; }
}

/* ---- Controls ----
   Hidden until storefront-promo.js says the carousel can actually move. Before that they would be
   buttons that do nothing, and a dead control is worse than an absent one. */
.promo-controls{ display:none; }
/* The room above the row is not decoration. A dot is 9px of paint inside a 44px target, so the half
   of that target above it reached 17px up into the artwork — an invisible button over the bottom of
   the banner, taking the click that was meant for the picture. The dots also sat flush against its
   bottom edge, reading as part of the composition rather than as the shop's own controls.

   Paid for automatically where there is a script: storefront-promo.js measures this row's offsetHeight
   and takes it off the height the banner may have, so the padding cannot push the picture past the
   fold. Without a script the row is display:none and costs nothing. */
.promo[data-promo-ready] .promo-controls{
  display:flex; align-items:center; justify-content:center; gap:18px;
  padding:var(--promo-control-pad) 0;
}

.promo-nav{
  flex:0 0 auto; width:44px; height:44px;
  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);
}
.promo-nav:hover{ background:var(--surface-hover); }

.promo-dots{ display:flex; align-items:center; gap:10px; }
.promo-dot{
  position:relative; width:9px; height:9px; padding:0; border-radius:50%;
  border:1px solid var(--border-accent); background:transparent;
  transition:background .2s ease, transform .2s ease;
}
.promo-dot[aria-current="true"]{ background:var(--surface-accent); transform:scale(1.35); }
/* A dot is 9px of paint; this is the 44px of target around it. */
.promo-dot::after{
  content:''; position:absolute; top:50%; left:50%;
  width:44px; height:44px; transform:translate(-50%,-50%); border-radius:50%;
}

/* On a wide screen the arrows leave the row and pin to the sides of the slide, which is where a
   shopper reaches for them, and the dots close up underneath on their own.

   The slide gets a gutter the width of an arrow plus a margin, and the arrows sit in it. Pinning
   them to the wrap's edge without that would put 12px of button over the start of the headline:
   an absolutely positioned child is placed against the wrap's padding box, so left:0 is outside
   the 32px of padding the copy actually begins at, and 44px of arrow reaches past it. */
@media(min-width:921px){
  /* The column's own 32px gutter, plus the 56px the arrow needs inside it. Together these put the
     copy exactly where the wrap used to put it, which is the point: the band went full width, the
     words did not move. */
  .promo-slide{
    padding-left:calc(var(--promo-column) + 88px);
    padding-right:calc(var(--promo-column) + 88px);
  }
  .promo-nav{
    /* Centred on the slide rather than on the wrap: the control row below is 44px of button over
       a padding, so the slide's own middle is half that height above the wrap's.

       The padding term is why it is written out rather than left as the 36px it used to be. The row
       gained the same padding above it, to keep the dots off the artwork, and an arrow positioned
       against a fixed half-height would have drifted down by half of whatever was added. */
    position:absolute; transform:translateY(-50%); z-index:2;
    top:calc(50% - 22px - var(--promo-control-pad));
  }
  /* Beside the slide's words, not out at the edge of the monitor. */
  .promo-prev{ left:var(--promo-column); }
  .promo-next{ right:var(--promo-column); }

  /* The band is as tall as the banner, and a product slide's words and picture come to a third of
     that however wide the monitor is — so the taller the screen, the more of the slide was surface
     with nothing on it. Measured at 1920x910 it was 336px of the 683px band.

     The picture takes that room rather than the slide floating in the middle of it. The words stay
     centred beside it, which is why this is on the figure and not on the slide.

     Only above the stacking breakpoint. Below it the banner is the shortest slide, not the tallest,
     and there is no spare height to give away — see fitHeight in storefront-promo.js. */
  .promo-figure{ align-self:stretch; }
}

@media(max-width:920px){
  /* Each slide keeps its own height here, and the band follows the one showing — the opposite of the
     rule above this breakpoint, and for the reason given there: stacked, the banner is the shortest
     slide rather than the tallest, and holding the band at the maximum left it in about 350px of
     empty surface with nothing that could be stretched to fill it.

     It is also what lets fitHeight measure a single slide at all. Flex stretches every item to the
     tallest by default, so without this the slide showing reports the band's height rather than its
     own, and the measurement can only ever come back with the maximum. */
  .promo[data-promo-ready] .promo-track{ align-items:flex-start; }
  .promo-slide{
    grid-template-columns:1fr; gap:24px; padding:36px 32px 28px;
  }
  /* Picture first on a phone, which is what a shopper scrolling a shop front expects, and what the
     hero above the fold used to give them. */
  .promo-figure{ order:-1; }
  .promo-headline{ font-size:var(--type-xl); }
}

/* ============ Hero ============
   Two halves: the shop's opening words, and the photograph beside them.

   660px was a flat floor, and a 1366×768 laptop has about 640px of page once the browser's own
   chrome is counted — so the band ran past the fold and the photograph had no visible end. The
   floor is capped at the room there is instead, which keeps the whole composition rather than
   cropping any of it. min() of the two, so a tall monitor still gets exactly the 660px it had.

   --hero-chrome is the header plus a margin, so the band stops short of the fold rather than
   precisely on it, which is what makes a page look as though it has finished.

   svh with vh behind it, the pair the promotion banner and the drawer already use: vh is the
   viewport with a phone's toolbars retracted, which is taller than the screen on load.

   IT NEEDS A SURFACE OF ITS OWN

   The hero sits between two plain sections and had no background, so its left half was the same
   alabaster as the page above and below it. The photograph looked like a picture that happened to be
   there and the copy like the start of another section, which is the opposite of what a hero is for.

   Sunken is the role for exactly this — "tinted bands, wells: the page, pressed in" — and it is the
   step the shop already uses to mark a band off, so the hero is now distinct without a colour being
   invented for it. The hairline above and below is what gives it edges; the tint alone leaves the
   boundary to be inferred from where the photograph starts. */
.hero{
  --hero-chrome:120px;
  background:var(--surface-sunken);
  border-top:1px solid var(--border);
  border-bottom:1px solid var(--border);
  display:grid; grid-template-columns:1fr 1fr; position:relative;
  min-height:660px;
  min-height:min(660px, calc(100vh - var(--hero-chrome)));
  min-height:min(660px, calc(100svh - var(--hero-chrome)));
}
@media(max-width:920px){ .hero{ grid-template-columns:1fr; min-height:auto; } }
/* The vertical padding gives way before the words do: 60px top and bottom is 120px of a band that
   may only be 420px tall. The horizontal padding is untouched, because it sets the measure. */
.hero-left{
  display:flex; flex-direction:column; justify-content:center; gap:24px;
  padding:clamp(36px, 6vh, 60px) 60px clamp(36px, 6vh, 60px) 32px;
  max-width:560px; margin:0 auto; position:relative; z-index:2;
}
@media(max-width:920px){ .hero-left{ padding:52px 24px; max-width:none; } }
.pill{
  display:inline-flex; align-self:flex-start; align-items:center; gap:8px; background:var(--surface-inverse); color:var(--text-accent-on-inverse-bright);
  padding:7px 16px; border-radius:999px; font-size:var(--type-xs); letter-spacing:0.18em; font-weight:600; text-transform:uppercase;
}
.split-h1{ font-size:var(--type-hero); line-height:1.1; letter-spacing:-0.01em; }
/* One step down, not two. The headline used to drop to 30px and then to 28px, which is the kind of
   distinction the scale exists to stop making: the second breakpoint was buying two pixels. */
@media(max-width:640px){ .split-h1{ font-size:var(--type-xl); } }
/*
   The one effect kept on this page. It runs once on arrival, on the brand's own words, and then
   the page is still — which is the difference between a flourish and a demo reel.

   The per-word delay is set inline by storefront-fx, and the shared reduced-motion rule only
   flattens durations, not delays. Left alone, someone who has asked for less motion would still
   watch the headline arrive a word at a time over two seconds. So it is switched off outright.
*/
.split-h1 .word{
  display:inline-block; opacity:0; transform:translateY(28px) rotateX(35deg);
  animation:splitReveal .8s cubic-bezier(.2,.8,.2,1) forwards; transform-origin:bottom;
}
@keyframes splitReveal{ to{ opacity:1; transform:translateY(0) rotateX(0deg); } }
@media (prefers-reduced-motion: reduce){
  .split-h1 .word{ animation:none; animation-delay:0s !important; opacity:1; transform:none; }
}
.hero-left p.sub{ font-size:var(--type-md); color:var(--text-secondary); max-width:460px; margin:0; }
.hero-ctas{ display:flex; flex-wrap:wrap; gap:14px; margin-top:6px; }
/* Its own floor, capped the same way. A 440px floor inside a band allowed down to 420px is the half
   that wins, and the cap above would have had no effect at all. */
.hero-right{
  position:relative; overflow:hidden; background:var(--surface-inverse);
  min-height:440px;
  min-height:min(440px, calc(100vh - var(--hero-chrome)));
  min-height:min(440px, calc(100svh - var(--hero-chrome)));
}
@media(max-width:920px){ .hero-right{ min-height:340px; max-height:360px; } }
/* Called tilt-frame while it leaned with the pointer. It no longer leans, so it no longer needs a
   perspective to lean into, a compositor layer to lean on, or that name. */
.hero-frame{ position:absolute; inset:0; }
.hero-frame img{ width:100%; height:100%; object-fit:cover; }
.hero-right::after{
  content:''; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(to bottom, transparent 45%, var(--scrim-photo) 100%);
}
/* The two floating badges over the hero photograph are withdrawn, and their rules go with them.
   Dead selectors are how a stylesheet comes to describe a page that no longer exists. */

/* ============ Trust bar ============ */
.trust{ background:var(--surface-sunken); border-top:1px solid var(--border); border-bottom:1px solid var(--border); padding:36px 0; }
.trust-grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:28px; }
@media(max-width:820px){ .trust-grid{ grid-template-columns:1fr 1fr; } }
@media(max-width:480px){ .trust-grid{ grid-template-columns:1fr; } }
.trust-item{ display:flex; gap:14px; align-items:flex-start; }
/* A drawn mark rather than words, so this is one of the jobs gold keeps. */
.trust-icon{ line-height:1; color:var(--surface-accent); flex-shrink:0; }
.trust-item h4{ font-size:var(--type-sm); font-family:var(--sans); font-weight:700; color:var(--text-heading); margin-bottom:3px; }
.trust-item p{ font-size:var(--type-sm); color:var(--text-subtle); margin:0; }

/* ============ Section heading ============
   96px above and below is 192px of a band, a third of a laptop window spent on nothing. It is the
   right figure on a tall monitor, so it is kept as the ceiling and allowed to come in on a short
   screen. 10vh reaches 96px at a 960px viewport, which covers every desktop the shop will meet.

   The phone keeps its flat 64px rather than inheriting the scale: a tall phone would take 84px from
   its 844px, which is more room between sections than the shop gives them today. */
.section{ padding:clamp(64px, 10vh, 96px) 0; }
@media(max-width:640px){ .section{ padding:64px 0; } }
.section-tinted{ background:var(--surface-sunken); }
.section-head{ text-align:center; max-width:600px; margin:0 auto 52px; }
.section-head h2{ font-size:var(--type-2xl); margin-top:10px; }
@media(max-width:640px){ .section-head h2{ font-size:var(--type-xl); } }
.section-head p{ color:var(--text-subtle); font-size:var(--type-md); margin-top:12px; }
/* .section-head.on-dark went with the anatomy section. It was the only heading on this page that
   sat on a dark ground, and nothing renders it now. */

/* ============ The four shelves ============
   A category, and one product out of it. Replaces the tiles that were captions burned into a
   photograph: the name and the price are text on the card now, so they wrap, they scale with the
   reader's font size, and they are readable without the picture having loaded.

   A card needs a largest size as well as a smallest. There are between one and four cards — see
   HomeShelfService.SHELVES — and the row used to divide itself between however many there were, so
   the two the shop launched with took 586px each. On a square crop that is a 586px-tall photograph:
   two pictures the height of a laptop screen, with the heading off the top and the browse links off
   the bottom of it.

   WHY THIS IS A FLEX ROW AND NOT A GRID

   Two attempts at capping it with `grid-template-columns` failed, and both failures are worth
   recording because each looked right:

     repeat(auto-fit, minmax(220px, 320px))   auto-fit counts how many tracks fit using the *max*
                                              when the max is a definite length, so it asked for
                                              320px a track in a 1196px column and answered three.
                                              Four categories would have shown three and wrapped one.

     repeat(auto-fit, minmax(220px, 1fr))     counts correctly, but auto-fit only collapses the
     + a max-width on the grid                *empty* tracks; the ones holding a card still take 1fr
                                              of the whole row. So the cap had to come from the grid's
                                              own width, which meant knowing the number of cards, which
                                              meant an inline custom property — and the day that
                                              property is missing for any reason the cards are 586px
                                              again. It is a layout that depends on being told
                                              something CSS can see for itself.

   A flex row needs to be told nothing. Each card starts at 220px, grows into whatever the row has
   spare, and stops at --shelf-card; the line centres what it could not give away. Four cards come out
   at 281px because that is what four 220px cards grow to in this column, which is exactly where the
   grid put them. Two stop at the cap instead of splitting the row. Nothing counts anything.

   NOTHING HERE IS A FIXED SIZE

   --shelf-card is the ceiling, and it is read off the viewport in the axis that was actually running
   out of room: the height. The photograph is square, so a card 320px across is 320px tall before any
   of its text, and on a short laptop window the heading, two cards and the browse links under them
   came to more than one screen. Subtracting the section's own chrome from the viewport is what keeps
   the whole of it in view; on anything with normal height the 320px ceiling is what applies and this
   term never bites.

   svh with vh behind it, the pair used by the banner and the hero above. */
.shelf-grid{
  --shelf-gap:24px;
  --shelf-card:clamp(170px, calc(100vh - 360px), 320px);
  --shelf-card:clamp(170px, calc(100svh - 360px), 320px);
  display:flex; flex-wrap:wrap; gap:var(--shelf-gap); justify-content:center;
}
@media(max-width:920px){
  .shelf-grid{
    --shelf-gap:18px;
    --shelf-card:clamp(150px, calc(100vh - 330px), 300px);
    --shelf-card:clamp(150px, calc(100svh - 330px), 300px);
  }
}
@media(max-width:560px){ .shelf-grid{ --shelf-gap:12px; } }

/* Deliberately the same treatment as .prod-card in storefront.css. The two sit within a screen of
   each other on this page, and a second, nearly-identical card style is how they drift apart.

   The last line is the sizing .shelf-grid is written around: start at 220px, grow into whatever the
   row has spare, stop at the ceiling. */
.shelf-card{
  display:flex; flex-direction:column;
  background:var(--surface-raised); border:1px solid var(--border);
  border-radius:var(--radius-lg); overflow:hidden; transition:box-shadow .3s ease;
  flex:1 1 220px; max-width:var(--shelf-card);
}
@media(max-width:920px){ .shelf-card{ flex-basis:180px; } }
/* Two across, down to the narrowest phone. One per row turns four cards into four screens of
   scrolling, and the point of the section is to be able to see the choice at once.

   Half the row each, so two fit however narrow it gets — a 220px basis would put them on separate
   lines on a small phone. The ceiling comes off with it: a phone scrolls, nothing is competing for
   the height there, and this is the layout the shop already gets right. */
@media(max-width:560px){
  .shelf-card{ flex-basis:calc(50% - (var(--shelf-gap) / 2)); max-width:none; }
}
.shelf-card:hover{ box-shadow:var(--shadow); }

/* Square rather than the catalogue's 4:5. A product photograph is the whole of a product card; here
   it shares the card with three lines of text, and a portrait crop pushed the browse link off the
   bottom of a phone screen. */
.shelf-photo{ position:relative; aspect-ratio:1/1; background:var(--surface-sunken); overflow:hidden; }
.shelf-photo a{ display:block; height:100%; }
.shelf-photo img{ width:100%; height:100%; object-fit:cover; display:block; transition:transform .5s ease; }
.shelf-card:hover .shelf-photo img{ transform:scale(1.05); }

.shelf-body{ display:flex; flex-direction:column; gap:6px; padding:18px 18px 20px; flex:1; }
@media(max-width:560px){ .shelf-body{ padding:14px; } }
.shelf-name{ font-family:var(--serif); font-size:var(--type-lg); color:var(--text-heading); line-height:1.25; }
@media(max-width:560px){ .shelf-name{ font-size:var(--type-md); } }
/* Grows, so the browse link sits on the same line across all four cards however many lines the
   category and product names take between them. */
.shelf-pick{ margin:0; display:flex; flex-direction:column; gap:2px; flex:1; }
.shelf-product{ font-size:var(--type-sm); color:var(--text-secondary); }
.shelf-product:hover{ text-decoration:underline; text-underline-offset:3px; }
.shelf-price{ font-size:var(--type-sm); font-weight:700; color:var(--text-heading); }
/* The same three parts again, on a card about a third the width of a slide. They wrap here rather
   than shrink: four of these sit in a row on a laptop and two on a phone, and a percentage pill
   squeezed onto the same line as two prices is unreadable at this size. */
.shelf-price.is-on-sale{ display:flex; align-items:baseline; flex-wrap:wrap; gap:2px 8px; }
.shelf-price.is-on-sale .price-now{ color:var(--sale-ink); }
.shelf-price .price-was{ font-weight:600; color:var(--text-muted); }
.shelf-price .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:1px 7px;
}
.shelf-empty{ font-size:var(--type-sm); color:var(--text-subtle); }
/* Underlined at rest rather than on hover. It is the one control on the card that is not a
   photograph or a product name, and colour alone is not allowed to be what says so. */
.shelf-browse{
  font-size:var(--type-sm); font-weight:600; color:var(--text-heading);
  text-decoration:underline; text-underline-offset:3px; padding-top:6px;
}
.shelf-browse::after{ content:' \2192'; }

/* ============ Tech anatomy ============
   Gone. The section was three markers over a stock photograph, captioned with claims about a rug
   the shop never named, and the copy behind them was not editable from the console. The split hero
   stands in its slot now. Its rules — .anatomy, .anatomy-stage, .anatomy-hint, .hotspot and
   .hotspot-card, here and in the two media queries below — went with it. */

/* ============ Quiz banner ============
   The dark banner between the anatomy section and the product grid — three captioned steps and a
   button into the Fit Finder. Withdrawn with the quiz, and its rules with it. */

/*
  The product grid, the card, the filter pills and the empty state used to live here. They moved
  to storefront.css when the category and search pages started rendering the same grid: three
  pages sharing one set of rules is not "home page styling" any more, and a copy per page is how
  two grids end up looking subtly different.
*/

/* ============ Collections ============
   Links to the category pages, above the grid.

   This was a centred flex row that wrapped. Centring is what made it ragged: every line starts at
   a different place, because where a line begins depends on how many names happened to fit on it
   and how long they were. With seven collections it was untidy; the owner adds them from the admin,
   so at twelve it was a band of uneven text.

   A grid instead of a scrolling row or a capped list, because this is the top of the page and the
   question it answers is "what does this shop sell" — a shopper scanning that wants to see all of
   it. A scroller hides options behind a gesture, and a cap hides them behind a click, at the exact
   moment the shop is trying to show them. The header already has one sideways-scrolling strip; a
   second one directly beneath it would teach nothing and cost the same discovery twice.

   The grid cannot be ragged, because a column is a column: an incomplete last row lines up under
   the row above it, and a name long enough to take two lines makes its whole row taller rather
   than only itself. It also needs no script, no fade and no affordance to explain. */
.filter-bar{
  display:grid; grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));
  gap:10px; margin:0 auto 14px; max-width:1080px;
}
.filter-bar .filter-pill{ justify-content:center; text-align:center; }
/* Below this a 190px track leaves one collection per line, which turns eight of them into a
   column the length of the screen. Two abreast instead. */
@media(max-width:560px){ .filter-bar{ grid-template-columns:1fr 1fr; } }
.filter-summary{
  display:flex; align-items:center; justify-content:center; gap:12px; flex-wrap:wrap;
  margin:0 0 28px; font-size:var(--type-sm); color:var(--text-subtle);
}
.filter-summary:empty{ margin:0; }

/* ============ Catalog paging ============ */
.catalog-more{ display:flex; flex-direction:column; align-items:center; gap:8px; margin:32px 0 0; }
.catalog-more:empty{ margin:0; }
.catalog-more-count{ font-size:var(--type-sm); color:var(--text-muted); }
.load-more{
  border:1.5px solid var(--border-accent); background:var(--surface-raised); color:var(--text-heading); border-radius:999px;
  min-height:44px; padding:0 30px; font-size:var(--type-sm); font-weight:700; letter-spacing:.04em;
  text-transform:uppercase; transition:all .2s ease;
}
.load-more:hover{ background:var(--surface-selected); color:var(--text-on-selected); border-color:var(--surface-selected); }
.load-more[disabled]{ opacity:.55; cursor:default; }

/* ============ Riders of Equinova carousel ============
   Four complete photographs from a laptop down to an iPad, one on a phone, and the rest a swipe
   away. Whichever it is, a tile is never part-cut: the tile width is derived from the number
   showing, so the last one visible ends exactly at the edge of the track.

   The track is a plain scroller with snapping rather than a translated strip. That buys the finger
   swipe, the trackpad, the wheel and the browser's own scroll-the-focused-tile-into-view for
   nothing, and it means tabbing through the tiles moves the carousel without a line of script. */

.ugc-carousel{ position:relative; }

.ugc-track{
  display:flex; gap:16px;
  overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  /* Room for the ring on a focused tile, which would otherwise be clipped by the scroller. */
  padding:4px;  margin:-4px;
  scrollbar-width:none;
}
.ugc-track::-webkit-scrollbar{ display:none; }

/* An error or an empty message is written into the track, and neither is a row of tiles. */
.ugc-track > .no-results, .ugc-track > .span-grid{ flex:1 1 auto; scroll-snap-align:none; }

.ugc-item{
  /* 0 0 auto, so three photographs are three tiles of the usual size rather than three stretched
     to fill the row. The width is the four-up share of the track minus the three gaps between. */
  flex:0 0 calc((100% - 48px) / 4);
  scroll-snap-align:start;
  position:relative; aspect-ratio:1/1; border-radius:var(--radius-md); overflow:hidden; cursor:pointer;
  padding:0; border:0; background:none;
}
@media(max-width:700px){
  /* One at a time, and no gap to subtract because there is only ever one across. */
  .ugc-item{ flex:0 0 100%; }
}
.ugc-item img{ width:100%; height:100%; object-fit:cover; transition:transform .45s ease; }
.ugc-item:hover img{ transform:scale(1.08); }
@media (prefers-reduced-motion: reduce){ .ugc-item img{ transition:none; } }
.ugc-overlay{
  position:absolute; inset:0; background:linear-gradient(to top, var(--scrim-photo-soft), transparent 60%);
  display:flex; align-items:flex-end; justify-content:center; padding-bottom:18px; opacity:0; transition:opacity .3s ease;
}
.ugc-item:hover .ugc-overlay, .ugc-item:focus-visible .ugc-overlay{ opacity:1; }

/* ---- Controls ----
   Absent until the script finds there is more to see than fits, so three photographs on a wide
   screen get no arrows at all rather than two that refuse to do anything. */
.ugc-controls{ display:none; }
.ugc-carousel.ugc-scrollable .ugc-controls{ display:block; }
.ugc-nav{
  position:absolute; top:50%; transform:translateY(-50%); z-index:2;
  width:44px; height:44px; 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);
}
.ugc-nav:hover{ background:var(--surface-hover); }
/* At an end rather than gone, so the row beside them does not shift as you reach the last photo. */
.ugc-nav[disabled]{ opacity:.4; cursor:default; }
/*
  Overlapping the edge of the outermost photograph rather than sitting clear of it. There is 32px
  of gutter beside the track and an arrow is 44px, so clear of it means partly off the side of the
  screen at any window narrower than about 1360px. Over the photograph is what a photo carousel
  does anyway, it costs a corner of one image, and it is the same at every width — which the
  alternative, two rules and a breakpoint that only helps the widest monitors, is not.
*/
.ugc-prev{ left:-8px; }
.ugc-next{ right:-8px; }
.ugc-overlay span{
  background:var(--surface-glass-light); backdrop-filter:blur(8px); color:var(--text-heading); font-size:var(--type-sm); font-weight:700;
  padding:9px 18px; border-radius:999px; border:1px solid var(--border-accent-on-inverse-strong); text-align:center;
}

/*
  The quiz modal's styling was here before it moved to storefront.css, beside the modal shell it
  filled. Both have since been withdrawn along with the quiz itself.
*/

/* ============ PDP quick view ============ */
.pdp-panel{ width:820px; max-width:94vw; max-height:90vh; max-height:90dvh; }
.pdp-body{ overflow-y:auto; }
.pdp-grid{ display:grid; grid-template-columns:1fr 1fr; }
@media(max-width:760px){ .pdp-grid{ grid-template-columns:1fr; } }
.pdp-image-col{ background:var(--surface-sunken); padding:34px; display:flex; align-items:center; justify-content:center; }
.pdp-image-col img{ border-radius:var(--radius-md); max-height:420px; width:100%; object-fit:cover; box-shadow:var(--shadow); }
.pdp-image-col .skeleton{ width:100%; aspect-ratio:4/5; max-height:420px; border-radius:var(--radius-md); }

/* ---- Every photograph of one product, in the quick view --------------------------------------
   The panel showed the primary photograph and offered the rest only by sending the shopper to the
   product page. It shows all of them now, by button, by arrow key and by swipe.

   The column becomes a stack — photograph over controls — where it was a single centred image, so
   the flex direction and the stage's own ratio are what keep the panel from changing height as the
   shots go by. The controls sit under the photograph rather than over it: there is room here, and
   an arrow floated over a 420-pixel photograph covers the thing it is there to show. */
.pdp-image-col[data-gallery]{ flex-direction:column; gap:14px; }
.pdp-stage{ width:100%; aspect-ratio:4/5; max-height:420px; position:relative; }
.pdp-shot{ display:none; height:100%; }
.pdp-shot.is-shown{ display:block; }
.pdp-no-shot{ font-size:var(--type-sm); color:var(--text-muted); text-align:center; }

.pdp-gallery{ display:flex; align-items:center; justify-content:center; gap:14px; }
.pdp-gallery-nav{
  flex:0 0 auto; width:40px; height:40px;
  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);
}
.pdp-gallery-nav:hover{ background:var(--surface-hover); }
.pdp-gallery-count{ font-size:var(--type-sm); font-weight:700; color:var(--text-heading); }
.pdp-info-col{ padding:32px 34px; overflow-y:auto; }
.pdp-info-col h3{ font-size:var(--type-lg); }
.pdp-price{ font-size:var(--type-lg); font-weight:700; color:var(--text-heading); margin:10px 0 4px; }
/* The quick view's reduced price, laid out exactly as the product page lays its own out. The two
   panels show the same product and they should not look like two different shops. */
.pdp-price.is-on-sale{ display:flex; align-items:baseline; flex-wrap:wrap; gap:6px 12px; }
.pdp-price .price-now{ color:var(--sale-ink); }
.pdp-price .price-was{ font-size:var(--type-md); font-weight:600; color:var(--text-muted); }
.pdp-price .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:3px 10px;
}
.pdp-oos{ color:var(--danger); font-weight:700; margin-top:8px; }
.pdp-label{ font-size:var(--type-sm); text-transform:uppercase; letter-spacing:0.08em; color:var(--text-heading); font-family:var(--sans); font-weight:700; margin:20px 0 10px; }
.pdp-swatches{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.size-pills{ display:flex; gap:8px; flex-wrap:wrap; }
.size-pill{
  border:1.5px solid var(--border); border-radius:999px; padding:9px 16px; font-size:var(--type-sm); font-weight:600;
  color:var(--text-heading); transition:all .2s ease; background:var(--surface-raised);
}
.size-pill:hover{ border-color:var(--border-accent); }
.size-pill.active{ background:var(--surface-selected); color:var(--text-on-selected); border-color:var(--surface-selected); }
/* What the shop knows about how this item sizes. Only rendered when somebody has typed it in. */
.pdp-sizing-note{ margin:14px 0 4px; padding:11px 13px; border-radius:10px; background:var(--accent-wash); border-left:3px solid var(--surface-accent); font-size:var(--type-sm); line-height:1.55; display:flex; flex-direction:column; gap:3px; }
.pdp-sizing-note strong{ font-size:var(--type-sm); }
.pdp-sizing-note span{ color:var(--text-secondary); }
/* .pdp-sizing-link was the quick view's "Unsure? Launch Fit Quiz", withdrawn with the quiz. The
   sizing note above it stays: it is the product's own copy and does not need a quiz to be true. */
/* The way out of the quick view and into the product's own page. Underlined already, so the
   hover state can be the underline thickening rather than a colour that fails to be read. */
.pdp-full-link{ display:block; margin-top:12px; text-align:center; font-size:var(--type-sm); color:var(--text-heading); text-decoration:underline; text-underline-offset:3px; }
.pdp-full-link:hover{ text-decoration-thickness:2px; }
.pdp-feature-list{ list-style:none; padding:0; margin:16px 0 22px; display:flex; flex-direction:column; gap:9px; }
.pdp-feature-list li{ font-size:var(--type-sm); color:var(--text-secondary); display:flex; gap:8px; align-items:flex-start; }
.pdp-feature-list li::before{ content:'✓'; color:var(--surface-accent); font-weight:700; }
.pdp-add{ width:100%; justify-content:center; }

/* ============ "Shop the look" lightbox ============ */
.lightbox-panel{ width:940px; max-width:94vw; max-height:88vh; max-height:88dvh; position:relative; }
.lightbox-grid{ display:grid; grid-template-columns:1.2fr 1fr; height:100%; }
@media(max-width:760px){ .lightbox-grid{ grid-template-columns:1fr; grid-template-rows:220px 1fr; } }
.lightbox-photo{ background:var(--surface-inverse-deep); overflow:hidden; }
.lightbox-photo img{ width:100%; height:100%; object-fit:cover; }
.lightbox-info{ padding:32px; overflow-y:auto; }
.lightbox-handle{ display:block; font-size:var(--type-sm); color:var(--text-accent); font-weight:700; margin-bottom:6px; }
.lightbox-caption{ font-size:var(--type-sm); color:var(--text-secondary); margin:0 0 24px; }
.lightbox-sub{ font-size:var(--type-sm); color:var(--text-muted); }
.lightbox-cta{ width:100%; justify-content:center; }
.lightbox-close-btn{
  position:absolute; top:16px; right:16px; z-index:6; color:var(--text-on-inverse); background:var(--surface-scrim-soft);
  border-radius:50%; width:34px; height:34px; display:flex; align-items:center; justify-content:center;
}

/* ============ Mobile layer ============ */
/* The shared modal shell — padding, panel size, sticky head, fixed close — is in storefront.css,
   so the bag page gets it too. What is left here is the two panels that only exist on this page
   and have to override their own desktop widths. */
@media (max-width:768px){
  .pdp-panel, .lightbox-panel{
    width:92vw; max-width:92vw; max-height:85vh; max-height:85dvh;
    overflow-y:auto; margin:auto;
  }
  .pdp-info-col{ padding:16px; }
  .pdp-grid{ grid-template-columns:1fr; }
  .pdp-image-col{ padding:16px; }
  .pdp-image-col img{ max-height:240px; }
  .lightbox-grid{ grid-template-columns:1fr; grid-template-rows:220px 1fr; }
  /*
     What is left of this rule is the mobile treatment: the safe-area insets, so the button clears
     a notch in landscape, and a stronger tint, because on a phone it sits directly on the
     photograph with no head behind it.

     It used to also say position:fixed and z-index:100, added while the header was covering close
     buttons and the cause had not been found. Neither did anything. .modal-panel is transformed,
     which seals its descendants into its own stacking context, so that 100 was never visible to
     the header — and in the same breath it makes the panel the containing block for a fixed child,
     which Chrome then scrolls with the panel's content exactly as it would an absolute one.
     Measured at 390x450 and 390x380 with the rule and without it: the button ends up at the same
     pixel either way. The pinning was decoration on a workaround.
  */
  .modal.open .lightbox-close-btn{
    top:calc(20px + env(safe-area-inset-top)); right:calc(20px + env(safe-area-inset-right));
    background:var(--surface-scrim-strong); color:var(--text-accent-on-inverse-bright);
  }

}

@media (max-width:480px){
  .filter-pill{ padding:8px 14px; font-size:var(--type-sm); }
}

/* ============ Touch devices ============ */
@media (pointer:coarse){
  /* Quick-add has no hover to reveal it, so it stays put. */
  .quick-add{ bottom:14px; min-height:44px; }

  /* Likewise the "Shop This Look" caption, which was hover-only and so invisible on a phone. */
  .ugc-overlay{ opacity:1; }

  .filter-pill,
  .size-pill,
  .tagged-product button{ min-height:44px; }

  /* The product title opens the quick view, so it is a control, not just a heading. */
  .prod-info h4 a{ display:flex; align-items:center; min-height:44px; }
  .pdp-full-link{ min-height:44px; display:flex; align-items:center; justify-content:center; }

  .modal.open .modal-close,
  .modal.open .lightbox-close-btn,
  /* The quick view's photograph arrows, which a thumb reaches for before it tries a swipe. */
  .pdp-gallery-nav,
  .lightbox-close-btn{ width:44px; height:44px; }

  /* The card's colour dots keep a small footprint but grow their hit area to 44px. The pitch is
     widened to match so neighbouring targets cannot overlap.

     Scoped to the card's dots, which is the only thing it was ever measured against. Written as a
     bare .swatch it also caught the colour filter's chips, which the home page renders in the
     panel directly above this grid — so on every touch device the chips became 28px squares with
     their labels spilling out, and the row that is meant to wrap could not, because each chip was
     narrower than the word inside it. */
  .card-swatches{ gap:16px; }
  .card-swatch{ width:28px; height:28px; }
  .card-swatch::after{
    content:''; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    width:44px; height:44px; border-radius:50%;
  }
  .card-swatch-label{ margin-left:0; }
}

/* The offer strip that stood under the carousel is gone. Its one featured code is now part of the
   offers strip under the announcement bar, which runs on every page — see storefront.css. */
