/*
  Site styles for shop.trovemarket.com (catalog storefront).
  Loaded after Bootstrap 5's CSS — this file both themes the small set of
  Bootstrap components we use (explicit overrides, not reliant on Bootstrap's
  internal variable wiring) and defines everything else.
*/

/* =======================================================================
   Reset & base
======================================================================= */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  font-size: 100%; /* the font-size control (accessibility.js) scales this */
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--font-family-base);
  font-size: var(--step-0);
  line-height: var(--line-height-base);
  color: var(--color-text);
  background: var(--color-bg);
}

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

/* Site-wide: a disabled text input/dropdown/textarea must read as
   obviously, visually disabled - not just faintly dimmed by the browser's
   own often-too-subtle default rendering. Deliberately low specificity
   (a bare `input:disabled`) so it applies everywhere a control lives with
   no special wrapper (e.g. a bare table cell input in the product-create
   wizard) - a more specific rule (like .form-field's own, below) still
   needs its own matching `:disabled` variant to win the cascade, since a
   plain element+pseudo-class selector like this one can't out-specify a
   class selector on its own. */
input:disabled,
select:disabled,
textarea:disabled {
  background-color: var(--color-bg-disabled);
  color: var(--color-text-disabled);
  border-color: var(--color-border-disabled);
  cursor: not-allowed;
  opacity: 1;
}

/* Reported directly: "fields that failed should be highlighted" - `frontend/src/form-validation.ts`'s
   shared `highlightFields`/`checkRequiredFields*`/`applyServerValidationError` mark a failed field
   `aria-invalid="true"` (the real ARIA state for exactly this, not an invented CSS-only class) rather
   than requiring every one of this SPA's forms to hand-roll its own visual treatment - a screen reader
   announces it for free, and this is the one place the border/outline actually gets painted. Same
   deliberately-low specificity as the disabled-input rule just above, for the same reason (a bare
   table-cell input with no `.form-field` wrapper still needs this to work). */
input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"] {
  border-color: var(--color-danger);
  outline: 2px solid var(--color-danger);
  outline-offset: 1px;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-family-base);
  color: var(--color-heading);
  line-height: var(--line-height-heading);
  font-weight: 700;
  margin: 0 0 var(--space-3);
}

h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }

p { margin: 0 0 var(--space-4); }

a {
  color: var(--color-link);
}

a:visited {
  color: var(--color-link-visited);
}

.container {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-4);
}

.visually-hidden-label {
  /* alias kept for readability in markup; behavior defined in accessibility.css */
}

/* =======================================================================
   Buttons (explicit brand theming over Bootstrap's .btn structure)
======================================================================= */
.btn {
  font-weight: 700;
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-5);
  border: 2px solid transparent;
}

/* A button that cannot be used has to look like it, and until this rule none did: every variant
   below paints its own background and text color, so the browser's own faint dimming of a disabled
   <button> was overpainted and a disabled Save or Next was pixel-identical to a live one. That is
   worse than no disabling at all - somebody presses it, nothing happens, and nothing says why.

   Covers `[aria-disabled="true"]` as well as `:disabled`, because a wizard's Next is a link: an
   <a> cannot carry the disabled attribute, and aria-disabled is what tells a screen reader the same
   thing (the click is blocked in JS - see phone-verification.ts). `!important` because each variant
   sets its own background/border/color at the same specificity and this has to win over all of
   them without repeating itself once per variant. */
.btn:disabled,
.btn[aria-disabled="true"] {
  background-color: var(--color-bg-disabled) !important;
  border-color: var(--color-border-disabled) !important;
  color: var(--color-text-disabled) !important;
  cursor: not-allowed;
  /* Not opacity: a translucent button picks up whatever is behind it, and these sit on tinted
     callouts as often as on white. */
  box-shadow: none;
}

/* Hover and focus must not undo it - every variant has its own :hover rule at the same specificity. */
.btn:disabled:hover,
.btn:disabled:focus-visible,
.btn[aria-disabled="true"]:hover,
.btn[aria-disabled="true"]:focus-visible {
  background-color: var(--color-bg-disabled) !important;
  border-color: var(--color-border-disabled) !important;
  color: var(--color-text-disabled) !important;
}

/* Every button variant below repeats its own color on its own :visited rule.
   `a:visited` (a type selector + a pseudo-class) is MORE specific than a
   plain `.btn-primary` class selector, so without this a button-styled link
   silently darkens to the visited-link color the first time it is followed -
   including a same-page #hash link like a wizard's Next/Back, which counts
   as "visited" too. */

.btn-primary,
.btn-primary:visited {
  background-color: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-contrast);
}

.btn-primary:hover,
.btn-primary:focus-visible {
  background-color: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
  color: var(--color-primary-contrast);
}

/* Reported directly: "Leave the add to cart to say Currently unavailable and be disabled blue" - the
   PDP's own Add to cart button, disabled either because nothing has fully resolved yet ("Select
   options") or because the resolved variant is out of stock ("Currently unavailable" -
   `variant-picker.ts`) reads as a genuine, intentional state here, not a generic grayed-out/broken
   button - `--color-info`, the same blue this app already uses for informational badges elsewhere,
   rather than a plain disabled gray. Scoped to this one button, not every `.btn-primary:disabled`
   site-wide (a disabled "Save"/"Next" button elsewhere in the app was never part of this ask, and
   recoloring every one of them blue would be a much bigger, unrequested visual change). */
[data-role="add-to-cart-button"]:disabled {
  background-color: var(--color-info);
  border-color: var(--color-info);
  color: var(--color-primary-contrast);
  opacity: 1;
  cursor: not-allowed;
}

.btn-outline-primary,
.btn-outline-primary:visited {
  background-color: transparent;
  border-color: var(--color-primary);
  color: var(--color-primary);
}

.btn-outline-primary:hover,
.btn-outline-primary:focus-visible {
  background-color: var(--color-brand-magenta-light);
  color: var(--color-primary);
}

/* `.btn`'s own `border: 2px solid transparent` above overrides Bootstrap's variable-driven outline
   borders, so any outline variant in use needs its border color restated here or it renders as bare
   text. Danger is for consequential actions (cancel, refund, archive, reverse). */
.btn-outline-secondary,
.btn-outline-secondary:visited {
  background-color: transparent;
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.btn-outline-secondary:hover,
.btn-outline-secondary:focus-visible {
  background-color: var(--color-bg-alt);
  border-color: var(--color-text);
  color: var(--color-text);
}

.btn-outline-danger,
.btn-outline-danger:visited {
  background-color: transparent;
  border-color: var(--color-danger);
  color: var(--color-danger);
}

.btn-outline-danger:hover,
.btn-outline-danger:focus-visible {
  background-color: var(--color-danger);
  border-color: var(--color-danger);
  color: #ffffff;
}

.btn-accent,
.btn-accent:visited {
  background-color: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-accent-contrast);
}

.btn-accent:hover,
.btn-accent:focus-visible {
  background-color: var(--color-brand-gold-dark);
  border-color: var(--color-brand-gold-dark);
  color: #ffffff;
}

.btn-lg {
  padding: var(--space-3) var(--space-6);
  font-size: var(--step-1);
}

.btn svg {
  width: 1.1em;
  height: 1.1em;
  vertical-align: -0.2em;
  margin-inline-end: var(--space-2);
}

/* =======================================================================
   Header
======================================================================= */
.site-header {
  border-bottom: 1px solid var(--color-border);
  background: var(--color-bg);
}

.site-header__row {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding-block: var(--space-4);
  flex-wrap: wrap;
}

.site-logo {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
}

.site-logo img {
  height: 3.75rem;
  width: auto;
}

.site-logo__text {
  font-size: var(--step-3);
  font-weight: 800;
  color: var(--color-heading);
}

.site-logo__tag {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  font-weight: 600;
  border-left: 2px solid var(--color-border);
  padding-left: var(--space-3);
  margin-left: var(--space-1);
}

.site-search {
  flex: 1 1 20rem;
  display: flex;
  min-width: 14rem;
}

.site-search input[type="search"] {
  flex: 1;
  border: 2px solid var(--color-border-strong);
  border-right: none;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  padding: var(--space-2) var(--space-4);
  font-size: var(--step-0);
  font-family: inherit;
  min-height: 2.75rem;
}

.site-search button {
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  min-width: 3.25rem;
}

.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-left: auto;
}

.header-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.15rem;
  background: none;
  border: none;
  font-family: inherit;
  cursor: pointer;
  color: var(--color-text);
  font-size: var(--step--1);
  font-weight: 700;
  text-decoration: none;
  min-width: 3.5rem;
  min-height: 2.75rem;
  padding-block: var(--space-1);
}

.header-action svg {
  width: 1.5rem;
  height: 1.5rem;
}

/* Label must be its own single inline unit — inside a flex column, a bare
   text node next to a sibling element becomes a second stacked flex item,
   which is what made the Cart icon sit higher than Account's. */
.header-action__label {
  display: inline-flex;
  align-items: center;
  line-height: 1.2;
}

/* Icon + badge wrapper: the badge is an absolutely-positioned corner
   badge (not inline text), so it can be sized generously for readability
   without disturbing the icon/label flex layout at all. */
.header-action__icon-wrap {
  position: relative;
  display: inline-flex;
}

.header-action .badge-count {
  position: absolute;
  top: -0.65rem;
  right: -1rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--color-primary);
  color: #fff;
  border: 2px solid var(--color-bg);
  border-radius: 999px;
  font-size: var(--step--1);
  font-weight: 800;
  line-height: 1;
  min-width: 1.6rem;
  height: 1.6rem;
  padding: 0 0.3rem;
}

/* An empty cart hides the badge entirely (cart-badge.ts's `el.hidden = count === 0`) rather than
   showing "0" - this rule's own `display: inline-flex` above otherwise beats the `[hidden]` UA-
   stylesheet rule on specificity (a class selector outranks a bare attribute selector), the same
   CSS-specificity gotcha this codebase has hit before (see CLAUDE.md's disabled-checkbox fix). */
.header-action .badge-count[hidden] {
  display: none;
}

/* Account / Cart hover menus — reveal on :hover (mouse) and :focus-within
   (keyboard, once Tab lands on the trigger or a menu link). The menu is
   hidden with opacity + pointer-events, never visibility/display: those
   would pull it out of tab order entirely, so a keyboard user could never
   Tab into it to trigger :focus-within in the first place. Screen readers
   see the links in normal reading order regardless of visual state — this
   is intentionally *not* a role="menu" widget (that pattern needs real
   arrow-key roving-tabindex JS to be correct; a menu that announces as one
   but doesn't behave like one is worse than a plain list of links). Flush
   against the trigger (no gap) so a mouse moving straight down from the
   trigger to the panel never crosses a dead zone and drops :hover early. */
.header-action-wrap {
  position: relative;
  display: inline-flex;
}

/* Belle, the header's AI shopping assistant trigger - the real supplied artwork
   (/images/belle-ai-assistant.png), in a circular mascot badge deliberately not styled like the plain
   Account/Cart nav icons beside it (a character, not a functional nav glyph, same "different visual
   language for a different affordance" precedent CLAUDE.md's own drag-handle icon already
   establishes). Grows slightly and highlights on hover/focus - and, now that she has a real chat
   window (`open()`/`close()`, `belle-chat.ts`), that same highlighted look is held permanently while
   the panel is open (`.assistant-trigger[aria-expanded="true"]` below, keyed off the exact attribute
   `open()`/`closeVisualOnly()` already toggle - no separate JS-driven class needed for this one).
   `.belle-trigger-wrap` (below) replaces the earlier hover-only "I can help!" tooltip bubble with a
   permanent, always-visible "Belle" tab pinned under the icon - see that rule's own doc comment. */
.assistant-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 999px;
  border: 2px solid transparent;
  background: var(--color-bg-alt);
  color: var(--color-primary);
  cursor: pointer;
  padding: 0;
  overflow: hidden;
  transition: transform var(--motion-duration) var(--motion-easing), background var(--motion-duration) var(--motion-easing), border-color var(--motion-duration) var(--motion-easing);
}

/* Fills the frame itself (not a smaller icon-in-a-badge look) - a circular frame with real padding
   around the artwork read as shrinking the character down well below the Account/Cart icons beside
   her, reported directly: "the circle around here might be shrinking her too much." `object-fit:
   cover` crops the square source image to the circle rather than squashing it. */
