/** Shopify CDN: Minification failed

Line 191:0 All "@import" rules must come first

**/
/* CO-ORD theme styles.
 *
 * Generated by scripts/build-theme-css.mjs from the Next.js build's CSS.
 * Do not edit this file by hand: change the source CSS and re-run, or the
 * theme and the site will drift and the whole point of the port is lost.
 *
 * Class names are the only thing that differs from the original. Each
 * component's module is prefixed (OutfitCard -> .oc-*, BuyBox -> .buy-*),
 * because CSS Modules hashes names at build time and Liquid cannot.
 */


/* ===== app/styles/tokens.css ===== */

/* Design tokens. The single source of truth.
   Nothing in this build may hardcode a colour, a duration or a spacing step. */

:root {
  /* ---------- Colour ----------
     Monochrome, but no longer uniformly white. Full-black bands are a
     surface in their own right, and the page alternates between them.

     Contrast is measured, not estimated, and every value that carries text
     names the surface it clears. Text is NEVER de-emphasised with opacity:
     ink at 0.65 on white composites to #a1a1a1, which is 2.58:1 and fails AA.
     Use a real colour from this list. */
  --black: #0b0b0b;
  --ink: #141414; /* 18.42:1 on paper */

  /* The only colour on this site, and it earns its place.
   *
     Every outfit carries a real compare-at price — twenty per cent across the
     range — and a reduction is a functional signal, not decoration. Monochrome
     cannot carry it: struck-through alone is easy to miss at a glance, and any
     purely tonal treatment is a convention the customer would have to learn.
     This is the live store's own #cc0000, darkened to clear 4.5:1 on paper.
     Nothing else in the system may use it. */
  --sale: #c00000; /* 6.48:1 on paper */
  --gray-600: #5c5c5c; /* 6.69:1 on paper, 6.25:1 on --gray-050, 5.87:1 on --gray-100 */
  --gray-400: #8a8a8a; /* 3.45:1 — borders, icons, disabled fills. NEVER text. */
  --gray-300: #d4d4d4;
  --gray-200: #e7e7e7;
  --gray-100: #f1f0ee;
  --gray-050: #f8f7f5;
  --paper: #ffffff;

  /* On black. Measured against --black, not guessed. */
  --on-black: #f5f4f2; /* 17.91:1 */
  --on-black-muted: #a8a8a8; /* 8.28:1 */
  /* Two borders, because they do different jobs. A divider is decoration and
     may be faint; the outline of a control carries meaning and has to clear
     3:1 under SC 1.4.11. On this black that lands at #5d5d5d, so anything
     interactive uses --field-border and not --rule. */
  --rule-on-black: #333333;
  --field-border-on-black: #5d5d5d; /* 3.03:1 on --black */

  /* Semantic pairs. A section flips these once and every component inside it
     follows, so no component carries a dark-mode rule of its own. */
  --surface: var(--paper);
  --surface-sunk: var(--gray-050);
  --on-surface: var(--ink);
  --on-surface-muted: var(--gray-600);
  --rule: var(--gray-200);
  --rule-strong: var(--ink);
  --field-border: var(--gray-400); /* 3.45:1 on paper */

  /* ---------- Type ----------
     Archivo throughout for UI and display, Spectral for the few places a
     serif earns its keep. The monospace is gone from the interface: it was
     set uppercase at 12px for navigation and labels, which is the single
     hardest thing to read on a phone, and it made a clothing shop look like
     a terminal. It survives only for genuinely numeric annotation.

     COS and Fear of God both sit on the same idea: very few sizes, wide
     spacing, low contrast in weight, and letterforms left alone. So the
     scale below is short, the tracking is gentle, and nothing is uppercase
     unless it is one or two words. */
  --font-display: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-serif: "Spectral", Georgia, "Times New Roman", serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas,
    "Liberation Mono", monospace;

  /* Six sizes. Anything that does not fit one of these does not need to
     exist. Display is fluid; everything from --text-body down is fixed,
     because body copy that grows with the viewport is body copy that is
     wrong at one end of the range. */
  --text-hero: clamp(2.5rem, 7vw, 5.5rem);
  --text-display: clamp(2rem, 4.5vw, 3.25rem);
  --text-h2: clamp(1.5rem, 2.6vw, 2rem);
  --text-h3: 1.125rem;
  --text-lead: clamp(1.0625rem, 1.4vw, 1.1875rem);
  --text-body: 1rem;
  --text-small: 0.9375rem; /* 15px — the smallest size that carries a sentence */
  --text-label: 0.8125rem; /* 13px — one or two words, never a sentence */

  --leading-hero: 0.96;
  --leading-display: 1.04;
  --leading-snug: 1.2;
  --leading-body: 1.6;

  /* Display type needs negative tracking; small type needs the opposite.
     The old build tracked 13px uppercase at 0.14em, which is what made the
     header illegible on a phone. */
  --track-hero: -0.035em;
  --track-display: -0.025em;
  --track-heading: -0.015em;
  --track-nav: 0.01em;
  --track-label: 0.08em;

  /* ---------- Space ----------
     4px base grid. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-20: 80px;
  --space-24: 96px;
  --space-32: 128px;

  --container: 1440px;
  --gutter: clamp(1.25rem, 4vw, 4rem);
  /* In rem, not ch: `ch` resolves against the element's own font size, so a
     ch measure on a wrapper collapses when the type inside it is large. */
  --measure: 36rem;

  /* ---------- Form ----------
     Sharp corners, hairline rules, no shadows except on floating UI. */
  --radius: 0;
  --hairline: 1px solid var(--rule);
  /* Raised from 44 to 48: the brief's floor is 44 and this clears it with
     room, which matters most for the size chips. */
  --tap-target: 48px;
  --shadow-float: 0 2px 60px rgb(11 11 11 / 18%);

  /* ---------- Motion ----------
     Only where it helps someone understand something. Nothing loops, nothing
     runs unprompted. */
  --dur-instant: 90ms;
  --dur-fast: 180ms;
  --dur-base: 300ms;
  --dur-slow: 560ms;
  /* Slow to leave, decisive to arrive. One curve for anything that should
     feel weighted rather than snappy. */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-quiet: cubic-bezier(0.2, 0, 0.1, 1);
  --reveal-rise: 20px;
  --hover-zoom: 1.03;
  --press-scale: 0.985;

  /* ---------- Layering ---------- */
  --z-sticky-bar: 80;
  --z-header: 100;
  --z-drawer: 200;
}

/* Inverted surface. Applied to a section or the whole document; every
   semantic token flips, so components need no dark-specific rules. */
[data-surface="dark"] {
  --surface: var(--black);
  --surface-sunk: #161616;
  --on-surface: var(--on-black);
  --on-surface-muted: var(--on-black-muted);
  --rule: var(--rule-on-black);
  --rule-strong: var(--on-black);
  --field-border: var(--field-border-on-black);

  color-scheme: dark;
}

/* A faintly warm sunk band, for separating one section from the next without
   a box, a border or a shadow. */
[data-surface="sunk"] {
  --surface: var(--gray-050);
  --surface-sunk: var(--paper);
}


/* ===== app/styles/global.css ===== */

@import "./tokens.css";

/* ---------- Reset ---------- */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  background: var(--surface);
  color: var(--on-surface);
  font-family: var(--font-display);
  font-size: var(--text-body);
  font-weight: 400;
  line-height: var(--leading-body);
  letter-spacing: 0;
  font-synthesis-weight: none;
  font-feature-settings: "kern" 1, "liga" 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100dvh;
  /* One column, and no horizontal scroll anywhere. Any element that needs to
     scroll sideways does it inside its own container, never on the page. */
  overflow-x: hidden;
}

img,
picture,
svg,
video {
  display: block;
  max-width: 100%;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}

button {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

p,
h1,
h2,
h3,
h4 {
  overflow-wrap: break-word;
}

ul[role="list"],
ol[role="list"] {
  list-style: none;
  padding: 0;
}

a {
  color: inherit;
}

table {
  border-collapse: collapse;
}

/* ---------- Type ----------
   Six sizes, one family, gentle tracking. Headings are sentence case at
   normal weight: the previous build set every heading in uppercase 700 with
   wide tracking, which is what made a clothing shop read as a spreadsheet. */

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 500;
  line-height: var(--leading-snug);
  letter-spacing: var(--track-heading);
  text-wrap: balance;
}

h1 {
  font-size: var(--text-display);
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
}

h2 {
  font-size: var(--text-h2);
}

h3 {
  font-size: var(--text-h3);
}

/* The one display size, for a hero and nothing else. */
.hero-type {
  font-size: var(--text-hero);
  font-weight: 500;
  line-height: var(--leading-hero);
  letter-spacing: var(--track-hero);
  text-wrap: balance;
}

/* Small caps label. One or two words only — never a sentence, which is the
   rule the old build broke on every page. */
.label {
  font-family: var(--font-display);
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  line-height: 1.2;
  color: var(--on-surface-muted);
}

/* Numerals that need to line up in a column: prices, sizes, measurements. */
.tabular {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.lead {
  font-size: var(--text-lead);
  line-height: 1.5;
  color: var(--on-surface-muted);
  max-width: var(--measure);
}

.serif {
  font-family: var(--font-serif);
}

/* ---------- Focus ----------
   Visible on everything focusable, on every surface. */

:focus:not(:focus-visible) {
  outline: none;
}

:focus-visible {
  outline: 2px solid var(--rule-strong);
  outline-offset: 3px;
}

::selection {
  background: var(--ink);
  color: var(--paper);
}

[data-surface="dark"] ::selection {
  background: var(--on-black);
  color: var(--black);
}

/* ---------- Layout ---------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.measure {
  max-width: var(--measure);
}

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

.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: calc(var(--z-drawer) + 1);
  display: flex;
  align-items: center;
  min-height: var(--tap-target);
  padding-inline: var(--space-4);
  background: var(--ink);
  color: var(--paper);
  font-size: var(--text-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  text-decoration: none;
  transform: translateY(-200%);
}

.skip-link:focus-visible {
  transform: translateY(0);
}

/* A section declares what it is and inherits its own surface, text colour and
   rule colour. Alternating white and black bands are the spine of the site. */
[data-surface] {
  background: var(--surface);
  color: var(--on-surface);
}

.band {
  padding-block: clamp(var(--space-16), 9vw, var(--space-32));
}

.bandTight {
  padding-block: clamp(var(--space-12), 6vw, var(--space-20));
}

/* ---------- Scroll reveal ----------
   Never applied above the fold: Chrome does not count a zero-opacity element
   as painted, so an entrance animation on the LCP element becomes the LCP
   element and costs about a second. */

@keyframes reveal {
  from {
    opacity: 0;
    transform: translate3d(0, var(--reveal-rise), 0);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

[data-reveal] {
  opacity: 0;
}

@supports (animation-timeline: view()) {
  [data-reveal] {
    animation: reveal linear both;
    animation-timeline: view();
    /* Entry-only. A range that ends in the cover phase can never complete for
       an element near the end of the document, because there is no scroll
       left to advance the timeline, and it stays stuck at opacity 0. */
    animation-range: entry 8% entry 58%;
  }
}

@supports not (animation-timeline: view()) {
  [data-reveal] {
    opacity: 1;
  }
}

/* ---------- Reduced motion ----------
   Global, and the only place !important is permitted. */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto !important;
  }

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

  [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
}


/* ===== components/media/Frame.module.css  ->  .frm-* ===== */

.frm-frame {
  display: block;
  position: relative;
  /* The grey seamless of the photography, so the reserved box is the same
     tone as the image that lands in it and there is no flash of white. */
  background: var(--gray-100);
  overflow: hidden;
}

.frm-img {
  display: block;
  width: 100%;
  height: auto;
}

/* The load fade. `data-fade` is only ever added by script, and only to images
   below the fold, so nothing is transparent when JavaScript is off and
   nothing above the fold is transparent at all. */
.frm-img[data-fade] {
  opacity: 0;
}

.frm-img[data-fade][data-loaded] {
  opacity: 1;
  transition: opacity var(--dur-base) var(--ease-quiet);
}


/* ===== components/ui/controls.module.css  ->  .c-* ===== */

/* The button set. Three of them, shared by every surface, so a control looks
   the same wherever it appears.
 *
 * Every one clears 48px, above the 44px rule, and the primary is 56px because
 * it is the one a thumb reaches for. Press feedback is a scale; hover is a
 * fill that wipes in from the bottom rather than a colour swap, which reads
 * as deliberate rather than as a state change.
 *
 * Disabled uses opacity, the one place WCAG explicitly exempts from contrast. */

.c-base {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  padding: var(--space-3) var(--space-8);
  border: 1px solid transparent;
  font-family: var(--font-display);
  font-size: var(--text-small);
  font-weight: 500;
  letter-spacing: 0.01em;
  text-align: center;
  text-decoration: none;
  line-height: 1.2;
  cursor: pointer;
  overflow: hidden;
  isolation: isolate;
  transition:
    color var(--dur-base) var(--ease-quiet),
    border-color var(--dur-base) var(--ease-quiet),
    transform var(--dur-instant) var(--ease-quiet);
}

/* The wipe. A pseudo-element scaled from the bottom, composited on the GPU,
   so hovering a button never touches layout. */
.c-base::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform var(--dur-base) var(--ease-out);
}

/* A sheen that crosses the button once on hover. Two composited properties,
   no layout, and it is what makes a flat rectangle feel like a physical
   control rather than a coloured box. */
.c-base::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 40%;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    rgb(255 255 255 / 0%) 0%,
    rgb(255 255 255 / 16%) 50%,
    rgb(255 255 255 / 0%) 100%
  );
  transform: translateX(0);
  opacity: 0;
}

