/* Korai Studio — storefront
   Palette is sampled directly from the logo (app/static/img/logo.png),
   not approximated: Cream #FFF3DB · Forest #044B09 · Terracotta #DE8672.
   --charcoal was near-black (#232323); replaced with a deep brick red
   (#6B2419) — it's the site's one "ink" color, doing double duty as body
   text *and* solid UI blocks (buttons, the announce bar, the subscribe
   section), so it had to stay dark enough to read as text: contrast
   against cream is 10.1:1, still past WCAG AAA's 7:1 for body copy, not
   just safe as an accent.
   Variable names (--olive/--rose/--charcoal) predate this palette and
   are kept as-is to avoid renaming ~50 call sites across this file,
   admin.css, and templates for a cosmetic-only change — what matters is
   the value. --gold is unused here (kept only because admin.css references it). */

:root {
  --cream:      #FFF3DB;
  --cream-deep: #F5E4C3;
  --charcoal:   #6B2419;
  --ink-soft:   #6A6560;
  --olive:      #044B09;
  --olive-deep: #033807;
  --rose:       #DE8672;
  --gold:       #C9A35C;
  --line:       #E9D9BC;

  --display: "Cormorant Garamond", Georgia, serif;
  --body:    "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  --gutter: 1.25rem;
  --measure: 1180px;
}

@media (min-width: 900px) { :root { --gutter: 2.5rem; } }

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

/* Content is capped at --measure (1180px) and centered for a readable
   line length — on wide monitors that leaves real empty margin either
   side of it. Rather than stretch the layout (which would make text
   uncomfortably wide to read), fill that margin with a very faint
   Leheriya-inspired wave texture instead, so it reads as deliberate
   canvas rather than blank space. Kept at ~3% opacity specifically so it
   never fights with body text where sections don't have their own solid
   background — this is background texture, not a foreground pattern. */
body {
  margin: 0;
  background-color: var(--cream);
  background-image:
    repeating-linear-gradient(135deg, rgba(4,75,9,.09) 0, rgba(4,75,9,.09) 2px, transparent 2px, transparent 30px),
    repeating-linear-gradient(45deg, rgba(222,134,114,.08) 0, rgba(222,134,114,.08) 2px, transparent 2px, transparent 30px);
  color: var(--charcoal);
  font-family: var(--body);
  font-size: 15px;
  font-weight: 400;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

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

.skip {
  position: absolute; left: -999px;
  background: var(--charcoal); color: var(--cream);
  padding: .75rem 1rem; z-index: 100;
}
.skip:focus { left: 1rem; top: 1rem; }

/* Decorative figures in the empty side margins (margin-figure-left.svg /
   -right.svg — reuse art from the footer illustration, different figure
   pairs on each side so they don't read as a mirrored duplicate). Fixed
   so they stay put while the page scrolls, pointer-events:none since
   they're purely decorative and must never intercept a click meant for
   content behind them.

   Visibility is NOT a fixed CSS breakpoint — different pages have
   different content widths (.wrap vs .wrap-wide), so a single viewport
   threshold would either hide these on narrower pages that actually have
   room, or risk overlap on wider ones. korai.js measures the real gap
   next to whatever .wrap is on the current page and toggles `.is-visible`
   accordingly, on load and on resize. Base rule below only sets shape/
   position; `display` stays none until JS confirms there's room.

   The right figure's animation is offset by half a cycle (delay = -3.5s
   into a 7s loop) purely so the two sides don't bob in lockstep.
   prefers-reduced-motion already kills the float animation via the
   global override at the bottom of this file. */
.margin-figure {
  display: none; position: fixed; top: 50%;
  width: 90px; height: auto; z-index: 5; opacity: .9;
  pointer-events: none; user-select: none;
  animation: margin-figure-float 7s ease-in-out infinite;
}
.margin-figure.is-visible { display: block; }
.margin-figure-left { left: 0; }
.margin-figure-right { right: 0; animation-delay: -3.5s; }
@keyframes margin-figure-float {
  0%, 100% { transform: translateY(-50%); }
  50% { transform: translateY(calc(-50% - 14px)); }
}

/* ------------------------------------------------------------ header */

.announce {
  background: var(--olive);
  color: var(--cream);
  text-align: center;
  font-size: .68rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  padding: .6rem 1rem;
}

.site-header {
  position: sticky; top: 0; z-index: 40;
  display: grid;
  grid-template-columns: 2rem 1fr auto;
  align-items: center;
  gap: 1rem;
  padding: 1rem var(--gutter);
  background: rgba(255,243,219, .92);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}

.wordmark { display: flex; justify-content: center; }
.wordmark img { height: 44px; width: auto; }

.menu-toggle {
  display: grid; gap: 5px;
  background: none; border: 0; padding: 0; cursor: pointer;
  width: 22px;
}
.menu-toggle span { height: 1px; background: var(--charcoal); display: block; }

.nav {
  position: fixed; inset: 0 0 0 -100%;
  background: var(--cream);
  display: flex; flex-direction: column;
  gap: .5rem;
  padding: 6rem var(--gutter);
  font-family: var(--display);
  font-size: 2rem;
  transition: inset .35s cubic-bezier(.2,.8,.2,1);
  z-index: 45;
}
.nav[data-open] { inset: 0; }

/* Collections dropdown — mobile-first: the overlay nav is already a
   scrollable vertical list, so the panel just renders always-expanded
   inline (no toggle, no extra tap target). Desktop turns it into an
   actual hover/focus popup — see the media query below. Either way,
   zero JS: :hover and :focus-within do the whole job on desktop. */
.dropdown-trigger {
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
  background: none; border: 0; padding: 0; margin: 0; cursor: default;
}
.dropdown-panel { margin: .35rem 0 0 .1rem; }
.dropdown-group { margin: .6rem 0; }
.dropdown-heading {
  font-family: var(--body); font-size: .68em; letter-spacing: .14em;
  text-transform: uppercase; color: var(--charcoal); margin: 0 0 .4rem;
}
.dropdown-panel a { display: block; padding: .2rem 0; font-size: .82em; }

.header-actions { display: flex; align-items: center; gap: 1.1rem; }

.account-link {
  font-size: .72rem;
  letter-spacing: .14em;
  text-transform: uppercase;
}

.cart-link {
  font-size: .72rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  display: inline-flex; align-items: center; gap: .4rem;
}
.cart-count {
  background: var(--olive); color: var(--cream);
  border-radius: 999px;
  min-width: 1.15rem; height: 1.15rem;
  display: grid; place-items: center;
  font-size: .62rem;
}

@media (min-width: 900px) {
  .site-header { grid-template-columns: 1fr auto 1fr; }
  .menu-toggle { display: none; }
  .wordmark { order: -1; justify-content: flex-start; }
  .wordmark img { height: 52px; }
  .nav {
    position: static; inset: auto;
    flex-direction: row; align-self: stretch; align-items: center; gap: 2rem;
    padding: 0; background: none;
    font-family: var(--body);
    font-size: .74rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    justify-content: center;
  }
  .header-actions { justify-self: end; }

  /* `align-self: stretch` (on both .nav and .dropdown) makes .dropdown's
     own box span the full header row height, not just its trigger text's
     line height — so the absolutely-positioned panel's `top: 100%` lands
     exactly at the header's bottom edge on its own, no guessed pixel
     margin needed to clear the (taller, logo-driven) header above it. */
  .dropdown { position: relative; align-self: stretch; display: flex; align-items: center; }
  .dropdown-trigger { cursor: pointer; }
  .dropdown-panel {
    display: none;
    position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
    margin-top: 1.5rem; /* clears .site-header's padding-bottom (1rem) plus a small visual gap */
    background: var(--cream); border: 1px solid var(--line);
    padding: 1.5rem 1.4rem 1.1rem; min-width: 190px;
    box-shadow: 0 16px 32px rgba(107,36,25,.1);
    text-align: left; z-index: 46;
  }
  .dropdown:hover .dropdown-panel, .dropdown:focus-within .dropdown-panel { display: block; }
  .dropdown-group { margin: 0; }
  .dropdown-group + .dropdown-group { margin-top: .9rem; padding-top: .9rem; border-top: 1px solid var(--line); }
  .dropdown-panel a {
    font-size: .74rem; letter-spacing: .06em; text-transform: none;
    padding: .3rem 0;
  }
}

.flash {
  background: var(--olive);
  color: var(--cream);
  padding: .8rem var(--gutter);
  font-size: .8rem;
}

/* -------------------------------------------------------------- type */

h1, h2, h3 { font-family: var(--display); font-weight: 300; margin: 0; }
h1 { font-size: clamp(2.5rem, 9vw, 4.75rem); line-height: 1.02; }
h2 { font-size: clamp(1.7rem, 4.5vw, 2.5rem); line-height: 1.15; }

.eyebrow {
  font-size: .66rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0 0 1rem;
}

/* Solid backdrop on the reading column itself — without this, sections
   that put .wrap directly on the <section> (no separate inner div) show
   body's margin texture uniformly across their full width, so there's no
   visible line between "margin" and "content" and the texture reads as
   faint everywhere rather than filling the empty space specifically. */
.wrap { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter); background: var(--cream); }

