/* ============================================================
   singular | unique embroidered apparel
   ------------------------------------------------------------
   One stylesheet, no framework. Tokens first, then base, then
   components in the order a visitor meets them.

   The system in one breath: a warm bone-and-ink ground, a light
   serif carrying the lowercase product names, hairline rules
   instead of boxes, generous air, and almost no colour — a
   single ember hue appears only where stock is running out, so
   colour always means "act now". Everything else is achieved
   with weight, case, tracking and space.

   Companion reference: docs/design-system.md
   ============================================================ */

/* ------------------------------------------------------------
   1. Tokens
   ------------------------------------------------------------ */

:root {
  /* Bone is the brand. Everyone gets the warm light ground on
     first load, regardless of OS preference — dark exists only
     as an explicit opt-in (see [data-theme='dark'] below). */
  color-scheme: light;

  /* Ground — warm bone, never pure white. Deliberately creamy;
     a neutral grey-white reads cheap next to embroidery. */
  --bg: #f3eee2;            /* page */
  --bg-raised: #faf7ef;     /* drawer, sticky surfaces */
  --bg-sunk: #eae3d3;       /* image wells, hover washes */
  --fg: #221d14;            /* warm ink, never pure black */

  /* Atmosphere — the page has a light source, not a flat fill.
     --glow lights the top of the page; --vignette deepens the
     edges of the viewport a breath; --sheen is the broad fall of
     light across the ground in body::after.

     --sheen must be a token rather than a literal white: on the
     dark ground the same value paints a pale haze over the top of
     the page, which reads as a rendering fault rather than light. */
  --glow: #f9f5ea;
  --vignette: rgba(76, 58, 32, 0.05);
  --sheen: rgba(255, 255, 255, 0.62);

  /* The ground's deepening at the bottom edge (body::after), and the
     scrim behind drawer and sheet. Both are ink-mixed here and must
     be tokens for the same reason as --sheen: mixed from a bone --fg,
     the dark theme would paint fog where it needs shadow. */
  --shade: color-mix(in srgb, var(--fg) 6%, transparent);
  --scrim: color-mix(in srgb, var(--fg) 32%, transparent);

  /* Text hierarchy. --muted passes 4.5:1 on --bg; --faint is
     decorative only — never for text that must be read. */
  --muted: #6c6250;
  --faint: #a99d85;

  /* Hairlines. --line is the working rule; --line-soft is for
     rows and inner divisions. */
  --line: #d9d1bd;
  --line-soft: #e6dfcd;

  /* The only colour on the site. Ember = low stock / warnings.
     Spent = sold out (a grey, deliberately colourless). */
  --ember: #96530f;
  --ember-wash: #efe3cc;
  --spent: #99907b;

  /* Layout */
  --max: 1440px;
  --gutter: clamp(1.25rem, 5vw, 4rem);

  /* Spacing scale — 8px base. Use these, not ad-hoc pixels. */
  --s-1: 0.25rem;   /*  4 */
  --s-2: 0.5rem;    /*  8 */
  --s-3: 0.75rem;   /* 12 */
  --s-4: 1rem;      /* 16 */
  --s-5: 1.5rem;    /* 24 */
  --s-6: 2rem;      /* 32 */
  --s-7: 3rem;      /* 48 */
  --s-8: 4rem;      /* 64 */
  --s-9: 6rem;      /* 96 */

  /* Type */
  --serif: 'Cormorant Garamond', 'Iowan Old Style', Georgia, serif;
  --sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Fluid display sizes. The display size is deliberately
     large — the serif carries the brand and should not whisper. */
  --text-display: clamp(2.75rem, 1.9rem + 5.6vw, 5.25rem);
  --text-title: clamp(2.125rem, 1.5rem + 2.75vw, 3.25rem);
  --text-heading: clamp(1.625rem, 1.375rem + 1.25vw, 2.25rem);
  --text-card: 1.4375rem;

  /* Micro-label: the small tracked uppercase voice used for nav,
     meta, buttons and table heads. */
  --label-size: 0.6875rem;
  --label-track: 0.16em;

  /* Motion — one easing, three speeds. Long and unhurried. */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --t-fast: 0.25s;   /* colour shifts, focus */
  --t-med: 0.55s;    /* underlines, drawer, overlays */
  --t-slow: 1.2s;    /* image scale, reveals */

  --header-h: 72px;
}

/* Dark is an OPT-IN, never an OS default. A page owner enables
   it by setting <html data-theme="dark">. Same relationships,
   inverted: candle-lit near-black, bone text, ember warmed up
   to hold contrast, edges falling away into shadow.

   The switch itself is instant, deliberately. The ground is built
   from layered background-images — the glow gradient, the grain,
   the sheen — and images do not interpolate: a global crossfade
   would fade the solids while the atmosphere snapped, a two-speed
   smear that reads as cheap. One clean cut, like turning off the
   light — which is also what the toggle depicts. The bulb's glass
   simply fills or empties, keyed to this attribute rather than to
   aria-pressed, so a page loaded straight into dark draws the bulb
   already off instead of lighting it and correcting. */
:root[data-theme='dark'] {
  color-scheme: dark;

  --bg: #16120d;
  --bg-raised: #1e1913;
  /* A step further from --bg than the light theme takes: hover
     washes and empty wells live down where displays crush blacks,
     and an inversion of the light theme's distance disappears. */
  --bg-sunk: #262017;
  --fg: #ece6d7;
  --glow: #1f1a12;
  --vignette: rgba(0, 0, 0, 0.3);
  /* A candle does not bleach the wall it lights. */
  --sheen: rgba(255, 245, 225, 0.03);
  /* Real shadow, not bone-mixed ink: the light theme's mixes would
     paint pale fog over a dark page. The scrim leans heavier than
     its light twin because near black there is less room to dim. */
  --shade: rgba(0, 0, 0, 0.35);
  --scrim: rgba(0, 0, 0, 0.6);
  --muted: #a49a89;
  --faint: #6d6455;
  /* Hairlines sit a step brighter than a strict inversion would put
     them, for the same black-crush reason as --bg-sunk — a rule
     that cannot be seen is a box that has quietly vanished. */
  --line: #453b29;
  --line-soft: #2d2619;
  --ember: #e0954a;
  --ember-wash: #2f2414;
  --spent: #746b5a;
}