.c-base:active {
  transform: scale(var(--press-scale));
}

/* The label shifts a hair as the fill arrives, so the whole thing moves
   together instead of the background changing under static text. */
.c-label {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  transition: transform var(--dur-base) var(--ease-out);
}

.c-primary {

  min-height: 56px;
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

.c-primary::before {
  background: var(--gray-600);
}

.c-secondary {

  background: transparent;
  border-color: var(--rule-strong);
  color: var(--on-surface);
}

.c-secondary::before {
  background: var(--rule-strong);
}

/* A text button. Still a full-height target, so it is real on a phone. */
.c-quiet {

  min-height: var(--tap-target);
  padding-inline: 0;
  background: none;
  border-color: transparent;
  color: var(--on-surface);
  text-decoration: underline;
  text-underline-offset: 5px;
  text-decoration-thickness: 1px;
}

/* On a black band the primary inverts, so it is always the highest-contrast
   thing on its surface. */
[data-surface="dark"] .c-primary {
  background: var(--on-black);
  border-color: var(--on-black);
  color: var(--black);
}

[data-surface="dark"] .c-primary::before {
  background: var(--paper);
}

@media (hover: hover) and (pointer: fine) {
  .c-base:hover::before {
    transform: scaleY(1);
  }

  .c-base:hover::after {
    animation: sheen 900ms var(--ease-quiet) 120ms;
  }

  .c-secondary:hover {
    color: var(--surface);
  }
}

@keyframes sheen {
  from {
    opacity: 0;
    transform: translateX(0);
  }
  30% {
    opacity: 1;
  }
  to {
    opacity: 0;
    transform: translateX(420%);
  }
}

/* ---------- Confirmed ----------
   Set for a moment after something is added to the bag. The button reports
   what happened where the finger already is, instead of relying on the drawer
   sliding in from the far side of the screen to be noticed. */
.c-base[data-done] {
  border-color: var(--ink);
}

.c-base[data-done]::before {
  transform: scaleY(1);
  background: var(--ink);
}

.c-secondary[data-done] {
  color: var(--paper);
}

[data-surface="dark"] .c-base[data-done]::before {
  background: var(--on-black);
}

@keyframes tick {
  from {
    transform: scale(0.4);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

.c-tick {
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  animation: tick var(--dur-base) var(--ease-out) both;
}

/* aria-disabled rather than disabled: the control keeps its place in the tab
   order and stays announceable, so a keyboard user can find it and hear why
   it will not act yet. The handler blocks the action.
 *
 * It is drawn as an outline rather than faded out. A 40% black slab with
 * white text on it composites to about 2.6:1 and reads as broken; an outline
 * reads as "not yet", keeps its label at 6.7:1, and makes filling in when the
 * sizes are chosen feel like the form completing. */
.c-primary[aria-disabled="true"] {
  background: transparent;
  border-color: var(--field-border);
  color: var(--on-surface-muted);
}

.c-secondary[aria-disabled="true"] {
  border-color: var(--field-border);
  color: var(--on-surface-muted);
}

[data-surface="dark"] .c-primary[aria-disabled="true"] {
  background: transparent;
  border-color: var(--field-border);
  color: var(--on-surface-muted);
}

.c-primary[aria-disabled="true"],
.c-secondary[aria-disabled="true"] {
  cursor: not-allowed;
}

.c-primary[aria-disabled="true"]:active,
.c-secondary[aria-disabled="true"]:active {
  transform: none;
}

.c-primary[aria-disabled="true"]:hover::before,
.c-secondary[aria-disabled="true"]:hover::before {
  transform: scaleY(0);
}

.c-full {
  width: 100%;
}

@media (prefers-reduced-motion: reduce) {
  .c-base,
  .c-base::before {
    transition: none;
  }
}


/* ===== components/shop/Price.module.css  ->  .prc-* ===== */

.prc-price {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  white-space: nowrap;
}

/* The original. Struck, muted, and a touch smaller so the eye lands on the
   price that is actually being charged. --on-surface-muted rather than the
   theme's #999999, which measures 2.85:1 on white and fails AA outright. */
.prc-was {
  color: var(--on-surface-muted);
  text-decoration: line-through;
  font-weight: 400;
  font-size: 0.85em;
}

.prc-now {
  text-decoration: none;
  font-weight: 500;
}

/* Only when there is something to signal. A full-price item stays in ink, so
   the colour means "this is reduced" and never just "this is a price". */
.prc-price[data-reduced] .prc-now {
  color: var(--sale);
  font-weight: 700;
}

.prc-small {
  font-size: var(--text-small);
}

.prc-large {
  font-size: var(--text-h3);
}


/* ===== components/shop/Stars.module.css  ->  .str-* ===== */

.str-stars {
  display: inline-flex;
  flex: none;
  align-items: center;
}

.str-svg {
  display: block;
  /* Five 24-unit stars, so the box is 5:1. */
  height: 14px;
  width: 70px;
}

.str-stars[data-size="small"] .str-svg {
  height: 12px;
  width: 60px;
}

/* The unfilled track. A graphic, judged by SC 1.4.11 at 3:1, and --gray-400
   measures 3.45:1 on paper. */
.str-track {
  fill: var(--gray-400);
}

.str-fill {
  fill: var(--on-surface);
}

[data-surface="dark"] .str-track {
  fill: #6a6a6a;
}


/* ===== components/shop/SetLine.module.css  ->  .set-* ===== */

/* No box, no tint, no uppercase. A hairline above it separates it from the
   price without drawing a container, and sentence case at the body size means
   it can actually be read rather than decoded. */
.set-line {
  padding-top: var(--space-3);
  border-top: var(--hairline);
  font-size: var(--text-label);
  line-height: 1.5;
  color: var(--on-surface-muted);
}

.set-strong {
  font-weight: 600;
  color: var(--on-surface);
}


/* ===== components/shop/Notice.module.css  ->  .not-* ===== */

.not-notice {
  border: 1px solid var(--rule-strong);
  padding: var(--space-3);
  font-size: var(--text-label);
  line-height: 1.45;
}


/* ===== components/shop/SizePicker.module.css  ->  .sz-* ===== */

.sz-group {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.sz-legend {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  width: 100%;
  padding: 0;
  margin-bottom: var(--space-3);
}

/* TEE / BOTTOM stays uppercase. It is one word, and two selectors labelled by
   garment are the clearest statement on the page that two garments arrive. */
.sz-legendLabel {
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-surface);
}

.sz-legendGarment {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.sz-options {
  display: grid;
  /* Five sizes across a 390px phone: (390 - 40 gutter - 4 gaps) / 5 = 67px a
     target, well over the 44px rule and over the 48px used here. */
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-2);
}

.sz-option {
  position: relative;
  display: flex;
}

/* The input stays in the layout and stays focusable — not display:none, which
   would take it out of the tab order and break arrow-key navigation within
   the group. It is simply invisible behind its own label. */
.sz-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.sz-label {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: var(--tap-target);
  padding: var(--space-2) var(--space-1);
  border: 1px solid var(--field-border);
  background: var(--surface);
  color: var(--on-surface);
  font-size: var(--text-small);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  cursor: pointer;
  overflow: hidden;
  isolation: isolate;
  transition:
    color var(--dur-fast) var(--ease-quiet),
    border-color var(--dur-fast) var(--ease-quiet),
    transform var(--dur-instant) var(--ease-quiet);
}

/* The fill wipes up from the bottom when a size is chosen, so choosing reads
   as an action rather than as a repaint. */
.sz-label::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--ink);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform var(--dur-base) var(--ease-out);
}

[data-surface="dark"] .sz-label::before {
  background: var(--on-black);
}

.sz-input:checked + .sz-label {
  border-color: var(--ink);
  color: var(--paper);
}

.sz-input:checked + .sz-label::before {
  transform: scaleY(1);
}

[data-surface="dark"] .sz-input:checked + .sz-label {
  border-color: var(--on-black);
  color: var(--black);
}

.sz-input:active + .sz-label {
  transform: scale(var(--press-scale));
}

/* The focus ring renders on the label, since the input it belongs to is
   transparent. Focus must be visible on every focusable thing. */
.sz-input:focus-visible + .sz-label {
  outline: 2px solid var(--rule-strong);
  outline-offset: 3px;
}

.sz-input:disabled + .sz-label {
  color: var(--gray-400);
  cursor: not-allowed;
  /* A real line through the size, so being sold out is visible on a phone
     without hovering or tapping anything. */
  text-decoration: line-through;
}

@media (hover: hover) and (pointer: fine) {
  .sz-input:not(:checked):not(:disabled) + .sz-label:hover {
    border-color: var(--rule-strong);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sz-label,
  .sz-label::before {
    transition: none;
  }
}

/* ---------- Asked for and not yet chosen ----------
   Marked when the customer has pressed a buy control without picking this
   size. The button already moved focus here; on a phone the picker is
   usually on screen at that moment, so a focus ring alone was invisible
   feedback and the button read as broken. This makes the unanswered
   question the loudest thing in the frame. Border and weight carry it, so
   it survives without colour and does not rely on the animation. */
.sz-group[data-missing] .sz-legendLabel {
  color: var(--sale);
}

.sz-group[data-missing] .sz-options {
  animation: askAgain 620ms ease-out;
}

.sz-group[data-missing] .sz-label {
  border-color: var(--sale);
}

@keyframes askAgain {
  0%,
  100% {
    transform: translateX(0);
  }
  20% {
    transform: translateX(-4px);
  }
  45% {
    transform: translateX(4px);
  }
  70% {
    transform: translateX(-2px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sz-group[data-missing] .sz-options {
    animation: none;
  }
}


/* ===== components/chrome/Header.module.css  ->  .hdr-* ===== */

/* The header used to be 12px uppercase monospace at 0.06em tracking. That is
   the hardest possible thing to read at arm's length on a phone, and it made
   the shop look like a terminal. It is now 15px Archivo, sentence case, at
   normal tracking — the same treatment COS and Fear of God both use for
   navigation, which is to say almost none. */

/* No line, in any state, anywhere.
 *
 * There is no border here and none is added when the page scrolls. The header
 * separates itself from the page by being a slightly denser sheet of the same
 * colour with a blur behind it, which is enough; a hairline across the top of
 * every page was the single most-noticed piece of furniture on the site and it
 * earned nothing. Over the hero it is a wash instead. */
/* Solid at rest, frosted once the page moves.
 *
 * It was the wrong way round: 88% opaque at the top of every page, so whatever
 * sat under the header ghosted faintly through it before a finger had touched
 * anything, and the difference between resting and scrolled was six per cent
 * nobody could see. At the top there is nothing behind it worth showing, so it
 * is simply the page colour. The frost is what scrolling looks like. */
.hdr-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: var(--surface);
  border: 0;
  transition:
    background var(--dur-base) var(--ease-quiet),
    box-shadow var(--dur-base) var(--ease-quiet);
}

/* Opaque once scrolled, not translucent.
 *
 * 86% with a blur behind it looked like frosted glass over a photograph and
 * like a printing fault over anything else: scrolled onto the reviews band,
 * the words "From the men who already own it" read straight through the
 * wordmark and the nav. A header is furniture and it has to be able to sit on
 * top of arbitrary content without negotiating with it.
 *
 * The scrolled state is still distinct — it is what casts the shadow. That is
 * the difference the eye needs, and it costs nothing in legibility. */
.hdr-header[data-stuck] {
  background: var(--surface);
  box-shadow: 0 1px 0 rgb(11 11 11 / 6%), 0 6px 20px rgb(11 11 11 / 5%);
}

/* Over the hero photograph the header inverts to white and carries its own
   gradient rather than sitting on nothing.
 *
 * The gradient is not decoration. With `background: transparent` the header's
 * nearest resolvable background is the white page behind it, so white text on
 * it computes as 1:1 and pa11y reported five contrast errors on the homepage
 * — correctly, because nothing guaranteed what would be underneath. Owning a
 * dark fill here means the contrast is a property of the header instead of a
 * property of whichever photograph is loaded. The real composite is sampled
 * from rendered pixels by scripts/measure.hdr-mjs. */
body:has([data-hero]) .hdr-header:not([data-stuck]) {
  --surface: transparent;
  --on-surface: var(--on-black);
  --on-surface-muted: var(--on-black);
  /* The wash is split across two boxes on purpose. The header element owns the
     top of it, because a background it does not own is a background pa11y
     resolves to the white page behind — that reported five contrast failures
     on this page, correctly. The ::before carries the fade on down past the
     header so it ends in the photograph rather than on an edge. */
  background: linear-gradient(
    to bottom,
    rgb(11 11 11 / 70%) 0%,
    rgb(11 11 11 / 46%) 100%
  );
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  color: var(--on-black);
}

body:has([data-hero]) .hdr-header:not([data-stuck])::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 100%;
  height: 180px;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgb(11 11 11 / 46%) 0%,
    rgb(11 11 11 / 18%) 45%,
    rgb(11 11 11 / 0%) 100%
  );
}

/* Above the header own wash, so the wordmark and links are never dimmed
   by it. */
.hdr-inner {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  min-height: 60px;
}

.hdr-wordmark {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  color: inherit;
  white-space: nowrap;
}

.hdr-nav {
  min-width: 0;
}

.hdr-list {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.hdr-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  padding-inline: var(--space-3);
  font-size: var(--text-small);
  font-weight: 400;
  letter-spacing: var(--track-nav);
  text-decoration: none;
  color: inherit;
}

/* An underline that draws from the left rather than a colour change, so
   nothing needs a second colour to show state. */
.hdr-link::after {
  content: "";
  position: absolute;
  left: var(--space-3);
  right: var(--space-3);
  bottom: 30%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}

.hdr-link[aria-current="page"]::after {
  transform: scaleX(1);
}

@media (hover: hover) {
  .hdr-link:hover::after {
    transform: scaleX(1);
  }
}

/* The bag is a glyph and a corner badge, sized to the tap target rather than
   to its own contents, so it occupies a predictable 44px whatever the count
   is. It sits at the end of the row and its padding is trimmed on the outside
   edge only, so the icon lines up with the gutter instead of floating inside
   it. */
.hdr-bag {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: var(--tap-target);
  height: var(--tap-target);
  margin-inline-end: calc(var(--space-2) * -1);
  color: inherit;
}

.hdr-bagIcon {
  display: block;
  width: 22px;
  height: 22px;
}

/* Top-right of the glyph, the way every shop does it. Tabular figures so 1 and
   2 do not shift the badge, and a ring in the header's own surface colour so
   it reads as sitting above the tote rather than welded to it. */
.hdr-bagCount {
  position: absolute;
  top: 6px;
  right: 3px;
  display: grid;
  place-items: center;
  min-width: 1.0625rem;
  height: 1.0625rem;
  padding-inline: 0.25rem;
  border-radius: 999px;
  background: var(--ink);
  color: var(--surface);
  box-shadow: 0 0 0 2px var(--surface);
  font-size: 0.6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* Over the hero the header has no surface of its own, so the badge inverts and
   its ring becomes the wash rather than white. */
body:has([data-hero]) .hdr-header:not([data-stuck]) .hdr-bagCount {
  background: var(--on-black);
  color: var(--black);
  box-shadow: none;
}

/* ---------- Phones ----------
   390px, minus 2 x 16px gutters, is 358px to hold a wordmark, four links and
   the bag. Measured at 15px the links are 168px of text; the wordmark is 72px
   and the bag 44px. That leaves roughly 74px of air, which is what these rules
   spend — evenly, on the gaps between items, rather than letting the row run
   together as one string. */
/* ---------- Phones ----------
 *
 * The four links used to sit in a fixed-width huddle in the middle of the row
 * with whatever space was left over spilling either side of them, which is
 * what made them feel cramped: they were close to each other and far from
 * everything else. The nav now takes all the room between the wordmark and the
 * bag and spreads the links across it, so every one of them is a wide target
 * with real space around it and the spacing comes from the screen rather than
 * from a guess. */
@media (max-width: 560px) {
  .hdr-inner {
    gap: var(--space-1);
    min-height: 58px;
  }

  .hdr-nav {
    flex: 1;
  }

  .hdr-list {
    gap: 0;
    width: 100%;
  }

  /* Equal shares, not equal gaps.
   *
     space-between spread the links apart but left the space between them dead:
     the boxes were still only as wide as their own text, so "Fit" was a 26px
     target with 30px of untappable air either side of it. Giving every link
     flex: 1 puts that same air inside the target instead, so all four become
     the same comfortable width and the whole strip is live. */
  /* The li is the flex item, not the anchor inside it, so the share is taken
     there and the anchor fills it. */
  .hdr-list > li {
    flex: 1;
    display: flex;
  }

  .hdr-link {
    flex: 1;
    justify-content: center;
    padding-inline: 0;
  }

  .hdr-wordmark {
    font-size: 1rem;
    letter-spacing: 0.01em;
  }

  .hdr-link {
    padding-inline: 0.5rem;
    font-size: 0.9375rem;
  }
}

/* Narrow phones. Below this the four links genuinely do not fit at a readable
   size, so the tracking closes up before the type does. */
@media (max-width: 370px) {
  .hdr-inner {
    padding-inline: var(--space-3);
  }

  .hdr-link {
    padding-inline: 0.3125rem;
    font-size: 0.875rem;
    letter-spacing: 0;
  }
}

/* One beat on the count when something is added to the bag. */
@keyframes bagBump {
  0% { transform: scale(1); }
  40% { transform: scale(1.28); }
  100% { transform: scale(1); }
}

.hdr-bagCount[data-bumped] {
  animation: bagBump 600ms var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .hdr-bagCount[data-bumped] {
    animation: none;
  }
}


/* ===== components/chrome/Footer.module.css  ->  .ftr-* ===== */

/* The footer is a black band. It closes every page and it is where everything
   that is not on the fast path lives. */
.ftr-footer {
  background: var(--black);
  color: var(--on-black);
  --surface: var(--black);
  --on-surface: var(--on-black);
  --on-surface-muted: var(--on-black-muted);
  --rule: var(--rule-on-black);
  --rule-strong: var(--on-black);
  --field-border: var(--field-border-on-black);
  color-scheme: dark;
  padding-bottom: calc(var(--space-20) + env(safe-area-inset-bottom, 0px));
}

.ftr-inner {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ---------- Sign-off ---------- */

.ftr-top {
  display: grid;
  gap: var(--space-6);
  padding-block: clamp(var(--space-12), 7vw, var(--space-20));
  border-bottom: 1px solid var(--rule);
}

/* The wordmark, set as large as the column will carry it, with the statement
   underneath. It replaced the email capture: a signup form is the loudest
   thing you can put in the quietest part of a page, and this says the same
   amount about the brand without asking for anything. */
.ftr-mark {
  font-size: clamp(3rem, 13vw, 7.5rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 0.9;
  color: var(--on-black);
  margin: 0;
}

.ftr-topHeading {
  font-size: var(--text-h2);
  max-width: 16em;
  color: var(--on-black-muted);
  font-weight: 400;
}

/* ---------- Columns ---------- */

.ftr-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-10) var(--space-6);
  padding-block: clamp(var(--space-10), 5vw, var(--space-16));
}

.ftr-column {
  min-width: 0;
}

.ftr-heading {
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-surface-muted);
  margin-bottom: var(--space-2);
}

.ftr-list {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ftr-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-small);
  text-decoration: none;
  color: var(--on-surface);
}