/* Tried widening this to 1400px so the filter sidebar + 3-up grid had
   more room. Reverted: a sidebar + 3 cards don't actually need much past
   --measure's 1180px, so the extra width mostly just created new empty
   space elsewhere (widest visibly in .page-head) rather than being used
   by anything. Kept as its own class rather than deleted in case a wider
   grid (4-up, more filters) makes the extra room earn its keep later. */
.wrap-wide { max-width: var(--measure); }

/* ---------------------------------------------------------- carousel */

/* Slides stack full-bleed via .carousel-track's transform (set by
   korai.js) rather than CSS scroll-snap — scroll-snap fights the
   auto-advance/pause-on-video behaviour a real hero carousel needs, and
   this app already decided a little JS is worth it here (see korai.js). */
.carousel {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  outline: none;
}
@media (min-width: 700px) { .carousel { aspect-ratio: 16 / 8; } }

.carousel-track {
  display: flex;
  height: 100%;
  transition: transform .6s cubic-bezier(.2,.8,.2,1);
}
.carousel-slide {
  position: relative;
  flex: 0 0 100%;
  height: 100%;
}
.carousel-slide img, .carousel-slide video {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* A slide with no caption (its text is already baked into the banner
   image) is wrapped whole in an <a> instead — needs to fill the slide
   like the media it contains, not sit inline at its text line-height. */
.carousel-slide > a { display: block; height: 100%; }
.carousel-caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 2rem var(--gutter) clamp(2rem, 6vw, 3.5rem);
  background: linear-gradient(0deg, rgba(35,20,15,.55) 0%, rgba(35,20,15,0) 65%);
  color: var(--cream);
}
.carousel-caption .eyebrow { margin-bottom: .5rem; }
.carousel-caption h2 { color: var(--cream); font-size: clamp(1.5rem, 5vw, 2.5rem); max-width: 20ch; }
.carousel-caption .btn { margin-top: 1.25rem; background: var(--cream); color: var(--charcoal); border-color: var(--cream); }
.carousel-caption .btn:hover { background: var(--olive-deep); border-color: var(--olive-deep); color: var(--cream); }

