/* ============================================================
   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);

  /* 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. */
:root[data-theme='dark'] {
  color-scheme: dark;

  --bg: #16120d;
  --bg-raised: #1e1913;
  --bg-sunk: #211c14;
  --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);
  --muted: #a49a89;
  --faint: #6d6455;
  --line: #373021;
  --line-soft: #29241a;
  --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);
}

.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);
}

/* 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;
}

/* 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);
}

/* 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 {
    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));
  }
}

.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: color-mix(in srgb, var(--fg) 32%, transparent);
  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: color-mix(in srgb, var(--fg) 32%, transparent);
  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%,
      color-mix(in srgb, var(--fg) 6%, transparent) 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;
  }
}