.ftr-link:hover {
  text-decoration: underline;
  text-underline-offset: 5px;
}

/* ---------- Legal ---------- */

.ftr-legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block: var(--space-6);
  border-top: 1px solid var(--rule);
}

.ftr-copyright {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.ftr-legalLinks {
  display: flex;
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

@media (min-width: 720px) {
  .ftr-columns {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* A visible top edge. Every page now ends on a light band before the footer,
   so the tonal change does most of the separating; this is the hairline that
   makes the join deliberate rather than accidental where two dark surfaces
   ever meet. */
.ftr-footer {
  border-top: 1px solid var(--rule-on-black);
}

/* Instagram, as a mark rather than a bare handle. */
.ftr-social {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.ftr-socialIcon {
  flex: none;
  display: block;
}


/* ===== components/bag/Progress.module.css  ->  .prg-* ===== */

.prg-progress {
  display: grid;
  gap: var(--space-3);
  padding-bottom: var(--space-2);
}

.prg-line {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.prg-line strong {
  font-weight: 600;
  color: var(--on-surface);
}

.prg-track {
  position: relative;
  height: 3px;
  border-radius: 999px;
  background: var(--gray-200);
  overflow: visible;
}

.prg-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--ink);
  /* Grows from the left as outfits are added. Width only, so it composites
     without touching layout. */
  transition: width var(--dur-slow) var(--ease-out);
}

/* A rung on the ladder. Sits on the track, filled once passed. */
.prg-notch {
  position: absolute;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-left: -3.5px;
  border-radius: 999px;
  background: var(--gray-200);
  transform: translateY(-50%);
  transition: background var(--dur-base) var(--ease-quiet);
}

.prg-notch[data-reached] {
  background: var(--ink);
}

/* The reached state. The bar fills to ink and the sentence stops being a
   nudge, so it stops being muted — that tonal step is the whole reward, and it
   needs no second colour to land in a monochrome palette. */
.prg-unlocked {
  color: var(--on-surface);
}

.prg-progress[data-complete] .prg-track {
  background: var(--ink);
}

@media (prefers-reduced-motion: reduce) {
  .prg-fill,
  .prg-notch {
    transition: none;
  }
}


/* ===== components/bag/BagDrawer.module.css  ->  .bag-* ===== */

.bag-root {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
}

.bag-scrim {
  position: absolute;
  inset: 0;
  width: 100%;
  background: rgb(11 11 11 / 48%);
  border: 0;
  cursor: pointer;
  animation: scrimIn var(--dur-base) var(--ease-quiet);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

@keyframes scrimIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.bag-panel {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  display: flex;
  flex-direction: column;
  width: min(30rem, 100%);
  background: var(--surface);
  color: var(--on-surface);
  /* Floating UI is the one place a shadow is allowed. */
  box-shadow: var(--shadow-float);
  animation: slideIn var(--dur-base) var(--ease-out);
  overflow-y: auto;
  overscroll-behavior: contain;
}

@keyframes slideIn {
  from { transform: translate3d(100%, 0, 0); }
  to { transform: none; }
}

.bag-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--hairline);
  position: sticky;
  top: 0;
  background: var(--surface);
  z-index: 1;
}

.bag-title {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-size: var(--text-h3);
  font-weight: 500;
  letter-spacing: var(--track-heading);
}

.bag-count {
  font-size: var(--text-small);
  font-weight: 400;
  color: var(--on-surface-muted);
}

.bag-close {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  padding-inline: var(--space-2);
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.bag-close:hover {
  color: var(--on-surface);
}

.bag-empty {
  display: grid;
  gap: var(--space-5);
  padding: var(--space-12) var(--space-5);
}

/* The gap between pressing Add to bag and the cart coming back.
 *
   The drawer opens on the tap rather than on the response, which is what
   makes it feel immediate — but on the first add the bag is still empty at
   that moment, so it opened on "Your bag is empty" and sat there until the
   request landed. That is the worst thing to show somebody who has just
   added something, and it read as the drawer opening broken and then
   correcting itself.

   So while the request is out, the empty message is replaced by a line in
   the shape of the one that is about to arrive. theme.bag-js sets the flag on
   the tap and clears it in applyBag. */
.bag-root[data-pending] .bag-empty {
  display: none;
}

.bag-root[data-pending] .bag-panel::after {
  content: "Adding to your bag";
  display: block;
  padding: var(--space-12) var(--space-5);
  font-size: var(--text-small);
  color: var(--on-surface-muted);
  /* A slow pulse rather than a spinner. The wait is one request and usually
     under a second, and a spinning disc for that long reads as a stall; a
     breath of opacity says the same thing without claiming the wait is long.
     The line is the only thing on screen, so nothing else moves with it. */
  animation: bagPending 1.4s var(--ease-quiet) infinite;
}

@keyframes bagPending {
  0%,
  100% {
    opacity: 0.45;
  }

  50% {
    opacity: 1;
  }
}

/* Anyone who has asked for less movement gets the message and no pulse. */
@media (prefers-reduced-motion: reduce) {
  .bag-root[data-pending] .bag-panel::after {
    animation: none;
    opacity: 1;
  }
}

.bag-lines {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1;
}

/* Photograph left, everything else right. */
.bag-line {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  gap: var(--space-4);
  padding: var(--space-5);
  border-bottom: var(--hairline);
}

/* ---------- A bundle, shown as one item ----------
   Outlined rather than divided, so the outfits inside read as its contents
   rather than as three more things in the bag. */
.bag-bundle {
  display: grid;
  gap: var(--space-4);
  margin: var(--space-4) var(--space-4) 0;
  padding: var(--space-4);
  border: 1px solid var(--rule-strong);
}

.bag-bundleHead {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
}

.bag-bundleName {
  font-size: var(--text-body);
  font-weight: 500;
}

.bag-bundleSpec {
  margin-top: 2px;
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.bag-bundleItems {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: var(--space-4) 0 0;
  border-top: var(--hairline);
  list-style: none;
}

.bag-bundleItem {
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr);
  gap: var(--space-3);
  align-items: center;
}

.bag-bundleThumb {
  display: block;
  overflow: hidden;
}

.bag-bundleThumb img {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
}

.bag-bundleItemBody {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.bag-bundleItemName {
  font-size: var(--text-small);
  letter-spacing: var(--track-nav);
}

.bag-bundleItemSizes {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

/* No fill behind the photograph. The grey placeholder box used to show
   through as a light rectangle under the image, which read as a loading
   state that never finished. The frame is exactly the size of the picture
   and the picture fills it. */
.bag-thumb {
  display: block;
  overflow: hidden;
  background: none;
  line-height: 0;
}

.bag-thumb picture {
  background: none;
}

.bag-thumb img {
  aspect-ratio: 2 / 3;
  object-fit: cover;
  object-position: 50% 40%;
  width: 100%;
  height: 100%;
  transition: transform var(--dur-slow) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .bag-thumb:hover img { transform: scale(var(--hover-zoom)); }
}

.bag-lineBody {
  min-width: 0;
}

.bag-lineHead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.bag-lineName {
  font-size: var(--text-body);
  font-weight: 500;
  letter-spacing: var(--track-heading);
}

.bag-linePrice {
  font-size: var(--text-body);
  font-variant-numeric: tabular-nums;
}

/* Two rows, always. Both garments named, both sizes shown. This is the last
   surface before checkout and the last chance to make it plain that two
   things arrive. */
.bag-pieces {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-1);
}

.bag-piece {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.bag-pieceSize {
  font-variant-numeric: tabular-nums;
  color: var(--on-surface);
}

.bag-lineControls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-4);
}

.bag-qty {
  display: flex;
  align-items: center;
  border: 1px solid var(--field-border);
}

.bag-qtyButton {
  width: var(--tap-target);
  height: var(--tap-target);
  display: grid;
  place-items: center;
  font-size: var(--text-body);
  color: var(--on-surface);
  transition: background var(--dur-fast) var(--ease-quiet);
}

.bag-qtyButton:hover:not(:disabled) {
  background: var(--gray-100);
}

.bag-qtyButton:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.bag-qtyValue {
  min-width: 2.5rem;
  text-align: center;
  font-size: var(--text-small);
  font-variant-numeric: tabular-nums;
}

.bag-change {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-small);
  color: var(--on-surface-muted);
  text-decoration: underline;
  text-underline-offset: 4px;
}

.bag-change:hover:not(:disabled) {
  color: var(--on-surface);
}

.bag-change:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The pinned footer.
 *
   Held deliberately short. Everything in here is pinned, so every row it
   gains is a row taken off the scrolling list of outfits above it — and with
   two outfits in the bag the old footer was tall enough that the second line
   never came into view. The discount rows moved to .bag-offer at the top and the
   returns promise came out, leaving four short rows; the padding and the row
   gap come down with them. */
.bag-foot {
  position: sticky;
  bottom: 0;
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  border-top: var(--hairline);
  background: var(--surface);
}

.bag-ladder,
.bag-totalNote {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.bag-total {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  font-size: var(--text-h3);
  font-variant-numeric: tabular-nums;
}

.bag-error {
  margin: var(--space-4) var(--space-5) 0;
  padding: var(--space-3);
  border: 1px solid var(--rule-strong);
  font-size: var(--text-small);
  line-height: 1.45;
}

@media (max-width: 420px) {
  .bag-line {
    grid-template-columns: 76px minmax(0, 1fr);
    gap: var(--space-3);
    padding: var(--space-4);
  }
}

/* The tier a qualifying bag has earned. */
.bag-tier {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.bag-tier strong {
  font-weight: 600;
  color: var(--on-surface);
}

/* The discount block, at the top of the drawer above the outfits.
 *
   This is the whole of the saving: the progress bar and any discount the cart
   has actually earned. It sits here rather than in the footer because the
   footer is pinned and this is not — it is read on the way in and then
   scrolls out of the way, instead of permanently occupying the bottom third
   of the drawer and pushing the second outfit off the screen. */
.bag-offer {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--hairline);
}

.bag-offer:empty {
  display: none;
}

/* Where the garments come from, directly above Checkout.
 *
   It replaced the returns conditions ("Unworn, unwashed, tags on"), which
   asked something of the customer at the exact moment they were deciding to
   pay. This tells them something instead, and it is the last line they read
   before the button, so it carries full contrast and weight rather than the
   muted tone the old incentives row used. */
.bag-origin {
  margin: 0;
  font-size: var(--text-small);
  line-height: 1.5;
  font-weight: 600;
  color: var(--on-surface);
}

/* Accepted payment methods, centred under the Checkout button.
 *
   Smaller than they were: at 24px they read as a feature of the footer, and
   the point of them is only to answer "can I pay with Apple Pay" at a glance.
   16px and centred puts them at the foot of the frame as a quiet closing row
   rather than a band of logos. */
.bag-payments {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One line, always. The gap closes before anything wraps, and each mark
   shrinks with the row rather than pushing a seventh onto a second line. */
.bag-payments li {
  display: flex;
  min-width: 0;
}

.bag-payments img {
  height: 16px;
  width: auto;
  max-width: 100%;
  object-fit: contain;
}


/* ===== components/shop/OutfitCard.module.css  ->  .oc-* ===== */

/* Every card is the same size, aligned, with the price on a shared baseline.
   Comparability is the whole job of the grid: no offsets, no asymmetry. */
.oc-card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
  gap: var(--space-4);
  align-content: start;
}

@supports not (grid-template-rows: subgrid) {
  .oc-card {
    grid-template-rows: auto auto auto;
    grid-row: auto;
  }
}

.oc-link {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
  gap: var(--space-4);
  text-decoration: none;
  color: inherit;
}

@supports not (grid-template-rows: subgrid) {
  .oc-link {
    grid-template-rows: auto auto auto;
    grid-row: auto;
  }
}

/* The front and back shots are stacked in the same box. */
.oc-media {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--gray-100);
}

.oc-front,
.oc-back {
  display: block;
}

/* 2:3 rather than the source's 3:4. The studio frames leave a lot of grey
   either side of the figure, and cropping ~11% off the sides fills the card
   without touching the head or the shoes. */
.oc-front img,
.oc-back img {
  aspect-ratio: 2 / 3;
  object-fit: cover;
  object-position: 50% 42%;
  width: 100%;
}

.oc-back {
  position: absolute;
  inset: 0;
  opacity: 0;
}

.oc-front img,
.oc-back {
  transition:
    opacity var(--dur-base) var(--ease-quiet),
    transform var(--dur-slow) var(--ease-out);
}

/* Hover swaps to the back of the outfit, and the whole frame eases in very
   slightly so the change reads as deliberate rather than as a glitch.
   Pointer devices only — there is no hover-only information here, because
   the back shot is also the third frame of the product gallery. */
@media (hover: hover) and (pointer: fine) {
  .oc-link:hover .oc-back,
  .oc-link:focus-visible .oc-back {
    opacity: 1;
  }

  .oc-link:hover .oc-front img,
  .oc-link:focus-visible .oc-front img,
  .oc-link:hover .oc-back,
  .oc-link:focus-visible .oc-back {
    transform: scale(var(--hover-zoom));
  }
}

.oc-meta {
  display: grid;
  gap: var(--space-1);
  align-self: end;
  min-width: 0;
}

/* The name and the price, on one line with the price to the right — the same
   row the shop grid uses, so the two grids read as one thing. */
.oc-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
  min-width: 0;
}

.oc-name {
  font-size: var(--text-h3);
  font-weight: 500;
  letter-spacing: var(--track-heading);
}

/* The name and the price share the meta row, and the price now carries a
   struck original as well as the amount. At four cards across a 1024px window
   that is more than a 218px column can hold on one line, and it pushed the
   whole page 17px wider than the viewport. The figures themselves still never
   break — that is the Price component own nowrap — but "for both pieces" is
   allowed to drop underneath them. */
.oc-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 var(--space-2);
  font-size: var(--text-small);
  min-width: 0;
}

/* Never a bare number. The label is part of the price, not a footnote. */
.oc-forBoth {
  font-size: var(--text-label);
  color: var(--on-surface-muted);
}

.oc-pieces {
  font-size: var(--text-small);
  line-height: 1.45;
  color: var(--on-surface-muted);
  margin-top: var(--space-2);
}

.oc-rating {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.oc-ratingText {
  font-size: var(--text-small);
  font-variant-numeric: tabular-nums;
}

.oc-ratingCount {
  color: var(--on-surface-muted);
}

/* Bottom left of the photograph, flush into the corner, on one line.
 *
   It was a two-line chip inset from the top right. Two lines were there to
   keep it off the model's head, which is centred in all four shots — moving
   it to the bottom corner solves that outright, so it goes back to one line
   and a smaller face, and reads as a mark on the picture rather than a
   sticker over it.

   Black on a white box. It was white on black, which at this size read as a
   sale sticker; the white box is quieter and sits on the grey seamless as a
   label rather than a flag. Opaque either way — the mark has to stay legible
   over a white tee and a black one alike, so it never goes translucent. It
   sits above both frames so the hover cross-fade passes underneath it. */
.oc-badge {
  position: absolute;
  bottom: 0;
  left: 0;
  z-index: 2;
  padding: 0.35em 0.6em;
  background: var(--paper);
  color: var(--ink);
  font-size: 0.5625rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.15;
  text-align: left;
  white-space: nowrap;
}

/* On a phone the standfirst and the price would stack awkwardly beside each
   other, so the price drops under the name. */
@media (max-width: 560px) {
  .oc-row {
    display: grid;
    gap: var(--space-1);
  }

  .oc-price {
    justify-content: start;
  }
}

@media (prefers-reduced-motion: reduce) {
  .oc-front img,
  .oc-back {
    transition: none;
    transform: none !important;
  }
}


/* ===== components/shop/Gallery.module.css  ->  .gal-* ===== */

/* On a phone the hero is cropped, which is what buys the room for the name,
   the price, the set line and both size selectors above the fold. The crop is
   taken from 18% down, so it holds the tee and the top of the bottom rather
   than centring on the waist.
 *
 * The height is a viewport fraction rather than a ratio, because what has to
 * fit underneath is a fixed number of pixels: two 44px selectors, their
 * legends, and three lines of set line. Measured at 390x844 with the sticky
 * bar accounted for, 40svh leaves the second selector about 50px clear of the
 * fold. The box is fully determined by CSS before the image arrives, so none
 * of this costs anything in layout shift. */
/* On a phone the hero is a centred portrait plate, not a wide crop.
 *
   It used to be a full-width band capped at 34svh, which on a 3:4 source meant
   throwing away 39% of the frame vertically — his legs and shoes. Narrowing the
   picture instead of shortening it costs almost nothing in height (a 62% wide
   2:3 plate is about the same 300px tall) and keeps him whole, head to shoes,
   which is the point of a product photograph. The 11% taken off the sides is
   empty grey seamless. */
.gal-hero {
  margin-inline: auto;
}

/* The narrowing and the ceiling are both phone-only.
 *
   Everything above the size selectors on this page is fixed except this
   photograph, so this is the part that has to give when the fold gets tight —
   and tap-count.gal-mjs measures the result.

   The width rule used to apply at every size, which was a plain mistake: the
   comment above it said the desktop hero "should be as large as the layout
   allows" while the rule held it to 62% of its column, so the product page
   showed a smaller photograph than the shop grid that linked to it. On a
   desktop there is no fold to protect and the picture now fills its column. */
@media (max-width: 899px) {
  .gal-hero {
    /* 88% wide, at the photographs' own ratio, centred.
     *
       Both numbers are measured rather than chosen, and they are the answer
       to three things that are wanted at once and cannot all be had on a
       390x844 screen: the plate reaching both edges, nothing cropped off the
       head or the feet, and both size selectors above the fold.

       Uncropped is what fixes the ratio at 1200/1607, and once the ratio is
       fixed the width sets the height — so "wider" and "shorter" are the same
       dial. tap-count.gal-mjs has failed five times on the way to 88%: 88%/56svh
       put the BOTTOM selector 50px under, 78%/50svh left 3px (a rounding
       error away from failing on a real handset), full-bleed 4:5 went 25px
       under, and full-bleed at the source ratio — 522px tall — put the BOTTOM
       selector at y=852, where the script could not tap it and neither could
       a thumb. A photograph that costs the customer the second size is not a
       better photograph.

       88% is 343 x 459 against the 270 x 405 this started from: 44% more
       picture, no crop, and the BOTTOM selector at 840 of 844. The gutters
       either side are what the fold costs.

       The ratio is written as 1200 / 1607 rather than rounded to 3 / 4
       because at this width those differ by about two pixels of crop, and two
       pixels off the top of a head is visible. */
    width: 88%;
    margin-inline: auto;
    aspect-ratio: 1200 / 1607;
    overflow: hidden;
  }

  .gal-hero img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 50% 50%;
  }
}

.gal-hero img {
  aspect-ratio: 2 / 3;
  object-fit: cover;
  object-position: 50% 45%;
}

/* Below the hero: a swipe rail. The next frame is always partly in view, so
   it is visibly swipeable without any affordance being drawn on top of the
   photograph. This is the only horizontal scroll in the build. */
.gal-rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 78%;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  /* The rail bleeds to the screen edge, so the first frame lines up with the
     page gutter and the last one can still scroll fully into view. */
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  scrollbar-width: none;
}