.carousel-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 2.5rem; height: 2.5rem;
  display: grid; place-items: center;
  background: rgba(255,243,219,.85);
  border: 0; border-radius: 999px;
  font-size: 1.1rem; cursor: pointer;
  color: var(--charcoal);
}
.carousel-arrow.prev { left: 1rem; }
.carousel-arrow.next { right: 1rem; }
.carousel-arrow:hover { background: var(--cream); }

.carousel-dots {
  position: absolute; bottom: .9rem; left: 50%; transform: translateX(-50%);
  display: flex; gap: .5rem;
}
.carousel-dots button {
  width: .5rem; height: .5rem; border-radius: 999px;
  background: rgba(255,243,219,.5);
  border: 0; padding: 0; cursor: pointer;
}
.carousel-dots button[aria-current="true"] { background: var(--cream); width: 1.25rem; }

/* -------------------------------------------------------------- hero */

.hero {
  padding: clamp(3.5rem, 12vw, 8rem) var(--gutter) clamp(3rem, 8vw, 5rem);
  max-width: 900px;
}
.hero h1 em { font-style: italic; color: var(--olive-deep); }
.hero p {
  max-width: 46ch;
  margin: 1.75rem 0 2.25rem;
  color: var(--ink-soft);
  font-size: 1rem;
}

.btn {
  display: inline-block;
  font-size: .72rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  padding: .95rem 2rem;
  border: 1px solid var(--charcoal);
  background: var(--charcoal);
  color: var(--cream);
  cursor: pointer;
  font-family: var(--body);
  transition: background .2s, color .2s;
}
.btn:hover { background: var(--olive-deep); border-color: var(--olive-deep); }
.btn.ghost { background: none; color: var(--charcoal); }
.btn.ghost:hover { background: var(--charcoal); color: var(--cream); }
.btn[disabled] { opacity: .4; cursor: not-allowed; }
.btn.wide { width: 100%; text-align: center; }

/* ------------------------------------------------------------- grid */

.section { padding: clamp(3rem, 8vw, 5.5rem) 0; }

.section-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 1rem; margin-bottom: 2.5rem;
  border-bottom: 1px solid var(--line);
  padding-bottom: 1rem;
}
.section-head a { font-size: .7rem; letter-spacing: .14em; text-transform: uppercase; }

/* Product grid — 3 per row everywhere it's used (home, /shop, related
   products) once there's room; 2 on narrow phones only. Deliberately not
   4-up at wide viewports, so the row count stays 3 regardless of screen
   size instead of reflowing at the 1080px breakpoint. */
.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.25rem 1rem;
}
@media (min-width: 640px) { .grid { grid-template-columns: repeat(3, 1fr); gap: 2.5rem 1.75rem; } }

/* /shop, /shop/{category}: filter sidebar + product grid. Stacked
   (filters above grid) until there's room for both side by side — this
   is also what puts the wide side margins from .wrap's max-width to use
   on large screens instead of leaving them empty. */
.shop-layout { display: flex; flex-direction: column; gap: 2rem; padding-bottom: 3rem; }
@media (min-width: 860px) {
  .shop-layout { flex-direction: row; align-items: flex-start; gap: 3rem; }
  .filters { flex: 0 0 220px; position: sticky; top: 6.5rem; }
  .shop-main { flex: 1 1 auto; min-width: 0; }
}

.filters form { display: flex; flex-direction: column; gap: 1.5rem; }
.filter-group h3 {
  font-family: var(--body); font-size: .68rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-soft); margin-bottom: .6rem;
}
.filter-group select {
  width: 100%; font-family: var(--body); font-size: .82rem;
  border: 1px solid var(--line); background: var(--cream); padding: .55rem .6rem;
}
.price-range { display: flex; align-items: center; gap: .5rem; }
.price-range input {
  width: 0; flex: 1 1 auto; font-family: var(--body); font-size: .82rem;
  border: 1px solid var(--line); background: var(--cream); padding: .55rem .6rem;
}
.price-range span { color: var(--ink-soft); }
.filters .btn { width: 100%; }
.clear-filters {
  text-align: center; font-size: .72rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-soft); text-decoration: underline;
}
.clear-filters:hover { color: var(--charcoal); }

/* Closes out a product-list page once the pieces run out — one line,
   different per collection (app/copy.py), so browsing a category feels
   like it ends on a note instead of just stopping. */
.shop-quote {
  margin: 3rem 0 4rem;
  padding-top: 2.5rem;
  border-top: 1px solid var(--line);
  text-align: center;
}
.shop-quote p {
  font-family: var(--display);
  font-style: italic;
  font-size: clamp(1.15rem, 3vw, 1.5rem);
  max-width: 46ch;
  margin: 0 auto .75rem;
  line-height: 1.4;
}
.shop-quote cite {
  display: block;
  font-style: normal;
  font-size: .68rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* The card is the signature: a hairline index, then the name set in
   Cormorant, so the shop floor reads like a contents page. */
.card { position: relative; }
.card-media {
  position: relative;
  aspect-ratio: 3 / 4;
  background: var(--cream-deep);
  overflow: hidden;
}
.card-media img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform .8s cubic-bezier(.2,.8,.2,1);
}
.card:hover .card-media img { transform: scale(1.04); }

.card-index {
  position: absolute; top: .7rem; left: .7rem;
  font-size: .6rem; letter-spacing: .18em;
  color: var(--cream);
  mix-blend-mode: difference;
}
.card-flag {
  position: absolute; bottom: 0; left: 0; right: 0;
  background: rgba(107,36,25,.82); color: var(--cream);
  font-size: .6rem; letter-spacing: .16em; text-transform: uppercase;
  text-align: center; padding: .5rem;
}

.card-body { padding-top: .85rem; }
.card-name {
  font-family: var(--display);
  font-size: 1.2rem;
  line-height: 1.2;
}
.card-sub { color: var(--ink-soft); font-size: .74rem; margin: .2rem 0 .45rem; }
.card-price { font-size: .8rem; letter-spacing: .04em; }
.card-price s { color: var(--ink-soft); margin-left: .4rem; font-size: .72rem; }
.card-price .discount-badge { padding: .1rem .35rem; font-size: .6rem; margin-left: .4rem; }