.assistant-trigger__icon {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.assistant-trigger:hover,
.assistant-trigger:focus-visible,
.assistant-trigger[aria-expanded="true"] {
  transform: scale(1.12);
  background: var(--color-brand-magenta-light);
  border-color: var(--color-primary);
}

[data-motion="reduced"] .assistant-trigger {
  transition: none;
}

/* Wraps the trigger button, its permanent "Belle" tab, and (once open) the chat panel itself -
   `position: relative` here is what every one of those anchors against. `belle-chat.ts` appends the
   panel as a real DOM child of this wrap (not `document.body`) specifically so its own
   `position: absolute; top: 100%` lands relative to THIS element, the same anchor-a-dropdown-to-its-
   own-relative-wrap pattern `.header-action-wrap`/`.header-action-menu` already establish elsewhere in
   this header - `display: inline-flex` here is sizing only (just the icon; the tab and panel are both
   taken out of flow, below), not a layout host for stacked children. */
.belle-trigger-wrap {
  position: relative;
  display: inline-flex;
}

/* The shared "Belle" pull-tab look - trove magenta, rounded on its own bottom corners, white bold
   caption. Both instances are real `<button>`s now (reported directly: "I want to be able to click on
   the tab too to open and close"), not decorative spans - `.belle-trigger-tab` opens,
   `.belle-chat-panel__pull-tab` (below) closes, each carrying its own up/down arrow icon right after
   the "Belle" caption (reported directly, alongside removing the panel's old separate close
   button/header entirely - see that rule's own doc comment) - so the reset/hover/focus treatment below
   is real interactive-button styling, not just a label's look. No more triangle tail underneath
   (reported directly: "lose the nipple looking arrow at the bottom of the tab") - just the plain
   rounded tab shape. Right-aligned, not centered (reported directly: "line the tab up with the
   bottom-right of the chat window, so it blends in to the right edge") - both instances share the same
   `right: 0` so the tab reads as traveling straight down the same vertical line as it moves from resting
   under the header to attached to the open panel, rather than jumping sideways between the two. */
.belle-pull-tab {
  position: absolute;
  right: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: none;
  background: var(--color-brand-magenta);
  color: #ffffff;
  font-family: inherit;
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 0.2rem 0.65rem 0.4rem;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  white-space: nowrap;
  cursor: pointer;
}

.belle-pull-tab:hover,
.belle-pull-tab:focus-visible {
  background: var(--color-brand-magenta-dark);
}

/* Reported directly (twice - first cut wrongly sat this in normal flow *inside* the header, which just
   grew the header bar taller instead of overhanging past it): "I want the tab aligned with the end of
   the white bar, z-order top, like it's overhanging the rest of the page, pinned there." Taken out of
   flow entirely (`position: absolute`) so it can never affect `.site-header`'s own height, and offset
   far enough past the icon's own bottom edge to clear `.site-header__row`'s bottom padding and the
   header's `border-bottom` - it visibly hangs half over whatever content sits directly below the
   header (the primary nav / hero), not tucked inside the header bar itself. `z-index` puts it above
   that ordinary in-flow content underneath. "Pinned there" = pinned to the header/icon, not the
   viewport - it scrolls away with the header exactly like every other header control, since the
   header itself isn't sticky.
   Hidden the instant the panel is open (`:has()`, already an established technique in this codebase -
   `accessibility.css`'s own `.tooltip-wrap:has(:focus-visible)`) - reported directly: "I want it to
   slide out so the tab is on the bottom of the chat window, like it was attached, like you pulled on
   the tab to pull down the chat." There is only ever one visible "Belle" tab at a time - this one,
   resting under the header while closed, or `.belle-chat-panel__pull-tab` (below), attached to the
   open panel's own bottom edge - never both, so the illusion reads as the same tab having traveled
   down with the panel as it unfurled, not two independent decorations. */
.belle-trigger-tab {
  top: calc(100% + 1.2rem);
  z-index: 5;
}

.belle-trigger-wrap:has(.belle-chat-panel:not([hidden])) .belle-trigger-tab {
  display: none;
}

/* The proactive "there's something I can help with here" signal (`showProactiveHint`, `belle-chat.ts`)
   - reported directly, corrected twice (first "too slow" at a 1s-on/0.5s-off/1s-on cadence; then
   "speed up a bit more... out of the seizure range" once it became three flat half-second flashes):
   now three flashes, each a flat 0.3s on and 0.3s off - a 2Hz flash rate, comfortably under WCAG
   2.3.1's 3-flashes-per-second general flash threshold (the seizure-safety limit), while still
   reading noticeably snappier than the previous 1.2Hz/2Hz cadences. A square-wave keyframe (each
   state held flat across a percentage range, not eased into) rather than a smooth pulse, so it reads
   as a distinct blink each time instead of a glow - the percentage breakpoints themselves are
   unchanged from the previous cadence (each segment is still exactly 1/5 of the total duration), only
   the total duration shrank. Total duration (5 x 0.3s = 1.5s) matches the JS timeout that removes
   this class (`HINT_FLASH_MS`) exactly, so the class never lingers past the visual animation
   completing. Never flashes while the panel is already open (`showProactiveHint`'s own `isOpen()`
   guard) - the icon's already showing this exact look via `[aria-expanded="true"]` above, so flashing
   on top of that would be redundant. */
@keyframes belle-trigger-flash {
  0%,
  19.9% {
    transform: scale(1.12);
    background: var(--color-brand-magenta-light);
    border-color: var(--color-primary);
  }
  20%,
  39.9% {
    transform: scale(1);
    background: var(--color-bg-alt);
    border-color: transparent;
  }
  40%,
  59.9% {
    transform: scale(1.12);
    background: var(--color-brand-magenta-light);
    border-color: var(--color-primary);
  }
  60%,
  79.9% {
    transform: scale(1);
    background: var(--color-bg-alt);
    border-color: transparent;
  }
  80%,
  99.9% {
    transform: scale(1.12);
    background: var(--color-brand-magenta-light);
    border-color: var(--color-primary);
  }
  100% {
    transform: scale(1);
    background: var(--color-bg-alt);
    border-color: transparent;
  }
}

.assistant-trigger.is-hint-flashing {
  animation: belle-trigger-flash 1.5s var(--motion-easing);
}

/* Belle's floating chat window (`frontend/src/behaviors/belle-chat.ts`) - built and toggled
   entirely in JS via the plain `hidden` attribute, deliberately NOT the native `<dialog>`/
   `showModal()` pattern `error-modal.ts`/the product-create wizard's own popups use elsewhere in
   this codebase (see that file's own doc comment): a true modal blocks the rest of the page, and a
   shopper needs to keep browsing while chatting with Belle.
   Reported directly, across three rounds of correction: (1) the panel should visibly "slide out down,
   expanding, from the toolbar" under Belle's own icon, not fade up from an independent bottom-right
   corner; (2) once shipped, it was popping up in the middle of the page instead - `belle-chat.ts`'s
   `buildPanel()` was appending the panel to `document.body`, not as a real DOM descendant of
   `.belle-trigger-wrap` at all, so this `position: absolute` had no positioned ancestor to anchor
   against and fell back to the document's own initial containing block, fixed by actually appending it
   into the trigger wrap; (3) "the top of the chat shouldn't have rounded edges and should line up to
   the tool/search bar bottom, not overlapping it - it needs to look like it's coming out of the bar,
   not above it." `border-radius` is now bottom-corners-only (`0 0 var(--radius-lg) var(--radius-lg)`) -
   a flat top edge reads as a continuation of the header bar, not a separate floating card sitting just
   below it. `top` is offset past the icon's own bottom edge by roughly `.site-header__row`'s own
   bottom padding (`--space-4`) plus the small gap between the icon and the row's own tallest item (the
   site-logo image) - landing the panel's flat top edge right at the header's real `border-bottom`,
   flush against it rather than overlapping into it or floating below it with a visible gap. (An
   estimate, not a JS-measured value - if the real header's rendered height ever drifts from this,
   the offset would need adjusting to match.) The exact same anchor-a-dropdown-to-its-relative-wrap
   pattern `.header-action-menu` already uses against `.header-action-wrap` elsewhere in this same
   header. `@keyframes` rather than a `transition` for the entrance, since browsers restart a CSS
   animation on its own whenever an element goes from `display:none` (`[hidden]`) to displayed - no JS
   coordination needed to trigger it on open, and `accessibility.css`'s existing global
   `html[data-motion="reduced"] *` rule already collapses `animation-duration` to near-zero for every
   element site-wide, so this needs no special-casing of its own for reduced motion.
   Fourth correction, reported directly: "the right side of the chat box... shouldn't have a rounded
   corner, it should be flat, all the way to the rounded corner of the tab." The bottom-right corner
   was still rounded (`var(--radius-lg)`) - exactly where `.belle-chat-panel__pull-tab` (below) hangs
   off the panel's own bottom-right edge, so the panel's own curve and the tab's own curve collided
   visually right at that corner instead of reading as one continuous flat edge down to the tab. Now
   only the bottom-LEFT corner (away from the tab) stays rounded - the entire right side, top to
   bottom, is flat, flowing straight down into the tab's own rounded corner with nothing competing
   against it. */
.belle-chat-panel {
  position: absolute;
  top: calc(100% + 1.1rem);
  right: 0;
  width: min(22rem, calc(100vw - 2 * var(--space-5)));
  max-height: min(28rem, calc(100vh - 10rem));
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: 0 0 0 var(--radius-lg);
  box-shadow: var(--shadow-md);
  z-index: 1300;
  transform-origin: top right;
  animation: belle-chat-panel-slide-down var(--motion-duration) var(--motion-easing);
}

@keyframes belle-chat-panel-slide-down {
  from {
    opacity: 0;
    transform: scaleY(0.85) translateY(-0.5rem);
  }
  to {
    opacity: 1;
    transform: scaleY(1) translateY(0);
  }
}

/* The other half of the pull-tab illusion (see `.belle-trigger-tab`'s own doc comment above) - the
   same "Belle" tab now attached to the bottom edge of the OPEN panel, as if pulling it down is what
   unfurled the panel in the first place. Two corrections so far: a first cut hung this below the
   panel's own border via a guessed negative `bottom` offset (read as floating up near the send
   button); a second cut made it a plain last flex child instead, which fixed the alignment but put it
   INSIDE the panel's own box, not protruding from it - reported directly: "the top of the tab needs to
   be at the bottom edge of the chat window when opened." `position: absolute; top: 100%` is the exact,
   estimate-free way to say that in CSS - `.belle-chat-panel` is itself the positioned ancestor
   (`position: absolute` already), so `top: 100%` places this element's own top edge precisely at the
   panel's real bottom border, hanging entirely below/outside it - genuinely protruding, not resting on
   guessed pixel math the way the first cut did. */
.belle-chat-panel__pull-tab {
  top: 100%;
  right: 0;
}

/* Reported directly, corrected once (first cut sat this above the message log, in the removed
   header's old spot): "I wanted Clear Chat between the chat window and the message input window" -
   this slim, uncolored toolbar row now sits directly between the messages log and the composer
   instead, a thin top border reading as the divider between the two. No background/color block of its
   own, so there's no rounded-corner mismatch to account for either (the old header bar needed its own
   matching `border-radius` for exactly that reason - this one doesn't). `space-between` (not
   `flex-end`) now that the help toggle (below) shares this row too - the toggle reads left, "Clear
   chat" stays right, rather than the two crowding together on one side. */
.belle-chat-panel__toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-1) var(--space-3);
  border-top: 1px solid var(--color-border);
}

/* Reported directly: "add a help on/off toggle to show the help she can do for this page or not." A
   real `<label>` wrapping a real checkbox - clicking the text toggles it too, for free, and a screen
   reader gets the native checked/not-checked announcement with no extra `aria-*` wiring needed. */
.belle-chat-panel__hints-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--step--1);
  color: var(--color-text-muted, var(--color-text));
  cursor: pointer;
}

.belle-chat-panel__clear {
  border: none;
  background: transparent;
  color: var(--color-text);
  font-size: var(--step--1);
  text-decoration: underline;
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
}

.belle-chat-panel__clear:hover,
.belle-chat-panel__clear:focus-visible {
  color: var(--color-brand-magenta);
}

/* The message log - two-sided chat bubbles, Belle's own on the left (the conventional
   assistant/bot side), the shopper's own on the right. `flex-direction: column` +
   `align-items: flex-start` (the default) is what makes a bubble hug the left edge unless its own
   `--customer` modifier overrides it to `flex-end`. */
.belle-chat-panel__messages {
  flex: 1;
  min-height: 8rem;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
}

.belle-chat-panel__bubble {
  max-width: 85%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  align-self: flex-start;
  background: var(--color-bg-alt);
  color: var(--color-text);
}

/* The shopper's own messages - a solid brand-color fill with light text, the conventional "this is
   what I sent" treatment every real chat UI (iMessage, Intercom, etc.) uses to visually distinguish
   the two sides beyond just left/right position alone. */
.belle-chat-panel__bubble--customer {
  align-self: flex-end;
  background: var(--color-primary);
  color: var(--color-primary-contrast);
}

/* Reported directly: "make the help messages a slightly different color then her ch[at] messages,
   maybe a greenish hue but still honor the disability config." A light wash of `--color-success` (the
   same token every other success-colored element in this app already reads through, so it inherits
   that token's own `[data-contrast="high"]` override automatically - never a hardcoded green that
   could silently stop adapting under high contrast) plus a matching left border, layered on top of
   the ordinary `--belle` bubble styling above, not replacing it - text color/alignment/etc. stay
   identical to any other Belle reply. */
.belle-chat-panel__bubble--hint {
  background: color-mix(in srgb, var(--color-success) 12%, var(--color-bg));
  border: 1px solid var(--color-success);
}

/* A SHOW_PRODUCT action's own real product-embed tile (buyer-read's `embed` fragment) - strip the
   ordinary bubble's own background/padding so the tile's own card border/background show cleanly,
   the tile is the content here, not a caption around it. */
.belle-chat-panel__bubble--product {
  padding: 0;
  background: transparent;
  max-width: 100%;
}

.belle-chat-panel__bubble--product .product-embed {
  max-width: 100%;
}

/* "Belle is typing" - a bouncing-ellipsis loading indicator shown while a chat turn is in flight
   (reported directly: no feedback at all reads as "did this even work?"). Three plain dots, staggered
   animation delays, inside an ordinary Belle-side bubble so it reads as part of the same transcript
   rather than a separate loading widget. */
.belle-chat-panel__typing-dots {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: var(--space-1) 0;
}

.belle-chat-panel__typing-dots span {
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: var(--color-text-muted, var(--color-text));
  opacity: 0.4;
  animation: belle-typing-bounce 1.2s infinite ease-in-out;
}

.belle-chat-panel__typing-dots span:nth-child(2) {
  animation-delay: 0.2s;
}

.belle-chat-panel__typing-dots span:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes belle-typing-bounce {
  0%,
  60%,
  100% {
    transform: translateY(0);
    opacity: 0.4;
  }
  30% {
    transform: translateY(-0.25rem);
    opacity: 1;
  }
}

[data-motion="reduced"] .belle-chat-panel__typing-dots span {
  animation: none;
  opacity: 0.7;
}

.belle-chat-panel__bubble-text {
  margin: 0;
  font-size: var(--step--1);
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

/* A known page Belle mentions by path renders as a real link (reported directly: "this should
   generate a clickable link to get there, not put it in quotes") - underlined so it reads as
   interactive against the plain bubble text around it, using the same primary-color/customer-bubble
   pairing every other in-app link already does. */
.belle-chat-panel__bubble-text a {
  color: var(--color-primary);
  text-decoration: underline;
  font-weight: 600;
}

/* No `border-top` of its own any more - `.belle-chat-panel__toolbar` (above) now sits directly between
   this and the message log, and already carries that same divider line; a second border directly under
   it would read as a redundant double line rather than one clean divider. */
.belle-chat-panel__composer {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4) var(--space-4);
}

/* Guest mode: shown in the composer's place when nobody is signed in (chatting with Belle needs an
   account). Same padding as the composer so the panel's height barely changes between the two. */
.belle-chat-panel__guest {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4) var(--space-4);
}

.belle-chat-panel__guest-message {
  margin: 0;
  font-weight: 600;
}

.belle-chat-panel__guest-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.belle-chat-panel__help-list {
  margin: var(--space-2) 0;
  padding-left: 1.25rem;
}

.belle-chat-panel__help-list li + li {
  margin-top: var(--space-1);
}

/* Deliberately its own rule, not `.form-field textarea` (which this composer doesn't use at all) -
   a real, working vertical resize handle is the literal ask ("resizable text window"), so `resize`
   is set explicitly here rather than left to chance. */
.belle-chat-panel__textarea {
  width: 100%;
  min-width: 0;
  resize: vertical;
  min-height: 3.5rem;
  max-height: 12rem;
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-family: inherit;
  font-size: var(--step-0);
  color: var(--color-text);
  background: var(--color-bg);
}

.belle-chat-panel__send {
  align-self: flex-end;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  padding: 0;
  flex-shrink: 0;
}

[data-motion="reduced"] .belle-chat-panel {
  animation: none;
}

.header-action-menu {
  position: absolute;
  top: 100%;
  right: 0;
  min-width: 15rem;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-3);
  opacity: 0;
  pointer-events: none;
  transform: translateY(-4px);
  transition: opacity var(--motion-duration) var(--motion-easing), transform var(--motion-duration) var(--motion-easing);
  z-index: 1200;
}

[data-motion="reduced"] .header-action-menu {
  transition: none;
}

.header-action-wrap:hover .header-action-menu,
.header-action-wrap:focus-within .header-action-menu {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

.header-action-menu__link {
  padding: var(--space-2) var(--space-2);
  min-height: 2.75rem;
  display: flex;
  align-items: center;
  border-radius: var(--radius-sm);
  color: var(--color-text);
  text-decoration: none;
  font-size: var(--step--1);
  font-weight: 600;
}

.header-action-menu__link:hover,
.header-action-menu__link:focus-visible {
  background: var(--color-bg-alt);
  text-decoration: underline;
}

button.header-action-menu__link {
  border: none;
  background: none;
  width: 100%;
  text-align: left;
  font: inherit;
  cursor: pointer;
}

/* `.header-action-menu__link` sets display:flex, which would otherwise beat the UA stylesheet's
   low-specificity `[hidden] { display: none }` rule and leave a "hidden" login/logout link visible. */
.header-action-menu__link[hidden] {
  display: none;
}

.header-action-menu__divider {
  border: none;
  border-top: 1px solid var(--color-border);
  margin: var(--space-2) 0;
}

.header-action-menu__heading {
  font-size: var(--step--1);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  margin: var(--space-1) var(--space-2) var(--space-1);
}

.header-action-menu--cart {
  min-width: 18rem;
}

.header-cart-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 18rem;
  overflow-y: auto;
}

.header-cart-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
}

.header-cart-item img {
  width: 3rem;
  height: 3rem;
  object-fit: cover;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
  background: var(--color-bg-alt);
}

.header-cart-item__body {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.header-cart-item__title {
  font-size: var(--step--1);
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.header-cart-item__meta {
  font-size: var(--step--1);
  color: var(--color-text-muted);
}

.header-cart-menu__footer {
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
}

.header-cart-menu__subtotal {
  display: flex;
  justify-content: space-between;
  font-size: var(--step--1);
  font-weight: 700;
  margin: 0 0 var(--space-3);
}

.header-cart-menu__empty {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  padding: var(--space-3) var(--space-2);
  text-align: center;
}

/* Primary nav */
.primary-nav {
  background: var(--color-bg-inverse);
}

.primary-nav__list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
}

.primary-nav__list a {
  display: inline-block;
  color: var(--color-text-inverse);
  text-decoration: none;
  font-weight: 700;
  padding: var(--space-3) var(--space-4);
}

.primary-nav__list a:visited {
  color: var(--color-text-inverse);
}

.primary-nav__list a:hover,
.primary-nav__list a:focus-visible {
  background: rgba(255, 255, 255, 0.12);
  text-decoration: underline;
}

.primary-nav__list a[aria-current="page"] {
  background: var(--color-accent);
  color: var(--color-accent-contrast);
}

.nav-toggle {
  display: none;
}

/* =======================================================================
   Breadcrumbs
======================================================================= */
.breadcrumbs {
  padding-block: var(--space-3);
}

.breadcrumbs ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  font-size: var(--step--1);
  color: var(--color-text-muted);
}