/* ------------------------------------------------------------
   2. Base
   ------------------------------------------------------------ */

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

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

body {
  margin: 0;
  /* The ground is lit, not filled: a soft luminance wash falls
     from the top of the page down into plain bone. */
  background-color: var(--bg);
  background-image: linear-gradient(180deg, var(--glow), var(--bg) 34rem);
  background-repeat: no-repeat;
  color: var(--fg);
  font-family: var(--sans);
  font-size: 0.9375rem;
  font-weight: 400;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* The page body never scrolls horizontally; wide content
     scrolls inside its own container (.inv-scroll). */
  overflow-x: clip;
}

/* The cloth. A whisper of woven grain (inline feTurbulence at
   ~5% opacity) plus a warm vignette, fixed to the viewport and
   painted behind all content. It should never be consciously
   noticed — only missed when it's gone. Purely decorative and
   static, so it needs no reduced-motion handling. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(135% 100% at 50% 8%, transparent 55%, var(--vignette)),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='280' height='280'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)' opacity='0.05'/%3E%3C/svg%3E");
}

body.no-scroll {
  overflow: hidden;
}

img {
  display: block;
  max-width: 100%;
}

a {
  color: inherit;
  text-decoration: none;
}

p {
  margin: 0 0 1.1em;
}

button {
  font: inherit;
  color: inherit;
}

::selection {
  background: var(--fg);
  color: var(--bg);
}

/* ------------------------------------------------------------
   3. Typography
   ------------------------------------------------------------ */

h1,
h2,
h3,
h4 {
  font-family: var(--serif);
  font-weight: 300;
  line-height: 1.06;
  letter-spacing: -0.01em;
  margin: 0;
  /* Brand convention: every heading renders lowercase. */
  text-transform: lowercase;
}

h1 { font-size: var(--text-display); }
h2 { font-size: var(--text-heading); }
h3 { font-size: var(--text-card); }

/* The tracked uppercase micro-label — the site's structural
   voice. Anything navigational or informational speaks in it. */
.field-label,
.stat-label,
.nav a,
.nav button,
.section-head a,
.meta,
.site-footer {
  font-family: var(--sans);
  font-size: var(--label-size);
  font-weight: 400;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
}

.muted {
  color: var(--muted);
}

.fine {
  margin-top: var(--s-4);
  color: var(--muted);
  font-size: 0.75rem;
  line-height: 1.6;
}

/* ------------------------------------------------------------
   4. Motion primitives
   ------------------------------------------------------------
   One slow reveal, used sparingly. Content injected by app.js
   (cards, product detail) animates on insertion for free. */

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* No `both` fill mode, deliberately. `both` holds the element at
   opacity 0 before the animation starts, so anything that stops the
   animation running leaves the content permanently invisible — a
   blank page with no obvious cause. Without a fill mode the resting
   state is visible and the reveal is pure enhancement. */
.intro .wrap > *,
.section-head,
.card,
.product > *,
.prose > *,
.stats {
  animation: rise var(--t-slow) var(--ease);
}

/* A soft stagger. Enough rhythm to feel considered, not enough
   to make anyone wait. */
.intro .wrap > :nth-child(2) { animation-delay: 0.12s; }
.card:nth-child(2) { animation-delay: 0.08s; }
.card:nth-child(3) { animation-delay: 0.16s; }
.card:nth-child(4) { animation-delay: 0.24s; }
.card:nth-child(n + 5) { animation-delay: 0.3s; }
.product > :nth-child(2) { animation-delay: 0.12s; }

/* ------------------------------------------------------------
   5. Layout chrome
   ------------------------------------------------------------ */

.wrap {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* Demo banner — inverted band, quiet but unambiguous. */
.demo-banner {
  padding: 10px var(--gutter);
  background: var(--fg);
  color: var(--bg);
  font-size: var(--label-size);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: center;
  line-height: 1.7;
}

.demo-banner code,
.warn code {
  font-family: var(--mono);
  font-size: 0.9em;
  text-transform: none;
  letter-spacing: 0;
}

/* Header — sticky, translucent bone, single hairline below. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.15);
  backdrop-filter: blur(14px) saturate(1.15);
  border-bottom: 1px solid var(--line-soft);
}

.site-header .wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2) var(--s-6);
  min-height: var(--header-h);
  padding-top: var(--s-3);
  padding-bottom: var(--s-3);
}

/* Wordmark: the small S mark beside a light serif "singular".
   The mark is line art — keep it around 30px, never larger. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  font-family: var(--serif);
  font-size: 1.5rem;
  font-weight: 300;
  letter-spacing: 0.02em;
  text-transform: lowercase;
  line-height: 1;
}

/* The markup wraps the wordmark in .brand-name, and must keep doing so:
   .brand is a flex container, so the words either side of "by" would
   otherwise become anonymous flex items and the gap above would prise the
   name into three spaced-apart pieces. The wrapper needs no rule of its
   own — being a single element is the whole job. */

/* "by" joins the two names; it is not one of them. Set smaller it stops
   competing, and the eye reads "singular … syntax" as the mark.

   Sized in em, not rem, so it holds its proportion everywhere the name
   appears — 24px wordmark, 12px footer, 16px body copy — from one rule.
   The extra tracking is optical: type set this much smaller than its
   neighbours tightens up and needs the air given back. */
.brand-by {
  font-size: 0.62em;
  letter-spacing: 0.06em;
}

.brand::before {
  content: '';
  flex: none;
  width: 26px;
  height: 31px; /* native 94x111 ratio */
  background: url('../brand/logo.webp') center / contain no-repeat;
}

/* The mark is dark line art on transparency; in the opt-in dark
   theme, lift it to bone. */
:root[data-theme='dark'] .brand::before {
  filter: invert(1) sepia(0.12);
}

.nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2) clamp(1rem, 3vw, 2.5rem);
}

/* Narrow screens: the wordmark takes the first line with the cart
   pinned to its right, and the four links sit on the second line as a
   single row that must never wrap.

   Five items plus a wordmark cannot share one line at this width, and
   letting them wrap put the links on two ragged rows — the header
   grew taller than the content it sat above. Lifting the cart out of
   the flow is what buys the links their own clean line: it is an
   action rather than a destination, so it reads correctly in the
   corner and is the one control a shopper needs to find instantly. */