.card::after { content: ""; position: absolute; inset: 0; }

/* ----------------------------------------------------------- product */

.product { display: grid; gap: 2.5rem; padding: 2.5rem 0 4rem; }
@media (min-width: 900px) {
  .product { grid-template-columns: 1.15fr 1fr; gap: 4rem; align-items: start; }
  .product-info { position: sticky; top: 6rem; }
}

/* Product gallery — one .gallery-item per image/video, only `.active`
   shown at a time, switched by the thumbnail strip (korai.js). Falls
   back gracefully with a single item and no thumbs at all when a
   product only has one photo (still the case for all 8 seeded products
   today), which is exactly what rendered before this was built out. */
.gallery-main { position: relative; }
.gallery-item { display: none; }
.gallery-item.active { display: block; }
.gallery-item video { width: 100%; aspect-ratio: 3/4; object-fit: cover; background: var(--cream-deep); }

/* Hover-zoom: overflow:hidden crops the scaled-up image to the frame;
   korai.js tracks the cursor and sets --zoom-x/--zoom-y so the zoomed
   view follows the pointer instead of always zooming from center. Gated
   to real hover devices — on touch, `:hover` never fires anyway, but
   being explicit here avoids the exact class of bug hit earlier this
   session with hover-only content on mobile (see .story-details). */
.gallery-zoom {
  overflow: hidden; aspect-ratio: 3/4; background: var(--cream-deep);
  --zoom-x: 50%; --zoom-y: 50%;
}
.gallery-zoom img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .2s ease; transform-origin: var(--zoom-x) var(--zoom-y);
  -webkit-user-drag: none; user-select: none; -webkit-touch-callout: none;
}
@media (hover: hover) and (pointer: fine) {
  .gallery-zoom:hover img { transform: scale(2.2); cursor: zoom-in; }
}

.gallery-thumbs { display: flex; gap: .6rem; margin-top: .75rem; flex-wrap: wrap; }
.gallery-thumb {
  position: relative; width: 68px; aspect-ratio: 3/4; padding: 0; border: 1px solid var(--line);
  background: var(--cream-deep); cursor: pointer; overflow: hidden; opacity: .65;
  transition: opacity .15s ease, border-color .15s ease;
}
.gallery-thumb img, .gallery-thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-thumb.active, .gallery-thumb:hover { opacity: 1; border-color: var(--charcoal); }
.gallery-thumb .thumb-play {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,.25); color: #fff; font-size: .55rem;
}

.product-info h1 { font-size: clamp(2rem, 5vw, 2.9rem); }
.stars { color: var(--gold); letter-spacing: .1em; font-size: 1rem; }
/* Social proof, placed above the price on purpose — see product.html.
   A link (not a plain span) straight to #reviews below. */
.rating-summary {
  display: flex; align-items: center; gap: .45rem; margin-top: .6rem;
  color: var(--charcoal); text-decoration: none;
}
.rating-summary:hover .rating-summary-text { text-decoration: underline; }
.rating-summary-text { font-size: .78rem; color: var(--ink-soft); }
.product-price { font-size: 1.05rem; margin: 1rem 0 .5rem; }
.product-price s { color: var(--ink-soft); font-size: .85rem; margin-left: .5rem; }
.free-delivery-note {
  font-size: .8rem; font-weight: 600; color: var(--olive); margin: 0 0 1.75rem;
}
.discount-badge {
  display: inline-block; margin-left: .6rem; padding: .2rem .5rem;
  background: var(--olive); color: var(--cream);
  font-family: var(--body); font-size: .68rem; font-weight: 600;
  letter-spacing: .04em; border-radius: 3px; vertical-align: middle;
}
.surcharge-note {
  display: block; font-family: var(--body); font-size: .78rem;
  color: var(--ink-soft); margin-top: .3rem;
}

/* "Size" + the "Size guide" trigger — was riding on .eyebrow (font-size
   .66rem, light --ink-soft), fine for a decorative tag elsewhere on the
   site but too small/low-contrast for what's functionally a form label
   here. Own class instead of touching .eyebrow globally. */
.size-label {
  font-size: .85rem; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--charcoal); margin: 0 0 .5rem;
}
.size-guide-link {
  text-transform: none; letter-spacing: 0; font-weight: 400;
  border: 0; border-bottom: 1px solid var(--olive-deep); margin-left: .6rem;
  background: none; padding: 0; font: inherit; font-size: .8rem;
  color: var(--olive-deep); cursor: pointer;
}
.size-guide-link:hover { color: var(--olive); border-color: var(--olive); }

.sizes { display: flex; flex-wrap: wrap; gap: .5rem; margin: .75rem 0 1.5rem; }
.size-opt input { position: absolute; opacity: 0; }
.size-opt label {
  display: grid; place-items: center;
  min-width: 3.1rem; padding: .65rem .8rem;
  border: 1px solid var(--line);
  font-size: .76rem; letter-spacing: .08em; white-space: nowrap;
  cursor: pointer;
  transition: border-color .15s, background .15s;
}
.size-opt input:checked + label { border-color: var(--charcoal); background: var(--charcoal); color: var(--cream); }
.size-opt input:focus-visible + label { outline: 2px solid var(--olive); outline-offset: 2px; }
.size-opt input:disabled + label {
  color: var(--ink-soft); cursor: not-allowed;
  background: repeating-linear-gradient(-45deg, transparent 0 5px, var(--line) 5px 6px);
}