.breadcrumbs li:not(:last-child)::after {
  content: "›";
  margin-left: var(--space-2);
  color: var(--color-border-strong);
}

.breadcrumbs a {
  text-decoration: underline;
}

.breadcrumbs [aria-current="page"] {
  color: var(--color-text);
  font-weight: 700;
}

/* =======================================================================
   Hero
======================================================================= */
.hero {
  /* Uses the semantic --color-primary tokens (not the raw brand color)
     so high-contrast mode collapses this to solid black automatically. */
  background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
  color: #ffffff;
  padding-block-start: var(--space-5);
  padding-block-end: var(--space-8);
  transition: padding-block-end var(--motion-duration) var(--motion-easing);
}

html[data-hero="collapsed"] .hero {
  padding-block-end: var(--space-5);
}

.hero__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.hero__eyebrow {
  margin: 0;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: var(--step--1);
  color: #fbf7f5;
}

.hero__toggle {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.6);
  color: #ffffff;
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-3);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: inherit;
  font-size: var(--step--1);
  font-weight: 700;
  cursor: pointer;
}

.hero__toggle:hover {
  background: rgba(255, 255, 255, 0.12);
}

.hero__toggle-icon {
  width: 1em;
  height: 1em;
  transition: transform var(--motion-duration) var(--motion-easing);
}

html[data-hero="collapsed"] .hero__toggle-icon {
  transform: rotate(-180deg);
}

/* Grid-rows animation trick: no JS height measuring, and it collapses to
   zero cleanly regardless of content length. */
.hero__collapsible {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--motion-duration) var(--motion-easing);
}

.hero__collapsible-inner {
  min-height: 0;
  overflow: hidden;
  padding-top: var(--space-5);
}

html[data-hero="collapsed"] .hero__collapsible {
  grid-template-rows: 0fr;
}

html[data-hero="collapsed"] .hero__collapsible-inner {
  padding-top: 0;
}

.hero h1 {
  color: #ffffff;
  max-width: 40rem;
}

.hero p {
  max-width: 36rem;
  font-size: var(--step-1);
  color: #fbf7f5;
}

.hero__actions {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-5);
}

/* Store banner (seller / partner storefronts) */
.store-banner {
  background: var(--color-bg-inverse);
  color: #ffffff;
  padding-block: var(--space-6);
}

.store-banner__row {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
}

.store-avatar {
  width: 5.5rem;
  height: 5.5rem;
  border-radius: var(--radius-lg);
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--step-3);
  font-weight: 800;
  flex-shrink: 0;
}

/* A seller/partner with a real photo or logo uses an <img> instead of the
   initials-in-a-tile placeholder above - same size/shape, cropped to fill. */
img.store-avatar {
  object-fit: cover;
}

.store-banner h1 {
  color: #ffffff;
  margin-bottom: var(--space-2);
}

.store-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  font-size: var(--step--1);
  color: #dce7ec;
  margin-top: var(--space-2);
  list-style: none;
  padding: 0;
}

.store-meta li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.store-meta svg {
  width: 1em;
  height: 1em;
}

/* =======================================================================
   Section headings / layout
======================================================================= */
.section {
  padding-block: var(--space-7);
}

.section--alt {
  background: var(--color-bg-alt);
}

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

.section__head p {
  margin: 0;
  color: var(--color-text-muted);
}

/* =======================================================================
   Category carousel — a horizontally scrolling row with prev/next
   buttons, not an auto-advancing slideshow. Scrolling is native
   (scroll-snap + scrollBy), so it's keyboard/trackpad/touch accessible
   for free; the buttons are the explicit, obvious control for everyone
   else. See js/accessibility.ts initCategoryCarousel for the behavior,
   including the reduce-motion handling (instant jump, no slide).
======================================================================= */
.category-carousel {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.category-carousel__nav {
  flex-shrink: 0;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 999px;
  border: 2px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.category-carousel__nav svg {
  width: 1.25rem;
  height: 1.25rem;
}

.category-carousel__nav:hover:not(:disabled) {
  border-color: var(--color-primary);
  background: var(--color-brand-magenta-light);
}

.category-carousel__nav:disabled {
  opacity: 0.35;
  cursor: default;
}

.category-grid {
  display: flex;
  gap: var(--space-4);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  scroll-padding-inline: var(--space-2);
  padding-block: var(--space-1);
  flex: 1;
  min-width: 0;
  scrollbar-width: none;
}

.category-grid::-webkit-scrollbar {
  display: none;
}

.category-tile {
  flex: 0 0 9.5rem;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  text-align: center;
  text-decoration: none;
  color: var(--color-text);
  padding: var(--space-5) var(--space-3);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-bg);
}

.category-tile:hover,
.category-tile:focus-visible {
  border-color: var(--color-primary);
  background: var(--color-brand-magenta-light);
}

.category-tile__icon {
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--tile-color, var(--color-brand-magenta-light));
  color: var(--tile-fg, var(--color-brand-magenta));
}

.category-tile__icon svg {
  width: 1.9rem;
  height: 1.9rem;
}

.category-tile span {
  font-weight: 700;
  font-size: var(--step--1);
}

/* "Show all categories" (#category-toggle, js/accessibility.ts
   initCategoryToggle) swaps the horizontally-scrolling carousel for a
   plain wrapping grid showing every tile at once - nothing is hidden
   from anyone either way, this only changes layout, so no `inert` is
   needed the way the collapsible toggles below use it. */
.category-carousel.is-expanded .category-carousel__nav {
  display: none;
}

.category-carousel.is-expanded .category-grid {
  flex-wrap: wrap;
  overflow-x: visible;
  scroll-snap-type: none;
}

/* =======================================================================
   Catalog layout: filters + results
======================================================================= */
.catalog-layout {
  display: grid;
  grid-template-columns: 16rem 1fr;
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 62rem) {
  .catalog-layout {
    grid-template-columns: 1fr;
  }
}

/* Filters show/hide toggle — mobile/narrow only. On desktop the filters
   sidebar is always visible and this button is hidden entirely, matching
   .catalog-layout's own 62rem breakpoint so the toggle only ever appears
   once the layout has actually stacked to one column. */
.filters-toggle {
  display: none;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 2.75rem;
  margin-bottom: var(--space-4);
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: var(--step-0);
  font-weight: 700;
  color: var(--color-text);
  cursor: pointer;
}

.filters-toggle svg {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
}

.filters-toggle__chevron {
  margin-left: auto;
  transition: transform 0.15s ease;
}

[data-motion="reduced"] .filters-toggle__chevron {
  transition: none;
}

.filters-toggle[aria-expanded="true"] .filters-toggle__chevron {
  transform: rotate(180deg);
}

.filters-collapsible {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 0.2s ease;
}

[data-motion="reduced"] .filters-collapsible {
  transition: none;
}

.filters-collapsible__inner {
  min-height: 0;
  overflow: hidden;
}

@media (max-width: 62rem) {
  .filters-toggle {
    display: flex;
  }

  .filters-collapsible.is-collapsed {
    grid-template-rows: 0fr;
  }
}

.filters {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}

.filters__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.filters h2 {
  font-size: var(--step-1);
  margin: 0;
}

.filters__clear {
  background: none;
  border: none;
  padding: var(--space-1) 0;
  color: var(--color-link);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  font-size: var(--step--1);
  font-weight: 700;
  cursor: pointer;
}

.filter-group {
  border: none;
  padding: 0;
  margin: 0 0 var(--space-5);
}

.filter-group legend {
  font-weight: 700;
  padding: 0 0 var(--space-2);
  font-size: var(--step-0);
}

.filter-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.75rem; /* the checkbox stays compact; the whole label is the 44px tap target */
  cursor: pointer;
  font-size: var(--step--1);
}

.filter-group__extra {
  display: block;
}

.filter-group__more {
  background: none;
  border: none;
  padding: var(--space-1) 0;
  margin-top: var(--space-1);
  color: var(--color-link);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  font-size: var(--step--1);
  font-weight: 700;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
}

/* Price — a "mini slider": two independent native range inputs (min,
   max) rather than one dual-thumb control. Native <input type="range">
   is keyboard- and screen-reader-accessible out of the box; a custom
   overlapping-thumbs widget would need its own ARIA slider wiring and
   drag handling to match that, for a compact sidebar filter. */
.price-slider {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.price-slider__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--step--1);
  font-weight: 600;
  margin-bottom: var(--space-2);
}

.price-slider__value {
  font-weight: 700;
  color: var(--color-heading);
}

.price-slider input[type="range"] {
  width: 100%;
  accent-color: var(--color-primary);
  min-height: 2rem;
  cursor: pointer;
}

.results-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.results-toolbar label {
  font-weight: 700;
  font-size: var(--step--1);
}

.results-count {
  color: var(--color-text-muted);
  font-size: var(--step--1);
}

/* =======================================================================
   Product cards
======================================================================= */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
  gap: var(--space-5);
}

.product-card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  height: 100%;
}

.product-card:focus-within {
  border-color: var(--color-primary);
}

/* Draft watermark - only ever rendered for the owning seller (product-read's search() forces
   status=ACTIVE for everyone else), so seeing this at all already implies "this is your own
   listing." A translucent overlay + bold label, not color alone, so it still reads clearly under
   grayscale/high-contrast modes. pointer-events:none keeps the wishlist heart and card link
   clickable through it. */
.product-card--draft .media-tile {
  opacity: 0.85;
}

.product-card__draft-watermark {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: #fff;
  background: rgba(18, 58, 75, 0.55);
  pointer-events: none;
}

.media-tile {
  position: relative;
  aspect-ratio: 4 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--tile-color, var(--color-brand-magenta-light));
  color: var(--tile-fg, var(--color-brand-magenta));
}

/* Wishlist heart toggle, pinned to the top-right corner of a product's media tile - `card.njk`'s
   catalog tile uses `.wishlist-toggle` bare, with no wrapper, so this positioning has to live on the
   button itself, not only on the wrapper below. */
.wishlist-toggle {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 1;
}

/* Groups the wishlist heart and the Share button (CLAUDE.md "Share button next to the wishlist
   option") into one top-right cluster on the PDP's main gallery tile, instead of each button
   separately absolute-positioning itself - adding a second button next to the first is then just
   another flex child, with no manual pixel-offset math to keep in sync. */
.media-tile-actions {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: flex;
  gap: var(--space-2);
  z-index: 1;
}

/* Reported directly, a real regression: once `.media-tile-actions` (above) took over positioning the
   PDP's own wishlist+share cluster, `.wishlist-toggle`'s own bare `position: absolute` rule (also
   above, for `card.njk`'s unwrapped catalog-tile case) was left applying *inside* the wrapper too -
   two absolute-positioned elements both claiming the same top-right corner independently, instead of
   the wishlist button simply being one flex child of its own already-positioned wrapper. Reset back
   to a normal flex child specifically when nested inside `.media-tile-actions`, so this only ever
   positions itself when it's the *bare*, unwrapped `card.njk` case this whole split exists for. */
.media-tile-actions .wishlist-toggle,
.media-tile-actions .share-button {
  position: static;
}

.wishlist-toggle,
.share-button {
  width: 2.25rem;
  height: 2.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}

.wishlist-toggle:focus-visible,
.share-button:focus-visible {
  outline: 3px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.wishlist-toggle__icon,
.share-button__icon {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: var(--color-text);
  stroke-width: 2;
}

.wishlist-toggle[aria-pressed="true"] {
  border-color: var(--color-primary);
}

.wishlist-toggle[aria-pressed="true"] .wishlist-toggle__icon {
  fill: var(--color-primary);
  stroke: var(--color-primary);
}

.media-tile svg {
  width: 40%;
  height: 40%;
}

.media-tile--photo {
  overflow: hidden;
  background: var(--color-neutral-100, #eee);
}

.media-tile--photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A product with no photos yet falls back to its category icon (CLAUDE.md "Get product images
   working") - .media-tile--photo's own background is a hardcoded neutral gray meant for a real
   photo behind it, so the no-image variant reinstates .media-tile's own --tile-color/--tile-fg
   background so the fallback icon actually reads as the category's own color, not a flat gray box. */
.media-tile--photo.media-tile--no-image {
  background: var(--tile-color, var(--color-brand-magenta-light));
  color: var(--tile-fg, var(--color-brand-magenta));
}

.media-tile__no-image-icon {
  width: 40%;
  height: 40%;
}

.product-card__body {
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: 1;
}

.product-card__seller {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.product-card__seller a {
  color: var(--color-text-muted);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* Fixed to exactly 2 lines' worth of height regardless of actual title
   length, so price/badge/button line up across every card in a row —
   a 1-line title still reserves the same space a 2-line one would use. */
.product-card__title {
  font-size: var(--step-0);
  font-weight: 700;
  line-height: 1.3;
  min-height: 2.6em;
  margin: 0;
}

.product-card__title a {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: underline;
  text-underline-offset: 0.15em;
  color: var(--color-heading);
}

.product-card__price {
  font-size: var(--step-1);
  font-weight: 800;
  color: var(--color-heading);
  white-space: nowrap;
}

.product-card__variants {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  margin: 0;
}

.product-card__footer {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

/* Badges — always icon + text, never color alone */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  font-size: var(--step--1);
  font-weight: 700;
  padding: 0.25em 0.65em;
  border-radius: 999px;
  border: 1px solid transparent;
}

.badge svg {
  width: 1em;
  height: 1em;
}

.badge--in-stock {
  background: #e3f3f0;
  color: var(--color-success);
  border-color: var(--color-success);
}

.badge--low-stock {
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  border-color: var(--color-brand-gold-dark);
}

.badge--out-of-stock {
  background: #f8e9e7;
  color: var(--color-danger);
  border-color: var(--color-danger);
}

.badge--pickup {
  background: var(--color-brand-magenta-light);
  color: var(--color-brand-magenta);
  border-color: var(--color-brand-magenta);
}

/* The PDP's fulfillment-type badge (CLAUDE.md "Fulfillment-type badge") - a genuinely different fact
   from the neighboring stock badge (.badge--in-stock/--low-stock/--out-of-stock, above): "how you get
   it" vs. "is it available right now." Deliberately a distinct, neutral/info color from every stock
   badge's own success/warning/danger palette, so the two never read as competing signals about the
   same thing. */
.badge--fulfillment {
  background: #e6eef8;
  color: var(--color-info);
  border-color: var(--color-info);
}

/* General-purpose chip usage (e.g. an applied variant type in the product-create wizard) - no
   particular business meaning of its own, unlike the stock-status/pickup modifiers above. */
.badge--neutral {
  background: var(--color-bg-subtle, #f1f4f6);
  color: var(--color-text);
  border-color: var(--color-border);
}

.badge--neutral button {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  color: inherit;
  font-weight: 700;
  line-height: 1;
}

/* The dotted underline is the only visual cue that a chip's own value text is a button (renames it
   in place) rather than plain label text - product-create-wizard.ts's "Rename or remove an
   already-added value" flow. */
.badge__edit-trigger {
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}

.badge__edit-trigger:hover,
.badge__edit-trigger:focus-visible {
  text-decoration-style: solid;
}

/* Replaces one chip, in place, with an inline rename form (product-create-wizard.ts's
   `renderVariantOptionGroups` edit row) - same inline-editing shape as the variant table's own SKU/
   price/qty cells, just for a single value instead of a whole row. */
.badge-edit-row {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* Gradient color picker (frontend/src/color-swatches.ts) - the product-create wizard's "Add a
   variant type" modal, Color type only. The square's own hue-tinted background is set live in JS
   (`setGradientPickerState`); the two gradient overlays here (black-to-transparent bottom-to-top,
   white-to-transparent left-to-right) are what turn that flat hue into a full saturation/brightness
   picking surface - the standard technique every real color-square widget uses. */
.color-sv-square {
  position: relative;
  width: 100%;
  max-width: 22rem;
  height: 10rem;
  margin: 0 auto;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  cursor: crosshair;
  touch-action: none;
  background-image: linear-gradient(to top, #000000, rgba(0, 0, 0, 0)), linear-gradient(to right, #ffffff, rgba(255, 255, 255, 0));
}

.color-sv-square:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

.color-sv-marker {
  position: absolute;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/* The hue slider - a native <input type="range"> so hue selection is keyboard-operable for free;
   only its track/thumb are re-skinned as a rainbow gradient + plain round handle. */
.color-hue-slider {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 1rem;
  border-radius: 999px;
  background: linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000);
  outline-offset: 2px;
}

.color-hue-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--color-text);
  cursor: pointer;
}

.color-hue-slider::-moz-range-thumb {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--color-text);
  cursor: pointer;
}

.color-hue-slider::-moz-range-track {
  height: 1rem;
  border-radius: 999px;
  background: transparent;
}

/* =======================================================================
   Pagination
======================================================================= */
.pagination-nav {
  display: flex;
  justify-content: center;
  margin-top: var(--space-7);
}

.pagination-nav ul {
  list-style: none;
  display: flex;
  gap: var(--space-2);
  padding: 0;
  margin: 0;
}

.pagination-nav a,
.pagination-nav button,
.pagination-nav span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  text-decoration: none;
  color: var(--color-text);
  font-weight: 700;
  background: none;
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
}

.pagination-nav a:hover,
.pagination-nav a:focus-visible,
.pagination-nav button:hover,
.pagination-nav button:focus-visible {
  border-color: var(--color-primary);
  color: var(--color-primary);
}

.pagination-nav [aria-current="page"] {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* =======================================================================
   Seller grid (used on the Partner storefront to list hosted sellers)
======================================================================= */
.seller-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--space-5);
}

.seller-card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  display: flex;
  gap: var(--space-4);
  align-items: flex-start;
}

.seller-card__avatar {
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 999px;
  background: var(--color-brand-gold-light);
  color: var(--color-brand-gold-dark);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  flex-shrink: 0;
  font-size: var(--step-1);
}

/* A seller/partner with a real photo or logo uses an <img> instead of the
   initials-in-a-circle placeholder above - same size/shape, cropped to fill. */
img.seller-card__avatar {
  object-fit: cover;
}

.seller-card h3 {
  margin: 0 0 var(--space-1);
  font-size: var(--step-0);
}

.seller-card h3 a {
  text-decoration: underline;
  text-underline-offset: 0.15em;
  color: var(--color-heading);
}

.seller-card p {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--step--1);
}

/* =======================================================================
   Callout / info panels (used for plain-language help text)
======================================================================= */
.callout {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-left-width: 6px;
  border-left-color: var(--color-primary);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
}

.callout svg {
  width: 1.5rem;
  height: 1.5rem;
  flex-shrink: 0;
  color: var(--color-primary);
}

.callout p:last-child {
  margin-bottom: 0;
}

/* Outcome tones for a status message (an action's "saved"/"failed" line) - several views already
   set these classes; the left border carries the tone so the text keeps full contrast. */
.callout--success {
  border-left-color: var(--color-success);
}
.callout--danger {
  border-left-color: var(--color-danger);
}
.callout--info {
  border-left-color: var(--color-info);
}
/* Something is unfinished but nothing has gone wrong - the same gold the
   wizard's own unfinished-step badge uses, so the sentence and the "!" on
   the stepper read as one thing. The tinted background is what carries the
   tone here; the text keeps `--color-text` rather than a gold that would
   not clear AA on it. */
.callout--warning {
  border-left-color: var(--color-accent);
  background: var(--color-warning-bg);
}

/* A transient callout (`showTransientCallout`, dashboard-shell.ts) fades out just before it's
   removed from the DOM - collapses to a near-instant disappearance under reduced motion via
   accessibility.css's site-wide `html[data-motion="reduced"] *` transition-duration override
   (`!important`, so it always wins here regardless of stylesheet load order), no separate rule
   needed in this file. */
.callout--dismissing {
  opacity: 0;
  transition: opacity 0.4s ease;
}

/* =======================================================================
   Sign-in card (dev-login-form.ts) - a real, centered auth-page layout
   rather than the plain .callout notice box this used to sit inside.
======================================================================= */
.auth-page {
  max-width: 26rem;
  margin: var(--space-8) auto;
  padding: 0 var(--space-4);
}

.auth-card {
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: 0 1px 3px rgba(18, 56, 74, 0.08), 0 8px 24px rgba(18, 56, 74, 0.06);
  padding: var(--space-7) var(--space-6);
}

.auth-card__heading {
  margin: 0 0 var(--space-2);
  text-align: center;
}

.auth-card__subtext {
  margin: 0 0 var(--space-6);
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--step--1);
}