@media (max-width: 700px) {
  .site-header .wrap {
    position: relative;
    display: grid;
    grid-template-columns: 1fr;
    row-gap: var(--s-3);
    align-items: center;
  }

  .nav {
    flex-wrap: nowrap;
    gap: clamp(0.625rem, 3.4vw, 1.75rem);
  }

  .nav > * {
    flex: none;
    white-space: nowrap;
  }

  /* Positioned against .wrap, not .nav — the containing block is the
     padding box, so --gutter lines it up with the content column and
     the mark's own height centres it against the wordmark.

     The toggle owns the outermost corner and the cart steps inward
     past it: the icon has a fixed width, the cart label does not, so
     this is the one arrangement that needs no measuring.
     36px = the 20px icon plus a thumb's gap. */
  .nav [data-cart-open] {
    position: absolute;
    top: var(--s-3);
    right: calc(var(--gutter) + 36px);
    height: 31px;
    display: inline-flex;
    align-items: center;
  }

  /* The toggle takes the corner. Anchored to the gutter, not offset
     from the cart: two pages have no cart button, and the corner must
     hold either way. The 44px box is the touch target; the negative
     offset centres the 20px icon on the gutter line, and the top calc
     centres the box on the cart's 31px row. */
  .nav .theme-toggle {
    position: absolute;
    top: calc(var(--s-3) + (31px - 44px) / 2);
    right: calc(var(--gutter) - (44px - 20px) / 2);
    width: 44px;
    height: 44px;
    justify-content: center;
  }

  /* No underline under a lone icon — a 44px rule beneath a 20px
     circle reads as a mistake. */
  .nav .theme-toggle::after {
    content: none;
  }
}

/* Under ~400px the four links need both the gaps and the letterforms
   tightened to hold one line. Set here rather than as tokens: nothing
   else on the site needs a size below the micro-label.

   Measured against the longest set the nav can hold — MARKET, OUR
   STORY, CONTACT, INSTAGRAM — these values clear 320px with room to
   spare. Adding a fifth link would not fit, and that is the real
   constraint on this nav rather than anything about the styling. */
@media (max-width: 400px) {
  .nav {
    gap: var(--s-2);
  }

  .nav a {
    font-size: 0.625rem;
    letter-spacing: 0.08em;
  }

  /* The corner pair shares the top row with the wordmark, and the
     cart now sits 30px further in to make room for the toggle. At
     this width its label must tighten with the links or the two
     collide over a full cart. The toggle needs nothing: its mark is
     13px at any width, and its 44px target is already positioned. */
  .nav [data-cart-open] {
    font-size: 0.625rem;
    letter-spacing: 0.08em;
  }
}

.nav a,
.nav button {
  position: relative;
  padding: 2px 0;
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}

/* Hairline that draws in from the left. The site's only
   hover flourish besides the slow image scale. */
.nav a::after,
.nav button::after,
.section-head a::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -4px;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--t-med) var(--ease);
}

.nav a:hover,
.nav button:hover,
.nav a[aria-current='page'] {
  color: var(--fg);
}

.nav a:hover::after,
.nav button:hover::after,
.nav a[aria-current='page']::after,
.section-head a:hover::after {
  transform: scaleX(1);
}

/* The theme toggle is a nav sibling and speaks the nav's voice —
   micro-label, muted-to-ink hover, drawn underline — all inherited
   from the .nav button rules above. It adds only its mark: a small
   circle, half ink, half open. The one round form this system can
   justify, because it depicts the thing being switched — a light. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

/* A bulb inside a hairline ring, drawn in currentColor so it dims
   and brightens with the rest of the nav.

   20px, not the 11px a text-sized mark would take: the whole point
   is that it is recognisable at a glance, and a bulb loses its
   shape below about 16.

   The glass nearly fills the ring, and the base is a single bar. An
   earlier version had a smaller bulb and two base ticks: correct as
   a drawing, but at this size the bulb read as a dot floating in a
   circle and the two ticks blurred into one. Rendered at actual size
   and compared side by side before choosing. */
.theme-toggle-icon {
  display: block;
  flex: none;
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
}

/* The ring sits back a step so the bulb reads first — but not so far
   that it disappears at 20px. */
.tt-ring {
  stroke-width: 1.1;
  opacity: 0.62;
}

.tt-base {
  stroke-width: 1.6;
}

/* Lights on: the glass is filled. Lights off: it is an outline.
   Filled-versus-hollow is the one distinction that still survives at
   18px — rays and filaments turn to mush at this size, and a moon
   asks the viewer to decode a metaphor before they can act.

   Keyed to the root attribute, never to aria-pressed: the head
   bootstrap sets the attribute before first paint while the ARIA
   state waits for app.js, so a page loaded straight into dark draws
   the bulb already off rather than lighting it and correcting. */
.tt-glass {
  fill: currentColor;
  stroke: none;
}

:root[data-theme='dark'] .tt-glass {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
}

/* Hero — typographic, unhurried. Pages that want the brand
   photograph put it in their own .media block below. */
/* Intro — a caption, not a hero. The market is the page; this
   block is read once and left behind, so the first row of shirts
   must land inside the first viewport. */
.intro {
  padding: clamp(1.75rem, 4vw, 2.75rem) 0 clamp(1.25rem, 3vw, 2rem);
}

.intro h1 {
  font-size: var(--text-heading);
  text-wrap: balance;
}

.intro p {
  max-width: 62ch;
  margin: var(--s-3) 0 0;
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.7;
}

/* Full-bleed photographic bands.

   The height is COMPUTED rather than written as an aspect-ratio with a
   max-height cap. Those two together are not portable: given a ratio
   and a max-height that binds, browsers disagree about whether the
   width may shrink to keep the ratio true. WebKit narrows the box —
   the photograph stops reaching the edges of the window and sits
   marooned in the middle of a wide page — while Blink holds the width
   and lets the ratio give. The bug only shows on short, wide
   viewports, which is why a laptop hits it and a phone never does.

   min() says the thing once and behaves the same everywhere: always
   the full width of the window, height whichever of the two limits is
   smaller. 66.67vw is a 3:2 box at full width; 56.25vw is 16:9. */