.gal-rail::-webkit-scrollbar {
  display: none;
}

.gal-railItem {
  scroll-snap-align: start;
}

.gal-figure {
  margin: 0;
  display: grid;
  gap: var(--space-2);
}

.gal-caption {
  font-size: var(--text-label);
  letter-spacing: 0;
  text-transform: none;
  color: var(--on-surface-muted);
}

@media (min-width: 900px) {
  /* Full frame on a desktop: there is width and height to spare, so the plate
     opens out to the column and nothing is cropped at all. */
  .gal-hero {
    width: 100%;
  }

  .gal-hero img {
    aspect-ratio: auto;
    object-fit: fill;
  }

  /* No rail. A single column, because a horizontal scroll on a desktop is
     something people miss. */
  .gal-rail {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    gap: var(--space-3);
    overflow: visible;
    scroll-snap-type: none;
    margin-inline: 0;
    padding-inline: 0;
  }
}

/* A product video sits in the rail on the same terms as a photograph: same
   width, same ratio, same caption underneath. */
.gal-videoWrap {
  position: relative;
  display: block;
  background: var(--gray-100);
}

.gal-video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  background: var(--gray-100);
}

/* The play affordance: the whole frame is the target, with a disc in the
   middle of it. Covering the frame rather than just the disc means a thumb
   anywhere on the poster starts the clip, which is what people try first.
   It disappears on the first play and the native controls take over. */
.gal-playButton {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  width: 100%;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--paper);
}