.auth-card .form-grid {
  gap: var(--space-4);
}

.auth-card .btn {
  width: 100%;
  margin-top: var(--space-2);
}

.auth-card__status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* =======================================================================
   Footer
======================================================================= */
.site-footer {
  background: var(--color-bg-inverse);
  color: #dce7ec;
  padding-block: var(--space-7);
  margin-top: var(--space-8);
}

.site-footer h2 {
  color: #ffffff;
  font-size: var(--step-0);
}

.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-6);
}

.footer-grid ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.footer-grid a {
  color: #dce7ec;
  text-decoration: underline;
}

/* "Cookie choices" in the footer is a <button>, not an <a>: it opens the preferences dialog rather
   than navigating. It has to READ as one of the links beside it, though - a default-styled button in a
   list of links looks like a mistake - so this strips the button chrome and inherits the link colour.
   Deliberately not `.btn`, which would make it look like a call to action. */
.site-footer__link-button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-decoration: underline;
  cursor: pointer;
  /* The 44px minimum from accessibility.css applies to every button; a footer link does not want it. */
  min-height: auto;
  min-width: auto;
}

.site-footer__link-button:hover,
.site-footer__link-button:focus-visible {
  text-decoration: none;
}

.site-footer__bottom {
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: 1px solid rgba(255, 255, 255, 0.2);
  font-size: var(--step--1);
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* =======================================================================
   Responsive: header/nav collapse
======================================================================= */
@media (max-width: 48rem) {
  .nav-toggle {
    display: inline-flex;
  }

  .primary-nav__list {
    flex-direction: column;
    display: none;
  }

  .primary-nav.is-open .primary-nav__list {
    display: flex;
  }

  .primary-nav__list a {
    padding: var(--space-3) var(--space-4);
  }
}

/* =======================================================================
   Status pills — order/account status, always icon + text (WCAG 1.4.1,
   same "never color alone" rule the stock .badge modifiers already
   follow). Colors reuse the same already-contrast-checked token pairs
   the stock badges use, rather than introducing new untested pairings.
======================================================================= */
.badge--status-success {
  background: #e3f3f0;
  color: var(--color-success);
  border-color: var(--color-success);
}

.badge--status-info {
  background: #e6eef8;
  color: var(--color-info);
  border-color: var(--color-info);
}

.badge--status-warning {
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  border-color: var(--color-brand-gold-dark);
}

.badge--status-danger {
  background: #f8e9e7;
  color: var(--color-danger);
  border-color: var(--color-danger);
}

.badge--status-neutral {
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  border-color: var(--color-border-strong);
}

/* =======================================================================
   Dashboard layout — seller/partner management, buyer account. Same
   sidebar+content grid shape .catalog-layout already establishes, not a
   new pattern; the sidebar is real in-page-anchor navigation (not a JS
   tab widget), so it's fully keyboard/screen-reader operable with zero
   custom ARIA or script.
======================================================================= */
.dashboard-layout {
  display: grid;
  grid-template-columns: 15rem 1fr;
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 62rem) {
  .dashboard-layout {
    grid-template-columns: 1fr;
  }
}

.dashboard-nav {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}

.dashboard-nav h2 {
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  padding: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-1);
}

.dashboard-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.dashboard-nav a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  text-decoration: none;
  font-weight: 700;
  min-height: 2.75rem;
}

.dashboard-nav a:hover,
.dashboard-nav a:focus-visible {
  background: var(--color-bg-alt);
}

.dashboard-nav a[aria-current="true"] {
  background: var(--color-brand-magenta-light);
  color: var(--color-primary);
}

.dashboard-nav svg {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
}

.dashboard-section {
  padding-top: var(--space-6);
  margin-top: var(--space-6);
  border-top: 1px solid var(--color-border);
}

.dashboard-section:first-child {
  padding-top: 0;
  margin-top: 0;
  border-top: none;
}

.dashboard-section__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

.dashboard-section__head p {
  margin: 0;
  color: var(--color-text-muted);
}

/* =======================================================================
   Wizard shell — .wizard-steps is a real connected-circle stepper (numbered
   .wizard-step-badge circles joined by a connecting line) IS the progress
   indicator now - there is no separate native <progress> bar underneath it
   any more (a first cut had both; reported as wanting the circles layered
   directly on the bar itself, "z-order over it, not above it" - overlaying
   interactive, tooltip-bearing circles pixel-precisely on a native
   <progress> element's own inconsistent-across-browsers internal box model
   was the fragile part, so this took the offered fallback instead: drop the
   separate bar, keep just the circles and the connecting line). Grouped
   visually with the "Step X of N / About N minutes left" meta line right
   above the panels instead of a permanent vertical sidebar column - a
   13-step wizard (product-create) no longer reserves a tall 15rem-wide
   column for the full height of the page just to list its own steps. Each
   circle's own accessible name (from the link's aria-label, since the
   visible text label was dropped in favor of the circle-only design)
   doubles as a hover/focus tooltip via content: attr(aria-label) - "hover
   over the circle, it shows you the step name" for sighted mouse users,
   while the same aria-label already gives screen readers the step name
   regardless of hover/focus. Completed steps (li.is-complete, mirroring the
   <a>'s own .is-complete CLAUDE.md already uses) and the current step
   (li[data-current], mirroring the <a>'s own aria-current="step") turn the
   connecting line leading up to them the same trove brand color
   (--color-primary) the completed circles themselves use - toggled in JS
   (initWizardStepNav, js/accessibility.js) since a pure-CSS :has() selector
   would work in every evergreen browser this codebase targets but the JS
   toggle needs no browser-support caveat at all. .dashboard-section is
   still each step's content block. Steps stay in the normal document flow
   (no JS show/hide of whole sections) so the wizard works with no
   JavaScript at all - initWizardStepNav is a progressive enhancement on
   top that syncs the stepper and the step badges to whichever step's
   anchor is currently in view.
======================================================================= */
.wizard-steps ul {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: var(--space-1) 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: var(--space-4);
  /* Deliberately not overflow-x: auto - a scrollable track clips its own
     vertical overflow too (an axis pair with only one set to auto/scroll
     forces the other to auto as well, per the CSS overflow spec), which
     silently cropped away the hover/focus tooltip below (it renders above
     the circle, i.e. outside this element's own box). Wrapping instead of
     scrolling means the tooltip is never clipped by an ancestor. */
}

.wizard-steps li {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}

/* Every li but the first grows (not just its own ::before connector) - a
   bare justify-content: space-between on the <ul> only creates gaps BETWEEN
   li boxes, which a fixed-size li's own internal connector has no way to
   grow into, leaving short dashes that don't actually touch either circle
   (reported directly, screenshot showed the gap). Making the li itself
   grow means the extra space lands inside it, where its own connector - the
   only other growable child alongside the fixed-size badge - can claim all
   of it and span edge-to-edge between the two circles it connects. */
.wizard-steps li:not(:first-child) {
  flex: 1 1 auto;
}

.wizard-steps li:not(:first-child)::before {
  /* No margin - the line has to touch both the badge before it and the badge after it with zero
     gap on either end (reported directly, with a screenshot: "the lines between the wizard circles
     arent connecting between circles fully"). Any margin here recreates exactly that gap. */
  content: '';
  flex: 1 1 auto;
  min-width: 0.75rem;
  height: 2px;
  background: var(--color-border);
}

.wizard-steps li.is-complete::before,
.wizard-steps li[data-current]::before {
  background: var(--color-primary);
}

.wizard-steps a {
  position: relative;
  display: flex;
  text-decoration: none;
}

/* Hover/focus tooltip - the step name, pulled straight from the link's own
   accessible name so there is exactly one place this text lives. */
.wizard-steps a::after {
  content: attr(aria-label);
  position: absolute;
  bottom: calc(100% + var(--space-2));
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-text);
  color: #fff;
  font-size: var(--step--1);
  font-weight: 600;
  white-space: nowrap;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
  z-index: 5;
}

.wizard-steps a:hover::after,
.wizard-steps a:focus-visible::after {
  opacity: 1;
}

.wizard-steps .wizard-step-badge {
  width: 2rem;
  height: 2rem;
  font-size: var(--step--1);
}

.wizard-progress {
  margin-bottom: var(--space-6);
}

.wizard-progress__meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
  font-size: var(--step--1);
  color: var(--color-text-muted);
}

.wizard-progress__meta strong {
  color: var(--color-text);
}

.wizard-step-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  flex-shrink: 0;
  border-radius: 999px;
  border: 1px solid var(--color-border-strong);
  background: var(--color-bg-alt);
  font-size: var(--step--1);
  font-weight: 700;
}

.wizard-steps a[aria-current="step"] .wizard-step-badge {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-contrast);
}

.wizard-steps a.is-complete .wizard-step-badge {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-contrast);
}

.wizard-step-badge svg {
  width: 0.9rem;
  height: 0.9rem;
}

/* A step that has been moved past while some of its required answers are
   still missing. It gets a gold "!" in place of the check, never a check -
   a check on a step that is not actually finished is the one thing this
   badge must never say.

   --color-accent / --color-accent-contrast rather than a new pair of
   tokens: they are already gold-on-dark in the default theme and
   #ffcc00-on-black under [data-contrast="high"], so the high-contrast
   theme needs no override of its own here. Both pairs clear WCAG AA for
   normal text (7.3:1 and 15.9:1).

   Colour is never the only signal - the glyph itself changes from a check
   to "!", so this reads the same to someone who cannot tell gold from
   magenta, and the link's own aria-label carries the step name. */
.wizard-steps a.is-incomplete .wizard-step-badge {
  background: var(--color-accent);
  border-color: var(--color-accent-contrast);
  color: var(--color-accent-contrast);
  font-weight: 700;
}

/* The connector leading up to an unfinished step stays neutral: a solid
   brand-coloured line into a "!" would read as progress that is complete. */
.wizard-steps li.is-incomplete::before {
  background: var(--color-border);
}

/* =======================================================================
   Wizard panels — one step visible at a time, carousel-style, instead of
   every step stacked on one long scrolling page. Visibility is driven
   entirely by JavaScript (the `hidden` attribute, toggled in
   accessibility.ts) rather than CSS :target - real-browser testing showed
   :target does not reliably re-match after history.pushState(), only after
   a genuine hash navigation, which made the previous :target-based rule
   silently freeze on step 1 after the second transition.

   Every panel but the first carries a hard-coded `hidden` attribute in the
   HTML itself, not just at runtime - the very first paint already matches
   the JS's own initial state, so there is nothing for JS to "correct" after
   load. That's what closes the flash-of-every-step-stacked bug: relying on
   JS to hide steps 2+ after the fact meant they were briefly all visible
   until the deferred script ran, which is exactly the kind of flicker that
   can trigger symptoms for visitors with vestibular or seizure disorders,
   so it can't be tolerated even for a moment. A no-JS visitor is left on
   step 1 with no way to advance - an accepted trade-off, not a regression,
   since Next/Back already depended on JS (to stop the native scroll-jump a
   plain href would otherwise cause), so a no-JS visitor never had a working
   multi-step experience here to begin with.

   --wizard-min-height (set inline per wizard page, from the tallest step's
   actual measured height) keeps the panel area, the Back/Next buttons, and
   the footer below it all at one fixed height across every step - without
   it, a short step (e.g. "Review") would leave the page shorter than a long
   one (e.g. "Variants"), so the footer and buttons would jump position on
   every transition. .form-actions's margin-top:auto is what pins Back/Next
   to the bottom of that fixed-height box even on a short step, rather than
   letting them float directly under a small amount of content.

   Reduced motion (the toolbar toggle OR the OS setting) is already handled
   globally above (html[data-motion="reduced"] * / prefers-reduced-motion) -
   that rule collapses these animations to ~0ms, so a step just appears
   instead of sliding. No special-casing needed here.
======================================================================= */
.wizard-panels {
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: var(--wizard-min-height, 26rem);
}

.wizard-step-panel {
  /* Only one panel is ever visible at a time, so the "not the first section"
     top divider .dashboard-section normally adds between stacked sections
     would just be empty space above every single step - never wanted here. */
  padding-top: 0;
  margin-top: 0;
  border-top: none;
}