.media-band {
  position: relative;
  width: 100%;
  height: min(82vh, 66.67vw);
  overflow: hidden;
  background: var(--bg-sunk);
}

.media-band-wide {
  height: min(70vh, 56.25vw);
}

/* A band holding more than one photograph slides between them.

   Sliding rather than cross-fading, for two reasons. It is legible as
   movement — a fade between two near-identical arrangements can be
   missed entirely, which is exactly what happened. And it degrades
   honestly: with the stylesheet missing the frames stack down the page
   as ordinary images, and with the script missing the first frame
   simply sits there. A stack of absolutely positioned layers has
   neither property; lose the CSS and every frame piles onto one spot. */
.band {
  display: flex;
  width: 100%;
  height: 100%;
  transition: transform 900ms var(--ease);
}

.band-slide {
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Two hairline marks, not dots: the site draws rules, never pills.
   The visible bar is 2px but the button is 22px square, because the
   mark is a target on a phone before it is a decoration. */
.band-dots {
  position: absolute;
  left: 50%;
  bottom: var(--s-4);
  transform: translateX(-50%);
  display: flex;
  gap: var(--s-2);
}

.band-dot {
  width: 26px;
  height: 22px;
  padding: 10px 0;
  border: 0;
  background: color-mix(in srgb, var(--fg) 30%, transparent);
  background-clip: content-box;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease);
}

.band-dot:hover,
.band-dot.is-on {
  background: var(--fg);
  background-clip: content-box;
}

/* The marks sit on the photograph, and the photograph does not
   change with the theme — so the marks may not either. In dark,
   --fg is bone, which vanishes against a bright studio shot; the
   ink of the pair is now the page ground, so the marks use that. */
:root[data-theme='dark'] .band-dot {
  background: color-mix(in srgb, var(--bg) 30%, transparent);
  background-clip: content-box;
}

:root[data-theme='dark'] .band-dot:hover,
:root[data-theme='dark'] .band-dot.is-on {
  background: var(--bg);
  background-clip: content-box;
}

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

/* Sections */
.section {
  padding: clamp(3.5rem, 9vw, 7.5rem) 0;
}

/* A section that follows the intro gives back its top padding —
   the caption already provided the air. */
.section-flush {
  padding-top: 0;
}

/* Where sections stack, a single hairline marks the seam. */
.section + .section {
  border-top: 1px solid var(--line-soft);
}

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4) var(--s-6);
  flex-wrap: wrap;
  padding-bottom: var(--s-4);
  margin-bottom: var(--s-7);
  border-bottom: 1px solid var(--line);
}

.section-head a {
  position: relative;
  color: var(--muted);
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease);
}

.section-head a:hover {
  color: var(--fg);
}

/* Footer — a slightly sunk band, like the colophon page of a
   catalogue. */
.site-footer {
  margin-top: var(--s-8);
  padding: var(--s-7) 0 var(--s-8);
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--bg-sunk) 45%, transparent);
  color: var(--muted);
}

.site-footer .wrap {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--s-2) var(--s-6);
}

.site-footer a {
  transition: color var(--t-fast) var(--ease);
}

.site-footer a:hover {
  color: var(--fg);
}

/* ------------------------------------------------------------
   6. Market
   ------------------------------------------------------------
   A narrow filter rail beside the grid on wide screens; a
   disclosure above it on small ones. Same markup, one class. */

.market {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-5);
}

.market-main {
  min-width: 0;
}

.market-bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  margin-bottom: var(--s-5);
}

/* Micro-label, like every other piece of meta on the site. Set in
   lowercase 12px it was the only thing in the bar not speaking the
   house voice, beside an uppercase FILTER. */
.market-count {
  margin: 0;
  color: var(--muted);
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

/* The toggle exists only where the rail does not. */
.filter-toggle {
  padding: 12px 0;
  border: 0;
  background: none;
  color: var(--fg);
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  cursor: pointer;
}

/* Collapsed until asked for — the market leads, the filters wait. */
.filters {
  display: none;
  min-width: 0;
}

.filters.is-open {
  display: block;
  padding-bottom: var(--s-5);
  border-bottom: 1px solid var(--line-soft);
}

/* 1100px, not 900px. The rail costs 220px plus the gap, and the
   grid needs 643px to hold two 300px columns. Between 900 and
   1090 the market therefore collapsed to a single card wider than
   any card at full desktop width — which is exactly where a
   landscape tablet sits. The rail waits until it can afford
   itself. */
@media (min-width: 1100px) {
  .market {
    grid-template-columns: 220px 1fr;
    gap: clamp(2.5rem, 5vw, 4.5rem);
    align-items: start;
  }

  .filters,
  .filters.is-open {
    display: block;
    position: sticky;
    top: calc(var(--header-h) + var(--s-5));
    max-height: calc(100vh - var(--header-h) - var(--s-7));
    overflow-y: auto;
    scrollbar-width: thin;
    padding-bottom: 0;
    border-bottom: 0;
  }

  .filter-toggle {
    display: none;
  }
}

/* Head: "Filter" left, a clear action right, under the same
   hairline the section heads use. */
.filter-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--line);
}

.filter-heading {
  font-family: var(--sans);
  font-size: var(--label-size);
  font-weight: 400;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
}

.filter-clear {
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}

.filter-clear:hover {
  color: var(--fg);
}

.filter-group {
  margin-top: var(--s-5);
}

.filter-title {
  margin-bottom: var(--s-2);
  font-family: var(--sans);
  font-size: var(--label-size);
  font-weight: 400;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--muted);
}

/* Rows read as a list, not a form: no boxes around anything,
   just a generous target and a count sitting quietly at the
   right edge. */
.filter-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 10px 0;
  font-size: 0.8125rem;
  cursor: pointer;
}

.filter-label {
  flex: 1;
  min-width: 0;
}

/* --muted, not --faint: --faint is decorative by contract, and at
   12px it fails contrast on a count the shopper is meant to read. */