.custom-measurements { margin: -.5rem 0 1.5rem; padding: 1rem; background: var(--cream-deep); }
.custom-fields { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; }
.custom-fields label { display: block; font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); }
.custom-fields input {
  display: block; width: 100%; margin-top: .35rem; padding: .5rem .6rem;
  font-family: var(--body); font-size: .9rem; border: 1px solid var(--line); background: var(--cream);
}
@media (max-width: 480px) { .custom-fields { grid-template-columns: 1fr; } }

.trust-badges {
  list-style: none; margin: 1.75rem 0 0; padding: 1.5rem 0 0; border-top: 1px solid var(--line);
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; text-align: center;
}
.trust-badges li { display: flex; flex-direction: column; align-items: center; gap: .5rem; }
.trust-badges svg { width: 26px; height: 26px; color: var(--charcoal); }
.trust-badges span {
  font-size: .66rem; letter-spacing: .04em; color: var(--ink-soft); line-height: 1.3;
}
@media (max-width: 420px) { .trust-badges { grid-template-columns: repeat(2, 1fr); row-gap: 1.25rem; } }

/* Size guide — native <dialog> rather than a hand-rolled overlay div:
   free backdrop, free Escape-to-close, free focus trapping, and
   .showModal()/.close() in korai.js is the entire JS footprint. Body is
   the scrollable part (max-height + overflow-y), header stays pinned, so
   a long guide never pushes the close button off-screen. */
.size-guide-dialog {
  padding: 0; border: 0; border-radius: 4px; max-width: 480px; width: calc(100% - 2.5rem);
  max-height: 80vh; box-shadow: 0 24px 60px rgba(0,0,0,.25);
}
.size-guide-dialog::backdrop { background: rgba(35,20,15,.55); }
.size-guide-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.1rem 1.4rem; border-bottom: 1px solid var(--line); background: var(--cream);
  position: sticky; top: 0;
}
.size-guide-head h2 { font-size: 1.2rem; margin: 0; }
.size-guide-close {
  background: none; border: 0; font-size: 1.5rem; line-height: 1; cursor: pointer;
  color: var(--ink-soft); padding: .2rem .4rem;
}
.size-guide-close:hover { color: var(--charcoal); }
.size-guide-body { padding: 1.4rem; overflow-y: auto; max-height: calc(80vh - 4rem); }

/* Community reviews — replaces the old "Goes with" section's spot below
   the product grid. .section-head is the same header style already used
   for "The first collection" etc. --gold exists as a palette variable
   but was otherwise unused site-wide (see the palette comment at the top
   of this file) — stars are a fitting, honest use for it rather than
   reaching for --olive and making it do double duty. */
/* Fabric band — 3-panel strip (photo / olive / rose), same "wide
   editorial band" idea as a reference layout the user shared, but not a
   copy of it: original icons (drawn here, not the reference's), the
   photo panel uses this product's *own* cover image rather than a
   stock/generic texture shot, and the copy correctly says Cotton — this
   catalog has no linen pieces. Stays within .wrap's own content width
   like every other section on this page (Reviews, Style Suggestions,
   Community Styles) rather than bleeding to the true viewport edge —
   the `100vw` full-bleed trick was tried first and caused a real
   horizontal-scroll bug (100vw doesn't account for the scrollbar's own
   width, so it overflows the actual visible viewport by however wide
   the scrollbar is). Not worth that fragility for a decorative band. */
.fabric-band {
  display: grid; grid-template-columns: 1fr 1fr 1fr;
  margin-top: 3rem; border-radius: 4px; overflow: hidden;
}
.fabric-panel { position: relative; min-height: 280px; padding: 2rem; display: flex; flex-direction: column; }
.fabric-panel-photo { padding: 0; }
.fabric-panel-photo img { width: 100%; height: 100%; object-fit: cover; display: block; -webkit-user-drag: none; }
.fabric-panel-label {
  position: absolute; left: 1.5rem; bottom: 1.5rem; color: #fff;
  font-family: var(--display); font-size: 1.5rem; font-weight: 600;
  text-shadow: 0 2px 12px rgba(0,0,0,.45);
}
.fabric-panel-olive { background: var(--olive); color: var(--cream); justify-content: flex-end; }
.fabric-panel-rose { background: var(--rose); color: var(--charcoal); justify-content: flex-end; }
.fabric-icon { width: 36px; height: 36px; margin-bottom: 1.5rem; }
.fabric-panel h3 { font-size: 1.1rem; margin-bottom: .6rem; }
.fabric-panel-olive h3 { color: var(--cream); }
.fabric-panel p { font-size: .86rem; line-height: 1.55; max-width: 30ch; }
.fabric-panel-olive p { color: rgba(255,243,219,.85); }
.fabric-panel-rose p { color: var(--charcoal); }
@media (max-width: 800px) {
  .fabric-band { grid-template-columns: 1fr; }
  .fabric-panel { min-height: 220px; }
}

.reviews { padding: 3rem 0; border-top: 1px solid var(--line); margin-top: 3rem; }
.reviews-avg { font-size: .8rem; color: var(--ink-soft); white-space: nowrap; }
.reviews-empty { color: var(--ink-soft); font-size: .88rem; margin-bottom: 2rem; }

.review-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 2rem; margin-bottom: 2.5rem; }
.review-stars { color: var(--gold); letter-spacing: .1em; font-size: 1rem; margin-bottom: .5rem; }
.review-body { font-size: .88rem; line-height: 1.6; margin-bottom: .75rem; color: var(--charcoal); }
.review-media { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .75rem; }
.review-media img, .review-media video {
  width: 72px; height: 72px; object-fit: cover; border-radius: 4px;
  background: var(--cream-deep); -webkit-user-drag: none;
}
.review-meta { font-size: .7rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .06em; }

.review-form-guest { background: var(--cream-deep); padding: 1.75rem; text-align: center; max-width: 520px; }
.review-form-guest p { margin-bottom: 1rem; color: var(--ink-soft); font-size: .88rem; }