.wizard-step-panel:not([hidden]) {
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* Only the final Back/Next row - a direct child of the panel, and the last
   div among those direct children - gets pushed to the bottom. A step with
   an in-content .form-actions of its own (e.g. product-create's "+ Add a
   variant" row) keeps that one in its natural place; it's never the last
   div child, so this selector skips it. */
.wizard-step-panel > .form-actions:last-of-type {
  margin-top: auto;
  padding-top: var(--space-5);
}

@keyframes wizard-slide-in-forward {
  from { transform: translateX(1.5rem); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

@keyframes wizard-slide-in-back {
  from { transform: translateX(-1.5rem); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

@keyframes wizard-slide-out-forward {
  from { transform: translateX(0); opacity: 1; }
  to { transform: translateX(-1.5rem); opacity: 0; }
}

@keyframes wizard-slide-out-back {
  from { transform: translateX(0); opacity: 1; }
  to { transform: translateX(1.5rem); opacity: 0; }
}

.wizard-step-panel.is-sliding-in-forward {
  animation: wizard-slide-in-forward 220ms ease-out both;
}

.wizard-step-panel.is-sliding-in-back {
  animation: wizard-slide-in-back 220ms ease-out both;
}

.wizard-step-panel.is-sliding-out-forward {
  animation: wizard-slide-out-forward 220ms ease-in both;
}

.wizard-step-panel.is-sliding-out-back {
  animation: wizard-slide-out-back 220ms ease-in both;
}

/* Photos & media step's table (product-create.html) - a `.data-table` like every other reorderable
   list in this wizard (Custom details), not the flex `<ul>`/`<li>` list this used to be. Only the
   thumbnail/label/badge cell styling below is still needed; table layout/borders come from
   `.table-wrap`/`.data-table` (shared with Custom details) same as everywhere else. */
.media-reorder-item__thumb {
  width: 3.5rem;
  height: 3.5rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--color-bg-alt);
}

.media-reorder-item__label {
  font-weight: 700;
  font-size: var(--step--1);
}

.media-reorder-item__badge {
  display: inline-block;
  font-size: var(--step--1);
  font-weight: 700;
  color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 12%, transparent);
  border-radius: var(--radius-pill, 999px);
  padding: 0.15em 0.65em;
  margin-left: var(--space-2);
}

/* Shared drag-and-drop reordering (frontend/src/drag-reorder.ts) - the product-create wizard's photo
   list and its "Custom details" table. */
[data-reorder-item] {
  transition: opacity 0.15s ease;
}

[data-reorder-item].is-dragging {
  opacity: 0.4;
}

[data-reorder-handle] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-sm);
  color: var(--color-text-subtle, var(--color-text));
  cursor: grab;
  flex-shrink: 0;
}

[data-reorder-handle]:hover,
[data-reorder-handle]:focus-visible {
  background: var(--color-bg-alt);
  color: var(--color-text);
}

[data-reorder-handle]:active {
  cursor: grabbing;
}

/* =======================================================================
   Product tabs (PDP "Product description" / "Meet the seller") — visibility
   is JS-driven via the `hidden` attribute, same lesson learned from the
   wizard carousel: no :target/CSS-only mechanism, so there's no risk of the
   pushState/:target re-match quirk that broke the wizard earlier. Without
   JavaScript both panels simply show stacked, one under the other, since
   the tab buttons have nothing to intercept their click without JS anyway.
======================================================================= */
.product-tabs__list {
  display: flex;
  gap: var(--space-2);
  border-bottom: 2px solid var(--color-border);
  margin-bottom: var(--space-5);
}

.product-tabs__tab {
  background: none;
  border: none;
  border-bottom: 3px solid transparent;
  margin-bottom: -2px;
  padding: var(--space-3) var(--space-2);
  font-size: var(--step-0);
  font-weight: 700;
  color: var(--color-text-muted);
  cursor: pointer;
}

.product-tabs__tab[aria-selected="true"] {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}

.product-tabs__tab:hover {
  color: var(--color-primary);
}

.product-tabs__panel[hidden] {
  display: none;
}

.product-tabs__seller-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.product-tabs__seller-head img {
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 999px;
  object-fit: cover;
}

/* =======================================================================
   Stat cards (dashboard KPI tiles)
======================================================================= */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11.5rem, 1fr));
  gap: var(--space-5);
}

.stat-card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  background: var(--color-bg);
}

.stat-card__label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--step--1);
  font-weight: 700;
  color: var(--color-text-muted);
  margin: 0 0 var(--space-2);
}

.stat-card__label svg {
  width: 1.1em;
  height: 1.1em;
  color: var(--color-primary);
  flex-shrink: 0;
}

.stat-card__value {
  font-size: var(--step-3);
  font-weight: 800;
  color: var(--color-heading);
  margin: 0;
}

.stat-card__meta {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  margin: var(--space-1) 0 0;
}

/* -----------------------------------------------------------------------
   Admin Overview — the one hero figure a page leads with, plus the
   two-column breakdown beneath it.

   Deliberately NOT tabular-nums: that gives every digit the width of a
   zero, which reads loose at display sizes. Tabular figures are for
   columns that must align vertically — the tables below, not this.
   ----------------------------------------------------------------------- */
.admin-hero {
  font-size: var(--step-5);
  font-weight: 800;
  line-height: 1.1;
  color: var(--color-heading);
  margin: var(--space-5) 0 0;
}

.admin-hero__caption {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  font-size: var(--step--1);
  color: var(--color-text-muted);
  margin: var(--space-1) 0 0;
}

/* State is carried by the icon and the words as well as the colour, so it
   reads the same without colour vision. */
.admin-hero__status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-weight: 700;
}

.admin-hero__status svg {
  width: 1.1em;
  height: 1.1em;
  flex-shrink: 0;
}

.admin-hero__status--attention {
  color: var(--color-warning-text);
}

.admin-hero__status--clear {
  color: var(--color-success);
}

/* A drill-in tile: the same card, wrapped in a link. */
.stat-card--link {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.stat-card--link:hover,
.stat-card--link:focus-visible {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 1px var(--color-primary);
}

[data-motion="reduced"] .stat-card--link {
  transition: none;
}

.admin-overview-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--space-5);
  margin-top: var(--space-5);
}

/* =======================================================================
   Data tables — orders, hosted sellers, order history. The wrapper
   scrolls horizontally on its own (WCAG 1.4.10 Reflow) rather than
   forcing the whole page to scroll sideways at narrow widths/high zoom.
======================================================================= */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
}

.data-table caption {
  text-align: left;
  padding: var(--space-4);
  font-weight: 700;
  font-size: var(--step-0);
  color: var(--color-heading);
  border-bottom: 1px solid var(--color-border);
}

.data-table th,
.data-table td {
  padding: var(--space-3) var(--space-4);
  text-align: left;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}

.data-table thead th {
  background: var(--color-bg-alt);
  font-weight: 700;
  color: var(--color-heading);
  white-space: nowrap;
}

.data-table tbody tr:last-child td {
  border-bottom: none;
}

/* Tracking timeline - a <details> row nested under an order row, no JS needed for the
   expand/collapse (native disclosure widget), consistent with this site's preference for HTML-only
   progressive disclosure wherever a control doesn't need persisted state. */
.data-table .tracking-row td {
  padding-top: 0;
  background: var(--color-bg-alt);
}

.tracking-timeline summary {
  cursor: pointer;
  font-weight: 700;
  padding: var(--space-2) 0;
  min-height: 2.75rem;
  display: flex;
  align-items: center;
}

.tracking-timeline__list {
  list-style: none;
  margin: 0 0 var(--space-2);
  padding: 0 0 var(--space-2) var(--space-2);
  border-left: 2px solid var(--color-border-strong);
}

.tracking-timeline__list li {
  position: relative;
  padding: 0 0 var(--space-4) var(--space-4);
}

.tracking-timeline__list li::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-2) - 5px);
  top: 0.3em;
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: var(--color-primary);
  border: 2px solid var(--color-bg);
}

.tracking-timeline__list li:last-child {
  padding-bottom: 0;
}

.tracking-timeline__status {
  font-weight: 700;
  display: block;
}

.tracking-timeline__date {
  font-size: var(--step--1);
  color: var(--color-text-muted);
}

.data-table tbody tr:hover {
  background: var(--color-bg-alt);
}

.data-table td.is-numeric,
.data-table th.is-numeric {
  text-align: right;
}

/* A single-checkbox cell (e.g. admin/variant-attribute-types' own Tile/Price/Dimensions/Weight
   columns) - `.filter-option` is `display: flex` (block-level) by default so a plain `text-align:
   center` on the cell alone has no effect on it; `inline-flex` here makes it respond to that
   centering the same way ordinary inline/text content would. */
.data-table td.toggle-cell {
  text-align: center;
}

.data-table td.toggle-cell .filter-option {
  display: inline-flex;
  justify-content: center;
}

.row-actions {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* Inside a real .data-table (as opposed to a card/list layout elsewhere .row-actions is also used
   for), wrapping is actively harmful: two 2.75rem-tall actions (each sized for a real tap target)
   stacking vertically balloons that one row far past its siblings' height - reported directly against
   the Options table (`#step-variants`): "the Option table rows are unneccissarily high... please
   condense." `.table-wrap`'s own `overflow-x: auto` (WCAG 1.4.10 Reflow) is already the intended
   fallback for a column that doesn't fit, so a table's own row-actions cell never needs to wrap - it
   scrolls instead, keeping every row the same, correct height. */
.data-table .row-actions {
  flex-wrap: nowrap;
}

/* Reported directly, confirmed live via a real browser screenshot: whenever `.row-actions` IS the
   `<td>` itself (every table across this app that has a real row-actions column - `seller-products-
   view.ts`/`seller-orders-view.ts`/`coupons-view.ts`/`partner-sellers-view.ts`/`account/orders-
   view.ts`/`admin-product-types-view.ts`/`admin-variant-attribute-types-view.ts`/the product-create
   wizard's own Options table, confirmed genuinely global as suspected), `display: flex` directly on a
   table cell breaks that row's own `border-collapse: collapse` border-bottom - it visibly truncates
   right after the row-actions cell's own flex content ends instead of reaching the table's real right
   edge, worse the wider that content is (e.g. "Edit Remove" vs. just "Edit"). Confirmed by direct
   experiment: wrapping the same content in a plain non-flex `<td>` (children laid out via the
   `inline-flex` each link/button already has, not a flex *container*) restores a correct, unbroken
   border on every row. Fixed here, CSS-only, with **zero markup changes needed in any of those
   files** - `.data-table td.row-actions` (higher specificity than the plain `.row-actions` rule above,
   which stays exactly as-is for the genuinely-flex, non-table `<div class="row-actions">` case a card/
   list layout elsewhere in this app also uses) reverts `display` back to `table-cell` and achieves the
   identical "one row, never wraps, evenly spaced" look via `white-space: nowrap` (each link/button is
   already its own `inline-flex` box) plus an explicit margin standing in for the flex `gap` this cell
   no longer has. */
.data-table td.row-actions {
  display: table-cell;
  white-space: nowrap;
}

.data-table td.row-actions > :not(:last-child) {
  margin-right: var(--space-3);
}

/* .link-action — a plain button/link styled as underlined text, for
   secondary row-level actions (table rows, list cards) outside a form's
   primary button hierarchy. Not scoped to .data-table so it's reusable
   anywhere a .row-actions group appears (e.g. an address card). */
.row-actions a,
.row-actions button,
.link-action {
  font-size: var(--step--1);
  font-weight: 700;
  color: var(--color-link);
  background: none;
  border: none;
  padding: var(--space-1) 0;
  text-decoration: underline;
  text-underline-offset: 0.15em;
  cursor: pointer;
  min-height: 2.75rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  font-family: inherit;
}

/* The small leading icon every row-action link/button carries (`frontend/src/action-icons.ts`) -
   a trash can for Remove, a pencil for Edit, and so on - so the action is recognizable at a glance,
   not just readable, for CLAUDE.md's own cognitive-accessibility bar. Purely decorative
   (aria-hidden on the <svg> itself), sized to the surrounding text rather than a fixed pixel size
   so it scales with any font-size override. */
.row-actions a svg,
.row-actions button svg,
.link-action svg {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
}

.link-action--danger {
  color: var(--color-danger);
}

/* =======================================================================
   Admin console pages (`renderAdminShell` adds `.admin-page`) - CLAUDE.md "Admin console layout".
   A wider page than the shop, and tables that keep short values (dates, ids, statuses, money,
   actions) on one line and scroll sideways inside `.table-wrap` instead of squeezing every column.
   Long free text (an error, a reason, a description) opts back into wrapping with `.is-wrap`.
   Scoped to `.admin-page` so seller and shop tables are unchanged.
======================================================================= */
.admin-page.container {
  max-width: 96rem;
}

/* `minmax(0, 1fr)`, not `1fr`: a one-line table must scroll inside its own wrapper, never widen the page. */
.admin-page .dashboard-layout {
  grid-template-columns: 13rem minmax(0, 1fr);
}

@media (max-width: 62rem) {
  .admin-page .dashboard-layout {
    grid-template-columns: minmax(0, 1fr);
  }
}

.admin-page .data-table th,
.admin-page .data-table td {
  padding: var(--space-3);
  white-space: nowrap;
}

.admin-page .data-table th.is-wrap,
.admin-page .data-table td.is-wrap {
  white-space: normal;
  min-width: 16rem;
  max-width: 32rem;
  overflow-wrap: anywhere;
}

/* The Actions column keeps its links on one line when the table has room, and only wraps them
   (never narrower than about two actions to a line) when it doesn't - so a row with several actions
   (View, Unpublish, Rebuild, Archive) never pushes the last ones out of sight past the table's edge. */
.admin-page .data-table td.row-actions {
  white-space: normal;
  min-width: 12rem;
}

.admin-page .data-table td.row-actions > * {
  white-space: nowrap;
  margin-block: 0.125rem;
}

/* A second, smaller fact under a cell's main value (a person's email under their name). */
.admin-page .data-table .admin-cell__sub {
  display: block;
  font-size: 0.875em;
  color: var(--color-text-muted, #4b5a63);
}

.admin-page .table-wrap + .form-actions,
.admin-page .table-wrap + .pagination-nav {
  margin-top: var(--space-4);
}

/* Each variant's own "Options" summary (e.g. "Color: Brown") gets its own full-width heading row
   above that variant's editable/read-only fields, instead of squeezing into a same-row column next
   to them - a long, multi-dimension options string (e.g. "Color: Brown, Size: Large") would otherwise
   compete with the SKU/Price/Qty input boxes for horizontal space. Used by both the Variants step's
   own table (`renderVariantRows`) and the Inventory step's per-variant table
   (`updateInventoryStepForVariants`) - both product-create-wizard.ts. */
.data-table tr.variant-group-heading th {
  background: var(--color-bg-alt);
  font-weight: 700;
  border-bottom: none;
  padding-bottom: var(--space-2);
}

.data-table tr.variant-group-heading + tr td {
  border-top: none;
  padding-top: var(--space-2);
}

.data-table tr.variant-group-heading:not(:first-child) th {
  border-top: 2px solid var(--color-border);
}

/* The Product options table's own Photo column - one rowspan="2" cell straddling both a variant's
   group-heading row and its fields row directly below, so the thumbnail reads as belonging to the
   whole option, not just one of its two rows. */
.variant-photo-cell {
  vertical-align: middle;
  text-align: center;
  width: 4rem;
}

/* The Options table's own leading drag-handle column - reported directly, "the Options table
   horizontally scrolls and doesn't need to, tighten it up." A narrow utility column carrying nothing
   but a small icon doesn't need this table's own generous `space-3 space-4` content padding on every
   side (`.data-table th, .data-table td`, above) - that alone was adding roughly 2rem of pure padding
   this column's actual content never used, on every single row. A plain class on the cell itself
   (`variant-reorder-cell`, `renderVariantRows`), not a `:has()` selector - this codebase has already
   deliberately avoided `:has()` elsewhere specifically to carry no browser-support caveat at all. */
.variant-reorder-cell {
  width: 2.5rem;
  padding-inline: var(--space-2);
  text-align: center;
}

/* The Options table's own plain (unwrapped-by-.form-field) SKU/Price-override inputs (`renderVariantRows`)
   used to fall back to the browser's own default text-input width (roughly 20 characters, ~170-200px)
   - reasonable for a bare form field, far too wide for a compact table cell, and the real reason this
   table needed horizontal scrolling at all once a 7th (drag-handle) column was added. Scoped to just
   this table's own SKU/Price cells, not `.data-table input` broadly, since other tables in this app
   (e.g. the Inventory step's own Qty input) size themselves differently on purpose. */
.data-table [data-variant-field="sku"],
.data-table [data-variant-field="price"] {
  width: 7rem;
  max-width: 100%;
}

/* Reported directly: these thumbnails looked "squished." The site-wide `img { height: auto }` reset
   (Reset & base, top of this file) wins over the `<img>` tag's own `width="48" height="48"`
   attributes - HTML presentation attributes always lose to any matching CSS rule - so the image was
   rendering at its own real aspect ratio (whatever a seller's uploaded photo happens to be, rarely
   square) squeezed into a 48px-wide column instead of a clean 48x48 square. `object-fit: cover` was
   already set but had no effect without a real, CSS-set height box to crop into. Explicit `width`/
   `height` here (not just the HTML attributes) is what makes `object-fit: cover` actually do its job. */
.variant-photo-cell img {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-sm);
  object-fit: cover;
}

/* =======================================================================
   Forms — settings pages (bank verification, addresses, checkout)
======================================================================= */
.form-fieldset {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  margin: 0 0 var(--space-5);
}

.form-fieldset legend {
  font-weight: 800;
  font-size: var(--step-1);
  padding: 0 var(--space-2);
}

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

.form-grid--2col {
  grid-template-columns: 1fr 1fr;
}

.form-grid--4col {
  /* minmax(0, 1fr), not a bare 1fr - a bare 1fr still floors each track at
     its content's own min-content width, which is what let 3 number inputs
     + a unit select overflow a 34rem modal into horizontal scrolling. The
     minmax(0, ...) floor lets every column actually shrink to equal width. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: end;
}

@media (max-width: 40rem) {
  .form-grid--2col {
    grid-template-columns: 1fr;
  }

  .form-grid--4col {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.form-field label {
  font-weight: 700;
  font-size: var(--step--1);
}

/* :not([type="checkbox"]):not([type="radio"]) - a checkbox/radio nested
   inside a .form-field (e.g. the Inventory step's "Unlimited" checkbox,
   itself inside a .filter-option label inside a .form-field) must stay its
   own small native size. Without this exclusion, this rule's text-input
   styling (width: 100%, a 2.75rem min-height, 2px border/padding) stretched
   a checkbox into a giant full-width box, pushing its own label text onto
   the next line - .filter-option's own doc comment already establishes
   "the checkbox stays compact" as the intended, pre-existing design. */
.form-field input:not([type="checkbox"]):not([type="radio"]),
.form-field select,
.form-field textarea {
  /* width: 100% + min-width: 0 - without these, a bare number/select input
     defaults to its own intrinsic content width instead of filling (and
     shrinking to) whatever its grid/flex parent actually allocates it,
     which is what let the Add-option modal's Length/Width/Height/Unit row
     overflow into horizontal scrolling instead of laying out as four equal,
     properly shrunk columns. */
  width: 100%;
  min-width: 0;
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-family: inherit;
  font-size: var(--step-0);
  min-height: 2.75rem;
  color: var(--color-text);
  background: var(--color-bg);
}

/* Higher specificity than the base rule above (adds :disabled to the same
   selectors) so a disabled .form-field input/select/textarea actually gets
   the global grayed-out treatment below, instead of the base rule's
   --color-text/--color-bg winning by source order. */
.form-field input:not([type="checkbox"]):not([type="radio"]):disabled,
.form-field select:disabled,
.form-field textarea:disabled {
  background: var(--color-bg-disabled);
  color: var(--color-text-disabled);
  border-color: var(--color-border-disabled);
}

/* Same higher-specificity-than-the-base-rule need as :disabled just above, for the same reason -
   `.form-field input:not(...)`'s own `border: 2px solid var(--color-border-strong)` would otherwise
   win the cascade over the plain `input[aria-invalid="true"]` rule (Reset & base, top of this file)
   inside any `.form-field`-wrapped control, which is most of them. */
.form-field input:not([type="checkbox"]):not([type="radio"])[aria-invalid="true"],
.form-field select[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
  border-color: var(--color-danger);
  outline: 2px solid var(--color-danger);
  outline-offset: 1px;
}

.form-field .field-hint {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  margin: 0;
}

/* Shared "?" info tooltip (`frontend/src/field-help.ts`) - the global replacement for a standalone
   explanatory paragraph that was crowding a form. Hover-over, not click-to-open (reported directly) -
   mirrors the accessibility toolbar's own already-proven `.tooltip-wrap`/`.tooltip-bubble` pattern
   (`accessibility.css`) almost exactly, just re-themed to sit in a light-background form instead of
   the toolbar's own dark bar: `:hover`/`:focus-visible` (not `:focus-within` - see that pattern's own
   doc comment for why), `position: absolute` + `visibility`/`opacity` (never `display`, so
   `aria-describedby` can still read it from the accessibility tree regardless of visual state). Sits
   inside `.mfe-wizard-modal` (`overflow-y: auto`) in real use - confirmed live (not just reasoned
   about) that a bubble anchored to the *left* edge of its trigger and opening *downward* renders
   correctly inside the modal's own scrollable content box without clipping, unlike the wizard-stepper
   tooltip's own earlier, structurally different bug (that one needed to render *above* a container
   with no reserved space at all, right at the top of the page - not the same shape as this one). */
.field-help {
  position: relative;
  display: inline-flex;
  vertical-align: middle;
  margin-left: var(--space-2);
}

/* The same real bug the rich-text-editor toolbar's own buttons already hit, for the identical reason
   (see that rule's own doc comment): accessibility.css's global WCAG 2.5.8 touch-target rule (`button,
   .btn, ... { min-height: 2.75rem }`) applies to every real `<button>` site-wide - `min-height`/
   `min-width` were never set here, so the global rule's own 44px minimum had nothing smaller to lose
   to and silently floored this trigger's height back up to 44px regardless of the `height: 1.25rem`
   above (`min-height` beats a smaller `height` in the box model itself, not through cascade
   specificity), stretching a 20px-wide circle into a 44px-tall oval. Fixed the same way: explicit
   `min-width`/`min-height` matching this control's own real size - a small "?" trigger next to a
   label is the same deliberate, disclosed exception to the 44px minimum every other genuinely small,
   densely-packed control in this app already carries. */
.field-help__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  min-width: 1.25rem;
  min-height: 1.25rem;
  border-radius: 50%;
  border: 1.5px solid var(--color-border-strong);
  background: var(--color-bg);
  color: var(--color-text-muted);
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}

.field-help__trigger:hover,
.field-help__trigger:focus-visible {
  border-color: var(--color-info);
  color: var(--color-info);
}

.field-help__bubble {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  background: var(--color-brand-navy-dark);
  color: #ffffff;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--step--1);
  line-height: 1.4;
  width: max-content;
  max-width: 20rem;
  box-shadow: var(--shadow-md);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--motion-duration) var(--motion-easing), transform var(--motion-duration) var(--motion-easing);
  transform: translateY(-4px);
  z-index: 1300;
}