.gal-playCircle {
  display: grid;
  place-items: center;
  width: 84px;
  height: 84px;
  border-radius: 999px;
  /* Dark disc rather than a white one: the posters are light and a white disc
     vanished into the grey seamless. 62% keeps the photograph readable behind
     it while the triangle stays at full contrast. */
  background: rgb(11 11 11 / 62%);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  transition:
    transform var(--dur-base) var(--ease-out),
    background var(--dur-base) var(--ease-quiet);
}

@media (hover: hover) and (pointer: fine) {
  .gal-playButton:hover .gal-playCircle {
    background: rgb(11 11 11 / 78%);
    transform: scale(1.06);
  }
}

@media (prefers-reduced-motion: reduce) {
  .gal-playCircle {
    transition: none;
  }

  .gal-playButton:hover .gal-playCircle {
    transform: none;
  }
}

@media (max-width: 560px) {
  .gal-playCircle {
    width: 68px;
    height: 68px;
  }
}


/* The focus ring for the video, drawn on its figure rather than on itself.
 *
 * Tabbing to a <video controls> puts focus inside Chrome's own shadow DOM for
 * the control bar. document.gal-activeElement reports the video, but the video
 * itself never matches :focus or :focus-visible, so neither the global ring
 * nor a rule on .gal-video ever applies — and a screenshot of the tabbed state
 * confirmed there was no indicator at all, which is a 2.4.7 failure rather
 * than a quirk of the harness. :focus-within matches an ancestor when focus is
 * anywhere inside it, including a shadow root, so the figure carries the ring.
 *
 * Scoped to the figure that actually holds a video: the others contain only
 * images and nothing in them can take focus. */
.gal-figure:focus-within,
/* The clip moved out of the rail into its own band, and the ring has to move
   with it. It did not, for one commit: interaction-check.gal-mjs caught a focus
   stop on the product page with no indicator at all, because the only rule
   painting one was scoped to the rail figure the video no longer sits in. */
.gal-motionFrame:focus-within {
  outline: 2px solid var(--rule-strong);
  outline-offset: 3px;
}

/* ---------- In motion ----------
   Its own band between Details and Reviews. The clip is portrait, so it is
   held to a column rather than allowed to run the full width of a desktop,
   where a 9:16 video stretched across 1400px is all letterbox and no
   garment. */
.gal-motion {
  padding-block: clamp(var(--space-10), 7vw, var(--space-16));
}

.gal-motionHeading {
  font-size: var(--text-h2);
  margin-bottom: clamp(var(--space-6), 3vw, var(--space-10));
}

.gal-motionFrame {
  max-width: 30rem;
}


/* ===== components/shop/BuyBox.module.css  ->  .buy-* ===== */

.buy-form {
  display: grid;
  gap: var(--space-5);
}

.buy-sizes {
  display: grid;
  gap: var(--space-5);
}

.buy-actions {
  display: grid;
  gap: var(--space-2);
}


/* ---------- Sticky buy bar ----------
   Fixed, so it can never contribute to layout shift, and pinned to the bottom
   so the primary action sits in the thumb zone once the real one has scrolled
   away. It is translated out of view rather than unmounted, so appearing is a
   transform rather than a reflow. */

.buy-bar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: var(--z-sticky-bar);
  background: var(--surface);
  border-top: 1px solid var(--rule-strong);
  transform: translate3d(0, 101%, 0);
  transition: transform var(--dur-base) var(--ease-out);
  /* Clear of the iOS home indicator. */
  padding-bottom: env(safe-area-inset-bottom, 0);
}

.buy-bar[data-visible] {
  transform: none;
}

.buy-barInner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-3) var(--gutter);
}

.buy-barText {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.buy-barName {
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The price on the bar is labelled too. A bare number on a persistent bar is
   exactly where someone reads "$132" and thinks "t-shirt". */
.buy-barPrice {
  font-size: var(--text-label);
  letter-spacing: 0;
  text-transform: none;
  font-variant-numeric: tabular-nums;
  color: var(--on-surface-muted);
}

@media (prefers-reduced-motion: reduce) {
  .buy-bar {
    transition: none;
  }
}

/* The outstanding question, stated in words. Sits between the pickers and the
   buttons so it is never under the sticky bar. */
.buy-asking {
  margin: var(--space-3) 0 0;
  font-size: var(--text-small);
  font-weight: 600;
  color: var(--sale);
}


/* ===== components/shop/BundleBuy.module.css  ->  .bb-* ===== */

.bb-form {
  display: grid;
  gap: var(--space-6);
}

/* Each outfit is a compact row: a thumbnail, its name and price, and a
   checkbox. The size selectors only unfold for the ones chosen, so a tier
   page opens as three or four short rows rather than as forty radio buttons.
   That was the whole problem with it. */
.bb-outfits {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--rule);
}

/* One outfit: the head row, and its two size groups when it is chosen.
 *
   The rows were set on a tight block padding, which was fine while a chosen
   outfit was the exception. On a fixed tier every outfit is chosen, so the
   page is four heads and eight size groups in a single column and the rows ran
   into each other — the sizes belonging to an outfit sat as close to the next
   outfit's name as to their own. More air between rows than inside them is
   what makes the grouping legible. */
.bb-outfit {
  padding-block: var(--space-6);
  border-bottom: 1px solid var(--rule);
}

.bb-outfit:last-child {
  border-bottom: 0;
}

/* One line: thumbnail, name, garments, price.
 *
   It was a three-column grid with the garments on a second row underneath,
   indented past the thumbnail — two rows of text per outfit before any size
   button appeared. Flex rather than grid because the row has a different
   number of children depending on the tier: a fixed tier renders the
   thumbnail and the name, a chooseable one puts a checkbox in front of them,
   and a grid with fixed columns cannot take both without one of them
   wrapping. */
.bb-outfitHead {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.bb-outfitThumb {
  flex: 0 0 56px;
  overflow: hidden;
  line-height: 0;
}

.bb-outfitThumb img {
  /* The photographs' own ratio, so nothing is cropped off the figure. */
  aspect-ratio: 1200 / 1607;
  object-fit: cover;
  object-position: 50% 50%;
  width: 100%;
}

/* One tap to include an outfit. A real checkbox, so it works with the
   keyboard, with a screen reader and with no JavaScript at all. */
.bb-include {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-target);
  cursor: pointer;
  min-width: 0;
}

.bb-includeInput {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.bb-includeBox {
  position: relative;
  flex: none;
  width: 22px;
  height: 22px;
  border: 1px solid var(--field-border);
  background: var(--surface);
  transition:
    background var(--dur-fast) var(--ease-quiet),
    border-color var(--dur-fast) var(--ease-quiet);
}

/* The tick is two borders rotated, so there is no icon file and no font. */
.bb-includeBox::after {
  content: "";
  position: absolute;
  left: 7px;
  top: 3px;
  width: 6px;
  height: 11px;
  border: solid var(--surface);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0);
  transition: transform var(--dur-fast) var(--ease-out);
}

.bb-includeInput:checked + .bb-includeBox {
  background: var(--ink);
  border-color: var(--ink);
}

.bb-includeInput:checked + .bb-includeBox::after {
  transform: rotate(45deg) scale(1);
}

.bb-includeInput:focus-visible + .bb-includeBox {
  outline: 2px solid var(--rule-strong);
  outline-offset: 3px;
}

.bb-includeInput:disabled ~ .bb-outfitName,
.bb-includeInput:disabled + .bb-includeBox {
  opacity: 0.4;
}

.bb-outfitName {
  flex: 0 0 auto;
  font-size: var(--text-body);
  font-weight: 500;
  letter-spacing: var(--track-heading);
  white-space: nowrap;
}

/* Takes the slack in the row, and gives it back first. The garments are the
   least load-bearing thing on the line — the name identifies the outfit and
   the price is what is being agreed to — so this is what shortens when the
   column is tight rather than letting the price wrap under it. */
.bb-outfitPieces {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-small);
  color: var(--on-surface-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bb-outfitPrice {
  flex: 0 0 auto;
  margin-left: auto;
  font-size: var(--text-body);
  font-variant-numeric: tabular-nums;
  color: var(--on-surface-muted);
  white-space: nowrap;
}

/* The two size groups, indented to start under the outfit's name rather than
   under its thumbnail — so the eye reads them as belonging to the row above.
   The gap between them is deliberately tighter than the gap between outfits. */
.bb-sizes {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding-left: calc(56px + var(--space-4));
}

/* Two groups side by side once there is width for it.
 *
   Stacked, a chosen outfit is four rows tall — name, tee legend, tee sizes,
   bottom legend, bottom sizes — and on a fixed tier that repeats four times
   down a column that already has room to spare beside it. Tee and bottom are
   the same kind of choice made at the same moment, so they sit level. */
@media (min-width: 700px) {
  .bb-sizes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3) var(--space-8);
  }
}

.bb-totals {
  display: grid;
  gap: var(--space-2);
}

.bb-totalRow,
.bb-grandTotal {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  font-size: var(--text-small);
  font-variant-numeric: tabular-nums;
  color: var(--on-surface-muted);
}

.bb-struck {
  text-decoration: line-through;
}

.bb-grandTotal {
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
  font-size: var(--text-h3);
  color: var(--on-surface);
}

.bb-prompt {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.bb-actions {
  display: grid;
  gap: var(--space-2);
}

@media (max-width: 560px) {
  .bb-outfitHead {
    gap: var(--space-3);
  }

  .bb-outfitThumb {
    flex: 0 0 48px;
  }

  /* On a phone the row cannot hold all four. The garments drop to their own
     line under the name rather than being reduced to an ellipsis, which is
     the one place the stacked layout was the right answer. */
  .bb-outfitHead {
    flex-wrap: wrap;
  }

  .bb-outfitPieces {
    flex: 1 0 100%;
    order: 1;
    white-space: normal;
    overflow: visible;
  }

  .bb-sizes {
    padding-left: 0;
  }
}

/* Shown when the tier's code is not live on the store, so the total on screen
   is the total that will be charged. */
.bb-pending {
  font-size: var(--text-small);
  line-height: 1.5;
  color: var(--on-surface-muted);
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}


/* ===== components/shop/Reviews.module.css  ->  .rev-* ===== */

.rev-section {
  padding-block: clamp(var(--space-16), 9vw, var(--space-32));
}

.rev-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: clamp(var(--space-8), 4vw, var(--space-12));
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--rule);
}

.rev-heading {
  font-size: var(--text-h2);
}

.rev-summary {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

.rev-stars {
  font-size: var(--text-small);
  letter-spacing: 0.14em;
  color: var(--on-surface);
}

.rev-count {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.rev-list {
  display: grid;
  gap: var(--space-8);
  margin: 0;
  padding: 0;
  list-style: none;
}

.rev-review {
  display: grid;
  gap: var(--space-3);
  align-content: start;
}

/* The quote is the loudest thing in the section, which is the point of
   putting it on black. */
.rev-quote {
  margin: 0;
  font-size: var(--text-lead);
  line-height: 1.45;
  letter-spacing: var(--track-heading);
  color: var(--on-surface);
  max-width: 26rem;
}

.rev-quote::before {
  content: "\201C";
}

.rev-quote::after {
  content: "\201D";
}

.rev-meta {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

@media (min-width: 720px) {
  .rev-list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-12) var(--space-8);
  }
}

@media (min-width: 1100px) {
  .rev-list {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-8);
  }
}


/* ===== app/home.module.css  ->  .home-* ===== */

/* ---------- 1. Hero ----------
   Full bleed photograph with the type over it. The header is transparent
   above it, so the picture runs to the top of the window. */

.home-hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* Pulled up under the transparent header. */
  margin-top: calc(var(--tap-target) * -1 - 12px);
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
}

.home-heroImage {
  position: absolute;
  inset: 0;
  z-index: -2;
  background: var(--black);
}

.home-heroImage img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* On a phone the frame is portrait, so the crop holds the man on the right
     rather than centring on empty pavement. */
  object-position: 72% 50%;
}

/* The scrim is what makes the type legible, and it is measured rather than
   guessed. Over the brightest pixel in the headline area the darkest stop
   composites to about #4a4a4a, which carries white text at roughly 8:1; the
   lightest stop the text ever sits on is 0.55, which still clears 4.7:1. */
.home-heroScrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    /* A short wash at the very top so the header's wordmark and links sit on
       a known tone rather than on whatever the photograph happens to put
       there. Without it the header is legible in this frame and illegible in
       the next one somebody swaps in. */
    linear-gradient(
      to bottom,
      rgb(11 11 11 / 55%) 0%,
      rgb(11 11 11 / 22%) 12%,
      rgb(11 11 11 / 0%) 22%
    ),
    linear-gradient(
      to top,
      rgb(11 11 11 / 88%) 0%,
      rgb(11 11 11 / 62%) 28%,
      rgb(11 11 11 / 18%) 58%,
      rgb(11 11 11 / 0%) 78%
    );
}

.home-heroInner {
  position: relative;
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-bottom: clamp(var(--space-12), 8vw, var(--space-24));
  display: grid;
  gap: var(--space-5);
  justify-items: start;
}


.home-headline {
  color: var(--on-black);
  max-width: 12em;
}

.home-heroLine {
  font-size: var(--text-lead);
  line-height: 1.45;
  color: var(--on-black);
  max-width: 30rem;
}

.home-heroActions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