.filter-count {
  color: var(--muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* The native checkbox, redrawn as a hairline square that fills
   with ink. Still a real input — focus and keyboard are free. */
.filter-box {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 15px;
  height: 15px;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: 0;
  background: none;
  cursor: pointer;
  transition:
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease);
}

.filter-row:hover .filter-box {
  border-color: var(--fg);
}

.filter-box:checked {
  background: var(--fg);
  border-color: var(--fg);
}

/* ------------------------------------------------------------
   7. Product grid
   ------------------------------------------------------------ */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  /* Gallery spacing: tall row gaps so each piece hangs alone,
     tighter columns so the row still reads as a collection. */
  gap: clamp(3rem, 6vw, 5.5rem) clamp(1.5rem, 3vw, 3rem);
}

.card {
  display: block;
  min-width: 0;
  /* Anchors the quick-add overlay. */
  position: relative;
}

/* The link wraps photo and body; quick-add is a sibling, since a
   button may not live inside an anchor. */
.card-link {
  display: block;
}

/* Photography sits in a bone well with no frame — the image is
   the luxury; the chrome stays out of its way. */
.media {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--bg-sunk);
}

.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--t-slow) var(--ease);
}

.card:hover .media img,
.card-link:focus-visible .media img {
  transform: scale(1.035);
}

/* Dark only: a studio photograph on its near-white ground becomes a
   glaring cut-out against near-black. A hairline at the boundary
   mounts the picture — an edge, not a frame — and gives an empty
   well a findable shape. The photograph itself is never dimmed or
   filtered; the goods must look like the goods. Box-sizing keeps
   the 3:4 border box, so the quick-add overlay still lands true. */
:root[data-theme='dark'] .media,
:root[data-theme='dark'] .thumb,
:root[data-theme='dark'] .prose img {
  border: 1px solid var(--line-soft);
}

/* Full-bleed bands meet the viewport at the sides; only the top
   and bottom seams need the edge. */
:root[data-theme='dark'] .media-band {
  border-top: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
}

/* A small tag set into the corner of the photograph, for the two states
   a shopper must know before they click: the run is gone, or it has not
   been made yet.

   Set in lowercase, like the product names it sits above — the micro-
   label uppercase is the voice of the interface, and this is speaking
   about the garment. A translucent bone plate with a hairline border,
   the text taking the border's colour through currentColor so the two
   can never drift apart.

   These colours do NOT follow the theme, and that is the point. The tag
   sits on product photography, and the photography is the same light
   studio shot whichever ground the page is on. Inverting with the theme
   would put bone on bone: the dark-theme ember against a pale plate
   measures 2.21:1, unreadable. Fixed, both clear 6:1 on any card in
   either theme.

   0.7 rather than the half-transparency it looks like it wants: a card
   whose photograph is dark under the corner would otherwise drag the
   plate to mid-grey and take the text with it. The blur does the rest —
   it settles the tag onto the picture instead of floating over it. */
.card-flag {
  position: absolute;
  top: var(--s-3);
  left: var(--s-3);
  z-index: 1;
  padding: 6px 12px;
  border: 1px solid currentColor;
  background: rgba(249, 246, 239, 0.7);
  -webkit-backdrop-filter: blur(6px) saturate(1.05);
  backdrop-filter: blur(6px) saturate(1.05);
  font-family: var(--sans);
  font-size: 0.625rem;
  font-weight: 400;
  letter-spacing: 0.16em;
  /* Tracking hangs off the last letter; the indent re-centres the word. */
  text-indent: 0.16em;
  text-transform: lowercase;
  /* The card link owns the click; the tag states, it does not block. */
  pointer-events: none;
}

/* Gone. A quiet warm ink — near-black would be the one flat black on a
   site built to avoid it, and this is a fact rather than an alarm. */
.card-flag-out {
  color: #3a3229;
}

/* Not yet made. Ember is the site's one colour and has always meant
   "act on this"; a pre-order makes the same claim on attention as a run
   running out, so it earns the same hue rather than a second one
   invented for it. Deepened a step from the light-theme ember, because
   a hairline border and 10px letters need more weight than a word set
   in running text. */
.card-flag-pre {
  color: #8a4c0d;
}

/* Typographic placeholder for products without photography. */
.ph {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(2.5rem, 7vw, 4rem);
  color: var(--faint);
}

.card-body {
  padding-top: var(--s-4);
}

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

.card-row h3 {
  min-width: 0;
  overflow-wrap: anywhere;
  transition: opacity var(--t-fast) var(--ease);
}

.card:hover .card-row h3 {
  opacity: 0.55;
}

.price {
  font-family: var(--sans);
  font-size: 0.8125rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Sizes left, edition status right, under a hairline. */
.card-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line-soft);
}

.meta {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  min-width: 0;
}

/* The attention signals. Low stock warms to ember and gains a
   half-step of weight — a colour shift, not a badge. Sold out
   drains to the colourless "spent" grey and stays quiet. */
.meta-low {
  color: var(--ember);
  font-weight: 500;
}

.meta-out {
  color: var(--spent);
}

/* Notices — loading / empty / error copy inside dynamic areas. */
.notice {
  grid-column: 1 / -1;
  padding: var(--s-6) 0;
  color: var(--muted);
  font-size: 0.875rem;
}

.notice p:last-child {
  margin: 0;
}

/* ------------------------------------------------------------
   8. Quick add
   ------------------------------------------------------------
   Buying from the grid without leaving it. The whole control is
   held back until the visitor engages with the card, the same
   bargain the gallery arrows strike. */

/* Spans exactly the photograph: same width and 3/4 ratio, pinned
   to the card's top, so its bottom edge is the image's bottom
   edge without ever measuring the card body. Clicks fall through
   the empty area to the card link beneath. */
.quick {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  aspect-ratio: 3 / 4;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  pointer-events: none;
}

.quick-open {
  pointer-events: auto;
  margin: var(--s-3);
  padding: 11px 14px;
  border: 0;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: var(--fg);
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  cursor: pointer;
  /* visibility rides along so the hidden bar cannot steal clicks
     meant for the photograph. */
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    visibility var(--t-fast);
}

.card:hover .quick-open,
.card:focus-within .quick-open,
.quick.is-open .quick-open {
  opacity: 1;
  visibility: visible;
}

.quick-open:hover {
  background: var(--bg);
}

/* Touch has no hover, so the bar stays put — quiet, but there.
   Where it is permanent it is also the only way to buy from the
   grid, so it earns a real target: 44px, the smallest thing a
   thumb can be asked to hit. Same for anything else that becomes
   a primary control once there is no pointer. */
