/* ============================================================
   textile.css — the Tanmayaa visual language
   ============================================================

   Implements docs/STOREFRONT_DESIGN_DIRECTION.md sections 3-5.

   The brand's visual system is the CONSTRUCTION OF THE CLOTH, drawn in CSS,
   not ornament applied to the page. Four motifs, each with one job:

     kattam        the check grid        section boundaries, weave tiles
     temple border the triangular band   hero bottom edge, footer top edge
     rettai pettu  the double rule       heading underlines, active states
     warp grid     the thread texture    empty/fallback states only

   A motif used outside the table in section 3.2 of that document is a bug.

   WHY THE MOTIFS ARE DRAWN AND NOT PHOTOGRAPHED

   No photography reaches a customer today. 189 real images sit in the tracked
   `products/` folder, but that folder is served by no route, the 182 image
   paths in seed_products.sql name files that do not exist, and server.js
   answers every one of those misses with a brown gradient SVG at HTTP 200 -
   so nothing reports a problem while every product renders a gradient.
   See docs/STOREFRONT_DESIGN_DIRECTION.md F1.

   Drawn geometry is honest — nobody mistakes a repeating-linear-gradient for a
   picture of a saree — whereas a brown gradient at a photograph's aspect ratio
   inside a product grid is read as a photograph. So the decoration here is
   explicitly decoration, and an unconfigured image slot renders as
   .tw-slot-empty below rather than as a gradient pretending.

   LOADED BY: the storefront pages and the EJS templates. Shared, not inlined —
   Phase 5 must not add new duplicated inline implementations.
*/

/* ---------- geometry constants ---------- */
:root {
  --weave-unit: 8px;
  --kattam-band: 24px;
  --kattam-line: 2px;
  --temple-width: 16px;
  --temple-height: 10px;
  --pettu-gap: 3px;
  --warp-grid: 4px;

  /* Dye palette. Every name is a dye or a cloth the store actually stocks, so
     the palette stays checkable against the catalogue instead of being mood
     words. These are DECORATIVE FILLS. No text may take its colour from one.
     Text colours are --clr-ink / --clr-muted / --clr-gold / --clr-accent,
     which assets/css/a11y.css owns and the browser suite asserts. */
  --dye-indigo: #2B3A55;
  --dye-madder: #7E3B32;
  --dye-turmeric: #A8791C;
  --dye-kattam: #4A5D4E;
  --ground-unbleach: #F5F0E8;
  --ground-mull: #FBF8F3;

  /* The thread colour used by every motif. Low-contrast on purpose: these are
     textures, and a texture that competes with the text is a mistake. */
  --thread: rgba(61, 43, 31, 0.14);
  --thread-strong: rgba(61, 43, 31, 0.26);
}

/* ---------- (a) kattam — the check grid ---------- */
/* Chettinad cotton's warp/weft grid. Section boundary marker. */
.tw-kattam {
  background-image:
    repeating-linear-gradient(
      to right,
      var(--thread) 0 var(--kattam-line),
      transparent var(--kattam-line) var(--kattam-band)),
    repeating-linear-gradient(
      to bottom,
      var(--thread) 0 var(--kattam-line),
      transparent var(--kattam-line) var(--kattam-band));
}

/* A band that separates one section from the next, replacing bare whitespace. */
.tw-section-edge {
  height: calc(var(--weave-unit) * 3);
  border-top: 1px solid var(--thread-strong);
  border-bottom: 1px solid var(--thread-strong);
  background-image:
    repeating-linear-gradient(
      to right,
      var(--thread) 0 var(--kattam-line),
      transparent var(--kattam-line) var(--kattam-band));
}

/* ---------- (b) temple border (kumbam) ---------- */
/* A row of triangles along one edge. Emphasis only: the hero's bottom edge and
   the footer's top edge, and nowhere else. Used everywhere it reads as bunting. */