@media (min-width: 900px) {
  .home-heroImage img {
    object-position: 62% 50%;
  }

  .home-heroScrim {
    /* On a wide frame the man sits right of centre, so the scrim leans left
       where the type is and lets the photograph breathe on the right. */
    background:
      linear-gradient(
        105deg,
        rgb(11 11 11 / 78%) 0%,
        rgb(11 11 11 / 58%) 34%,
        rgb(11 11 11 / 12%) 62%,
        rgb(11 11 11 / 0%) 100%
      ),
      linear-gradient(to top, rgb(11 11 11 / 55%) 0%, rgb(11 11 11 / 0%) 40%);
  }

  .home-heroInner {
    padding-bottom: clamp(var(--space-16), 7vw, var(--space-24));
  }

  .home-headline {
    max-width: 9em;
  }
}

/* ---------- 2. The four ---------- */

.home-four {
  padding-block: clamp(var(--space-16), 9vw, var(--space-32));
}

.home-sectionHead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: clamp(var(--space-8), 4vw, var(--space-12));
  padding-bottom: var(--space-4);
  border-bottom: var(--hairline);
}

.home-sectionHeading {
  font-size: var(--text-h2);
}

.home-sectionNote {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.home-sectionLink {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-small);
  text-underline-offset: 5px;
  text-decoration-thickness: 1px;
}

.home-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: auto auto auto;
  gap: clamp(var(--space-8), 4vw, var(--space-12)) var(--space-4);
}

/* ---------- 3. The claim, on black ---------- */

.home-claim {
  padding-block: clamp(var(--space-20), 14vw, var(--space-32));
}

.home-claimLine {
  color: var(--on-black);
}

.home-claimLineMuted {
  color: var(--on-black-muted);
}

/* ---------- 4. Close ----------
   Over a photograph, so the bottom of the page is not a third flat black
   band stacked straight onto the footer. */

/* Photograph and words side by side, so neither is compromised for the other.
   One column on a phone, image first.
 *
 * Held to 62rem rather than the full 1440 container. Stretched across the whole
 * width the picture had to be enormous or the words had to sit a very long way
 * from it; at this width the two read as one composed block with the page
 * around them, which is what a closing statement wants to be. */
.home-close {
  display: grid;
  gap: clamp(var(--space-8), 5vw, var(--space-12));
  align-items: center;
  width: 100%;
  max-width: 62rem;
  margin-inline: auto;
  padding: clamp(var(--space-16), 9vw, var(--space-24)) var(--gutter);
}

.home-closeImage {
  background: var(--gray-100);
}

/* The box matches the source exactly, so there is no crop at all. The rule
   that matters here is that one follows the other: an earlier version put a
   4:3 landscape box over a 3:4 portrait photograph and took the difference
   off the top and bottom, which cut the legs off the jeans.
 *
 * Both are 4:3 now. The folded photograph that replaced the flat lay came out
 * of the generator with the two stacks filling 84% of the width but only 35%
 * of the height — a third of the frame was empty grey above and another third
 * below, which on a phone read as a small picture in a large void. The master
 * is cropped to 1760x1320 centred on the garments, leaving even margins, and
 * the box moved with it. */
.home-closeImage img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.home-closeInner {
  display: grid;
  gap: var(--space-6);
  justify-items: start;
}

.home-closeHeading {
  font-size: var(--text-display);
  letter-spacing: var(--track-display);
  color: var(--ink);
  max-width: 9em;
  text-wrap: balance;
}

@media (min-width: 1000px) {
  .home-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-12) var(--space-6);
  }
}

@media (min-width: 760px) {
  /* A portrait plate and the words beside it. The image column is the narrower
     of the two on purpose: at 3:4 it gains height as it gains width, and a
     half-width plate on a wide screen would run the section past a full
     viewport on its own. */
  .home-close {
    grid-template-columns: minmax(0, 24rem) max-content;
    /* Centred as a pair rather than stretched across the block, so the words
       sit next to the picture instead of at the far side of the page. */
    justify-content: center;
    column-gap: clamp(var(--space-8), 5vw, var(--space-16));
  }
}

/* ---------- Phones: keep his head out of the navigation ----------
   The hero photograph is landscape and the phone viewport is portrait, so
   `cover` pins the image to the height and there is no vertical slack at all —
   object-position cannot move him down, which is why this needs a transform.
   At 1.18 the crop is a little tighter and he drops far enough that the nav
   sits on the dark shopfront behind him rather than across his hair. Checked
   at 360, 375, 390, 414 and 430. */
@media (max-width: 899px) {
  .home-heroImage img {
    transform: scale(1.18) translateY(7%);
    transform-origin: 70% 0%;
  }
}

/* ---------- A stagger on the four ----------
   On a phone the cards enter the viewport one at a time, so they already
   arrive in sequence. Across a desktop row they all cross the threshold on the
   same frame and land as one block, which reads as a page redrawing rather
   than as content arriving. Scroll-driven animations ignore animation-delay,
   so the stagger is done by starting each card a little later in its own entry
   range. Reduced motion is handled globally in app/styles/global.home-css. */
@media (min-width: 1000px) {
  .home-grid > *:nth-child(2) { animation-range: entry 14% entry 64%; }
  .home-grid > *:nth-child(3) { animation-range: entry 20% entry 70%; }
  .home-grid > *:nth-child(4) { animation-range: entry 26% entry 76%; }
}


/* ===== app/shop/shop.module.css  ->  .shop-* ===== */

.shop-page {
  padding-bottom: 0;
}

.shop-head {
  display: grid;
  gap: var(--space-4);
  padding-block: clamp(var(--space-12), 7vw, var(--space-20));
}

.shop-gridWrap {
  padding-bottom: clamp(var(--space-16), 9vw, var(--space-24));
}

/* One column on a small phone, two from 620px, four from 1000px. Every card
   is the same shape, so the four can be read across. */
.shop-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(var(--space-10), 5vw, var(--space-12)) var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.shop-item {
  min-width: 0;
}

.shop-link {
  display: grid;
  gap: var(--space-4);
  text-decoration: none;
  color: inherit;
}

.shop-media {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--gray-100);
}

.shop-front img,
.shop-back img {
  aspect-ratio: 2 / 3;
  object-fit: cover;
  object-position: 50% 42%;
  width: 100%;
  transition:
    opacity var(--dur-base) var(--ease-quiet),
    transform var(--dur-slow) var(--ease-out);
}

.shop-back {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-quiet);
}

/* Hover shows the back of the outfit. Pointer devices only, and additive:
   the back shot is the third frame of the product gallery, so a phone is not
   missing anything. */
@media (hover: hover) and (pointer: fine) {
  .shop-link:hover .shop-back,
  .shop-link:focus-visible .shop-back {
    opacity: 1;
  }

  .shop-link:hover .shop-front img,
  .shop-link:hover .shop-back img,
  .shop-link:focus-visible .shop-front img,
  .shop-link:focus-visible .shop-back img {
    transform: scale(var(--hover-zoom));
  }
}

.shop-body {
  display: grid;
  gap: var(--space-1);
}

.shop-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.shop-name {
  font-size: var(--text-h3);
  font-weight: 500;
  letter-spacing: var(--track-heading);
}

.shop-price {
  font-size: var(--text-h3);
  white-space: nowrap;
}

.shop-forBoth {
  font-size: var(--text-label);
  color: var(--on-surface-muted);
}

.shop-pieces {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
  margin-top: var(--space-2);
}

.shop-rating {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.shop-ratingText {
  font-size: var(--text-small);
  font-variant-numeric: tabular-nums;
}

.shop-ratingCount {
  color: var(--on-surface-muted);
}

/* ---------- Bundles prompt ---------- */

.shop-more {
  padding-block: clamp(var(--space-16), 9vw, var(--space-24));
}

.shop-moreInner {
  display: grid;
  gap: var(--space-8);
  justify-items: start;
}

.shop-moreHeading {
  font-size: var(--text-h2);
}

.shop-moreLine {
  font-size: var(--text-lead);
  color: var(--on-surface-muted);
  margin-top: var(--space-2);
}

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

@media (min-width: 1000px) {
  .shop-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-12) var(--space-6);
  }

  .shop-moreInner {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    justify-items: stretch;
  }
}

/* Bottom left of the photograph, flush into the corner, on one line — matching
   the homepage grid exactly. See the note on .shop-badge in OutfitCard.shop-module.shop-css
   for why the chip stays rather than laying white type on the seamless. */
.shop-badge {
  position: absolute;
  bottom: 0;
  left: 0;
  z-index: 2;
  padding: 0.35em 0.6em;
  background: var(--paper);
  color: var(--ink);
  font-size: 0.5625rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.15;
  text-align: left;
  white-space: nowrap;
}


/* ===== app/bundles/bundles.module.css  ->  .bun-* ===== */

.bun-page {
  padding-bottom: 0;
}

.bun-head {
  display: grid;
  gap: var(--space-4);
  padding-block: clamp(var(--space-12), 7vw, var(--space-20));
}

.bun-tiersWrap {
  padding-bottom: clamp(var(--space-16), 9vw, var(--space-24));
}

/* One tier per row, at every width.
 *
 * Three columns was the wrong shape for this page. A tier card is only as wide
 * as a third of the container, and the strip inside it divides that width by
 * however many outfits the tier holds — so The Set's four frames came out at
 * half the size of The Pair's two, side by side, and the range appeared to
 * shrink as it got bigger. Exactly backwards.
 *
 * As rows, each tier gets the full container and the frames are sized rather
 * than divided (see .bun-stripImage). The Set fills more of the row than The Pair
 * because it contains more, which is the one thing this page is selling. */
.bun-tiers {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  margin: 0;
  padding: 0;
  list-style: none;
}

.bun-tier {
  min-width: 0;
  border-top: var(--hairline);
}

.bun-tier:last-child {
  border-bottom: var(--hairline);
}

.bun-tierLink {
  display: grid;
  gap: var(--space-5);
  padding-block: clamp(var(--space-8), 4vw, var(--space-12));
  text-decoration: none;
  color: inherit;
}

/* One frame per outfit the tier actually contains — two, three or four. The
   count is the entire proposition of this page, so the picture has to carry
   it; it used to show two frames on every tier regardless. */
/* Two across on a phone, wrapping — never four in a row.
 *
 * Dividing the screen width by the tier size meant The Set showed four 86px
 * frames on a 390px phone, which is half the size of the same frame on a
 * desktop and smaller than the thumbnail in the bag. The tier with the most in
 * it looked like the least. Two columns holds every cell at about 170px
 * whatever the tier contains, which is the desktop size, and a tier with more
 * outfits simply takes a second row.
 *
 * `min-width: 0` is still load-bearing: without it the cells consult the
 * image intrinsic width, which is what once blew this page out to eleven
 * thousand pixels tall. 3:4 is the source ratio, so nothing is cropped. */
.bun-strip {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px;
  width: 100%;
  overflow: hidden;
  /* No fill. Two columns means an odd tier leaves one cell empty, and a grey
     box in that hole reads as a photograph that failed to load rather than as
     a tier with three outfits in it. */
  background: none;
}

.bun-stripImage {
  min-width: 0;
  /* The photographs are 1200 x 1607, so this is their own ratio to the pixel
     and cover has nothing to crop. Rounded to 3/4 it trimmed a couple of
     pixels off the top and bottom of every frame, which on a standing figure
     is the head and the shoes. */
  aspect-ratio: 1200 / 1607;
  background: none;
  overflow: hidden;
}

/* On a phone the strip runs to both screen edges.
 *
   Inside the page gutters two columns gave 165px frames — smaller than the
   same photograph in the shop grid, and small enough that the outfits in a
   bundle were harder to make out than the ones you buy singly. Cancelling the
   gutter is worth about 25px a side and costs nothing else on the page. */
@media (max-width: 899px) {
  .bun-strip {
    margin-inline: calc(var(--gutter) * -1);
  }
}

.bun-stripImage img {
  width: 100%;
  height: 100%;
  /* The box is the source ratio, so cover crops nothing. */
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .bun-tierLink:hover .bun-stripImage img {
    transform: scale(var(--hover-zoom));
  }
}

.bun-body {
  display: grid;
  gap: var(--space-1);
}

.bun-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.bun-tierName {
  font-size: var(--text-h3);
  font-weight: 500;
  letter-spacing: var(--track-heading);
}

.bun-save {
  font-size: var(--text-h3);
  font-variant-numeric: tabular-nums;
}