@media (hover: none) {
  .quick-open {
    opacity: 1;
    visibility: visible;
  }

  .quick-open,
  .quick-size,
  .seg-btn,
  .theme-toggle {
    min-height: 44px;
  }

  .filter-row {
    padding: 12px 0;
  }
}

/* The size row rises from the bar on open. */
.quick-panel {
  display: none;
  pointer-events: auto;
  margin: 0 var(--s-3) var(--s-1);
  border: 1px solid var(--line-soft);
  background: var(--bg-raised);
}

.quick.is-open .quick-panel {
  display: flex;
  animation: rise var(--t-med) var(--ease);
}

.quick-size {
  flex: 1;
  padding: 10px 4px;
  border: 0;
  background: none;
  color: var(--fg);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition:
    background var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease);
}

.quick-size + .quick-size {
  border-left: 1px solid var(--line-soft);
}

/* Low stock keeps its ember even here; hover trades it for the
   ink fill, the same way a selected variant does. */
.quick-size.is-low {
  color: var(--ember);
  font-weight: 500;
}

.quick-size:hover {
  background: var(--fg);
  color: var(--bg);
}

/* ------------------------------------------------------------
   9. Product detail
   ------------------------------------------------------------ */

.product {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(2rem, 5vw, 3.5rem);
  padding: clamp(2rem, 6vw, 4.5rem) 0 clamp(4.5rem, 12vw, 9rem);
}

@media (min-width: 900px) {
  .product {
    grid-template-columns: 1.25fr 1fr;
    gap: clamp(3rem, 7vw, 7rem);
    align-items: start;
  }

  .product-info {
    position: sticky;
    top: calc(var(--header-h) + var(--s-7));
  }

  /* Text first, photograph second. The wide column stays with the text
     here, because this photograph is a portrait: given the wide column
     it would stand nearly 900px tall beside two short paragraphs and
     drag the section open. On narrow screens the DOM order already
     reads correctly and this does nothing. */
  .product-mirror {
    grid-template-columns: 1.15fr 0.85fr;
  }

  /* Prose in the narrow column wants a shorter measure than a product
     description, and must not stick: a two-paragraph block pinned to
     the viewport hangs in space once the photograph scrolls past. */
  .product-mirror .product-info {
    position: static;
  }
}

.product-info {
  min-width: 0;
}

.product-info h1 {
  font-size: var(--text-title);
}

.product-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  margin-top: var(--s-4);
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--line);
}

.price-lg {
  font-size: 0.9375rem;
  color: var(--fg);
}

/* Description paragraphs (rendered as p.muted by app.js). */
.product-info .muted {
  margin-top: var(--s-4);
  font-size: 0.875rem;
  line-height: 1.8;
}

/* Gallery */
.gallery {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  min-width: 0;
}

.media-lg {
  aspect-ratio: 3 / 4;
}

.thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: var(--s-3);
}

.thumb {
  padding: 0;
  border: 0;
  background: var(--bg-sunk);
  cursor: pointer;
  aspect-ratio: 1;
  overflow: hidden;
  opacity: 0.45;
  transition: opacity var(--t-med) var(--ease);
}

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

.thumb:hover,
.thumb.is-selected {
  opacity: 1;
}

/* ------------------------------------------------------------
   10. Variants
   ------------------------------------------------------------
   Each size carries its own count. Stock is primary information
   for a numbered edition — the exact figure matters to the
   customer as much as to the studio. */

.field-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  margin: var(--s-7) 0 var(--s-4);
}

.field-label {
  color: var(--fg);
}

.field-note {
  color: var(--muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.variants {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: var(--s-3);
  margin-bottom: var(--s-6);
}

.variant {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 13px 8px;
  background: none;
  border: 1px solid var(--line);
  border-radius: 0;
  color: var(--fg);
  cursor: pointer;
  transition:
    border-color var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease);
}

.variant-size {
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
}

.variant-stock {
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.variant:hover:not(:disabled) {
  border-color: var(--fg);
}

/* In stock — the default voice; nothing to prove. */
.variant.is-in .variant-stock {
  color: var(--muted);
}

/* Unlimited / pre-order — same calm register as in-stock. */
.variant.is-unlimited .variant-stock {
  color: var(--muted);
  font-style: normal;
}

/* Low — the count itself turns ember; the border warms a step. */
.variant.is-low .variant-stock {
  color: var(--ember);
  font-weight: 500;
}

.variant.is-low:not(.is-selected) {
  border-color: color-mix(in srgb, var(--ember) 42%, var(--line));
}

/* Out — struck through, drained, untouchable. */
.variant.is-out {
  color: var(--spent);
  border-color: var(--line-soft);
  cursor: not-allowed;
}

.variant.is-out .variant-size {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.variant.is-out .variant-stock {
  color: var(--spent);
}

/* Selected — full ink. Wins over every stock state. */
.variant.is-selected {
  background: var(--fg);
  border-color: var(--fg);
  color: var(--bg);
}

.variant.is-selected .variant-stock,
.variant.is-selected.is-low .variant-stock {
  color: color-mix(in srgb, var(--bg) 72%, transparent);
  font-weight: 400;
}

/* ------------------------------------------------------------
   11. Buttons
   ------------------------------------------------------------ */

.btn {
  display: inline-block;
  width: 100%;
  padding: 17px 24px;
  border: 1px solid var(--fg);
  border-radius: 0;
  background: none;
  color: var(--fg);
  font-family: var(--sans);
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  text-align: center;
  cursor: pointer;
  transition:
    background var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease),
    opacity var(--t-fast) var(--ease);
}

.btn:hover:not(:disabled) {
  background: var(--fg);
  color: var(--bg);
}

.btn-primary {
  background: var(--fg);
  color: var(--bg);
}

.btn-primary:hover:not(:disabled) {
  background: transparent;
  color: var(--fg);
}

.btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

/* Inline variant for prose/forms — hugs its label. */
.btn-inline {
  width: auto;
  padding-left: 40px;
  padding-right: 40px;
}

/* A button dressed as a footnote — for actions that inform
   rather than sell (the size guide). */
.link-quiet {
  display: inline-block;
  margin: 0 0 var(--s-5);
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 0.75rem;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-underline-offset: 3px;
  transition:
    color var(--t-fast) var(--ease),
    text-decoration-color var(--t-fast) var(--ease);
}

.link-quiet:hover {
  color: var(--fg);
  text-decoration-color: currentColor;
}

/* ------------------------------------------------------------
   12. Cart drawer
   ------------------------------------------------------------ */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: var(--scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-med) var(--ease);
}

.overlay.is-open {
  opacity: 1;
  pointer-events: auto;
}

.drawer {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 40;
  width: min(440px, 100%);
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--bg-raised);
  border-left: 1px solid var(--line-soft);
  transform: translateX(calc(100% + 1px));
  visibility: hidden;
  transition:
    transform var(--t-med) var(--ease),
    visibility var(--t-med);
}