.field-help:hover .field-help__bubble,
.field-help:has(.field-help__trigger:focus-visible) .field-help__bubble {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.form-field .required-marker {
  color: var(--color-danger);
  font-weight: 700;
  margin-left: 0.15em;
}

.form-field .field-error {
  font-size: var(--step--1);
  color: var(--color-danger);
  font-weight: 700;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
}

.form-field .field-error svg {
  width: 1.1em;
  height: 1.1em;
  flex-shrink: 0;
}

/* Quill's own snow-theme toolbar (`.ql-toolbar.ql-snow button`, `frontend/build/css/quill.snow.css`)
   sizes every button 28x24px with zero gap *between* buttons in the same format group (bold/italic/
   underline sit flush against each other, spaced only by their own 3px/5px padding) - reported
   directly as "too big and too close together" for this wizard's own compact form, and a first pass
   at shrinking it (22x22px) was still reported as "too large and looks sloppy." Scoped under
   `.rich-text-editor` with three real classes + the `button` tag (specificity 0,3,1) so it reliably
   beats Quill's own `.ql-snow.ql-toolbar button` rule (0,2,1) regardless of which stylesheet's `<link>`
   happens to load second.

   Button box is now 20x20px, matching `action-icons.ts`'s own 16x16 icon size (the "common" compact
   icon size already used for every row-action link throughout this app) once its 2px padding is
   subtracted - not an arbitrary smaller number, the same size this app already uses everywhere else
   for a small inline icon. Quill's own `.ql-stroke`/`.ql-stroke-miter` default to a fixed 2px stroke
   regardless of icon size (`quill.snow.css`'s own `svg { height: 100% }` ties icon size directly to
   the button box) - at the smaller 16px icon size a still-2px stroke reads noticeably chunkier/blobbier
   than this app's own icons (which use that same 2px stroke at a larger 16-24px box), which is the
   concrete "looks sloppy" this pass also fixes: thinned to 1.5px to match the proportionally thinner
   line weight `action-icons.ts`'s own icons read at. */
.rich-text-editor .ql-toolbar.ql-snow {
  padding: var(--space-2);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

.rich-text-editor .ql-toolbar.ql-snow .ql-formats {
  margin-right: var(--space-2);
}

/* The real root cause of "still huge after the 20px pass above" (reported directly, with a screenshot
   showing DevTools' own Network panel with caching disabled, ruling out a stale-CSS explanation):
   accessibility.css's global WCAG 2.5.8 touch-target rule (`button, .btn, ... { min-height: 2.75rem }`,
   44px) applies to every <button> site-wide, Quill's toolbar buttons included - a bare `button` element
   selector, so `width`/`height` above (0,3,1 specificity) genuinely won the cascade for THOSE two
   properties, but `min-height`/`min-width` were never set here at all, so the global rule's own
   `min-height: 2.75rem` had nothing to lose to and silently floored every button back up to 44px
   regardless of `height: 20px` - `min-height` beats a smaller `height` in the box model itself, not
   through specificity. Fixed by explicitly overriding `min-height`/`min-width` too, the same "small
   control genuinely doesn't need the 44px minimum" exception accessibility.css already carves out for
   checkboxes/radios right next to this same rule, for the same reason: a dense toolbar of many
   small, closely-packed format controls (bold/italic/underline/heading/lists/link/clean) is the
   conventional, accepted shape for a rich-text toolbar (Word/Docs/Notion all size theirs well under
   44px too), and forcing each to 44px would make the toolbar impractically wide for zero readability
   gain - a deliberate, disclosed trade-off, not an oversight. */
.rich-text-editor .ql-toolbar.ql-snow button {
  width: 20px;
  height: 20px;
  min-width: 20px;
  min-height: 20px;
  padding: 2px;
  margin: 0 2px 0 0;
}

.rich-text-editor .ql-toolbar.ql-snow button:last-child {
  margin-right: 0;
}

.rich-text-editor .ql-toolbar.ql-snow .ql-picker {
  height: 20px;
  font-size: var(--step--1);
}

.rich-text-editor .ql-toolbar.ql-snow .ql-stroke,
.rich-text-editor .ql-toolbar.ql-snow .ql-stroke-miter {
  stroke-width: 1.5;
}

.rich-text-editor .ql-container.ql-snow {
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  font-family: inherit;
  font-size: var(--step-0);
}

/* Add-option modal's Photo field - a real <details>/<summary> disclosure, not a native <select>,
   because a plain select's <option> text cannot show an image in any browser. Reported directly:
   "people with cognitive issues need to see the pictures, not just the name." <details>/<summary>
   is fully native (Enter/Space toggles, Tab-focusable, no custom ARIA listbox to reimplement) - each
   option inside the panel is a real <button role="option"> carrying its own thumbnail, and the
   summary's own accessible name (aria-labelledby pointing at both the "Photo" label and the current
   selection's own text) announces "Photo, Photo 2" the same way a real form control's label + value
   would. The chosen value itself still lives in one plain hidden input (`data-field="variant-photo-
   index"`), read exactly like every other `[data-field]` in this modal. */
.photo-picker__label {
  display: block;
  font-weight: 700;
  font-size: var(--step--1);
  margin-bottom: var(--space-2);
}

.photo-picker {
  position: relative;
}

.photo-picker__summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  cursor: pointer;
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  min-height: 2.75rem;
  background: var(--color-bg);
  font-size: var(--step-0);
}

.photo-picker__summary::-webkit-details-marker {
  display: none;
}

.photo-picker__summary::marker {
  content: '';
}

.photo-picker__summary::after {
  content: '';
  width: 0.6em;
  height: 0.6em;
  margin-left: auto;
  border-right: 2px solid var(--color-text-muted);
  border-bottom: 2px solid var(--color-text-muted);
  transform: rotate(45deg);
  flex-shrink: 0;
}

.photo-picker[open] .photo-picker__summary::after {
  transform: rotate(-135deg);
}

.photo-picker__summary img {
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
}

.photo-picker__panel {
  position: absolute;
  z-index: 10;
  top: calc(100% + var(--space-1));
  left: 0;
  right: 0;
  max-height: 14rem;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 0.5rem 1rem rgba(11, 37, 47, 0.2);
  padding: var(--space-1);
}

.photo-picker__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  text-align: left;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  font: inherit;
  color: var(--color-text);
  cursor: pointer;
}

.photo-picker__option:hover,
.photo-picker__option:focus-visible {
  background: var(--color-bg-alt);
}

.photo-picker__option[aria-selected="true"] {
  border-color: var(--color-primary);
  background: var(--color-brand-magenta-light);
}

.photo-picker__option img {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
}

.photo-picker__option-none-icon {
  display: inline-block;
  width: 2.5rem;
  height: 2.5rem;
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}

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

.or-divider {
  position: relative;
  text-align: center;
  margin: var(--space-6) 0;
}

.or-divider::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  border-top: 1px solid var(--color-border);
}

.or-divider span {
  position: relative;
  display: inline-block;
  background: var(--color-bg);
  padding: 0 var(--space-4);
  font-weight: 700;
  font-size: var(--step--1);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* =======================================================================
   Cart & checkout
======================================================================= */
.cart-layout {
  display: grid;
  grid-template-columns: 1fr 20rem;
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 62rem) {
  .cart-layout {
    grid-template-columns: 1fr;
  }
}

.cart-seller-group {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-5);
  overflow: hidden;
}

.cart-seller-group__head {
  background: var(--color-bg-alt);
  padding: var(--space-3) var(--space-4);
  font-weight: 700;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  border-bottom: 1px solid var(--color-border);
}

.cart-seller-group__head a {
  color: var(--color-heading);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.cart-line {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-4);
  border-bottom: 1px solid var(--color-border);
  align-items: flex-start;
  flex-wrap: wrap;
}

.cart-seller-group .cart-line:last-child {
  border-bottom: none;
}

.cart-line__media {
  width: 5.5rem;
  height: 5.5rem;
  border-radius: var(--radius-sm);
  overflow: hidden;
  flex-shrink: 0;
  background: var(--color-bg-alt);
}

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

.cart-line__body {
  flex: 1 1 12rem;
  min-width: 0;
}

.cart-line__title {
  font-weight: 700;
  margin: 0 0 var(--space-1);
  font-size: var(--step-0);
}

.cart-line__title a {
  color: var(--color-heading);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.cart-line__meta {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  margin: 0 0 var(--space-2);
}

/* CLAUDE.md "Checkout reliability" - a per-line availability badge ("out of stock" / "only 2 left" / "no longer available"), re-validated fresh on every cart load, so a shopper sees a stale cart line before they ever reach checkout. */
.cart-line__availability {
  margin: var(--space-1) 0 0;
}

.cart-line__controls {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.qty-stepper {
  display: inline-flex;
  align-items: center;
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
}

.qty-stepper button {
  background: none;
  border: none;
  width: 2.75rem;
  height: 2.75rem;
  font-size: var(--step-1);
  font-weight: 700;
  cursor: pointer;
  color: var(--color-text);
  font-family: inherit;
}

.qty-stepper button:hover {
  background: var(--color-bg-alt);
}

.qty-stepper input {
  width: 3rem;
  border: none;
  text-align: center;
  font-family: inherit;
  font-size: var(--step-0);
  height: 2.75rem;
  color: var(--color-text);
  background: transparent;
}

.cart-line__price-col {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  margin-left: auto;
}

.cart-line__price {
  font-weight: 800;
  font-size: var(--step-1);
  color: var(--color-heading);
  white-space: nowrap;
}

.cart-line__remove {
  background: none;
  border: none;
  color: var(--color-danger);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  font-weight: 700;
  font-size: var(--step--1);
  cursor: pointer;
  padding: var(--space-2) 0;
  min-height: 2.75rem;
  font-family: inherit;
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
}

.cart-line__remove svg {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
}

.order-summary {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-5);
}

.order-summary h2 {
  font-size: var(--step-1);
}

.summary-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--step--1);
  padding: var(--space-2) 0;
}