.review-form { max-width: 520px; }
.review-form h3 { margin-bottom: 1.2rem; font-size: 1.15rem; }
.review-form label {
  display: block; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-soft); margin-bottom: 1.1rem;
}
.review-form label small { text-transform: none; letter-spacing: 0; }
.review-form select, .review-form textarea, .review-form input[type="file"] {
  display: block; width: 100%; margin-top: .45rem; padding: .6rem .7rem;
  font-family: var(--body); font-size: .88rem; border: 1px solid var(--line); background: var(--cream);
}
.review-form textarea { resize: vertical; }

.style-suggestions { padding: 3rem 0; border-top: 1px solid var(--line); }
/* minmax(..., 1fr) with only 1-2 cards stretches each column to fill the
   whole row — a card could end up ~590px wide in a 1180px container.
   Capping the track at 200px (not 1fr) keeps cards a normal thumbnail
   size regardless of count; justify-content lets leftover width become
   margin instead of forcing cards wider. */
.style-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 200px));
  justify-content: start; gap: 1.5rem;
}
.style-card { margin: 0; }
.style-card img, .style-card video {
  /* height: auto is load-bearing here, not decorative — the <img> has an
     HTML height="" attribute (the real photo's pixel height), which is a
     genuine style (low-priority, but still wins when nothing else sets
     height). With no CSS height at all, that attribute silently
     determined the box's height and `aspect-ratio` had nothing left to
     compute, so a tall source photo rendered at its full native height
     instead of the intended 3:4 crop. */
  width: 100%; height: auto; aspect-ratio: 3/4; object-fit: cover; border-radius: 4px;
  background: var(--cream-deep); -webkit-user-drag: none;
}
.style-card figcaption { margin-top: .6rem; font-size: .82rem; color: var(--ink-soft); line-height: 1.4; }
.style-card-text {
  aspect-ratio: 3/4; display: flex; align-items: center; justify-content: center;
  padding: 1rem; background: var(--cream-deep); border-radius: 4px;
}
.style-card-text figcaption { margin: 0; text-align: center; font-size: .85rem; }

/* "How our community styles it" — same scroll-snap + horizontal-track
   shape as the homepage's .stories-track (see korai.js's storiesNext for
   the auto-advance this pairs with), independent class names so either
   can change without touching the other. */
.community-styles { padding-top: 2.5rem; }
.community-track {
  display: flex; gap: 1rem; overflow-x: auto; scroll-snap-type: x mandatory;
  padding-bottom: .5rem; -webkit-overflow-scrolling: touch; scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) { .community-track { scroll-behavior: auto; } }
.community-card {
  position: relative; flex: 0 0 auto; width: clamp(130px, 16vw, 180px);
  aspect-ratio: 9/16; border-radius: 8px; overflow: hidden;
  scroll-snap-align: start; background: var(--cream-deep);
}
.community-card img, .community-card video { width: 100%; height: 100%; object-fit: cover; display: block; -webkit-user-drag: none; }

.detail { border-top: 1px solid var(--line); }
.detail summary {
  padding: 1rem 0; cursor: pointer; list-style: none;
  font-size: .72rem; letter-spacing: .14em; text-transform: uppercase;
  display: flex; justify-content: space-between;
}
.detail summary::-webkit-details-marker { display: none; }
.detail summary::after { content: "+"; }
.detail[open] summary::after { content: "\2212"; }
.detail p { margin: 0 0 1.25rem; color: var(--ink-soft); font-size: .86rem; }

/* -------------------------------------------------------- cart/forms */

/* Capped as one compact block, divider line included — tried leaving the
   border-bottom full-width with just the text capped, but a short
   one-line heading over a long empty divider read as *more* spacious,
   not less. This is just an eyebrow + heading, not content that needs
   .wrap-wide's extra width; the filter sidebar + grid below it are what
   actually use that width. */
.page-head { max-width: 300px; padding: 3rem 0 2rem; border-bottom: 1px solid var(--line); }
.page-head-text { max-width: 300px; }

.lines { border-top: 1px solid var(--line); }
.line {
  display: grid; grid-template-columns: 72px 1fr auto;
  gap: 1rem; padding: 1.25rem 0;
  border-bottom: 1px solid var(--line);
  align-items: start;
}
.line img { width: 72px; aspect-ratio: 3/4; object-fit: cover; background: var(--cream-deep); }
.line-name { font-family: var(--display); font-size: 1.1rem; }
.line-meta { font-size: .72rem; color: var(--ink-soft); }
.qty { display: inline-flex; align-items: center; gap: .5rem; margin-top: .5rem; }
.qty select { font-family: var(--body); font-size: .76rem; padding: .3rem .4rem; border: 1px solid var(--line); background: none; }
.remove { font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); background: none; border: 0; cursor: pointer; padding: 0; }
.remove:hover { color: var(--rose); }

.totals { margin: 2rem 0; max-width: 380px; margin-left: auto; }
.totals div { display: flex; justify-content: space-between; padding: .45rem 0; font-size: .85rem; }
.totals .grand { border-top: 1px solid var(--line); margin-top: .5rem; padding-top: .9rem; font-size: 1.05rem; }
.totals .discount-line, .discount-line { color: var(--olive); }

.discount-box { max-width: 380px; margin: 1.5rem 0 0 auto; }
.discount-box form { display: flex; gap: .5rem; }
.discount-box input[type="text"] {
  flex: 1; padding: .55rem .7rem; border: 1px solid var(--line);
  background: var(--cream); font-family: var(--body); font-size: .82rem;
}
.discount-box input[type="text"]:focus { outline: none; border-color: var(--olive); }
.discount-applied { font-size: .82rem; color: var(--olive); margin: 0 0 .5rem; }
.discount-error { font-size: .78rem; color: var(--rose); margin: 0 0 .5rem; }