.tw-temple-edge {
  position: relative;
}
.tw-temple-edge::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--temple-height);
  background-repeat: repeat-x;
  background-size: var(--temple-width) var(--temple-height);
  /* One isosceles triangle, tiled. Encoded rather than a file so the motif
     cannot drift from the geometry constants above. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='10' viewBox='0 0 16 10'%3E%3Cpath d='M0 10 L8 0 L16 10 Z' fill='%23B8882A'/%3E%3C/svg%3E");
  pointer-events: none;
}

/* ---------- (c) rettai pettu — the double rule ---------- */
/* The plainest border finish on a daily-wear cotton: two parallel lines with a
   narrow gap. This is the heading underline and the active-state indicator. */
.tw-pettu {
  position: relative;
  padding-bottom: calc(var(--weave-unit) + var(--pettu-gap));
}
.tw-pettu::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: calc(var(--weave-unit) * 7);
  height: calc(var(--pettu-gap) + 2px);
  border-top: 1px solid var(--clr-gold, #916B21);
  border-bottom: 1px solid var(--clr-gold, #916B21);
}

/* Section headings are left-aligned, not centred with tracked-out capitals.
   Centred serif headings over a cream ground are the generic "heritage luxury"
   template the direction rejects; left alignment also puts the heading where
   the eye already is after the previous section. */
/* store.css centres `.section-title` and `.section-subtitle`, and also pins the
   subtitle with `margin-left/right: auto` and a 560px cap. Setting text-align
   on the wrapper alone does not undo any of that - the children carry their own
   rules - so a Phase 5 section head rendered left-aligned in the stylesheet and
   centred on the page. The children are addressed directly here, inside
   .tw-section-head only, so every other section keeps the centred treatment
   until it is converted. */
.tw-section-head {
  text-align: left;
  margin-bottom: calc(var(--weave-unit) * 3);
}
.tw-section-head .section-title,
.tw-section-head .tw-section-sub,
.tw-section-head .section-subtitle {
  text-align: left;
  margin-left: 0;
  margin-right: 0;
}
.tw-section-head .tw-section-sub {
  margin-top: var(--weave-unit);
  margin-bottom: 0;
  color: var(--clr-muted, #6F5A4A);
  max-width: 62ch;
}

/* ---------- (d) warp grid — thread texture ---------- */
/* The only motif permitted behind text: at this opacity it does not move a
   contrast ratio. Used as the ground for an unconfigured image slot. */
.tw-warp {
  background-color: var(--ground-mull);
  background-image:
    repeating-linear-gradient(
      to right,
      var(--thread) 0 1px,
      transparent 1px var(--warp-grid)),
    repeating-linear-gradient(
      to bottom,
      var(--thread) 0 1px,
      transparent 1px var(--warp-grid));
}

/* ---------- image slots: states A, B and C ---------- */
/*
   Every image slot is in exactly one of three states and the customer can tell
   which by looking (design direction section 5):

     A  a real configured photograph        -> .tw-slot img
     B  no photograph configured            -> .tw-slot-empty
     C  configured but failed to load       -> falls back to B via onerror

   State B is NOT a styled placeholder pretending to be a photo. It is the warp
   texture with the item's name as ordinary text, at the slot's exact aspect
   ratio so nothing shifts when a photograph is added later.
*/
.tw-slot {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--ground-mull);
}
.tw-slot > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.tw-slot-empty {
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  padding: var(--kattam-band);
  background-color: var(--ground-mull);
  background-image:
    repeating-linear-gradient(
      to right,
      var(--thread) 0 1px,
      transparent 1px var(--warp-grid)),
    repeating-linear-gradient(
      to bottom,
      var(--thread) 0 1px,
      transparent 1px var(--warp-grid));
  border: 1px solid var(--thread-strong);
}
.tw-slot-empty > .tw-slot-label {
  font-family: var(--font-heading, Georgia, serif);
  font-size: 1.05rem;
  line-height: 1.3;
  /* Ink on the mull ground. Measured in docs/CONTRAST.md. */
  color: var(--clr-ink, #1A0F14);
}
/* Said once, quietly, so the state is legible as "no picture yet" rather than
   as a broken image. Never the word "photo" — see the content requirements. */
.tw-slot-empty > .tw-slot-label::after {
  content: "";
  display: block;
  margin-top: 4px;
  font-family: var(--font-body, system-ui, sans-serif);
  font-size: 0.78rem;
  font-weight: 400;
  color: var(--clr-muted, #6F5A4A);
}

/* ---------- shop by weave ---------- */
/* Cloth types. Carries the kattam motif; answers "what is this cloth".
   Deliberately a different shape from the occasion section below, so the two
   can never read as the same wall of tiles repeated twice. */
/* `auto-fill` with a MAXIMUM, not `auto-fit` with `1fr`.

   With `1fr` the columns absorb all spare width, so a catalogue with two weave
   collections rendered two tiles ~590px wide on a 1440px screen - and at the
   4:5 ratio a saree wants, that is 740px tall each. Two enormous panels of
   empty texture is not a section; it is a wall.

   Capping the track at 360px keeps a tile tile-sized whether the store has two
   weaves or eight, and `justify-content: start` keeps them left-aligned with
   the heading above rather than floating in the middle. */
.tw-weave-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 360px));
  justify-content: start;
  gap: var(--kattam-band);
}
.tw-weave-tile {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  border: 1px solid var(--thread-strong);
  background: var(--clr-white, #fff);
}
.tw-weave-tile .tw-slot,
.tw-weave-tile .tw-slot-empty {
  aspect-ratio: 4 / 5;
  width: 100%;
}
.tw-weave-tile-body {
  padding: var(--kattam-band);
  border-top: 1px solid var(--thread-strong);
}
.tw-weave-tile-body h3 {
  margin: 0 0 4px;
  font-size: 1.15rem;
}
.tw-weave-tile-body p {
  margin: 0;
  font-size: 0.88rem;
  color: var(--clr-muted, #6F5A4A);
}
.tw-weave-tile-cta {
  display: inline-block;
  margin-top: var(--weave-unit);
  font-size: 0.85rem;
  color: var(--clr-gold, #916B21);
}
.tw-weave-tile:hover .tw-weave-tile-cta,
.tw-weave-tile:focus-visible .tw-weave-tile-cta {
  text-decoration: underline;
}

/* ---------- occasion guidance ---------- */
/* A short guidance list, not a second tile wall. Answers "what should I wear
   to this". One line per occasion, and that line is the category's own stored
   description — never copy invented here. */
.tw-occasion-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 0;
  border-top: 1px solid var(--thread-strong);
}
.tw-occasion-row {
  display: flex;
  align-items: baseline;
  gap: var(--kattam-band);
  padding: calc(var(--weave-unit) * 2) 0;
  border-bottom: 1px solid var(--thread-strong);
  text-decoration: none;
  color: inherit;
}
.tw-occasion-row > .tw-occasion-name {
  flex: 0 0 auto;
  min-width: 9rem;
  font-family: var(--font-heading, Georgia, serif);
  font-size: 1.1rem;
  color: var(--clr-ink, #1A0F14);
}
.tw-occasion-row > .tw-occasion-hint {
  flex: 1 1 auto;
  font-size: 0.88rem;
  color: var(--clr-muted, #6F5A4A);
}
.tw-occasion-row:hover .tw-occasion-name,
.tw-occasion-row:focus-visible .tw-occasion-name {
  text-decoration: underline;
}

/* ---------- honest section-level empty state ---------- */
/* When a section has no configured content it says so plainly and does not
   render filler. Distinguishable from an error state, which uses .tw-error. */
.tw-empty {
  padding: calc(var(--weave-unit) * 4);
  border: 1px dashed var(--thread-strong);
  background: var(--ground-mull);
  color: var(--clr-muted, #6F5A4A);
  font-size: 0.92rem;
}
.tw-error {
  padding: calc(var(--weave-unit) * 4);
  border: 1px solid var(--clr-error, #C62828);
  background: #FFF5F5;
  color: var(--clr-error, #C62828);
  font-size: 0.92rem;
}

/* ---------- motion ---------- */
@media (prefers-reduced-motion: reduce) {
  .tw-weave-tile,
  .tw-occasion-row {
    transition: none;
  }
}

/* ============================================================
   DISCOVERY — filters, applied chips, sort, drawer
   ============================================================
   Server-rendered links throughout; collection-filters.js only adds the
   drawer's focus behaviour. Everything here works with JavaScript off.
*/

.collection-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--kattam-band);
  padding: calc(var(--weave-unit) * 2) 0;
  border-bottom: 1px solid var(--thread-strong);
}
.collection-toolbar .product-count {
  margin: 0;
  font-size: 0.92rem;
  color: var(--clr-muted, #6F5A4A);
}

/* Sort: a list of links, so each order is a real URL. */
.tw-sort {
  display: flex;
  align-items: baseline;
  gap: var(--weave-unit);
  margin-left: auto;
}
.tw-sort-label {
  font-size: 0.85rem;
  color: var(--clr-muted, #6F5A4A);
}
.tw-sort-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--weave-unit);
  margin: 0;
  padding: 0;
  list-style: none;
}
.tw-sort-list a {
  display: inline-block;
  padding: 4px var(--weave-unit);
  font-size: 0.85rem;
  color: var(--clr-muted, #6F5A4A);
  text-decoration: none;
  border: 1px solid transparent;
}
.tw-sort-list a:hover { text-decoration: underline; }
/* rettai pettu marks the active sort: the double rule, not a colour change
   alone, so the state survives a colour-blind reading. */
.tw-sort-list a[aria-current="true"] {
  color: var(--clr-ink, #1A0F14);
  border-bottom: 1px solid var(--clr-gold, #916B21);
  box-shadow: 0 var(--pettu-gap) 0 -1px var(--clr-gold, #916B21);
}

/* Applied filters */
.tw-applied {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--weave-unit);
  padding: calc(var(--weave-unit) * 2) 0;
}
.tw-applied-label {
  font-size: 0.85rem;
  color: var(--clr-muted, #6F5A4A);
}
.tw-applied-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--weave-unit);
  margin: 0;
  padding: 0;
  list-style: none;
}
.tw-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* 44px minimum target: these are removal controls and are used on phones. */
  min-height: 44px;
  padding: 6px 12px;
  border: 1px solid var(--thread-strong);
  background: var(--ground-mull);
  color: var(--clr-ink, #1A0F14);
  font-size: 0.85rem;
  text-decoration: none;
}
.tw-chip:hover { background: var(--ground-unbleach); }
.tw-chip-x { font-size: 1.1rem; line-height: 1; }
.tw-clear-all {
  font-size: 0.85rem;
  color: var(--clr-gold, #916B21);
  text-decoration: underline;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* Filter panel */
.tw-filter-toggle {
  display: none;               /* shown at narrow widths only */
  min-height: 44px;
  padding: 8px 16px;
  border: 1px solid var(--thread-strong);
  background: var(--clr-white, #fff);
  color: var(--clr-ink, #1A0F14);
  font-size: 0.9rem;
}
/* THE PANEL KEEPS ITS HEADING AT EVERY WIDTH.

   This was `display: none` globally, shown again only inside the drawer's
   max-width:900px block. Above 900px that hid the <h2>Filters</h2> while
   leaving the <h3> facet legends in the document, so the heading order went
   h1 -> h3 and axe reported `heading-order` on every filtered collection at
   desktop-1024 and desktop-1440 - and only there, which is what identified
   the breakpoint as the cause.

   The heading is real content: the panel is a labelled region of the page
   whether it is a sidebar or a drawer. Only the CLOSE BUTTON is drawer-only,
   because only a drawer can be closed. */
.tw-filters-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--kattam-band);
  padding-bottom: var(--weave-unit);
  border-bottom: 1px solid var(--thread-strong);
}
.tw-filters-title { margin: 0; font-size: 1rem; }
.tw-filters-close { display: none; }

.tw-filters {
  padding: calc(var(--weave-unit) * 2) 0;
}
.tw-facet {
  border: 0;
  padding: 0;
  margin: 0 0 var(--kattam-band);
}
.tw-facet-legend {
  padding: 0;
  margin: 0 0 var(--weave-unit);
  font-family: var(--font-body, system-ui, sans-serif);
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--clr-muted, #6F5A4A);
}
.tw-facet-options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--weave-unit);
  margin: 0;
  padding: 0;
  list-style: none;
}
.tw-facet-option {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 6px 12px;
  border: 1px solid var(--thread-strong);
  background: var(--clr-white, #fff);
  color: var(--clr-ink, #1A0F14);
  font-size: 0.85rem;
  text-decoration: none;
}
.tw-facet-option:hover { background: var(--ground-mull); }
/* Selected state: filled ground AND the double rule. Not colour alone. */
.tw-facet-option.is-selected {
  background: var(--ground-unbleach);
  border-color: var(--clr-gold, #916B21);
  box-shadow: inset 0 -3px 0 -1px var(--clr-gold, #916B21);
  font-weight: 600;
}
.tw-facet-count {
  font-size: 0.78rem;
  color: var(--clr-muted, #6F5A4A);
}
.tw-facet-option.is-selected .tw-facet-count { color: var(--clr-ink, #1A0F14); }

.tw-filter-scrim { display: none; }

/* ---------- the drawer, at narrow widths ---------- */
/* THE DRAWER IS AN ENHANCEMENT, NOT THE LAYOUT.

   Everything below is scoped to `.tw-js`, a class set by a one-line capability
   check in the page head. Without it the filter panel stays an ordinary block
   above the grid: visible, tabbable and usable.

   This was found by the "discovery works with JavaScript disabled" test at
   390px. The drawer styles applied unconditionally, so with scripts off the
   panel sat at translateX(-100%) - off screen - and the only thing that could
   have brought it back was the JavaScript that was not running. The filters
   were server-rendered links the whole time and completely unreachable.

   The toggle is likewise hidden without `.tw-js`: a button that opens a drawer
   is meaningless when there is no drawer and nothing to open it with. */
@media (max-width: 900px) {
  .tw-js .tw-filter-toggle { display: inline-flex; align-items: center; }

  .tw-js .tw-filters {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: min(90vw, 360px);
    z-index: 1002;
    background: var(--clr-white, #fff);
    border-right: 1px solid var(--thread-strong);
    padding: var(--kattam-band);
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform .3s cubic-bezier(.4, 0, .2, 1);
  }
  .tw-js .tw-filters.is-open { transform: translateX(0); }

  .tw-js .tw-filters-head { align-items: center; }
  .tw-js .tw-filters-title { font-size: 1.1rem; }
  /* Only inside the drawer is there anything to close. */
  .tw-js .tw-filters-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    font-size: 1.5rem;
    background: none;
    border: none;
    color: var(--clr-ink, #1A0F14);
  }

  .tw-js .tw-filter-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 1001;
    background: rgba(26, 15, 20, 0.5);
  }
  .tw-js .tw-filter-scrim[hidden] { display: none; }

  .tw-sort { margin-left: 0; width: 100%; }
}

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

/* ============================================================
   PRODUCT — gallery, purchase controls, reviews, recently viewed
   ============================================================ */

/* ---------- gallery ---------- */
.tw-gallery-main {
  position: relative;
  overflow: hidden;
  background: var(--ground-mull);
}
.tw-gallery-main > img {
  display: block;
  width: 100%;
  height: auto;
  transition: transform .3s var(--ease-smooth, cubic-bezier(.4, 0, .2, 1));
  transform-origin: center center;
}
/* Zoom is a toggle, not a hover effect: hover is unreachable from a keyboard
   and from a touchscreen, which is most of this store's traffic. */
.tw-gallery.is-zoomed .tw-gallery-main > img {
  transform: scale(1.8);
  cursor: zoom-out;
}
.tw-gallery.is-zoomed .tw-gallery-main {
  overflow: auto;
}

.tw-gallery-nav,
.tw-gallery-zoom {
  position: absolute;
  /* 44px minimum target. These are used mid-scroll on a phone. */
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--thread-strong);
  background: rgba(255, 255, 255, 0.92);
  color: var(--clr-ink, #1A0F14);
  font-size: 1.4rem;
  line-height: 1;
  z-index: 2;
}
.tw-gallery-nav:hover,
.tw-gallery-zoom:hover { background: #fff; }
.tw-gallery-prev { left: var(--weave-unit); top: 50%; transform: translateY(-50%); }
.tw-gallery-next { right: var(--weave-unit); top: 50%; transform: translateY(-50%); }
.tw-gallery-zoom { right: var(--weave-unit); bottom: var(--weave-unit); font-size: 1.1rem; }

.tw-gallery-status {
  margin: var(--weave-unit) 0 0;
  font-size: 0.82rem;
  color: var(--clr-muted, #6F5A4A);
}

.tw-gallery-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--weave-unit);
  margin-top: var(--weave-unit);
}
.tw-gallery-thumbs .gallery-thumb {
  padding: 0;
  border: 1px solid var(--thread-strong);
  background: none;
  line-height: 0;
}
/* Selected thumbnail: the double rule, not a colour change alone. */
.tw-gallery-thumbs .gallery-thumb.active {
  border-color: var(--clr-gold, #916B21);
  box-shadow: 0 0 0 1px var(--clr-gold, #916B21);
}

@media (prefers-reduced-motion: reduce) {
  .tw-gallery-main > img { transition: none; }
}

/* ---------- purchase controls ---------- */
.tw-qty-label {
  display: block;
  margin-bottom: 4px;
  font-size: 0.82rem;
  color: var(--clr-muted, #6F5A4A);
}
.tw-qty {
  width: 5rem;
  min-height: 44px;
  padding: 8px 10px;
  margin-bottom: var(--weave-unit);
  border: 1px solid var(--thread-strong);
  background: var(--clr-white, #fff);
  color: var(--clr-ink, #1A0F14);
  font-size: 1rem;
}

.tw-cart-status {
  margin: var(--weave-unit) 0 0;
  font-size: 0.88rem;
  color: var(--clr-ink, #1A0F14);
  min-height: 1.2em;   /* reserved, so an announcement shifts nothing */
}
.tw-cart-status.is-error { color: var(--clr-error, #C62828); }

/* ---------- shipping / returns assurance ---------- */
.tw-assurance {
  margin: var(--kattam-band) 0;
  padding: var(--kattam-band) 0;
  border-top: 1px solid var(--thread-strong);
  border-bottom: 1px solid var(--thread-strong);
}
.tw-assurance-line {
  margin: 0 0 6px;
  font-size: 0.88rem;
  color: var(--clr-ink, #1A0F14);
}
.tw-assurance-line:last-child { margin-bottom: 0; }
.tw-assurance-note { color: var(--clr-muted, #6F5A4A); }

/* ---------- sticky purchase bar, phones only ---------- */
.tw-buybar { display: none; }

@media (max-width: 767px) {
  .tw-buybar {
    display: flex;
    align-items: center;
    gap: var(--kattam-band);
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 900;
    padding: var(--weave-unit) var(--kattam-band);
    /* Clear of the iOS home indicator, so the button is not half a bar. */
    padding-bottom: calc(var(--weave-unit) + env(safe-area-inset-bottom, 0px));
    background: var(--clr-white, #fff);
    border-top: 1px solid var(--thread-strong);
    box-shadow: 0 -2px 12px rgba(26, 15, 20, 0.08);
  }
  .tw-buybar[hidden] { display: none; }

  .tw-buybar-info {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
  }
  .tw-buybar-name {
    font-size: 0.82rem;
    color: var(--clr-muted, #6F5A4A);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .tw-buybar-price {
    font-size: 1rem;
    font-weight: 600;
    color: var(--clr-ink, #1A0F14);
  }
  .tw-buybar-btn { flex: 0 0 auto; min-height: 44px; }

  /* The bar sits over the page, so the footer needs room to be read past it. */
  .site-footer { padding-bottom: 72px; }
}

/* ---------- reviews ---------- */
.tw-reviews { margin: calc(var(--weave-unit) * 6) 0; }
.tw-reviews-summary {
  margin: 0 0 var(--kattam-band);
  font-size: 0.92rem;
  color: var(--clr-ink, #1A0F14);
}
.tw-reviews-summary .stars { color: var(--clr-gold, #916B21); margin-right: 6px; }

.tw-review-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--kattam-band);
  margin: 0;
  padding: 0;
  list-style: none;
}
.tw-review {
  padding: var(--kattam-band);
  border: 1px solid var(--thread-strong);
  background: var(--ground-mull);
}
.tw-review-rating {
  margin: 0 0 var(--weave-unit);
  color: var(--clr-gold, #916B21);
  font-size: 0.95rem;
}
.tw-review-verified {
  margin-left: var(--weave-unit);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--clr-success, #2E7D32);
}
.tw-review-title { margin: 0 0 6px; font-size: 1rem; }
.tw-review-body {
  margin: 0 0 var(--weave-unit);
  font-size: 0.9rem;
  color: var(--clr-ink, #1A0F14);
}
.tw-review-meta {
  margin: 0;
  font-size: 0.82rem;
  color: var(--clr-muted, #6F5A4A);
}

/* ---------- recently viewed ---------- */
.tw-recent { margin: calc(var(--weave-unit) * 6) 0; }
.tw-recent[hidden] { display: none; }
.tw-recent-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--kattam-band);
}
.tw-recent-card {
  display: block;
  text-decoration: none;
  color: inherit;
  border: 1px solid var(--thread-strong);
}
.tw-recent-card .tw-slot,
.tw-recent-card .tw-slot-empty { width: 100%; }
.tw-recent-name {
  display: block;
  padding: var(--weave-unit);
  font-size: 0.85rem;
  color: var(--clr-ink, #1A0F14);
}
.tw-recent-card:hover .tw-recent-name { text-decoration: underline; }

/* ============================================================
   FORM ERRORS — shared by checkout and cart
   ============================================================
   Colour is never the only signal: the summary has a heading and a list, and
   each field message is text associated with the field through
   aria-describedby. A red border alone conveys nothing to a screen reader and
   nothing to a colour-blind reader.
*/
.tw-error-summary {
  margin: 0 0 var(--kattam-band);
  padding: var(--kattam-band);
  border: 2px solid var(--clr-error, #C62828);
  background: #FFF5F5;
  color: var(--clr-error, #C62828);
}
.tw-error-summary:focus-visible {
  outline: 3px solid var(--focus-ring, #1A0F14);
  outline-offset: 2px;
}
.tw-error-summary-title {
  margin: 0 0 var(--weave-unit);
  font-weight: 600;
  font-size: 0.95rem;
}
.tw-error-summary ul {
  margin: 0;
  padding-left: 1.2rem;
  list-style: disc;
}
.tw-error-summary li { margin-bottom: 4px; font-size: 0.9rem; }
.tw-error-summary a {
  color: var(--clr-error, #C62828);
  text-decoration: underline;
}

.tw-field-error {
  margin: 4px 0 0;
  font-size: 0.82rem;
  color: var(--clr-error, #C62828);
}

/* The field's own mark. Paired with the message above, never used alone. */
[aria-invalid="true"] {
  border-color: var(--clr-error, #C62828) !important;
  /* A second visual cue that survives a greyscale or colour-blind reading. */
  box-shadow: inset 3px 0 0 0 var(--clr-error, #C62828);
}

/* ============================================================
   COLLECTION HERO — no photograph, no 400px of nothing
   ============================================================
   store.css gives .collection-hero a fixed 400px height and a dark gradient
   ::after scrim, which exist to make white text readable over a photograph.
   With no `image_path` configured there IS no photograph, so a collection page
   opened with 400px of empty grey-brown gradient and the heading floating in
   it - the largest single instance on the storefront of a gradient standing in
   for a picture.

   `.tw-hero-empty` is added by the template when the collection has no image.
   The band becomes ordinary page: no fixed height, no scrim, ink on the page
   ground. When an image is configured nothing here applies and the original
   treatment is untouched.
*/
.collection-hero.tw-hero-empty {
  height: auto;
  min-height: 0;
  padding: calc(var(--weave-unit) * 5) 0 calc(var(--weave-unit) * 2);
  background: none;
  border-bottom: 1px solid var(--thread-strong);
}
.collection-hero.tw-hero-empty::after { content: none; }
.collection-hero.tw-hero-empty h1,
.collection-hero.tw-hero-empty p {
  color: var(--clr-ink, #1A0F14);
  text-shadow: none;
}
.collection-hero.tw-hero-empty p {
  color: var(--clr-muted, #6F5A4A);
  max-width: 62ch;
}

/* ============================================================
   SPECIFICATIONS — a definition list that reads as pairs
   ============================================================
   `.specs-grid` had no rule anywhere: not in store.css, not in a11y.css. A <dl>
   with no styling renders as alternating block lines, so the product page
   listed "Fabric / Chettinad / Weave / Handloom / Region / Chettinad" down the
   page as ten separate paragraphs instead of five label-value pairs.

   Two columns on anything wider than a phone; stacked pairs below that, where
   two columns would leave a label with two characters of room.
*/
.specs-grid {
  display: grid;
  grid-template-columns: minmax(7rem, 11rem) 1fr;
  gap: var(--weave-unit) var(--kattam-band);
  margin: var(--kattam-band) 0;
  padding: var(--kattam-band) 0;
  border-top: 1px solid var(--thread-strong);
  border-bottom: 1px solid var(--thread-strong);
}
.specs-grid dt {
  margin: 0;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--clr-muted, #6F5A4A);
}
.specs-grid dd {
  margin: 0;
  font-size: 0.92rem;
  color: var(--clr-ink, #1A0F14);
}

@media (max-width: 480px) {
  .specs-grid { grid-template-columns: 1fr; gap: 2px; }
  .specs-grid dd { margin-bottom: var(--weave-unit); }
}

/* ---------- design and colour collections: links between them ----------
   Below the grid on a design or colour collection page (collection.ejs) and
   on a saree's own page (product.ejs). Plain links in wrapping rows, each a
   44px target, so a phone never scrolls sideways to reach one. The current
   collection is marked by fill and a rule, never by colour alone - the same
   treatment as a selected filter. */
.tw-taxonomy-links {
  margin: var(--kattam-band) 0;
  padding: var(--kattam-band) 0;
  border-top: 1px solid var(--thread-strong);
}
.tw-taxonomy-group + .tw-taxonomy-group { margin-top: var(--kattam-band); }
.tw-taxonomy-heading {
  margin: 0 0 var(--weave-unit);
  font-size: 1.1rem;
}
.tw-taxonomy-list,
.tw-product-collections-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--weave-unit);
  margin: 0;
  padding: 0;
  list-style: none;
}
.tw-taxonomy-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 6px 12px;
  border: 1px solid var(--thread-strong);
  background: var(--clr-white, #fff);
  color: var(--clr-ink, #1A0F14);
  font-size: 0.85rem;
  text-decoration: none;
  overflow-wrap: anywhere;
}
.tw-taxonomy-link:hover { background: var(--ground-mull); }
.tw-taxonomy-link[aria-current="page"] {
  background: var(--ground-unbleach);
  border-color: var(--clr-gold, #916B21);
  box-shadow: inset 0 -3px 0 -1px var(--clr-gold, #916B21);
  font-weight: 600;
}
.tw-product-collections {
  margin-top: var(--kattam-band);
  padding-top: var(--kattam-band);
  border-top: 1px solid var(--thread-strong);
}
.tw-product-collections-title {
  margin: 0 0 var(--weave-unit);
  font-size: 1rem;
}
.tw-product-collections-list li {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  min-height: 44px;
}
.tw-product-collections-kind {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--clr-muted, #6F5A4A);
}
.tw-product-collections-list a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--clr-ink, #1A0F14);
  text-decoration: underline;
  text-underline-offset: 3px;
}