.summary-row--total {
  font-size: var(--step-1);
  font-weight: 800;
  color: var(--color-heading);
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-3);
  margin-top: var(--space-2);
}

/* CLAUDE.md "Checkout reliability" - the Shipping row's own value reads as gray/pending while a real
   carrier rate is still being calculated, distinct from the ordinary dark summary-row text an already-
   resolved amount uses. */
.checkout-shipping-calculating {
  color: var(--color-text-muted);
  font-style: italic;
}

/* Reported directly: put "Edit cart" and the reservation timer on the same line, timer on the right. */
.checkout-summary-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  margin-top: 0;
}

/* The real inventory/coupon reservation countdown, shown once a real hold exists (CLAUDE.md
   "Checkout reliability" - "make the reservation timer real"). Muted, small - informational, not an
   alarm, until the final stretch (checkout-page.ts applies `--warning` below) actually approaches. */
.checkout-reservation-timer {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-text-muted);
  font-size: var(--step--1);
  margin: 0;
}

.checkout-reservation-timer--warning {
  color: var(--color-danger, #b00020);
  font-weight: 700;
}

.empty-state {
  text-align: center;
  padding: var(--space-8) var(--space-4);
}

.empty-state svg {
  width: 4rem;
  height: 4rem;
  color: var(--color-text-muted);
  margin-bottom: var(--space-4);
}

.empty-state h2 {
  margin-bottom: var(--space-2);
}

.empty-state p {
  color: var(--color-text-muted);
  max-width: 28rem;
  margin-inline: auto;
  margin-bottom: var(--space-5);
}

/* =======================================================================
   Checkout real Stripe Elements card mount - Stripe's own iframe fills this
   div's box completely but paints no border/background of its own, so this
   mirrors `.form-field input`'s exact border/padding/radius/min-height look
   (styles.css's own base input rule, above) so the real card frame reads as
   an ordinary text field, not a visibly different embedded widget.
======================================================================= */
.checkout-card-element {
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-3);
  min-height: 2.75rem;
  background: var(--color-bg);
  display: flex;
  align-items: center;
}

/* Real bug, reported directly: "I cant see anything being typed into the stripe fields" - live
   measurement against the real running page (getBoundingClientRect + a full ancestor-chain walk, not
   guessed) found the true culprit: Stripe.js mounts its real iframe inside its OWN internal wrapper div
   (`<div class="__PrivateStripeElement">`, inserted between `.checkout-card-element` and the iframe,
   not documented/stable API - the leading underscore is Stripe's own signal not to rely on this class
   name directly). THAT wrapper div, not the iframe, was the element collapsing to a genuine 1px width -
   a `display: block` div with no explicit width, sitting as a flex item inside `.checkout-card-element`
   (`display: flex`), shrinking to its own tiny intrinsic content size (a flex item's default
   `min-width: auto` allows exactly this). Styling the iframe alone (an earlier, insufficient fix) never
   reached the real problem, since the iframe's own `width: 100%` still resolves against ITS parent's
   collapsed 1px box. Fixed via a direct-child selector (`>`) instead of Stripe's own private class name,
   so this survives a future Stripe.js version wrapping things slightly differently - `min-width: 0`
   overrides the shrink-to-content default, `flex: 1 1 auto` + `width: 100%` is what actually makes
   whatever Stripe mounts (a wrapper div today, or a bare iframe in some other version) claim the
   container's full real width, the same "min-width:0 lets a flex/grid child actually fill its parent's
   real allocated size" fix this codebase already applied once before for the Add-option modal's own
   form-grid overflow bug. */
.checkout-card-element > div,
.checkout-card-element > iframe {
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
}

.checkout-card-element iframe {
  border: 0;
}

.checkout-card-element--focused {
  border-color: var(--color-primary, var(--color-brand-magenta));
}

.checkout-card-element--invalid {
  border-color: var(--color-danger, #b00020);
}

/* A real trust badge next to the split card fields (reported: "is there like a stripe logo or
   something... so the customer has confidence the iframe is from stripe" - the embedded fields
   themselves carry no Stripe branding by design, so this is the standard "Secured by X" pattern real
   checkout pages use instead). Icon + text, not a hotlinked logo image. */
.checkout-stripe-badge {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-text-muted, var(--color-text));
}

.checkout-stripe-badge svg {
  flex-shrink: 0;
  width: 1.25rem;
  height: 1.25rem;
}

/* =======================================================================
   Checkout mini-cart (order summary sidebar) - a compact, read-only echo of
   `.cart-line`'s own layout/tokens, scaled down since there's no qty
   stepper/remove control here (editing still happens on /cart).
======================================================================= */
.checkout-mini-cart {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
}

.checkout-mini-cart__item {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
}

.checkout-mini-cart__item:last-child {
  border-bottom: none;
}

.checkout-mini-cart__image {
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-sm);
  overflow: hidden;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--color-bg-alt);
}

.checkout-mini-cart__image--placeholder {
  display: block;
}

.checkout-mini-cart__body {
  flex: 1 1 auto;
  min-width: 0;
}

.checkout-mini-cart__title {
  font-weight: 700;
  font-size: var(--step--1);
  margin: 0;
}

.checkout-mini-cart__option {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  margin: var(--space-1) 0 0;
}

.checkout-mini-cart__qty {
  font-size: var(--step--1);
  color: var(--color-text-muted);
  margin: var(--space-1) 0 0;
}

/* =======================================================================
   Confirmation / status pages (order submitted, etc.)
======================================================================= */
.confirmation-panel {
  text-align: center;
  max-width: 34rem;
  margin-inline: auto;
  padding: var(--space-7) var(--space-4);
}

.confirmation-panel__icon {
  width: 4.5rem;
  height: 4.5rem;
  border-radius: 999px;
  background: #e3f3f0;
  color: var(--color-success);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-bottom: var(--space-5);
}

.confirmation-panel__icon svg {
  width: 2.25rem;
  height: 2.25rem;
}

/* =======================================================================
   Product detail page — ratings summary, histogram, and written reviews
======================================================================= */
.pdp-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-7);
  align-items: start;
}

@media (max-width: 55rem) {
  .pdp-layout {
    grid-template-columns: 1fr;
  }
}

.pdp-gallery__main {
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-bottom: var(--space-3);
}

.pdp-gallery__thumbs {
  display: flex;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.pdp-gallery__thumbs li {
  flex-shrink: 0;
  scroll-snap-align: start;
}

.pdp-gallery__thumbs button {
  width: 4rem;
  height: 4rem;
  padding: 0;
  border: 2px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  cursor: pointer;
}

.pdp-gallery__thumbs button[aria-current="true"] {
  border-color: var(--color-primary);
}

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

/*
 * The PDP "Details" accordion (CLAUDE.md "Collapsible Details section") - one real <details>/<summary>
 * per fact, all closed by default (native <details> behavior, not forced with JS). The chevron rotates
 * open/closed with pure CSS (details[open] below) - <details>'s own native toggling needs no JS at all
 * for that either.
 */
.pdp-details-accordion details {
  border-bottom: 1px solid var(--color-border);
}

.pdp-details-accordion details:first-child {
  border-top: 1px solid var(--color-border);
}

.pdp-details-accordion summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) 0;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}

.pdp-details-accordion summary::-webkit-details-marker {
  display: none;
}

.pdp-details-accordion__chevron {
  width: 1.1em;
  height: 1.1em;
  flex: 0 0 auto;
  transition: transform 0.15s ease;
}

[data-motion="reduced"] .pdp-details-accordion__chevron {
  transition: none;
}

.pdp-details-accordion details[open] .pdp-details-accordion__chevron {
  transform: rotate(180deg);
}

.pdp-details-accordion__body {
  padding: 0 0 var(--space-3) calc(1.1em + var(--space-2));
  color: var(--color-text-muted);
}

/* =======================================================================
   PDP carousels — the thumbnail gallery strip and each variant-option row
   (Amazon-style option tiles). Same "native overflow-x scroll track +
   explicit Prev/Next buttons" mechanism as the homepage's own category
   carousel (see .category-carousel above) - a dedicated PDP-scoped class
   family since the sizing/spacing here differs, not because the mechanism
   does. See frontend/src/carousel.ts (attachCarousel) for the shared
   scroll-amount/disabled-at-extremes/reduced-motion behavior - unlike the
   static category carousel, the Prev/Next buttons here are hidden entirely
   (not just disabled) when a track's content doesn't overflow, so a short
   photo/option list never shows a permanently-dead button pair.
======================================================================= */
.pdp-carousel {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.pdp-carousel__nav {
  flex-shrink: 0;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 999px;
  border: 2px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}

.pdp-carousel__nav svg {
  width: 1.1rem;
  height: 1.1rem;
}

.pdp-carousel__nav:hover:not(:disabled) {
  border-color: var(--color-primary);
  background: var(--color-brand-magenta-light);
}

.pdp-carousel__nav:disabled {
  opacity: 0.35;
  cursor: default;
}

.pdp-carousel__track {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--space-1);
  padding-block: var(--space-1);
  flex: 1;
  min-width: 0;
  scrollbar-width: none;
}

.pdp-carousel__track::-webkit-scrollbar {
  display: none;
}

/* Amazon-style variant option tiles - a bordered/swatched card with its own label, sized and
   spaced consistently whether it's a plain (Size/Material/...) tile or a color swatch+name tile. */
.pdp-variant-tiles {
  align-items: stretch;
}

.pdp-variant-tile {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

/*
 * The plain (non-color) Amazon-style option tile (CLAUDE.md "The Amazon-style option carousel") - a
 * rounded-corner card showing its own photo/name/price, graying out with an "Out of stock" label when
 * the option it currently resolves to isn't available.
 */
.pdp-variant-tile:not(.pdp-variant-tile--color) {
  min-width: 6.5rem;
  min-height: 2.75rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  color: var(--color-text);
  cursor: pointer;
  text-align: center;
}

.pdp-variant-tile[aria-pressed="true"] {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 1px var(--color-primary);
}

.pdp-variant-tile__photo {
  width: 4rem;
  height: 4rem;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--color-bg-disabled);
}

.pdp-variant-tile__name {
  font-size: var(--step--1);
  font-weight: 600;
}

.pdp-variant-tile__price {
  font-size: var(--step--1);
  color: var(--color-text-muted);
}

.pdp-variant-tile__stock {
  font-size: var(--step--1);
  color: var(--color-danger);
  font-weight: 600;
}

/* Grayed out and non-selectable (aria-disabled, not display:none - a screen reader still needs to
   hear "Out of stock" for this option) once its currently-resolved variant is unavailable - the
   click handler itself (variant-picker.ts) also checks aria-disabled and refuses to change the
   selection, so this isn't just a visual dim with a live handler still firing underneath it. */
.pdp-variant-tile[aria-disabled="true"] {
  cursor: not-allowed;
  background: var(--color-bg-disabled);
  opacity: 0.85;
}

.pdp-variant-tile[aria-disabled="true"] .pdp-variant-tile__name,
.pdp-variant-tile[aria-disabled="true"] .pdp-variant-tile__price,
.pdp-variant-tile[aria-disabled="true"] .pdp-variant-tile__label {
  color: var(--color-text-disabled);
}

.pdp-variant-tile--color {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  width: 4.5rem;
  padding: var(--space-2) var(--space-1);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  cursor: pointer;
}

.pdp-variant-tile__swatch {
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  border: 1px solid var(--color-border-strong);
}

.pdp-variant-tile__label {
  font-size: var(--step--1);
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* CLAUDE.md "Structured variant dimensions" - one row per independently-choosable dimension
   (Design/Color/Size), replacing the single combined-tile carousel. Each dimension's own label must
   stay genuinely visible now (unlike the old carousel's single visually-hidden "Options" heading) -
   with more than one independent selector on the page, a shopper needs to see which control is which. */
.pdp-dimension__label {
  font-size: var(--step-0);
  font-weight: 700;
  margin: 0 0 var(--space-2);
}

.pdp-dimension__select {
  width: auto;
  min-width: 12rem;
}

.star-rating {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-brand-gold-dark);
}

.star-rating svg {
  width: 1.1rem;
  height: 1.1rem;
}

.star-rating__value {
  font-weight: 700;
  color: var(--color-text);
  margin-inline-start: var(--space-1);
}

.rating-histogram {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 26rem;
}

.rating-histogram__row {
  display: grid;
  grid-template-columns: 4.5rem 1fr 3rem;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--step--1);
}

.rating-histogram__bar {
  height: 0.6rem;
  border-radius: 999px;
  background: var(--color-bg-alt);
  overflow: hidden;
}

.rating-histogram__bar-fill {
  height: 100%;
  background: var(--color-brand-gold);
  border-radius: 999px;
}

.review-card {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-5);
  margin-top: var(--space-5);
}

.review-card:first-child {
  border-top: none;
  padding-top: 0;
  margin-top: 0;
}

.review-card__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
  flex-wrap: wrap;
}

.review-card__author {
  font-weight: 700;
}

.review-card__date {
  color: var(--color-text-muted);
  font-size: var(--step--1);
}

/* =======================================================================
   Message center (buyer account — conversations with trove and sellers)
======================================================================= */
.message-center {
  display: grid;
  grid-template-columns: minmax(14rem, 18rem) 1fr;
  gap: var(--space-5);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  min-height: 24rem;
}

@media (max-width: 48rem) {
  .message-center {
    grid-template-columns: 1fr;
  }
}

.message-center__list {
  list-style: none;
  margin: 0;
  padding: var(--space-2);
  border-right: 1px solid var(--color-border);
  background: var(--color-bg-alt);
  overflow-y: auto;
}

.message-center__thread-link {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: var(--space-3);
  cursor: pointer;
  color: var(--color-text);
}

.message-center__thread-link:hover,
.message-center__thread-link:focus-visible {
  background: var(--color-bg);
}

.message-center__thread-link[aria-current="true"] {
  background: var(--color-brand-magenta-light);
}

.message-center__thread-name {
  display: block;
  font-weight: 700;
}