/* Cart-abandonment recovery capture — see routers/checkout.py's
   cart_save_email. Deliberately quiet: max-width matches .totals above
   so it reads as a footnote to checkout, not a competing CTA. */
.cart-save {
  max-width: 380px; margin: 0 0 2rem auto; padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.cart-save p { font-size: .78rem; color: var(--ink-soft); margin: 0 0 .6rem; }
.cart-save form { display: flex; gap: .5rem; flex-wrap: wrap; }
.cart-save input {
  flex: 1; min-width: 10rem; padding: .55rem .7rem; border: 1px solid var(--line);
  background: var(--cream); font-family: var(--body); font-size: .82rem;
}
.cart-save input:focus { outline: none; border-color: var(--olive); }

.checkout { display: grid; gap: 2.5rem; padding: 2rem 0 4rem; }
@media (min-width: 880px) { .checkout { grid-template-columns: 1.3fr 1fr; gap: 4rem; align-items: start; } }

.field { display: grid; gap: .35rem; margin-bottom: 1.1rem; }
.field label { font-size: .68rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-soft); }
.field input, .field select, .field textarea {
  font-family: var(--body); font-size: .92rem;
  padding: .8rem .9rem;
  border: 1px solid var(--line);
  background: #fff;
  color: var(--charcoal);
  border-radius: 0;
}
.field input:focus, .field select:focus { border-color: var(--olive); outline: none; }
.field-note { font-size: .72rem; color: var(--olive); margin: 0; }
.field-note-error { color: var(--rose); }
.row { display: grid; gap: 0 1rem; grid-template-columns: 1fr 1fr; }

.pay-opt { display: flex; gap: .75rem; align-items: flex-start; border: 1px solid var(--line); padding: 1rem; margin-bottom: .75rem; cursor: pointer; }
.pay-opt:has(input:checked) { border-color: var(--charcoal); background: var(--cream-deep); }

.errors { border-left: 2px solid var(--rose); padding: .85rem 1rem; margin-bottom: 1.5rem; background: #fff; }
.errors p { margin: .2rem 0; font-size: .82rem; }

.summary-box { background: var(--cream-deep); padding: 1.5rem; }
.summary-box .line { grid-template-columns: 52px 1fr auto; border-color: rgba(0,0,0,.08); }
.summary-box .line img { width: 52px; }

.notice { text-align: center; padding: 5rem var(--gutter); }
.notice h1 { margin-bottom: 1rem; }
.notice p { color: var(--ink-soft); max-width: 42ch; margin: 0 auto 2rem; }

/* .about reuses .section's padding (clamp(3rem,8vw,5.5rem) top+bottom),
   which at desktop width stacks against the adjacent sections' own
   top/bottom padding — up to 176px of dead space between this and the
   product grid above it. Pulled back down on both sides of that seam
   (here, and on .featured-collection's bottom below) since these are
   short text/heading-led sections that don't need as much air as the
   image-heavy ones elsewhere. */
.about { padding-top: clamp(.75rem, 1.5vw, 1.25rem); padding-bottom: clamp(1.25rem, 2.5vw, 1.75rem); }
.featured-collection { padding-bottom: clamp(.75rem, 1.5vw, 1.25rem); }
.about-inner { max-width: 640px; margin: 0 auto; text-align: center; }
.about-inner h2 { margin-bottom: 1.2rem; }
.about-inner p { color: var(--ink-soft); margin-bottom: 1rem; }
.about-inner .btn { margin-top: .5rem; }

/* Same reasoning as .about/.featured-collection above — .more-than's
   default .section bottom padding was stacking with .subscribe's own
   top padding (168px combined) before hitting that section's dark
   background, which already reads as a clear visual break on its own. */
.more-than { padding-top: clamp(1.25rem, 2.5vw, 1.75rem); padding-bottom: clamp(1.25rem, 2.5vw, 1.75rem); }
.stories { padding-bottom: clamp(1.25rem, 2.5vw, 1.75rem); }
.more-than-grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem,5vw,4rem); align-items: center; }
.more-than-media { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.more-than-media img, .more-than-media video { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; display: block; }
.more-than-copy h2 { margin-bottom: .6rem; }
.more-than-copy .lede { color: var(--ink-soft); margin-bottom: 1.8rem; }
.feature-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 1.4rem; }
.feature-list h3 { font-family: var(--body); font-size: 1rem; font-weight: 600; margin-bottom: .3rem; }
.feature-list p { color: var(--ink-soft); font-size: .9rem; }

@media (max-width: 860px) {
  .more-than-grid { grid-template-columns: 1fr; }
}

.stories-track {
  display: flex; gap: 1rem; overflow-x: auto; scroll-snap-type: x mandatory;
  padding: 0 var(--gutter) .5rem; -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  .stories-track { scroll-behavior: auto; }
}
.story-card {
  position: relative; flex: 0 0 auto; width: clamp(150px, 20vw, 220px);
  aspect-ratio: 9 / 16; border-radius: 10px; overflow: hidden;
  scroll-snap-align: start; background: var(--olive); display: block;
}
.story-card img, .story-card video {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .35s ease;
}
.story-play {
  position: absolute; top: .6rem; left: .6rem; width: 1.8rem; height: 1.8rem;
  border-radius: 50%; background: rgba(0,0,0,.45); color: #fff;
  display: flex; align-items: center; justify-content: center; font-size: .6rem;
}
.story-card:hover img, .story-card:hover video,
.story-card:focus-visible img, .story-card:focus-visible video { transform: scale(1.06); }