.bun-spec {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.bun-price {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.bun-from {
  font-size: var(--text-label);
  color: var(--on-surface-muted);
}

.bun-amount {
  font-size: var(--text-h3);
}

/* ---------- How it works ---------- */

.bun-note {
  padding-block: clamp(var(--space-16), 9vw, var(--space-24));
}

.bun-noteInner {
  display: grid;
  gap: var(--space-8);
}

.bun-noteHeading {
  font-size: var(--text-h2);
}

.bun-steps {
  display: grid;
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

.bun-steps li {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr);
  gap: var(--space-4);
  align-items: baseline;
  font-size: var(--text-lead);
  padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
}

.bun-stepNumber {
  font-size: var(--text-label);
  color: var(--on-surface-muted);
  font-variant-numeric: tabular-nums;
}

@media (min-width: 760px) {
  .bun-steps {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-8);
  }
}

/* ---------- The tier as a wide row ---------- */

@media (min-width: 900px) {
  /* The image column is exactly four cells wide on every row, whatever the row
     holds. That is what keeps the text column starting at the same x on all
     three tiers — sizing it to each tier's own strip would leave the headings
     stepping rightwards down the page — while a tier with fewer outfits simply
     leaves the rest of its column empty. The cell size flexes with the
     viewport so the whole thing still fits at 900px. */
  .bun-tierLink {
    /* Was clamp(7rem, 11.5vw, 11rem) — 176px a cell on a wide screen, so The
       Set's strip ran to 710px and took half the width of the page. At that
       size the photographs stop being a description of the tier and become the
       tier, with the name, the saving and the price crowded into what is left.
       8.5rem tops out at 136px, which holds four frames inside 550px and
       leaves the text column room to breathe. */
    --cell: clamp(5.5rem, 8.5vw, 8.5rem);
    grid-template-columns:
      calc(var(--cell) * 4 + 6px)
      minmax(0, 1fr);
    align-items: center;
    column-gap: clamp(var(--space-8), 4vw, var(--space-16));
  }

  /* The frames stop dividing the row and take a size instead.
   *
     This is the whole fix. A cell is 190px on every tier, so a frame in The
     Set is exactly as large as a frame in The Pair — nothing shrinks as the
     range grows. What changes is how much of the row is filled: two frames
     leave most of it, four nearly complete it, and the strip reads as a
     measure of how much of the range you are taking. `flex-grow: 0` is what
     stops two frames stretching to fill the width and undoing it. */
  .bun-strip {
    display: flex;
    justify-content: start;
    background: none;
  }

  .bun-stripImage {
    flex: 0 1 var(--cell);
  }

  .bun-body {
    gap: var(--space-2);
  }

  .bun-cta {
    justify-self: start;
    width: auto;
    min-width: 12rem;
    margin-top: var(--space-4);
  }
}


/* The affordance inside the row link. It is a span wearing the secondary
   button's clothes, so it must not look interactive on its own — the row is
   the target and the hover state belongs to the row. */
.bun-cta {
  pointer-events: none;
  width: 100%;
}

.bun-tierLink:hover .bun-cta,
.bun-tierLink:focus-visible .bun-cta {
  background: var(--ink);
  color: var(--surface);
  border-color: var(--ink);
}

/* The saving in money, under the price. It is the strongest line on the card,
   so it takes the sale colour and sits on its own rather than competing with
   the figures above it. */
.bun-saved {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-small);
  font-weight: 600;
  color: var(--sale);
}


/* ===== app/bundles/[tier]/tier.module.css  ->  .tier-* ===== */

.tier-page {
  padding-bottom: 0;
}

.tier-top {
  display: grid;
  gap: var(--space-6);
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: clamp(var(--space-8), 5vw, var(--space-16));
}

.tier-media {
  min-width: 0;
}

.tier-strip {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.tier-strip img {
  /* The photographs' own ratio, so cover crops nothing in either direction.
     At 2/3 the frame was narrower than the source and took the difference off
     the sides — only seamless, but it also made these plates taller than they
     needed to be, which is part of what left the column so dominant. */
  aspect-ratio: 1200 / 1607;
  object-fit: cover;
  object-position: 50% 50%;
}

.tier-detail {
  display: grid;
  gap: var(--space-5);
  min-width: 0;
}

.tier-head {
  display: grid;
  gap: var(--space-3);
}

.tier-eyebrow {
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-surface-muted);
}

.tier-name {
  font-size: var(--text-display);
  font-weight: 500;
  letter-spacing: var(--track-display);
}

.tier-saving {
  font-size: var(--text-h2);
}

.tier-setLine {
  padding-top: var(--space-3);
  border-top: var(--hairline);
  font-size: var(--text-small);
  line-height: 1.5;
  color: var(--on-surface-muted);
}

.tier-setLine strong {
  font-weight: 600;
  color: var(--on-surface);
}

.tier-instruction {
  font-size: var(--text-small);
  line-height: 1.5;
  color: var(--on-surface-muted);
}

.tier-details {
  padding-block: clamp(var(--space-16), 9vw, var(--space-24));
}

.tier-detailsHeading {
  font-size: var(--text-h2);
  margin-bottom: clamp(var(--space-6), 3vw, var(--space-10));
}

.tier-spec {
  display: grid;
  margin: 0;
  border-top: 1px solid var(--rule);
  max-width: 52rem;
}

.tier-specRow {
  display: grid;
  gap: var(--space-1);
  padding-block: var(--space-5);
  border-bottom: 1px solid var(--rule);
}

.tier-specRow dt {
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-surface-muted);
}

.tier-specRow dd {
  margin: 0;
  font-size: var(--text-body);
  line-height: 1.5;
}

.tier-detailsLinks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-top: var(--space-8);
}

.tier-detailsLinks a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-small);
  text-underline-offset: 5px;
}

@media (min-width: 640px) {
  .tier-specRow {
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: var(--space-6);
    align-items: baseline;
  }
}

@media (min-width: 900px) {
  /* The photographs take the smaller half.
   *
     This was an even split, which on a wide screen gave the strip about
     640px — four plates the size of a shop card, stacked two by two, against
     a column of size buttons. The pictures are context here; the choosing is
     the page. They also ran out long before the list did, leaving a tall
     empty panel down the left. 0.72fr holds the strip near 440px, which is
     enough to read the outfits and lets the builder have the room it needs. */
  .tier-top {
    grid-template-columns: minmax(0, 0.72fr) minmax(0, 1fr);
    gap: clamp(var(--space-10), 5vw, var(--space-16));
    align-items: start;
  }

  .tier-media {
    position: sticky;
    top: calc(60px + var(--space-8));
  }
}


/* ===== app/outfits/[slug]/outfit.module.css  ->  .pdp-* ===== */

.pdp-page {
  padding-bottom: 0;
}

/* Phone order: image, name, price, set line, both size selectors, the two
   buttons, then the rest of the gallery. Nothing between the image and the
   selectors. */
.pdp-buy {
  display: grid;
  gap: var(--space-4);
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-top: var(--space-4);
  padding-bottom: clamp(var(--space-12), 7vw, var(--space-20));
}

.pdp-hero,
.pdp-rail {
  min-width: 0;
}

.pdp-rail {
  margin-top: var(--space-4);
}

/* A little more air between the photograph and the name, on a phone only.
 *
   The two sat on the page's default row gap, so the name started immediately
   under the frame and the block read as a caption to the picture rather than
   as the start of the buying column. Every pixel added here is a pixel closer
   to the fold for the second size selector, so this is deliberately small and
   tap-count.pdp-mjs is what says how much there is to spend. */
@media (max-width: 899px) {
  .pdp-detail {
    margin-top: var(--space-2);
  }

  /* The gap above the photograph pays for the gap below it.
   *
     The fold at 390x844 has no slack left: widening the picture and pushing
     the name down both push the second size selector towards it, and together
     they went 27px under. This is where the pixels come from — the space
     between the header and the top of the frame was doing nothing except
     separating two things that are already separated by a rule. The picture
     starts higher, the name sits lower, and the selectors stay reachable. */
  .pdp-buy {
    padding-top: var(--space-1);
  }
}

.pdp-detail {
  display: grid;
  gap: var(--space-4);
  min-width: 0;
  /* Sticky positioning always creates a stacking context, even at z-index
     auto — unlike relative or absolute, which only do it once a z-index is
     set. That made this column a lid over everything inside it, including the
     fixed buy bar: the bar's z-index of 80 was being resolved *within* this
     context, so it could never rise above a later sibling of this element.
     The reviews section is exactly that, and it painted straight over the bar.
     Raising it to 999999 changed nothing, which is what gave the game away.

     One is enough. It only has to beat the sections that follow it, all of
     which sit at auto, and it stays well under the header. */
  z-index: 1;
}

.pdp-head {
  display: grid;
  gap: var(--space-3);
}

.pdp-name {
  font-size: var(--text-display);
  font-weight: 500;
  letter-spacing: var(--track-display);
}

.pdp-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.pdp-amount {
  font-size: var(--text-h2);
  font-variant-numeric: tabular-nums;
}

.pdp-forBoth {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.pdp-standfirst {
  font-size: var(--text-lead);
  line-height: 1.5;
  color: var(--on-surface);
  max-width: var(--measure);
}

.pdp-fit {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

/* ---------- Details, on black ---------- */

.pdp-details {
  padding-block: clamp(var(--space-16), 9vw, var(--space-24));
}

.pdp-sectionHeading {
  font-size: var(--text-h2);
  margin-bottom: clamp(var(--space-6), 3vw, var(--space-10));
}

.pdp-spec {
  display: grid;
  margin: 0;
  border-top: 1px solid var(--rule);
  max-width: 52rem;
}

.pdp-specRow {
  display: grid;
  gap: var(--space-1);
  padding-block: var(--space-5);
  border-bottom: 1px solid var(--rule);
}

.pdp-specRow dt {
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-surface-muted);
}

.pdp-specRow dd {
  margin: 0;
  font-size: var(--text-body);
  line-height: 1.5;
}

.pdp-detailsLinks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-top: var(--space-8);
}

.pdp-detailsLinks a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-small);
  text-underline-offset: 5px;
}

@media (min-width: 640px) {
  .pdp-specRow {
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: var(--space-6);
    align-items: baseline;
  }
}

@media (min-width: 900px) {
  /* Two columns, and the rail moves back under the hero. The DOM order stays
     hero, detail, rail — the correct reading order and the order the phone
     needs — while the grid puts the pictures together. */
  .pdp-buy {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
    grid-template-areas:
      "hero detail"
      "rail detail";
    grid-template-rows: auto 1fr;
    column-gap: clamp(var(--space-8), 4vw, var(--space-16));
    row-gap: var(--space-3);
    align-items: start;
    padding-top: var(--space-8);
  }

  .pdp-hero {
    grid-area: hero;
  }

  .pdp-detail {
    grid-area: detail;
    gap: var(--space-5);
    position: sticky;
    top: calc(60px + var(--space-8));
  }

  .pdp-rail {
    grid-area: rail;
    margin-top: 0;
  }
}

/* The rating sits between the price and the set line, and links to the
   reviews rather than being decoration. */
.pdp-ratingLink {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  text-decoration: none;
  color: inherit;
  justify-self: start;
}

.pdp-ratingValue {
  font-size: var(--text-small);
}

.pdp-ratingCount {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* Price and rating on one row. */
.pdp-priceRow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
}

/* ---------- Phones: the price row must not cost two lines ----------
   Adding the struck compare-at price widened this row enough that both the
   label and the rating wrapped, taking it from about 50px to 98px and pushing
   the BOTTOM size selector 45px below the fold — which scripts/tap-count.pdp-mjs
   caught, and which breaks the one rule this page exists to keep. The figures
   step down one size on a phone and the row fits again. */
@media (max-width: 480px) {
  .pdp-amount {
    font-size: var(--text-h3);
  }

  .pdp-priceRow {
    gap: var(--space-1) var(--space-3);
  }

  .pdp-price {
    gap: var(--space-1) var(--space-2);
  }
}

/* The returns line, immediately under the two buttons. Quiet — it is
   reassurance, not a claim being sold. */
.pdp-reassure {
  font-size: var(--text-small);
  line-height: 1.5;
  color: var(--on-surface-muted);
  max-width: var(--measure);
}

.pdp-reassure a {
  color: inherit;
}


/* ===== app/fit/fit.module.css  ->  .fit-* ===== */

.fit-page {
  padding-bottom: 0;
}

.fit-head {
  display: grid;
  gap: var(--space-4);
  padding-block: clamp(var(--space-10), 6vw, var(--space-16));
}

.fit-band {
  padding-block: clamp(var(--space-10), 6vw, var(--space-16));
  border-top: var(--hairline);
}

.fit-rulesBand {
  padding-block: clamp(var(--space-12), 7vw, var(--space-20));
}

.fit-heading {
  font-size: var(--text-h2);
  margin-bottom: clamp(var(--space-5), 3vw, var(--space-8));
}

/* ---------- Measurement tables ---------- */

.fit-tables {
  display: grid;
  gap: clamp(var(--space-10), 5vw, var(--space-12));
}

.fit-tableBlock {
  min-width: 0;
}

.fit-tableHeading {
  font-size: var(--text-h3);
  font-weight: 500;
  margin-bottom: var(--space-4);
}

/* No sideways scrolling, at any width.
 *
 * This used to be a 26rem-wide table inside an overflow-x container, which on
 * a 390px phone meant the size guide was a thing you had to drag. A size guide
 * is the one page a customer opens when they are unsure, and making them
 * discover half of it by swiping is the worst possible moment to hide data.
 *
 * It never needed the width. Six columns of two-digit centimetre figures and a
 * short row label measure about 262px once the cell padding is spent on the
 * gaps that matter rather than on every side of every cell — comfortably
 * inside the 350px a 390px phone actually offers. The container stays, because
 * a 320px phone with the text scaled up 200% can still need it, but at that
 * point scrolling is the correct answer rather than the default one.
 *
 * `position: relative` is load-bearing where a sticky cell sits inside a
 * scroll container — see app/shop/shop.fit-module.fit-css. */
.fit-scroller {
  position: relative;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
}

.fit-table {
  width: 100%;
  text-align: left;
  font-size: var(--text-small);
}

.fit-table th,
.fit-table td {
  padding: var(--space-3) var(--space-4) var(--space-3) 0;
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
}

@media (max-width: 620px) {
  .fit-scroller {
    margin-inline: 0;
    padding-inline: 0;
  }

  .fit-table {
    font-size: 0.8125rem;
    table-layout: fixed;
  }

  .fit-table th,
  .fit-table td {
    padding: 0.6875rem 0.375rem 0.6875rem 0;
    text-align: right;
  }

  /* The row label keeps the left edge and the figures range right under their
     size, so the columns read as columns without any rules between them. */
  .fit-table tbody th,
  .fit-table thead th:first-child {
    text-align: left;
    padding-right: 0.25rem;
  }
}