.message-center__thread-preview {
  display: block;
  font-size: var(--step--1);
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.message-center__thread {
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.message-bubble {
  max-width: 32rem;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
}

.message-bubble--self {
  align-self: flex-end;
  background: var(--color-brand-magenta-light);
}

.message-bubble__meta {
  display: block;
  font-size: var(--step--1);
  color: var(--color-text-muted);
  margin-bottom: var(--space-1);
}

/* ============ Profile image editor (logo/cover photo, Cropper.js) ============ */
/* frontend/src/behaviors/profile-image-editor.ts builds this markup at runtime - no static HTML
   ships with these classes, this file just supplies the look. */

.profile-image-editor {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.profile-image-editor__preview {
  position: relative;
  overflow: hidden;
  background: var(--color-bg-muted, #f2f4f6);
  border: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
  font-size: var(--step--1);
  text-align: center;
}

.profile-image-editor__preview--logo {
  width: 88px;
  height: 88px;
  border-radius: 50%;
}

.profile-image-editor__preview--cover {
  width: 100%;
  max-width: 32rem;
  aspect-ratio: 3 / 1;
  border-radius: var(--radius-md);
}

.profile-image-editor__preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.image-crop-overlay {
  position: fixed;
  inset: 0;
  z-index: 2100;
  background: rgba(15, 23, 32, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
}

.image-crop-overlay__panel {
  background: var(--color-bg, #fff);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  max-width: 40rem;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
}

.image-crop-overlay__stage {
  margin: var(--space-4) 0;
  max-height: 60vh;
  background: #111;
}

.image-crop-overlay__stage img {
  display: block;
  max-width: 100%;
}

.image-crop-overlay__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
}

/* Friendly "something went wrong" modal (frontend/src/error-modal.ts) - native <dialog>, styled to
   match the rest of this site rather than the browser's unstyled default. */
.mfe-error-modal {
  max-width: 28rem;
  width: calc(100% - var(--space-6, 2rem));
  border: none;
  border-radius: var(--radius-lg, 0.75rem);
  padding: 0;
  box-shadow: 0 1.5rem 3rem rgba(11, 37, 47, 0.25);
}

.mfe-error-modal::backdrop {
  background: rgba(11, 37, 47, 0.55);
}

.mfe-error-modal__form {
  padding: var(--space-5, 1.5rem);
}

.mfe-error-modal__title {
  margin: 0 0 var(--space-3, 0.75rem);
  color: var(--color-heading);
}

.mfe-error-modal__body {
  margin: 0 0 var(--space-4, 1.25rem);
  color: var(--color-text);
}

.mfe-error-modal__actions {
  display: flex;
  justify-content: flex-end;
}

/* The product-create wizard's "+Add" modals (Add a variant / Add a customization field) - same
   native-<dialog> pattern as .mfe-error-modal above, wider to fit a real multi-field form. */
.mfe-wizard-modal {
  max-width: 34rem;
  width: calc(100% - var(--space-6, 2rem));
  max-height: 85vh;
  overflow-y: auto;
  border: none;
  border-radius: var(--radius-lg, 0.75rem);
  padding: 0;
  box-shadow: 0 1.5rem 3rem rgba(11, 37, 47, 0.25);
}

.mfe-wizard-modal::backdrop {
  background: rgba(11, 37, 47, 0.55);
}

.mfe-wizard-modal__body {
  padding: var(--space-5, 1.5rem);
}

/* The conversation about one issue raised with trovemarket, inside its own dialog. Two-sided by who
   said it: the person who raised it on the left in the page's own surface, trovemarket's own replies
   tinted, so a thread reads at a glance without colour being the only difference (each message also
   names who said it). */
.issue-conversation {
  list-style: none;
  margin: 0 0 var(--space-5, 1.5rem);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3, 0.75rem);
  max-height: 22rem;
  overflow-y: auto;
}

.issue-message {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md, 0.5rem);
  padding: var(--space-3, 0.75rem);
  background: var(--color-surface);
}

.issue-message__meta {
  margin: 0 0 var(--space-2, 0.5rem);
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.issue-message__body {
  margin: 0;
  white-space: pre-wrap;
}

/* A small, reusable "are you sure?" confirmation dialog - same native-<dialog> shape as
   .mfe-error-modal above, sized for a short message + two side-by-side buttons rather than a form. */
.mfe-confirm-modal {
  max-width: 26rem;
  width: calc(100% - var(--space-6, 2rem));
  border: none;
  border-radius: var(--radius-lg, 0.75rem);
  padding: 0;
  box-shadow: 0 1.5rem 3rem rgba(11, 37, 47, 0.25);
}

.mfe-confirm-modal::backdrop {
  background: rgba(11, 37, 47, 0.55);
}

.mfe-confirm-modal__body-wrap {
  padding: var(--space-5, 1.5rem);
}

.mfe-confirm-modal__title {
  margin: 0 0 var(--space-3, 0.75rem);
  color: var(--color-heading);
}

.mfe-confirm-modal__body {
  margin: 0 0 var(--space-4, 1.25rem);
  color: var(--color-text);
}

.mfe-confirm-modal__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3, 0.75rem);
  /* Reported directly - the Delete confirmation's "type the product name" input sat flush against
     the Cancel/Delete buttons below it, with no breathing room. `.form-field` (the input's own
     wrapper) has no bottom margin of its own (by design - it's used all over this app inside forms
     that already space their own fields via a parent .form-grid/.form-actions gap), so this modal's
     own actions row needs its own top margin instead of assuming one comes from the field above it. */
  margin-top: var(--space-4, 1.25rem);
}

/* ---------------------------------------------------------------------------------------------
   Admin console drill-in pages (CLAUDE.md "Admin console: drill-in and actions")
   --------------------------------------------------------------------------------------------- */

/* A shortened id ("110ce...8804f") - the full id is in its tooltip/accessible name. */
.admin-id-link code,
code.admin-id {
  white-space: nowrap;
}
.admin-id-link:hover code,
.admin-id-link:focus-visible code {
  text-decoration: underline;
}

.admin-back-link {
  margin: 0 0 var(--space-4);
}

.admin-detail-section + .admin-detail-section {
  margin-top: var(--space-6);
}

/* Label-and-value pairs. `.detail-list` is the same layout outside the admin
   console - the buyer's own read-only profile uses it - so the rules are
   shared rather than copied under a second name. The `__row` wrapper exists
   so each pair can be one element in the markup while still laying out as two
   grid cells. */
.admin-detail-list,
.detail-list {
  display: grid;
  grid-template-columns: minmax(10rem, max-content) 1fr;
  gap: var(--space-2) var(--space-5);
  margin: 0;
}
.admin-detail-list__row,
.detail-list > div {
  display: contents;
}
.admin-detail-list dt,
.detail-list dt {
  font-weight: 600;
  color: var(--color-text-muted);
}
.admin-detail-list dd,
.detail-list dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}
@media (max-width: 40rem) {
  .admin-detail-list,
  .detail-list {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .admin-detail-list dd,
  .detail-list dd {
    margin-bottom: var(--space-3);
  }
}

/* Users admin (`admin-users-view.ts`): the person's name with their type badge, their saved
   addresses, and the roles form under the current-roles facts. */
.admin-detail-title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-1);
}
.admin-roles [data-role="user-roles-form"] {
  margin-top: var(--space-5);
  max-width: 40rem;
}

.admin-actions {
  display: grid;
  gap: var(--space-5);
}
.admin-action {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.admin-action h3 {
  margin-top: 0;
}
.admin-action-buttons {
  flex-wrap: wrap;
  gap: var(--space-2);
}

.admin-thumb {
  width: 3rem;
  height: 3rem;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
}

/* ---------------------------------------------------------------------------------------------
   Admin console tabs (admin-tabs.ts) - clean underlined tabs; the selected tab carries a primary
   underline, and every tab and panel keeps a clearly visible keyboard focus ring.
   --------------------------------------------------------------------------------------------- */
.admin-tabs__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  border-bottom: 1px solid var(--color-border);
  margin: 0 0 var(--space-5);
}
.admin-tabs__tab {
  appearance: none;
  background: transparent;
  border: 0;
  border-bottom: 3px solid transparent;
  margin-bottom: -1px;
  padding: var(--space-2) var(--space-4);
  color: var(--color-text-muted);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.admin-tabs__tab:hover {
  color: var(--color-text);
  border-bottom-color: var(--color-border);
}
.admin-tabs__tab[aria-selected='true'] {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}
.admin-tabs__tab:focus-visible,
.admin-tabs__panel:focus-visible {
  outline: 3px solid var(--color-focus-ring);
  outline-offset: 2px;
}
.admin-tabs__panel {
  border-radius: var(--radius-sm);
}
/* A panel holding a single section: its heading only repeats the tab's name, so it stays for screen
   readers but is hidden on screen. A panel with several sections keeps every heading visible. */
.admin-tabs__panel > .dashboard-section:only-child > .dashboard-section__head:has(> h2:only-child),
.admin-tabs__panel > .admin-detail-section:only-child > .dashboard-section__head:has(> h2:only-child) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
/* Tabs nested inside a tab (Belle's settings) sit a little tighter under their parent. */
.admin-tabs__panel .admin-tabs .admin-tabs__list {
  margin-top: var(--space-2);
}
.admin-action-fields {
  margin-bottom: var(--space-4);
}

/* Bulk selection on admin list tables (admin-bulk.ts). */
.admin-bulk-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-3);
}
.admin-bulk-toolbar__count {
  font-weight: 600;
  min-width: 6rem;
}
.data-table .admin-bulk-cell {
  width: 2.5rem;
  text-align: center;
}
.data-table .admin-bulk-cell input[type='checkbox'] {
  width: 1.25rem;
  height: 1.25rem;
}

/* =======================================================================
   Belle admin settings (admin-belle-settings-section.ts, admin-belle-view.ts).
   Each setting's name sits on one line with plain muted "Value from: ..." text and a Reset action
   styled like every other admin action. Guidance is a normal admin table; its text is edited in a
   wider dialog.
======================================================================= */
.belle-setting {
  margin-bottom: var(--space-5);
}

.belle-setting__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  margin-bottom: var(--space-2);
}

.belle-setting__head label,
.belle-setting__head .belle-setting__label {
  font-weight: 700;
  margin: 0;
}

.belle-setting__source {
  color: var(--color-text-muted);
  font-size: var(--step--1);
}

/* `.link-action` sets display:inline-flex, which would otherwise beat the `[hidden]` rule. */
.belle-setting__head .link-action[hidden] {
  display: none;
}

/* Text boxes inside Belle's editable tables fill their cell instead of the browser's narrow default. */
.belle-edit-table input[type='text'] {
  width: 100%;
  min-width: 8rem;
}

.belle-guidance-dialog {
  max-width: 44rem;
}

.belle-guidance-dialog textarea {
  width: 100%;
}

/* The dead-lettered request, in its own dialog (admin Dead letters -> View). Wider than the standard
   wizard modal because the thing being read is a JSON message, and re-wrapping it at 34rem makes it
   materially harder to scan. */
.dead-letter-dialog {
  max-width: 52rem;
}

/* Facts about the failure as a term/value list rather than a table: there is only one record here,
   so a table would add a header row that says nothing. */
.dead-letter-facts {
  margin: 0 0 var(--space-5, 1.5rem);
  display: grid;
  grid-template-columns: minmax(0, 10rem) minmax(0, 1fr);
  gap: var(--space-2, 0.5rem) var(--space-4, 1rem);
}

.dead-letter-fact {
  display: contents;
}

.dead-letter-facts dt {
  font-weight: 600;
  color: var(--color-text-muted, #4b5a63);
}

.dead-letter-facts dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}

.dead-letter-error {
  color: var(--color-danger, #b23223);
}

/* The message itself. Scrolls in both directions rather than wrapping: a JSON payload's indentation
   is what makes it readable, and soft-wrapping long values destroys it. */
.dead-letter-payload {
  margin: 0;
  max-height: 22rem;
  overflow: auto;
  padding: var(--space-4, 1rem);
  background: var(--color-bg-alt, #f3efe9);
  border: 1px solid var(--color-border, #d8d2c8);
  border-radius: var(--radius-md, 0.5rem);
  font-family: Consolas, "SFMono-Regular", Menlo, monospace;
  font-size: 0.85rem;
  line-height: 1.5;
  white-space: pre;
}

/* Single column on a narrow viewport - a 10rem label column plus a JSON block does not fit a phone. */
@media (max-width: 40rem) {
  .dead-letter-facts {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1, 0.25rem);
  }
  .dead-letter-facts dd {
    margin-bottom: var(--space-3, 0.75rem);
  }
}

/* ── Empty and failed states (Belle art) ─────────────────────────────────────
   One layout for both "this page would not load" and "this search found nothing": a shopper who
   has met one should recognise the other, and they are the same idea - there is nothing here.

   The art sits beside the words on a wide viewport and above them on a narrow one, which is what
   `flex-wrap` gives for free without a media query. It is capped rather than sized to its intrinsic
   720px: these PNGs are 2x for a retina screen, so displaying them at full width would show them
   at half the sharpness they were exported for and dominate the message they are supporting.

   `min-width: 0` on the body is what stops a long message being pushed out of the row - a flex item
   defaults to `min-width: auto`, which refuses to shrink below its content. */
.state-panel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6);
  justify-content: center;
}

.state-panel__art {
  /* Never bigger than it was exported for, never wider than the viewport allows, and never so
     small on a phone that it reads as an icon rather than an illustration. */
  width: min(100%, 22rem);
  height: auto;
  flex: 0 1 22rem;
}

.state-panel__body {
  flex: 1 1 18rem;
  min-width: 0;
}

/* The empty-results panel is stacked and centred rather than side-by-side: it sits INSIDE a results
   area that already has a heading and filters above it, so a second left-aligned block of prose
   beside a picture reads as a second page rather than as the absence of results. */
.state-panel--empty {
  flex-direction: column;
  text-align: center;
  padding: var(--space-6) 0;
  /* The panel is a <figure> now, and figures carry a browser default margin of `1em 40px` that
     would indent the whole thing and break the centring. */
  margin: 0;
  /* Tighter than the panel default so the caption reads as belonging to the picture rather than as
     a separate block sitting above it. */
  gap: var(--space-3);
}

.state-panel--empty .state-panel__art {
  flex: 0 0 auto;
  /* Wider than the panel default: this is the whole content of an otherwise empty results area, so
     it carries the page rather than decorating a paragraph. Still `min(100%, ...)` so a narrow
     viewport shrinks it instead of overflowing, and still under the 720px it was exported at. */
  width: min(100%, 28rem);
}

/* The empty-results caption. It is a real <figcaption>, so the job here is to make it read as a
   caption for the picture rather than as a paragraph that happens to be near one: a tighter gap
   than the panel's default (`--space-6` reads as two separate blocks) and a width that keeps the
   line short.

   Sized in `rem`, and that is an accessibility requirement rather than a style preference: the
   accessibility bar's text-size control sets a percentage font-size on <html> (`accessibility.js`'s
   `applyFontStep`, up to 150%), so `rem` grows with it while `px` would pin this text at one size
   and silently ignore the setting. No `vw` either, for the same reason.

   The size, weight and line-height are the SAME TOKENS `h1` uses (`--step-4`, 700,
   `--line-height-heading`), not numbers picked to look similar. That is deliberate: the not-found
   page's message really is an `<h1>` - it is that page's heading - while the message in a results
   grid cannot be, because the home page already has `<h1>Shop with purpose.</h1>` in its hero and
   an `<h2>Products</h2>` immediately above the grid, and a second `<h1>` sitting after an `<h2>`
   breaks the heading outline a screen reader navigates by. Borrowing h1's tokens is how all three
   pages look identical while each keeps the element its position in the document actually calls
   for. Change h1's scale and this follows it.

   Note the weight renders as 700 whatever number is used here: Atkinson Hyperlegible is loaded
   with only 400 and 700 (the Google Fonts link in index.html), and CSS font matching picks the
   nearest real face rather than synthesising one - which is the better outcome, since a synthesised
   bold smears the letterforms this font exists to keep distinct.

   `--color-text` rather than a fixed colour so the high-contrast theme flips it with everything
   else. The `max-width` plus the panel's own centred column is what centres it on the picture. */
.state-panel--empty .state-panel__message {
  /* No max-width on purpose. A reading-width cap is right for body prose and wrong here: this is a
     headline in a results column, and capping it at 34rem left it visibly narrower than the column
     around it - reported as looking "scrunched up". It fills whatever area it is given, so the same
     rule works in the wide not-found page and in the narrower grid beside the filter sidebar. */
  margin: 0 auto;
  font-size: var(--step-4);
  font-weight: 700;
  line-height: var(--line-height-heading);
  color: var(--color-text);
}

/* The caption carries a <div role="status"> and, on the not-found page, an <h1> - both of which
   bring their own size and margins. `font: inherit` makes them take the caption's instead, so the
   caption looks the same whatever block a caller puts inside it. */
.state-panel--empty .state-panel__message > *,
.state-panel--empty .state-panel__message h1,
.state-panel--empty .state-panel__message p {
  margin: 0;
  font: inherit;
  color: inherit;
}

.state-panel--empty .state-panel__message p + p,
.state-panel--empty .state-panel__message h1 + p {
  margin-top: var(--space-4);
}

/* Supporting text below the message - the not-found page's "Return to the homepage" - is not part
   of the headline and should not be set at headline size. It needs a selector at least as specific
   as the `font: inherit` rule above (0,2,1), or that rule wins no matter which comes last in the
   file; three classes gets there without an !important. */
.state-panel--empty .state-panel__message .state-panel__message-detail {
  font-size: var(--step-0);
  font-weight: 400;
  line-height: var(--line-height-base, 1.6);
}

/* The art carries no information the text does not, so it is the first thing to go when someone has
   asked for less. */
@media (prefers-reduced-data: reduce) {
  .state-panel__art {
    display: none;
  }
}