.story-caption {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 1.6rem .7rem .6rem;
  background: linear-gradient(0deg, rgba(0,0,0,.72), rgba(0,0,0,0));
  color: #fff; font-size: .74rem; letter-spacing: .01em; transition: padding-top .25s ease;
}
.story-caption strong { display: block; font-weight: 600; }
.story-caption-product { padding-bottom: .7rem; }
.story-details {
  display: block; max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height .25s ease, opacity .2s ease, margin-top .25s ease;
}
.story-card:hover .story-details,
.story-card:focus-visible .story-details { max-height: 6rem; opacity: 1; margin-top: .35rem; }
.story-card:hover .story-caption,
.story-card:focus-visible .story-caption { padding-top: 2.6rem; }

/* Touch devices never fire :hover, so the price/subtitle/CTA in
   .story-details would otherwise stay permanently invisible (max-height:0)
   on mobile with no way to reveal it short of tapping through to the
   product. Show it by default there instead of gating it behind hover. */
@media (hover: none) {
  .story-details { max-height: 6rem; opacity: 1; margin-top: .35rem; }
  .story-caption { padding-top: 2.6rem; }
}
.story-sub { display: block; color: rgba(255,255,255,.78); font-size: .68rem; margin-bottom: .25rem; }
.story-price { display: block; font-weight: 600; }
.story-price s { color: rgba(255,255,255,.6); font-weight: 400; margin-left: .35rem; }
.story-cta { display: block; font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; margin-top: .3rem; color: rgba(255,255,255,.85); }

/* ------------------------------------------------------------ footer */

.subscribe { background: var(--charcoal); color: var(--cream); padding: clamp(3rem,8vw,5rem) var(--gutter); }
.subscribe .inner { max-width: 520px; margin: 0 auto; text-align: center; }
.subscribe h2 { color: var(--cream); }
.subscribe p { color: rgba(255,243,219,.66); font-size: .88rem; }
.subscribe form { display: flex; gap: .5rem; margin-top: 1.5rem; flex-wrap: wrap; }
.subscribe input {
  flex: 1 1 200px; padding: .9rem 1rem;
  background: none; border: 1px solid rgba(255,243,219,.3);
  color: var(--cream); font-family: var(--body); font-size: .9rem;
}
.subscribe input::placeholder { color: rgba(255,243,219,.45); }
.subscribe .btn { background: var(--cream); color: var(--charcoal); border-color: var(--cream); }

.site-footer { border-top: 1px solid var(--line); padding: 3rem var(--gutter) 2rem; }
.foot-mark { height: 72px; width: auto; margin: 0; }
.foot-line { font-family: var(--display); font-size: 1.1rem; color: var(--ink-soft); margin: .4rem 0 2.5rem; }
.foot-cols { display: grid; grid-template-columns: minmax(220px, 1.8fr) repeat(3, minmax(140px, 1fr)); gap: 2rem; }
.foot-cols h3 { font-family: var(--body); font-size: .66rem; letter-spacing: .18em; text-transform: uppercase; margin-bottom: .9rem; color: var(--ink-soft); }
.foot-cols a { display: block; font-size: .84rem; padding: .18rem 0; }
.foot-cols a:hover { color: var(--olive-deep); }
.foot-vision p { font-size: .84rem; line-height: 1.6; color: var(--ink-soft); max-width: 34ch; }

/* Desktop .foot-cols needs >=~816px (220+140*3 columns + 3*2rem gaps +
   gutter padding) to fit without overflowing — 720px left a real gap
   (721-815px) where neither the desktop grid fit nor this mobile
   fallback had kicked in yet, causing genuine horizontal page overflow.
   860px matches the breakpoint already used elsewhere in this file
   (.shop-layout) for consistency. */
@media (max-width: 860px) {
  .foot-cols { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
  .foot-vision p { max-width: none; }
}
/* Decorative footer illustration — original artwork (see the SVG's own
   comments for the figure-by-figure breakdown), not a reproduction of
   any reference image. Full-bleed regardless of .site-footer's own
   gutter padding, so it reads as a banner strip, not a boxed-in graphic. */
.foot-illustration {
  width: calc(100% + var(--gutter) * 2); margin: 3rem calc(var(--gutter) * -1) 0;
  max-width: none; height: auto; display: block;
}

.copyright { font-size: .68rem; color: var(--ink-soft); margin-top: 2rem; letter-spacing: .06em; }

/* Floating "picture-in-picture" styling video on the product page —
   bottom-right by default, freely draggable (korai.js), closeable for
   the rest of this page view (no persistence — a refresh/revisit brings
   it back, per how this was scoped). `touch-action: none` stops the
   browser's own scroll/zoom gestures from fighting the drag on touch.
   Chunked delivery for the video itself needs no special handling here
   — it's a plain <video src>, and browsers request those via HTTP Range
   automatically; the server already answers with 206 Partial Content. */
.pip-video {
  position: fixed; right: 1.25rem; bottom: 1.25rem; z-index: 200;
  width: 130px; aspect-ratio: 9/16; border-radius: 14px; overflow: hidden;
  box-shadow: 0 12px 32px rgba(0,0,0,.28); background: var(--cream-deep);
  cursor: grab; touch-action: none; -webkit-user-select: none; user-select: none;
}
.pip-video.dragging { cursor: grabbing; }
.pip-video video { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.pip-video-close, .pip-video-mute {
  position: absolute; border: 0; border-radius: 50%; background: rgba(0,0,0,.55); color: #fff;
  cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1;
}
.pip-video-close { top: .35rem; right: .35rem; width: 22px; height: 22px; font-size: .95rem; }
.pip-video-mute { bottom: .35rem; right: .35rem; width: 24px; height: 24px; font-size: .72rem; }
.pip-video-close:hover, .pip-video-mute:hover { background: rgba(0,0,0,.75); }
@media (max-width: 480px) { .pip-video { width: 100px; right: .85rem; bottom: .85rem; } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