.fit-table thead th {
  font-weight: 500;
  border-bottom: 1px solid var(--rule-strong);
}

.fit-table tbody th {
  position: sticky;
  left: 0;
  background: var(--surface);
  font-weight: 400;
  color: var(--on-surface-muted);
  min-width: 6.5rem;
}

/* The 6.5rem floor is what made the table wider than the phone, and the sticky
   cell only earns its keep while there is something to scroll past. */
@media (max-width: 620px) {
  .fit-table tbody th {
    position: static;
    min-width: 0;
    width: 38%;
  }
}

/* ---------- Choosing ---------- */

.fit-rules {
  display: grid;
  margin: 0;
  border-top: 1px solid var(--rule);
}

.fit-rule {
  display: grid;
  gap: var(--space-2);
  padding-block: var(--space-6);
  border-bottom: 1px solid var(--rule);
}

.fit-rule dt {
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-surface-muted);
}

.fit-rule dd {
  margin: 0;
  font-size: var(--text-lead);
  line-height: 1.5;
  max-width: var(--measure);
}

/* ---------- What the model wears ---------- */

.fit-references {
  display: grid;
  margin: 0 0 var(--space-8);
  border-top: var(--hairline);
  max-width: 44rem;
}

.fit-reference {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: var(--space-4);
  align-items: baseline;
  padding-block: var(--space-4);
  border-bottom: var(--hairline);
}

/* A full-height target, not a 16px line of text. These are standalone links in
   a list, so SC 2.5.8's inline-text exemption does not cover them and the 44px
   rule applies — scripts/measure.fit-mjs caught all four at 16px. */
.fit-reference dt a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-small);
  font-weight: 500;
  letter-spacing: var(--track-heading);
  text-decoration: none;
}

.fit-reference dt a:hover {
  text-decoration: underline;
  text-underline-offset: 4px;
}

.fit-reference dd {
  margin: 0;
  font-size: var(--text-small);
  color: var(--on-surface-muted);
}

.fit-help {
  font-size: var(--text-small);
  color: var(--on-surface-muted);
  max-width: var(--measure);
  margin-bottom: var(--space-4);
}

@media (min-width: 700px) {
  .fit-scroller {
    overflow-x: visible;
    margin-inline: 0;
    padding-inline: 0;
  }

  .fit-table {
    min-width: 0;
  }
}

@media (min-width: 1000px) {
  .fit-tables {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-12);
  }

  .fit-rules {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-8);
    border-top: 0;
  }

  .fit-rule {
    border-top: 1px solid var(--rule);
    border-bottom: 0;
  }
}



/* ===== app/about/about.module.css  ->  .abt-* ===== */

.abt-page {
  padding-bottom: 0;
}

/* The photograph that used to separate this from the first band is gone, so
   the bottom padding no longer has to clear it. */
.abt-opening {
  padding-block: clamp(var(--space-16), 10vw, var(--space-32))
    clamp(var(--space-8), 5vw, var(--space-12));
}

.abt-openingInner {
  display: grid;
}

.abt-title {
  font-size: var(--text-display);
  font-weight: 500;
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
  max-width: 16em;
}

/* The intro band: the three facts under the standfirst.
 *
   It closes on a hairline. The band below it changes surface, which ought to
   be separation enough, but the two blocks sat one directly under the other
   with nothing marking where the first ended — and because both are grids of
   short headed paragraphs, the eye ran them together into one long list. The
   rule is drawn inside the container so it lines up with the text rather than
   running the full bleed of the page, and it is the same hairline the
   principles use above each item, so it reads as part of the same system. */
.abt-band {
  padding-block: clamp(var(--space-8), 4vw, var(--space-12))
    clamp(var(--space-16), 9vw, var(--space-24));
}

.abt-columns {
  padding-bottom: clamp(var(--space-12), 7vw, var(--space-20));
  border-bottom: 1px solid var(--rule);
}

.abt-columns {
  display: grid;
  gap: var(--space-10);
}

.abt-column {
  display: grid;
  gap: var(--space-3);
  align-content: start;
}

.abt-columnHeading {
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-surface-muted);
}

.abt-body {
  font-size: var(--text-lead);
  line-height: 1.5;
  max-width: var(--measure);
}

.abt-closing {
  padding-block: clamp(var(--space-16), 9vw, var(--space-24));
}

.abt-closingInner {
  display: grid;
  gap: var(--space-10);
  align-items: center;
}

/* Centred and held to a measure, so the sign-off reads as a closing statement
   rather than as a column that lost its picture. */
.abt-closingText {
  display: grid;
  gap: var(--space-8);
  justify-items: start;
}

.abt-closingHeading {
  max-width: 16em;
  font-size: var(--text-display);
  letter-spacing: var(--track-display);
}

.abt-closingActions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6);
}

.abt-email {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-small);
  text-underline-offset: 5px;
}

@media (min-width: 900px) {
  .abt-columns {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-8);
  }

}

/* ---------- Added in the final pass ----------
   The page was a headline, three short columns and a photograph, which read as
   a placeholder for an About page rather than one. It now opens on a
   standfirst, shows the range it spends the whole page arguing for, and states
   how the shop works in four numbered principles. */

.abt-standfirst {
  font-size: var(--text-lead);
  line-height: 1.5;
  color: var(--on-surface-muted);
  max-width: var(--measure);
  margin-top: clamp(var(--space-5), 2vw, var(--space-8));
}

.abt-rangeHeading {
  font-size: var(--text-h2);
  margin-bottom: clamp(var(--space-6), 3vw, var(--space-10));
  padding-bottom: var(--space-4);
  border-bottom: var(--hairline);
}

@media (hover: hover) and (pointer: fine) {
}

.abt-principles {
  padding-block: clamp(var(--space-12), 7vw, var(--space-20));
}

.abt-principleList {
  display: grid;
  gap: clamp(var(--space-8), 4vw, var(--space-10)) var(--space-8);
  margin: 0;
  padding: 0;
  list-style: none;
}

.abt-principle {
  display: grid;
  gap: var(--space-2);
  align-content: start;
  padding-top: var(--space-5);
  border-top: 1px solid var(--rule);
}

.abt-principleNumber {
  font-size: var(--text-label);
  letter-spacing: var(--track-label);
  color: var(--on-surface-muted);
  font-variant-numeric: tabular-nums;
}

.abt-principleHeading {
  font-size: var(--text-h3);
  font-weight: 500;
  letter-spacing: var(--track-heading);
}

@media (min-width: 760px) {
  .abt-principleList {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

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

  /* Three columns for three facts.
   *
     This was repeat(4) with three items in it, so the fourth cell sat empty
     and the group stopped three quarters of the way across — directly above a
     principles grid that genuinely has four items and does reach the edge.
     The intro read as a truncated copy of the section below it rather than as
     a section of its own, which is most of what "merged" meant here. Three
     equal columns fill the measure and give the block its own proportion. */
  .abt-columns {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(var(--space-8), 5vw, var(--space-16));
  }
}


/* ===== app/policies/policies.module.css  ->  .pol-* ===== */

.pol-page {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: var(--space-10) var(--space-16);
}

.pol-head {
  margin-bottom: var(--space-10);
}

.pol-title {
  font-size: var(--text-display);
  margin-bottom: var(--space-4);
}

.pol-jump ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.pol-jump a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-label);
  letter-spacing: 0;
  text-transform: none;
  color: var(--on-surface-muted);
  text-underline-offset: 4px;
}

.pol-section {
  max-width: 44rem;
  padding-top: var(--space-10);
  border-top: var(--hairline);
  margin-bottom: var(--space-10);
  /* Clears the sticky header when jumped to. */
  scroll-margin-top: calc(var(--tap-target) + var(--space-4));
}

.pol-heading {
  font-size: var(--text-h2);
  margin-bottom: var(--space-4);
}

.pol-summary {
  font-size: var(--text-label);
  letter-spacing: 0;
  text-transform: none;
  margin-bottom: var(--space-4);
}

.pol-list {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.pol-list li {
  font-family: var(--font-serif);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  padding-left: var(--space-4);
  border-left: 1px solid var(--rule);
}

.pol-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  text-underline-offset: 4px;
}

.pol-spec {
  display: grid;
  margin: 0;
  border-top: var(--hairline);
}

.pol-specRow {
  display: grid;
  grid-template-columns: 7rem 1fr;
  gap: var(--space-4);
  padding-block: var(--space-4);
  border-bottom: var(--hairline);
}

.pol-specRow dt {
  font-size: var(--text-label);
  letter-spacing: 0;
  text-transform: none;
  color: var(--on-surface-muted);
}

.pol-specRow dd {
  margin: 0;
  font-size: var(--text-label);
  line-height: var(--leading-body);
}

.pol-note {
  margin-top: var(--space-4);
  font-size: var(--text-label);
  line-height: 1.5;
  color: var(--on-surface-muted);
}


/* ===== app/not-found.module.css  ->  .nf-* ===== */

.nf-page {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: var(--space-16) var(--space-24);
  display: grid;
  gap: var(--space-4);
  justify-items: start;
}

.nf-code {
  font-size: var(--text-label);
  letter-spacing: 0;
  color: var(--on-surface-muted);
}

.nf-title {
  font-size: var(--text-display);
}

.nf-body {
  font-family: var(--font-serif);
  font-size: var(--text-lead);
  line-height: var(--leading-body);
  max-width: var(--measure);
}

.nf-list {
  display: grid;
  margin: var(--space-2) 0 var(--space-4);
  padding: 0;
  list-style: none;
  border-top: var(--hairline);
  width: 100%;
  max-width: 28rem;
}

.nf-link {
  display: flex;
  align-items: center;
  min-height: var(--tap-target);
  border-bottom: var(--hairline);
  font-size: var(--text-label);
  letter-spacing: 0;
  text-transform: none;
  text-decoration: none;
}

.nf-link:hover {
  text-decoration: underline;
  text-underline-offset: 4px;
}


/* ===== Theme-only additions ===== */

/* Buying with scripting off.
   Two radio groups cannot be resolved into a variant id without script, so a
   plain select listing every combination is offered instead. Hidden from
   everyone else — and taken out of the accessibility tree as well as the
   layout, so a screen reader with script on is not told about a duplicate
   control for the same choice. */
.buy-nojs {
  display: none;
}

.no-js .buy-nojs {
  display: grid;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.no-js .buy-nojs select {
  min-height: 44px;
  padding: var(--space-2);
  border: var(--hairline);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
}

/* The cart page's two submits sit on one row. The drawer has a single
   Checkout; the page also has Update, because a quantity typed into a number
   field needs somewhere to be applied without script. */
.bag-cartActions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

/* The bag drawer is hidden by an attribute rather than unmounted, so the
   markup Shopify rendered is always there to re-read. */
.bag-root[hidden] {
  display: none;
}

/* An outfit's sizes appear when the outfit is taken, not before.
 *
   React does this by not rendering them: BundleBuy only mounts the two size
   pickers for a chosen outfit. Liquid has no equivalent — the markup for every
   outfit is emitted once, server side — so on a chooseable tier the theme drew
   all of them at once. That is ten extra size buttons per outfit sitting under
   a checkbox nobody had ticked, roughly 800px of page on The Pair, and it is
   the difference between a page you scan and a page you give up on.

   theme.js already keeps data-chosen accurate on each card; nothing was
   reading it. A fixed tier ships the attribute from Liquid, so its sizes are
   open from the start, exactly as on the site.

   Left visible when scripting is off: without JS the checkbox cannot report
   its state, and a page that hides every size control behind an attribute
   nothing sets is a page nobody can use. */
.bb-outfit:not([data-chosen]) .bb-sizes {
  display: none;
}

.no-js .bb-outfit .bb-sizes {
  display: grid;
}

/* And the totals stay empty until something has been chosen.
 *
   Same cause as the sizes above: React renders the rows only when at least
   one outfit is selected, so on a chooseable tier with nothing ticked the
   totals block is an empty 24px reservation. Liquid emits the rows either way,
   which drew "0 pieces, 0 outfits — $0" style furniture before the customer
   had done anything, and made the block 106px against the site's 24. That 82px
   then pushed the Details band and the whole footer down the page, which is
   most of what the pixel diff was still reporting.

   :has() rather than a JavaScript class, because theme.js already keeps
   data-chosen correct on each card and this is a question about the form's
   contents — exactly what :has() is for. It is already used for the
   transparent-header rule, so it is not a new dependency here. */
.bb-form:not(:has([data-outfit][data-chosen])) .bb-totalRow {
  display: none;
}

/* And its opposite: the standing-in line goes once there is a real total. */
.bb-form:has([data-outfit][data-chosen]) .bb-totalsEmpty {
  display: none;
}

/* Shopify wraps every {% section %} in a div, and that wrapper is what breaks
   a sticky header.
 *
   position: sticky pins an element inside its own parent's box. The wrapper
   Shopify generates is exactly as tall as the header inside it, so the header
   had nowhere to stick to and scrolled away on the first flick — while the
   same CSS on the site, where the header is a direct child of the body,
   behaves perfectly.
 *
   The sticking moves to the wrapper, which is a child of the body and as tall
   as the page. The header keeps its own background, transition and stuck
   state; it simply no longer does the positioning. */
#shopify-section-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
}

#shopify-section-header .hdr-header {
  position: relative;
  z-index: auto;
}

/* The drawer is a section too, so that it can be re-rendered on its own. It is
   position: fixed and must not be trapped in its wrapper's box either. */
#shopify-section-bag-drawer {
  position: relative;
  z-index: var(--z-drawer);
}