.drawer.is-open {
  transform: translateX(0);
  visibility: visible;
}

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s-5) var(--s-6);
  border-bottom: 1px solid var(--line);
}

.drawer-head h2 {
  font-family: var(--sans);
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  font-weight: 400;
}

.drawer-head button {
  padding: 4px 8px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 1.375rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}

.drawer-head button:hover {
  color: var(--fg);
}

.drawer-body {
  flex: 1;
  overflow-y: auto;
  padding: var(--s-5) var(--s-6);
  scrollbar-width: thin;
}

.line {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4) 0;
  border-bottom: 1px solid var(--line-soft);
}

.line-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.line-text strong {
  font-family: var(--serif);
  font-size: 1.125rem;
  font-weight: 400;
  text-transform: lowercase;
  overflow-wrap: anywhere;
}

.line-text .muted {
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* "All N remaining" — the cart holds everything that exists. */
.line-warn {
  color: var(--ember);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

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

.qty button {
  width: 30px;
  height: 30px;
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}

.qty button:hover:not(:disabled) {
  color: var(--fg);
}

.qty button:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}

.qty span {
  min-width: 24px;
  text-align: center;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

.drawer-foot {
  padding: var(--s-5) var(--s-6);
  border-top: 1px solid var(--line);
}

.drawer-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--s-5);
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
}

.drawer-total span:last-child {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

.drawer-error {
  margin: var(--s-2) 0 0;
  color: var(--ember);
  font-size: 0.75rem;
}

.drawer-error:empty {
  display: none;
}

.drawer-foot .fine {
  text-align: center;
}

/* ------------------------------------------------------------
   13. Size guide
   ------------------------------------------------------------
   The cart drawer's system — scrim, raised bone, hairline
   regions — arriving as a centred sheet instead of a side panel.
   Above the drawer layers so it can open over anything. */

.sg-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-med) var(--ease);
}

.sg-overlay.is-open {
  opacity: 1;
  pointer-events: auto;
}

.sg {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 60;
  width: min(620px, calc(100% - 2 * var(--s-4)));
  max-height: calc(100vh - 2 * var(--s-4));
  max-height: calc(100dvh - 2 * var(--s-4));
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--s-5) clamp(1rem, 4vw, 2rem) var(--s-6);
  background: var(--bg-raised);
  border: 1px solid var(--line-soft);
  /* Settles upward into place, echoing the rise reveal. */
  transform: translate(-50%, calc(-50% + 14px));
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--t-med) var(--ease),
    transform var(--t-med) var(--ease),
    visibility var(--t-med);
}

.sg.is-open {
  transform: translate(-50%, -50%);
  opacity: 1;
  visibility: visible;
}

.sg-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--line);
}

.sg-head h2 {
  font-family: var(--sans);
  font-size: var(--label-size);
  font-weight: 400;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
}

.sg-close {
  padding: 4px 8px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 1.375rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}

.sg-close:hover {
  color: var(--fg);
}

.sg-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin: var(--s-5) 0 var(--s-4);
}

.sg-note {
  margin: 0 0 var(--s-4);
  color: var(--muted);
  font-size: 0.8125rem;
}

/* Hairline segmented control. On-state inverts to ink, the same
   move as a selected variant. */
.seg {
  display: inline-flex;
  border: 1px solid var(--line);
}

.seg-btn {
  padding: 8px 14px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: var(--label-size);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition:
    background var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease);
}

.seg-btn + .seg-btn {
  border-left: 1px solid var(--line);
}

/* Unit symbols are lowercase or they are not unit symbols. "CM" is
   merely shouted; "IN" reads as the preposition. */
.seg-unit .seg-btn {
  text-transform: none;
  letter-spacing: 0.02em;
}

.seg-btn:hover {
  color: var(--fg);
}

.seg-btn.is-on {
  background: var(--fg);
  color: var(--bg);
}

/* The table scrolls here on narrow screens, never the page. */
.sg-scroll {
  overflow-x: auto;
  scrollbar-width: thin;
  padding-bottom: 2px;
}

/* Same numeric voice as the inventory table; measurement cells
   reuse .inv-num for their right alignment. */
.sg-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

/* Same cell rhythm as the inventory table: two tables in one
   numeric voice should not keep two different rhythms. */
.sg-table th,
.sg-table td {
  padding: 13px 16px;
  border-bottom: 1px solid var(--line-soft);
  white-space: nowrap;
}

.sg-table thead th {
  font-weight: 400;
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--muted);
  border-bottom-color: var(--line);
}

.sg-size {
  padding-left: 0;
  text-align: left;
  font-weight: 500;
}

/* ------------------------------------------------------------
   14. Prose, forms, notices
   ------------------------------------------------------------ */

.prose {
  max-width: 62ch;
  padding: clamp(3rem, 8vw, 6.5rem) 0 clamp(4.5rem, 12vw, 9rem);
}

.prose h1 {
  margin-bottom: var(--s-6);
}

.prose h2 {
  font-size: 1.5rem;
  margin: var(--s-7) 0 var(--s-3);
}

.prose p {
  color: color-mix(in srgb, var(--fg) 82%, var(--bg));
  line-height: 1.8;
}

.prose img {
  margin: var(--s-6) 0;
  background: var(--bg-sunk);
}

.prose a {
  border-bottom: 1px solid var(--line);
  transition: border-color var(--t-fast) var(--ease);
}

.prose a:hover {
  border-color: currentColor;
}

.form {
  margin: var(--s-6) 0 var(--s-7);
}

.field {
  display: block;
  margin-bottom: var(--s-5);
}

.field .field-label {
  display: block;
  margin-bottom: var(--s-2);
  color: var(--muted);
}

.field input,
.field textarea,
.field select {
  font: inherit;
  width: 100%;
  padding: 12px 0;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: none;
  color: var(--fg);
  transition: border-color var(--t-fast) var(--ease);
}

.field input::placeholder,
.field textarea::placeholder {
  color: var(--faint);
}

.field input:focus,
.field textarea:focus,
.field select:focus {
  outline: none;
  border-bottom-color: var(--fg);
}

.field textarea {
  resize: vertical;
  min-height: 6em;
}

/* Warning block — configuration/setup notes, demo caveats. */
.warn {
  margin-bottom: var(--s-6);
  padding: var(--s-4) var(--s-5);
  border-left: 2px solid var(--ember);
  background: var(--ember-wash);
  font-size: 0.8125rem;
  line-height: 1.7;
}

.warn p:last-child {
  margin: 0;
}

/* ------------------------------------------------------------
   15. Inventory
   ------------------------------------------------------------ */

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-bottom: var(--s-7);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-6) var(--s-5) var(--s-6) 0;
}

.stat-num {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(2.25rem, 4vw, 2.75rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.stat-label {
  color: var(--muted);
}

/* The table scrolls inside this container; the page never does. */
.inv-scroll {
  overflow-x: auto;
  scrollbar-width: thin;
  /* Room for the focus ring on row links. */
  padding-bottom: 2px;
}

.inv {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

.inv th,
.inv td {
  padding: 13px 16px;
  border-bottom: 1px solid var(--line-soft);
  white-space: nowrap;
}

.inv thead th {
  font-weight: 400;
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--muted);
  border-bottom-color: var(--line);
}

/* Numeric cells — everything except the product name. */
.inv-num {
  text-align: right;
}

/* Product column stays pinned while sizes scroll beneath it. */
.inv-name {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bg);
  padding-left: 0;
  text-align: left;
  font-family: var(--serif);
  font-size: 1.125rem;
  text-transform: lowercase;
}

/* The header and footer of the name column speak micro-label,
   not serif — only product names get the serif voice. */
.inv thead .inv-name {
  font-family: var(--sans);
  font-size: var(--label-size);
}

.inv-name a {
  transition: opacity var(--t-fast) var(--ease);
}

.inv-name a:hover {
  opacity: 0.6;
}

.inv-tag {
  display: block;
  font-family: var(--sans);
  font-size: 0.625rem;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--faint);
}

.inv tbody tr:hover td {
  background: var(--bg-sunk);
}

/* Cell states. Low keeps its wash even on row hover so the
   signal never disappears under the cursor. */
.inv-low,
.inv tbody tr:hover td.inv-low {
  color: var(--ember);
  font-weight: 500;
  background: var(--ember-wash);
}

.inv-out {
  color: var(--spent);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.inv-na {
  color: var(--faint);
}

.inv-unlimited,
.inv-sold {
  color: var(--muted);
}

.inv-total {
  border-left: 1px solid var(--line-soft);
}

.inv tfoot td,
.inv tfoot th {
  border-top: 1px solid var(--fg);
  border-bottom: 0;
  color: var(--fg);
  font-weight: 500;
}

.inv tfoot .inv-name {
  font-family: var(--sans);
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  font-weight: 400;
}

/* ------------------------------------------------------------
   16. Accessibility
   ------------------------------------------------------------ */

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

/* Filled controls: pull the ring in so the gap reads against
   the surrounding page, not the ink fill. */
.btn-primary:focus-visible,
.variant.is-selected:focus-visible,
.seg-btn.is-on:focus-visible,
.filter-box:checked:focus-visible {
  outline-offset: 2px;
}

.demo-banner a:focus-visible {
  outline-color: var(--bg);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ------------------------------------------------------------
   17. Print — a stockroom might want the inventory on paper.
   ------------------------------------------------------------ */

@media print {
  .site-header,
  .site-footer,
  .demo-banner,
  .drawer,
  .overlay,
  .sg,
  .sg-overlay,
  .quick,
  .btn {
    display: none !important;
  }

  body {
    background: #fff;
    color: #000;
  }

  .inv-scroll {
    overflow: visible;
  }

  .gallery-arrow {
    display: none !important;
  }
}

/* ------------------------------------------------------------
   The ground
   ------------------------------------------------------------
   No pattern, no shapes, no motion. A luxury surface is a good
   material lit well — cloth under a window — and anything drawn
   on top of that reads as dirt on the screen.

   So: warm bone, one broad soft light falling from upper left,
   the ground deepening at the bottom edge, and the existing grain
   for tooth. Large radii and single digit alpha values are
   deliberate — no edge should ever be locatable. If you can point
   at where it starts, it is too much.

   There was a third layer here, an ember-tinted fall from the
   right. It is gone on purpose: ember is the one colour on this
   site and it means "this run is nearly over". Spending it on
   atmosphere costs the signal everywhere it actually matters. */

body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(120% 85% at 18% -10%, var(--sheen) 0%, transparent 58%),
    radial-gradient(140% 110% at 50% 115%, var(--shade) 0%, transparent 60%);
}

/* ------------------------------------------------------------
   Product gallery
   ------------------------------------------------------------
   Every frame is in the DOM at once and cross-faded. Swapping the
   src of a single <img> meant each click started a fresh download
   of a full-size photograph, which felt broken. */

.gallery-frame {
  position: relative;
  overflow: hidden;
}

.gallery-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--t-med) var(--ease);
}

.gallery-layer.is-active {
  opacity: 1;
}

.gallery-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  cursor: pointer;
  color: var(--fg);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  /* Held back until the visitor engages with the image. */
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease);
}

.gallery-arrow svg {
  width: 17px;
  height: 17px;
}

.gallery-prev {
  left: var(--s-3);
}

.gallery-next {
  right: var(--s-3);
}

.gallery-frame:hover .gallery-arrow,
.gallery-arrow:focus-visible {
  opacity: 1;
}

.gallery-arrow:hover {
  background: var(--bg);
}

/* Touch has no hover, so the arrows stay put. */
@media (hover: none) {
  .gallery-arrow {
    opacity: 1;
  }
}
