/*
 * PetalOS storefront design system.
 *
 * This file is the ENTIRE stylesheet for customer-facing pages. It deliberately
 * does not import or depend on application.css: that file is a ~19,000-line
 * monolith shared by the admin console and every staff station, and shipping it
 * to a shopper meant ~350KB of render-blocking CSS of which the storefront used
 * maybe 8%.
 *
 * Everything is driven by custom properties set per-shop on <body> by
 * StorefrontHelper#storefront_theme_style, so the asset itself stays static and
 * cacheable across every storefront on the platform. The five values a shop
 * actually configures (primary, secondary, accent, background, text) are
 * expanded into a full tonal system with color-mix() rather than asking shop
 * owners to pick fifteen colours they have no way to judge.
 *
 * Sections:
 *   1. Tokens
 *   2. Reset & base
 *   3. Layout primitives
 *   4. Typography
 *   5. Buttons & links
 *   6. Forms
 *   7. Header / navigation
 *   8. Hero
 *   9. Product cards & grids
 *  10. Category pages
 *  11. Product detail
 *  12. Cart
 *  13. Checkout
 *  14. Payment fields
 *  15. Confirmation
 *  16. Account, subscriptions, recipients
 *  17. Blog, pages, inquiry forms
 *  18. Footer
 *  19. Toasts, modals, misc chrome
 *  20. Rich text
 *  21. Motion & print
 */

/* ==========================================================================
   1. Tokens
   ========================================================================== */

.storefront-body {
  /* Fallbacks matter: the inline style on <body> normally supplies these, but
     a Turbo-morphed fragment or an error page rendered without the helper
     should still look like a storefront rather than unstyled HTML. */
  --theme-primary: #b45b3f;
  --theme-secondary: #234039;
  --theme-accent: #8c3f27;
  --theme-background: #f6f2ec;
  --theme-text: #1f1d1a;
  --theme-radius: 18px;
  --font-heading: "Cormorant Garamond", Georgia, serif;
  --font-body: "Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* --- Ink ---------------------------------------------------------------
     Only two greys are used for text, and both clear WCAG AA (4.5:1) against
     --paper: muted lands near 6:1, soft near 4.8:1. The old ramp went down to
     34%, which measured 2.1:1 — every micro-label, SKU and date caption on the
     site failed. A third step that reads as "quieter still" is not worth
     shipping text nobody can read; --ink-decor exists for the hairlines and
     separators that carry no information. */
  --ink: var(--theme-text);
  --ink-muted: color-mix(in oklab, var(--theme-text) 72%, transparent);
  --ink-soft: color-mix(in oklab, var(--theme-text) 64%, transparent);
  --ink-decor: color-mix(in oklab, var(--theme-text) 30%, transparent);

  /* --- Surfaces --------------------------------------------------------- */
  --paper: var(--theme-background);
  --paper-raised: color-mix(in oklab, var(--theme-background) 45%, #fff);
  --paper-panel: color-mix(in oklab, var(--theme-background) 78%, #fff);
  --paper-sunken: color-mix(in oklab, var(--theme-background) 93%, var(--theme-text));
  --paper-inverse: color-mix(in oklab, var(--theme-secondary) 88%, var(--theme-text));

  /* --- Lines ------------------------------------------------------------ */
  --line: color-mix(in oklab, var(--theme-text) 11%, transparent);
  --line-strong: color-mix(in oklab, var(--theme-text) 22%, transparent);
  --line-brand: color-mix(in oklab, var(--theme-primary) 34%, transparent);

  /* --- Brand ------------------------------------------------------------ */
  --brand: var(--theme-primary);
  --brand-deep: var(--theme-accent);
  --brand-tint: color-mix(in oklab, var(--theme-primary) 12%, var(--theme-background));
  --brand-tint-strong: color-mix(in oklab, var(--theme-primary) 22%, var(--theme-background));
  --on-brand: #fff;
  --secondary: var(--theme-secondary);
  --secondary-tint: color-mix(in oklab, var(--theme-secondary) 12%, var(--theme-background));

  /* --- Status ----------------------------------------------------------- */
  --danger: #a3311b;
  --danger-tint: color-mix(in oklab, #a3311b 12%, var(--theme-background));
  --success: #2f6b4f;
  --success-tint: color-mix(in oklab, #2f6b4f 12%, var(--theme-background));

  /* --- Card surface (respects the shop's light/dark card preference) ----- */
  --card-surface: rgba(255, 255, 255, 0.92);
  --card-text: #1f1d1a;
  --card-muted: rgba(31, 29, 26, 0.66);
  --card-hairline: rgba(31, 29, 26, 0.1);

  /* --- Radii ------------------------------------------------------------ */
  --r-xs: calc(var(--theme-radius) * 0.34);
  --r-sm: calc(var(--theme-radius) * 0.6);
  --r-md: var(--theme-radius);
  --r-lg: calc(var(--theme-radius) * 1.55);
  --r-pill: 999px;

  /* --- Elevation --------------------------------------------------------
     Two-layer shadows: a tight contact shadow plus a wide ambient one. A
     single large blur reads as fog on a warm paper background. */
  --shadow-xs: 0 1px 2px color-mix(in oklab, var(--theme-text) 8%, transparent);
  --shadow-sm:
    0 1px 2px color-mix(in oklab, var(--theme-text) 6%, transparent),
    0 4px 12px color-mix(in oklab, var(--theme-text) 6%, transparent);
  --shadow-md:
    0 2px 4px color-mix(in oklab, var(--theme-text) 6%, transparent),
    0 12px 28px color-mix(in oklab, var(--theme-text) 9%, transparent);
  --shadow-lg:
    0 4px 8px color-mix(in oklab, var(--theme-text) 7%, transparent),
    0 28px 64px color-mix(in oklab, var(--theme-text) 13%, transparent);

  /* --- Space ------------------------------------------------------------ */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 3rem;
  --s-8: 4rem;
  --s-9: 6rem;

  /* --- Layout -----------------------------------------------------------
     Two page-rhythm steps, used everywhere instead of per-component clamps so
     the vertical spacing stays consistent and adjustable in one place.
     --block separates sections inside a page; --page is the top/bottom margin
     of the page's own content against the header and footer. */
  --gutter: clamp(1.15rem, 3.5vw, 3rem);
  --block: clamp(1.75rem, 3.5vw, 3rem);
  --page: clamp(2rem, 4vw, 3.5rem);
  --shell: 1280px;
  --shell-narrow: 880px;
  --shell-text: 68ch;
  --header-h: 4rem;

  /* --- Motion ----------------------------------------------------------- */
  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-fast: 120ms;
  --dur: 220ms;
  --dur-slow: 420ms;
}

/* ==========================================================================
   2. Reset & base
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 1rem);
  /* Has to be on the root, not just body: the closed mobile drawer is
     position: fixed, so its containing block is the viewport and it escapes any
     clip set on body. Without this the phone home page dragged 283px sideways.
     `clip` rather than `hidden` so no new scroll container appears and the
     sticky header keeps working. */
  overflow-x: clip;
}

body.storefront-body {
  margin: 0;
  min-height: 100dvh;
  /* The mobile drawer is position: fixed and parked off-screen with
     translateX(100%). Off-viewport fixed content still extends the document's
     scrollable width, so the phone home page could be dragged 283px sideways.
     `clip` rather than `hidden`: hidden would make the body a scroll container
     and break the sticky header. */
  overflow-x: clip;
  font-family: var(--font-body);
  font-size: clamp(1rem, 0.97rem + 0.14vw, 1.0625rem);
  line-height: 1.6;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-synthesis-weight: none;
  overflow-wrap: break-word;
}

.storefront-body [hidden] {
  display: none !important;
}

.storefront-main {
  display: block;
}

img,
svg,
video,
iframe {
  max-width: 100%;
}

/* Form controls do not inherit font-family by default, so anything not
   explicitly styled falls back to the UA's Arial. Components set their own
   sizes; this only fixes the family. */
.storefront-body button,
.storefront-body input,
.storefront-body select,
.storefront-body textarea {
  font-family: inherit;
}

img {
  height: auto;
  display: block;
}

/* Every photographic slot in the storefront is an aspect-ratio box with an
   object-fit image inside, so a slow image never reflows the page around it. */
.storefront-body figure {
  margin: 0;
}

.storefront-body hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: var(--s-6) 0;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: 0;
  left: var(--s-4);
  z-index: 300;
  transform: translateY(-120%);
  background: var(--ink);
  color: var(--paper);
  padding: var(--s-3) var(--s-5);
  border-radius: 0 0 var(--r-sm) var(--r-sm);
  font-weight: 600;
  text-decoration: none;
  transition: transform var(--dur) var(--ease);
}

.skip-link:focus-visible {
  transform: translateY(0);
}

/* One focus treatment for the whole storefront. Two rings — a light halo and a
   dark core — so it stays visible on both photography and pale paper. */
.storefront-body :focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

.storefront-body ::selection {
  background: var(--brand-tint-strong);
  color: var(--ink);
}

/* ==========================================================================
   3. Layout primitives
   ========================================================================== */

/* Sections below the fold are skipped during the first layout pass. On a home
   page with several category rows this measurably shortens the critical path;
   contain-intrinsic-size keeps the scrollbar honest so the page does not jump
   as they come into view. */
.defer-render {
  content-visibility: auto;
  contain-intrinsic-size: auto 640px;
}

/* ==========================================================================
   4. Typography
   ========================================================================== */

.storefront-body h1,
.storefront-body h2,
.storefront-body h3,
.storefront-body h4,
.storefront-body h5,
.storefront-body h6 {
  font-family: var(--font-heading);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.01em;
  margin: 0 0 var(--s-3);
  text-wrap: balance;
}

/* The display scale used to top out at 4rem, which put "Your cart" across a
   third of a laptop screen. Capped where a page heading still leads without
   becoming the page. */
.storefront-body h1 {
  font-size: clamp(1.9rem, 1.35rem + 2.2vw, 3rem);
  letter-spacing: -0.02em;
}

.storefront-body h2 {
  font-size: clamp(1.5rem, 1.2rem + 1.3vw, 2.15rem);
}

.storefront-body h3 {
  font-size: clamp(1.15rem, 1.05rem + 0.5vw, 1.4rem);
}

.storefront-body h4 {
  font-size: 1.125rem;
}

/* Element specificity for the same reason as `a` above: `.storefront-body p`
   outranked .form-note, .cart-item-name, .summary-value and every other
   single-class paragraph rule, so their colours and widths never took effect. */
p {
  margin: 0 0 var(--s-4);
  color: var(--ink-muted);
  max-width: var(--shell-text);
}

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

.storefront-body strong {
  font-weight: 600;
  color: var(--ink);
}

.storefront-body ul,
.storefront-body ol {
  margin: 0 0 var(--s-4);
  padding-left: 1.25rem;
  color: var(--ink-muted);
}

.storefront-body li + li {
  margin-top: var(--s-1);
}

/* The small caps line above a heading. It carries the section's category, so it
   is a label, not decoration — kept at 0.75rem with wide tracking so it reads
   as metadata even at a glance. */
/* --brand-deep, not --brand: at 12px the eyebrow is small text, and the default
   primary measures 4.18:1 on paper — just under AA. The accent tone clears it
   at ~6.6:1 and reads as the same colour family. */
.eyebrow {
  display: block;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand-deep);
  margin: 0 0 var(--s-3);
}

.section-subtitle,
.lede {
  font-size: clamp(1.05rem, 1rem + 0.3vw, 1.25rem);
  line-height: 1.55;
  color: var(--ink-muted);
  max-width: 56ch;
}

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

.section-heading h2,
.section-heading h3 {
  margin-bottom: 0;
}

.price {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--ink);
}

/* ==========================================================================
   5. Buttons & links
   ========================================================================== */

/* Element specificity (0,0,1), NOT `.storefront-body a` (0,1,1). The prefixed
   form outranks every single-class rule below — .text-link, .nav-cta,
   .nav-link, .skip-link — so none of their colours ever applied and each link
   silently inherited its parent's. The storefront stylesheet is loaded only by
   layouts/storefront, so a bare element selector cannot leak anywhere. */
a {
  color: inherit;
  text-decoration: none;
}

.text-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  color: var(--brand-deep);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  text-decoration-color: color-mix(in oklab, var(--brand-deep) 35%, transparent);
  transition: text-decoration-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

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

.button,
.storefront-body button.button,
.storefront-body input[type="submit"].button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  min-height: 2.75rem;
  padding: 0.6rem 1.35rem;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  font-family: var(--font-body);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  width: auto;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.button:active {
  transform: translateY(1px);
}

.button:disabled,
.button[disabled],
.button[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.button.primary {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-brand);
  box-shadow: var(--shadow-sm);
}

.button.primary:hover:not(:disabled) {
  background: var(--brand-deep);
  border-color: var(--brand-deep);
  box-shadow: var(--shadow-md);
}

.button.secondary {
  background: var(--paper-raised);
  border-color: var(--line-strong);
  color: var(--ink);
}

.button.secondary:hover:not(:disabled) {
  border-color: var(--brand);
  color: var(--brand-deep);
  box-shadow: var(--shadow-sm);
}

.button.ghost {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--ink);
}

.button.ghost:hover:not(:disabled) {
  background: var(--brand-tint);
  border-color: var(--line-brand);
  color: var(--brand-deep);
}

.button.danger {
  background: transparent;
  border-color: color-mix(in oklab, var(--danger) 40%, transparent);
  color: var(--danger);
}

.button.danger:hover:not(:disabled) {
  background: var(--danger-tint);
}

/* The selector list mirrors the base .button rule on purpose. That rule has to
   name `button.button` / `input.button` to beat the global
   `input, select, textarea { width: 100% }` on submit inputs, which makes its
   `width: auto` more specific than a plain `.button.block` — so a bare
   `.button.block { width: 100% }` silently did nothing. */
.button.block,
.storefront-body button.button.block,
.storefront-body input[type="submit"].button.block {
  width: 100%;
}

.button.small {
  min-height: 2.375rem;
  padding: 0.4rem 1rem;
  font-size: 0.85rem;
}

/* ==========================================================================
   6. Forms
   ========================================================================== */

.form-section {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: clamp(1rem, 2vw, 1.4rem);
  margin-bottom: var(--s-4);
}

/* h2 included: a section that is the first thing under a page's h1 uses h2 so
   the heading outline does not skip a level, and it must still look like a
   panel heading rather than a page title. */
.form-section > :is(h2, h3, h4) {
  font-size: clamp(1.15rem, 1.05rem + 0.5vw, 1.4rem);
  margin-bottom: var(--s-3);
}

.form-section:last-child {
  margin-bottom: 0;
}

/* Numbered checkout sections. The counter lives on the form so the numbers stay
   correct when a section (pickup vs delivery) is toggled off. */
.numbered-form {
  counter-reset: checkout-step;
}

.numbered-form .form-section > h3 {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}

.numbered-form .form-section > h3::before {
  counter-increment: checkout-step;
  content: counter(checkout-step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.85rem;
  height: 1.85rem;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--brand-tint-strong);
  color: var(--brand-deep);
  font-family: var(--font-body);
  font-size: 0.85rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: var(--s-3);
  min-width: 0;
}

.field:last-child {
  margin-bottom: 0;
}

.storefront-body label,
.field-label {
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
}

.storefront-body input[type="text"],
.storefront-body input[type="email"],
.storefront-body input[type="tel"],
.storefront-body input[type="password"],
.storefront-body input[type="number"],
.storefront-body input[type="search"],
.storefront-body input[type="date"],
.storefront-body input[type="time"],
.storefront-body input[type="url"],
.storefront-body select,
.storefront-body textarea {
  width: 100%;
  /* 2.75rem = 44px, the smallest comfortable touch target; going below this to
     save space would trade tidiness for mis-taps on a phone. */
  min-height: 2.75rem;
  padding: 0.55rem 0.85rem;
  font-family: var(--font-body);
  font-size: 1rem; /* 16px minimum: anything smaller makes iOS Safari zoom on focus */
  line-height: 1.4;
  color: var(--ink);
  background: var(--paper-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
  appearance: none;
}

.storefront-body textarea {
  min-height: 6rem;
  resize: vertical;
}

.storefront-body select {
  padding-right: 2.5rem;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 1.15rem) calc(50% + 2px), calc(100% - 0.8rem) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  cursor: pointer;
}

.storefront-body input:hover,
.storefront-body select:hover,
.storefront-body textarea:hover {
  border-color: color-mix(in oklab, var(--theme-text) 34%, transparent);
}

.storefront-body input:focus,
.storefront-body select:focus,
.storefront-body textarea:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand) 20%, transparent);
}

.storefront-body input::placeholder,
.storefront-body textarea::placeholder {
  color: var(--ink-soft);
}

/* Only after the field has been interacted with — :invalid alone paints every
   required field red before the shopper has typed anything. */
.storefront-body input:user-invalid,
.storefront-body select:user-invalid,
.storefront-body textarea:user-invalid {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--danger) 16%, transparent);
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: 0 var(--s-4);
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3) var(--s-4);
  margin-top: var(--s-4);
}

/* Quantity sits with the sizes, inside the same card, because it is a property
   of the arrangement being chosen and not of the add-ons below it. */
.quantity-field {
  margin-top: var(--s-4);
  margin-bottom: 0;
}

.form-actions .form-note {
  margin: 0;
  flex: 1 1 14rem;
}

.form-note {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0.15rem 0 0;
  max-width: 62ch;
}

.error-note {
  color: var(--danger);
  font-weight: 500;
}

.inline-field {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  margin-bottom: var(--s-4);
  cursor: pointer;
}

.inline-field input[type="checkbox"],
.inline-field input[type="radio"] {
  margin-top: 0.15rem;
}

.inline-label {
  font-weight: 500;
  color: var(--ink);
}

.storefront-body input[type="checkbox"],
.storefront-body input[type="radio"] {
  width: 1.15rem;
  height: 1.15rem;
  flex: none;
  accent-color: var(--brand);
  cursor: pointer;
}

/* Radio and checkbox cards. The whole tile is the hit target, which is the
   difference between a comfortable tap and a missed one on a phone. */
.radio-group {
  display: grid;
  gap: var(--s-3);
}

.radio-group.horizontal {
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
}

.radio-option,
.checkbox-item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.85rem 1rem;
  background: var(--paper-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.radio-option:hover,
.checkbox-item:hover {
  border-color: var(--line-brand);
}

.radio-option label,
.checkbox-item label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  cursor: pointer;
  margin: 0;
  font-weight: 500;
  flex: 1;
}

.radio-option:has(input:checked),
.checkbox-item:has(input:checked) {
  border-color: var(--brand);
  background: var(--brand-tint);
  box-shadow: inset 0 0 0 1px var(--brand);
}

.radio-option:has(input:focus-visible),
.checkbox-item:has(input:focus-visible) {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.checkbox-group {
  display: grid;
  gap: var(--s-2);
}

.fee-info {
  font-size: 0.85rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--brand-deep);
  background: var(--brand-tint-strong);
  border-radius: var(--r-pill);
  padding: 0.15rem 0.6rem;
}

.fee-info:empty {
  display: none;
}

.honeypot-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.alert {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.9rem 1.1rem;
  margin-bottom: var(--s-5);
  border: 1px solid color-mix(in oklab, var(--danger) 32%, transparent);
  border-left-width: 3px;
  border-radius: var(--r-sm);
  background: var(--danger-tint);
  color: color-mix(in oklab, var(--danger) 80%, var(--ink));
  font-size: 0.95rem;
  line-height: 1.5;
}

.alert.success {
  border-color: color-mix(in oklab, var(--success) 32%, transparent);
  background: var(--success-tint);
  color: color-mix(in oklab, var(--success) 80%, var(--ink));
}

/* ==========================================================================
   7. Header / navigation
   ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  /* Above the drawer (120) so the toggle stays tappable while the menu is open;
     the drawer's own top padding clears this bar. */
  z-index: 130;
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur) var(--ease);
}

/* The frosted backdrop lives on a pseudo-element, NOT on .site-header itself.
   backdrop-filter makes an element a containing block for its
   position: fixed descendants, which clipped the mobile drawer — a
   full-height panel — into the 4.5rem strip the header occupies. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: color-mix(in oklab, var(--paper) 82%, transparent);
}

@supports (backdrop-filter: blur(1px)) {
  .site-header::before {
    backdrop-filter: saturate(1.4) blur(14px);
  }
}

/* Set by the storefront-chrome controller once the page has scrolled past the
   header's own height, so the border and shadow appear only when there is
   actually content sliding underneath. */
.site-header.is-stuck {
  border-bottom-color: var(--line);
  box-shadow: var(--shadow-sm);
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding: 0.85rem var(--gutter);
  min-height: var(--header-h);
}

/* Shrinkable, not `flex: none`. A long shop name on a 390px phone would
   otherwise push the menu button and the cart clean off the right edge of the
   header instead of ellipsing the name. */
.brand {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex: 0 1 auto;
  margin-right: auto;
  min-width: 0;
}

.brand-logo {
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: var(--r-sm);
  object-fit: cover;
  border: 1px solid var(--line);
}

.brand-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.brand-title {
  font-family: var(--font-heading);
  font-size: 1.3rem;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: 0.01em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.brand-subtitle {
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nav-actions {
  display: contents;
}

.nav-menu {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.nav-utility {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.nav-item {
  position: relative;
}

.nav-link,
.nav-heading {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.55rem 0.75rem;
  border-radius: var(--r-sm);
  font-size: 0.92rem;
  font-weight: 500;
  color: var(--ink-muted);
  white-space: nowrap;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.nav-link:hover,
.nav-item:hover > .nav-link,
.nav-item:focus-within > .nav-link {
  color: var(--ink);
  background: var(--brand-tint);
}

.nav-link[aria-current="page"] {
  color: var(--brand-deep);
  font-weight: 600;
}

.nav-heading {
  cursor: default;
  color: var(--ink);
  font-weight: 600;
}

.has-dropdown > .nav-link::after,
.has-dropdown > .nav-heading::after {
  content: "";
  width: 0.4em;
  height: 0.4em;
  margin-top: -0.2em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.55;
  transition: transform var(--dur) var(--ease);
}

.nav-item:hover > .nav-link::after,
.nav-item:focus-within > .nav-link::after {
  transform: rotate(225deg) translate(-0.1em, -0.1em);
}

/* The hover bridge. Without it the pointer crosses a dead gap between the
   trigger and the panel and the menu closes mid-travel. */
.has-dropdown::after {
  content: "";
  position: absolute;
  inset: 100% 0 auto;
  height: 0.6rem;
}

/* The hover-revealed panel is a desktop affordance only, so the whole
   hide/reveal mechanism is scoped to the width where it applies. It used to be
   unscoped, which forced the mobile block below to re-assert
   `visibility: visible` on the dropdown — and that turned the closed drawer
   (position: fixed, translated off-screen) into 283px of live layout, so the
   phone home page scrolled sideways. */
@media (min-width: 60.0625rem) {
  .nav-dropdown {
    position: absolute;
    top: calc(100% + 0.6rem);
    left: 0;
    min-width: 13rem;
    display: grid;
    gap: 0.1rem;
    padding: 0.5rem;
    background: var(--paper-raised);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    box-shadow: var(--shadow-lg);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px);
    transition:
      opacity var(--dur) var(--ease),
      transform var(--dur) var(--ease),
      visibility 0s linear var(--dur);
    z-index: 20;
  }

  .nav-item:hover > .nav-dropdown,
  .nav-item:focus-within > .nav-dropdown {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    transition-delay: 0s;
  }
}

.nav-dropdown-link {
  padding: 0.55rem 0.7rem;
  border-radius: var(--r-xs);
  font-size: 0.92rem;
  color: var(--ink-muted);
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.nav-dropdown-link:hover {
  background: var(--brand-tint);
  color: var(--ink);
}

.nav-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 2.75rem;
  padding: 0.5rem 1.1rem;
  border-radius: var(--r-pill);
  background: var(--brand);
  color: var(--on-brand);
  font-size: 0.9rem;
  font-weight: 600;
  white-space: nowrap;
  box-shadow: var(--shadow-xs);
  transition: background-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.nav-cta:hover {
  background: var(--brand-deep);
  box-shadow: var(--shadow-sm);
}

.nav-cta svg {
  width: 1.05rem;
  height: 1.05rem;
  flex: none;
}

.cart-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.35rem;
  height: 1.35rem;
  padding: 0 0.35rem;
  border-radius: var(--r-pill);
  background: color-mix(in oklab, #fff 82%, var(--brand));
  color: var(--brand-deep);
  font-size: 0.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* Rendered without .has-items when the cart is empty; see
   shared/_cart_count.html.erb. */
.cart-count:not(.has-items) {
  display: none;
}

/* --- Mobile drawer ------------------------------------------------------ */

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  flex: none;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
}

.nav-toggle:hover {
  border-color: var(--brand);
  color: var(--brand-deep);
}

.nav-toggle-bars {
  position: relative;
  display: block;
  width: 1.05rem;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: background-color var(--dur-fast) var(--ease);
}

.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform var(--dur) var(--ease);
}

.nav-toggle-bars::before {
  top: -6px;
}

.nav-toggle-bars::after {
  top: 6px;
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bars {
  background: transparent;
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before {
  transform: translateY(6px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bars::after {
  transform: translateY(-6px) rotate(-45deg);
}

.nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: color-mix(in oklab, var(--ink) 45%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}

/* The phone header's cart: a count pinned to the corner of the menu button
   rather than a button of its own, which took more of a phone header than a
   number needs. Hidden on wide screens, where the full "Cart" button is already
   visible in the nav. */
.cart-pin-slot {
  display: none;
}

.cart-pin {
  position: absolute;
  top: -0.35rem;
  right: -0.35rem;
  min-width: 1.15rem;
  height: 1.15rem;
  padding-inline: 0.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-pill);
  /* Rides the header's own surface, so it reads as attached to the button
     rather than floating over whatever has scrolled underneath. */
  border: 2px solid var(--paper);
  background: var(--brand);
  color: var(--paper);
  font-size: 0.7rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

@media (max-width: 60rem) {
  /* Stacked above the drawer (z 120). Both live inside .site-header, which is a
     stacking context, so without an explicit z-index the drawer — later in the
     DOM — paints over the button that closes it. */
  .nav-toggle {
    display: inline-flex;
    position: relative;
    z-index: 121;
    margin-left: auto;
    /* The pin hangs over the button's corner. */
    overflow: visible;
  }

  .cart-pin-slot {
    display: block;
  }

  .nav {
    gap: var(--s-3);
  }

  .nav-actions {
    display: flex;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 120;
    width: min(22rem, 88vw);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: calc(var(--header-h) + var(--s-4)) var(--s-5) var(--s-6);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--paper-raised);
    border-left: 1px solid var(--line);
    box-shadow: var(--shadow-lg);
    transform: translateX(100%);
    visibility: hidden;
    transition:
      transform var(--dur-slow) var(--ease),
      visibility 0s linear var(--dur-slow);
  }

  .storefront-body.nav-open .nav-actions {
    transform: translateX(0);
    visibility: visible;
    transition-delay: 0s;
  }

  .storefront-body.nav-open .nav-scrim {
    opacity: 1;
    pointer-events: auto;
  }

  /* Locking the body's scroll is what keeps the page still behind the drawer,
     but `overflow: hidden` on the scroll container also stops `position:
     sticky` working: the header dropped back to its place in the document, so
     opening the menu part-way down a page took the close button off the top of
     the screen with it. Fixed for as long as the drawer is open, with the space
     it vacates held by padding so the page does not jump underneath. */
  .storefront-body.nav-open {
    overflow: hidden;
    padding-top: var(--header-h);
  }

  .storefront-body.nav-open .site-header {
    position: fixed;
    inset-inline: 0;
    top: 0;
  }

  .nav-menu,
  .nav-utility {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }

  .nav-utility {
    margin-top: var(--s-5);
    padding-top: var(--s-5);
    border-top: 1px solid var(--line);
    gap: var(--s-3);
  }

  .nav-link,
  .nav-heading {
    padding: 0.85rem 0.5rem;
    font-size: 1.05rem;
    border-radius: var(--r-xs);
    white-space: normal;
  }

  .nav-menu > .nav-link,
  .nav-menu > .nav-item {
    border-bottom: 1px solid var(--line);
  }

  /* Submenus are a static indented list here: a hover-revealed panel is
     unreachable on touch, and nesting a disclosure would hide links behind an
     extra tap for no benefit on a short menu. No visibility override — the
     desktop hide/reveal no longer applies at this width, so the dropdown
     inherits `visibility: hidden` from the closed drawer as it should. */
  .nav-dropdown {
    padding: 0 0 0.5rem 0.9rem;
  }

  .has-dropdown > .nav-link::after,
  .has-dropdown > .nav-heading::after {
    display: none;
  }

  .nav-dropdown-link {
    padding: 0.6rem 0.5rem;
    font-size: 0.98rem;
  }

  .nav-cta {
    justify-content: center;
    min-height: 3rem;
  }
}

/* ==========================================================================
   8. Hero
   ========================================================================== */

/* The hero is a third of the screen and no more. Its job is to say whose shop
   this is; the job of the screen is to show arrangements, and a shopper who has
   to scroll to meet the first one is a shopper deciding whether to bother.
   --hero-h is the whole band including its own padding, so every rule below
   sizes against one number. */
.storefront-hero {
  /* svh, NOT dvh. dvh is the *dynamic* viewport: on a phone it re-resolves as
     the URL bar hides on scroll down and comes back on scroll up, so a hero
     sized in dvh visibly grows and shrinks while the shopper is scrolling past
     it. svh is the viewport with the browser's chrome shown — a fixed number
     that does not move — and because it is the SMALLEST of the three, a third
     of it is at most a third of whatever the viewport later becomes. The cap
     still holds; it just stops breathing.

     This did not bite before the band was capped: the phone floor used to be
     min(<width-derived>, 32rem, 68dvh), where the width term won on any real
     phone and the dvh term never bound. At a third it binds, and the hero
     started resizing on scroll. */
  --hero-h: 33svh;
  /* Named rather than inlined because the picture's own cap is `--hero-h` minus
     exactly these two. A breakpoint that changes the padding changes only these,
     and the cap follows. */
  --hero-pad-top: var(--s-4);
  --hero-pad-bottom: var(--s-4);
  padding-block: var(--hero-pad-top) var(--hero-pad-bottom);
}

.hero-classic {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  align-items: center;
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding-inline: var(--gutter);
  gap: var(--block);
}

@media (min-width: 62rem) {
  .hero-classic {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  }

  /* Copy first in the DOM for screen readers and for the mobile stack; the
     picture sits on the right on wide screens. */
  .hero-content {
    order: -1;
  }
}

.hero-media {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--paper-sunken);
  box-shadow: var(--shadow-lg);
}

.hero-image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
}

@media (min-width: 62rem) {
  /* The picture is what a two-column hero sizes against — the copy beside it is
     shorter than this on any realistic title — so capping it caps the band.
     `min()` keeps the old 24rem ceiling for short viewports, where a third of
     the screen is generous, and lets a tall monitor take the third instead. */
  .hero-image {
    aspect-ratio: 5 / 4;
    max-height: min(24rem, calc(var(--hero-h) - var(--hero-pad-top) - var(--hero-pad-bottom)));
  }
}

/* Sized for this band rather than by the page's h1 scale: at a third of the
   screen a 44px headline spends a quarter of the hero on two lines of type. */
.hero-content h1 {
  margin-bottom: var(--s-3);
  font-size: clamp(1.6rem, 1.1rem + 1.4vw, 2.2rem);
  line-height: 1.15;
}

.hero-lede {
  font-size: clamp(1rem, 0.96rem + 0.3vw, 1.15rem);
  line-height: 1.55;
  color: var(--ink-muted);
  max-width: 42ch;
  margin-bottom: var(--s-4);
  /* Two lines, so a shop that writes a paragraph into the subtitle field cannot
     push the band past its third of the screen. The full text stays in the
     meta description, which is where a long one is actually read. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}

/* Phone and tablet: the copy lies on the foot of the photograph instead of in a
   block beneath it. Stacked, the picture and the block under it filled the
   entire first screen, so a shopper scrolled past the whole hero before meeting
   a single arrangement. The product page already lays its headline on the
   picture this way, so the two read as one idea rather than two.

   The band is only as tall as its contents and is anchored to the bottom of the
   frame, which leaves the top of the picture clean. */
@media (max-width: 61.999rem) {
  .storefront-hero {
    --hero-pad-top: var(--s-3);
    --hero-pad-bottom: var(--s-4);
  }

  .hero-classic {
    gap: 0;
  }

  /* One cell, two layers: the media paints it and the copy sits on top. Grid
     items honour z-index without being positioned, which matters here because
     .hero-media IS positioned and would otherwise paint over the copy. */
  .hero-media,
  .hero-content {
    grid-column: 1;
    grid-row: 1;
  }

  /* Portrait, so there is picture left above the copy.

     The height is a FLOOR, not a ratio box, and the frame stretches to whatever
     the row ends up being. That is what keeps the band on the photograph: if
     the copy grows past the floor — a long title, a narrow phone, large type —
     the row grows and the picture grows with it. An `aspect-ratio` here cannot
     do the same job, because a stretched height feeds back through the ratio
     and resizes the frame's WIDTH: at 320px it pushed the picture 102px out of
     its own column, and in landscape it left the band hanging off the side.

     The floor is 5/4 of the frame's width (the 4:5 portrait it replaces), then
     capped so a tablet or a landscape phone does not get a full screen of
     photograph before a word of copy. */
  .hero-media {
    align-self: stretch;
    /* Was 68dvh — two thirds of the phone — which put the first arrangement
       below the fold on every visit. The floor is now the same third the
       desktop band gets, and it is still a FLOOR: the note above still holds,
       and a long title grows the row and the picture with it. */
    min-height: min(
      calc((100vw - 2 * var(--gutter)) * 1.25),
      32rem,
      /* --hero-h is the whole band, so the frame gets it less the section's own
         padding. */
      calc(var(--hero-h) - var(--hero-pad-top) - var(--hero-pad-bottom))
    );
  }

  /* Lifted out of flow so the picture fills the frame instead of sizing it. In
     flow it contributes its own height, and a photograph's natural shape then
     overrides the cap above — a 5:6 portrait made an 839px-tall hero on a
     tablet. Cropping is what object-fit is for. */
  .hero-image {
    position: absolute;
    inset: 0;
    height: 100%;
    aspect-ratio: auto;
  }

  .hero-content {
    z-index: 1;
    align-self: end;
    padding: 1.25rem var(--s-5) var(--s-4);
    border-radius: 0 0 var(--r-lg) var(--r-lg);
    background: linear-gradient(
      to top,
      rgb(0 0 0 / 0.82),
      rgb(0 0 0 / 0.58) 52%,
      rgb(0 0 0 / 0) 100%
    );
  }

  /* Dropped on a phone. "Shop the collection" was a label for a band with two
     thirds of the screen to fill; in a third it costs a line the headline says
     better, and the headroom it frees is what keeps the promise above from
     depending on which fonts the device actually has. */
  .hero-content .eyebrow {
    display: none;
  }

  .hero-content h1 {
    margin-bottom: var(--s-2);
    font-size: clamp(1.3rem, 1.05rem + 1.5vw, 1.7rem);
    color: #fff;
    text-shadow: 0 1px 3px rgb(0 0 0 / 0.35);
    /* Two lines. A third line of headline is the difference between the first
       arrangement being on the screen and being just under it. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
  }

  /* Two lines of it over the picture. Three was the ceiling when the band had
     two thirds of the screen to spend; at a third, the third line is what tips
     the copy over the photograph it sits on. */
  .hero-lede {
    -webkit-line-clamp: 2;
    line-clamp: 2;
    margin-bottom: var(--s-3);
    color: rgb(255 255 255 / 0.88);
    text-shadow: 0 1px 3px rgb(0 0 0 / 0.35);
  }

  /* Two rows of full-width buttons cost 100px of a 281px band. Sharing one row
     costs half that, and the labels are short enough to read at this size.
     `flex: 1 1 0` rather than `nowrap`: a shop that writes a long CTA label
     gets a two-line button, not one with its label cut off. */
  .hero-actions .button {
    flex: 1 1 0;
    min-width: 0;
    padding-inline: 0.75rem;
    font-size: 0.85rem;
    text-align: center;
  }

  /* The ghost button is ink on a hairline: both vanish against the scrim. */
  .hero-actions .button.ghost {
    border-color: rgb(255 255 255 / 0.6);
    color: #fff;
  }

  .hero-actions .button.ghost:hover:not(:disabled) {
    background: rgb(255 255 255 / 0.14);
    border-color: #fff;
    color: #fff;
  }
}

/* A short screen — an older phone, or any phone turned sideways. A third of
   640px is 213px, and the phone band's copy alone came to 212 of it, so the cap
   above held everywhere except the devices that need it most. Nothing is
   removed here; the same four things are simply set at the size a third of a
   small screen can carry. */
@media (max-width: 61.999rem) and (max-height: 46rem) {
  .storefront-hero {
    --hero-pad-top: var(--s-2);
    --hero-pad-bottom: var(--s-3);
  }

  .hero-content {
    padding-top: 0.75rem;
  }

  .hero-content h1 {
    font-size: clamp(1.15rem, 1rem + 1.1vw, 1.45rem);
  }

  .hero-lede {
    -webkit-line-clamp: 1;
    line-clamp: 1;
    margin-bottom: var(--s-2);
  }

  .hero-actions .button {
    padding-block: 0.5rem;
  }
}

/* A slow-loading or missing photo still occupies its exact final box, with a
   soft tonal wash instead of a blank rectangle. */
.placeholder {
  background:
    radial-gradient(120% 90% at 25% 15%, var(--brand-tint-strong), transparent 60%),
    linear-gradient(150deg, var(--paper-panel), var(--paper-sunken));
  border: 0;
}

/* ==========================================================================
   8b. Reviews banner
   ==========================================================================
   The trust band between the hero and the first row of arrangements. It has two
   jobs and they pull against each other: say enough that a stranger believes the
   shop, and be short enough that the arrangements are still on the first screen
   after it. Everything here is in service of the second.

   Laid out mobile-first for the same reason .home-products is: the narrow case
   is a scrolling row and the wide case is a plain grid, and writing it this way
   means the wide rule only has to undo the scrolling rather than the narrow rule
   having to undo a grid it never wanted. */

.reviews-banner {
  padding-block: var(--s-4);
  background: var(--paper-panel);
  border-block: 1px solid var(--line);
}

/* Two cards across with the third cut off by the screen edge — the same shape
   and the same numbers as the product rows below, so the two read as one idea.

   A grid rather than a flex row on purpose: a non-shrinking flex row inside this
   band grew the document's own scroll width past the viewport, which dragged the
   page sideways on a 320px screen and un-clipped the parked nav drawer with it. */
.reviews-banner-inner {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: clamp(9.5rem, 44%, 13rem);
  gap: clamp(var(--s-3), 1.6vw, var(--s-5));
  overflow-x: auto;
  overscroll-behavior-x: contain;
  /* proximity, not mandatory, for the reason the product rows give: a thumb
     nudging the last card into view should not be fought. */
  scroll-snap-type: x proximity;
  /* The band is already full-bleed, so the row runs to the screen edge on its
     own — a card cut off by the edge is what says the row keeps going. The
     padding puts the gutter back inside the scroll box so the first card lines
     up with the hero above it, and scroll-padding makes snapping respect it. */
  padding-inline: var(--gutter);
  scroll-padding-inline: var(--gutter);
  scrollbar-width: thin;
  /* Paint containment, which .home-products does not need and this does.
     Without it this scroller extends the DOCUMENT's scrollable width — not its
     own — and the phone home page could be dragged 24px sideways, which also
     un-clips the nav drawer parked off-screen by html/body's overflow-x: clip.
     The two rows are otherwise computed-identical (same display, overflow,
     width, containment and parent), so the asymmetry is Chrome's, not the
     stylesheet's. `paint` is true of this box regardless: nothing inside a
     carousel is meant to paint outside it. */
  contain: paint;
}

.review {
  margin: 0;
  min-width: 0;
  scroll-snap-align: start;
}

.review-stars {
  margin: 0 0 var(--s-2);
  font-size: 0.85rem;
  line-height: 1;
  letter-spacing: 0.1em;
  color: var(--brand-deep);
}

.review-quote {
  margin: 0 0 var(--s-3);
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink);
  /* A ceiling on the quote, not a target. Without one a single long review
     pushes the arrangements off the screen this band exists to keep them on.
     Six lines in a two-across card is about the same amount of text as five in
     a full-width one; the admin form says so where a review is entered. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  line-clamp: 6;
  overflow: hidden;
}

.review-byline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--s-2);
  font-size: 0.78rem;
}

.review-name {
  font-weight: 600;
  color: var(--ink);
}

/* Naming where the review was left is what makes it checkable, so it is stated
   rather than implied — quietly, because the quote is the thing being read. */
.review-source {
  color: var(--ink-muted);
}

/* Wide enough for all three at once, so the row goes back to being a grid and
   gives up every scroll-container behaviour with it. Same breakpoint as
   .home-products, so the band and the row below it never disagree about whether
   this width scrolls. */
@media (min-width: 62rem) {
  .reviews-banner-inner {
    grid-auto-flow: row;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: start;
    gap: var(--s-5);
    width: min(var(--shell), 100%);
    margin-inline: auto;
    padding-inline: 0;
    overflow-x: visible;
    scroll-snap-type: none;
    contain: none;
  }

  /* A shop with one or two reviews gets one or two columns, centred, rather
     than two-thirds of a band and a hole. */
  .reviews-banner-inner:has(.review:only-child) {
    grid-template-columns: minmax(0, 40rem);
    justify-content: center;
  }

  .reviews-banner-inner:has(.review:nth-child(2):last-child) {
    grid-template-columns: repeat(2, minmax(0, 24rem));
    justify-content: center;
  }

  .review-stars,
  .review-quote {
    font-size: 0.95rem;
  }

  .review-quote {
    -webkit-line-clamp: 5;
    line-clamp: 5;
  }

  .review-byline {
    font-size: 0.85rem;
  }
}

/* ==========================================================================
   9. Product cards & grids
   ========================================================================== */

.home-categories {
  display: grid;
  gap: var(--page);
  width: min(var(--shell), 100%);
  margin-inline: auto;
  /* A little air under the reviews band's bottom rule, which now sits directly
     above the first category header. */
  padding: var(--s-5) var(--gutter) var(--page);
}

.home-category-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  padding-bottom: var(--s-3);
  margin-bottom: var(--s-4);
  border-bottom: 1px solid var(--line);
}

.home-category-header h2 {
  margin-bottom: 0;
}

/* Explicit column counts rather than auto-fill, so a category grid matches the
   home rows at every width. auto-fill with a 14rem minimum collapsed to a
   single full-width card on a phone, where the home page shows two. */
.category-products {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(var(--s-3), 1.6vw, var(--s-5));
}

@media (min-width: 48rem) {
  .category-products {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (min-width: 68rem) {
  .category-products {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* A home row shows at most three products (storefront/index.html.erb caps it),
   so the track count is fixed rather than auto-filled. With auto-fill a
   category holding one product got a card floating in four columns of dead
   space; with auto-fit that same card stretched to the full page width. */
/* A narrow screen scrolls a category sideways instead of wrapping it. Wrapped
   into two columns, a three-product row plus its collection tile became a
   two-row block, so a phone showed barely more than one category per screen and
   the categories below were a long scroll away. One row each keeps the next
   category's heading in reach.

   grid rather than flex so the desktop rule below only has to swap the flow
   direction, and every track is the same width without the children knowing. */
.home-products {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: clamp(9.5rem, 44%, 13rem);
  gap: clamp(var(--s-3), 1.6vw, var(--s-5));
  overflow-x: auto;
  overscroll-behavior-x: contain;
  /* proximity, not mandatory: a row is three or four items, and mandatory
     snapping fights a thumb trying to nudge the last one into view. */
  scroll-snap-type: x proximity;
  /* Out to the screen edge rather than stopping at the section gutter — a card
     cut off by the edge is what says the row keeps going. Padding puts the
     gutter back inside the scroll box so the first card still lines up with the
     heading above it, and scroll-padding makes snapping respect it. */
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  scroll-padding-inline: var(--gutter);
  /* Room for the cards' shadows, which the scroll box would otherwise clip. */
  padding-block: var(--s-2);
  scrollbar-width: thin;
}

.home-products > * {
  scroll-snap-align: start;
}

/* Wide enough for all three across, so the row goes back to being a grid and
   gives up every scroll-container behaviour with it. */
@media (min-width: 62rem) {
  .home-products {
    grid-auto-flow: row;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    overflow-x: visible;
    scroll-snap-type: none;
    margin-inline: 0;
    padding-inline: 0;
    padding-block: 0;
  }
}

.home-product-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--card-surface);
  color: var(--card-text);
  border: 1px solid var(--card-hairline);
  border-radius: var(--r-md);
  overflow: hidden;
  isolation: isolate;
  box-shadow: var(--shadow-xs);
  transition:
    transform var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.home-product-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
  border-color: var(--line-brand);
}

.home-product-card:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}

/* 5/6, because that is the shape the arrangements are shot in. Matching the
   frame to the photography is what lets this go back to cover: there is no
   letterbox to wash out and next to nothing to crop, so the top blooms and the
   base of the vase survive without the tile carrying empty margins.

   The taller tile is the deliberate cost — three across on a desktop this is
   ~500px of photo before the caption where the square was ~400px. */
.home-product-photo {
  width: 100%;
  aspect-ratio: 5 / 6;
  object-fit: cover;
  background: var(--paper-sunken);
  transition: transform var(--dur-slow) var(--ease);
}

.home-product-card:hover .home-product-photo {
  transform: scale(1.035);
}

/* The photo lives in its own clipping context so the zoom on hover does not
   bleed over the caption's rounded corner. */
.home-product-card > .home-product-photo,
.home-product-card > img.home-product-photo {
  overflow: hidden;
}

/* Historically this was an overlay floated on top of the photograph. It is now
   a caption strip below it: product names and prices sat on unpredictable
   photography and were routinely unreadable, and no amount of scrim fixes a
   white peony behind white text. The shop's light/dark card preference still
   drives its colours.
   One row, name and price on a shared baseline. There is no "View →" affordance
   because the entire card is already the link. */
.card-overlay {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  padding: 0.7rem 0.9rem 0.8rem;
  background: var(--card-surface);
  color: var(--card-text);
}

.home-product-name {
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 1.25;
  margin: 0;
  color: var(--card-text);
  text-wrap: pretty;
}

.card-overlay .price {
  flex: none;
  font-size: 0.92rem;
  color: var(--card-muted);
  white-space: nowrap;
}

.home-product-card:hover .price {
  color: var(--brand-deep);
}

/* Quiet and small: it qualifies the number without competing with it. */
.card-overlay .price-from {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-right: 0.2rem;
  opacity: 0.75;
}

/* Two-up on a phone leaves a card about 165px wide, where a name and a price on
   one line means the name wraps under a price pinned to the top corner. Stack
   them instead. */
@media (max-width: 30rem) {
  .card-overlay {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.1rem;
  }
}

/* Fills the row's leftover columns when a category has fewer than three
   products; see storefront/index.html.erb. */
.collection-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 9rem;
  overflow: hidden;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-md);
  background: color-mix(in oklab, var(--brand) 5%, transparent);
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.collection-tile:hover {
  border-color: var(--brand);
  background: var(--brand-tint);
}

.collection-tile-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease);
}

.collection-tile:hover .collection-tile-photo {
  transform: scale(1.04);
}

.collection-tile-body {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: var(--s-4);
}

/* With a photograph the tile becomes a banner: a hard border, and a scrim
   mixed from the shop's secondary colour so the label keeps its contrast no
   matter what the photo underneath is doing. */
.collection-tile.has-photo {
  border-style: solid;
  border-color: transparent;
  justify-content: flex-end;
}

.collection-tile.has-photo .collection-tile-body {
  background: linear-gradient(
    to top,
    color-mix(in oklab, var(--secondary) 88%, transparent),
    color-mix(in oklab, var(--secondary) 55%, transparent) 60%,
    transparent
  );
  padding-top: var(--s-7);
}

.collection-tile-label {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.collection-tile-name {
  font-family: var(--font-heading);
  font-size: 1.4rem;
  font-weight: 600;
  line-height: 1.15;
  color: var(--ink);
}

.collection-tile-arrow {
  font-size: 1.05rem;
  color: var(--brand-deep);
  transition: transform var(--dur) var(--ease);
}

.collection-tile.has-photo .collection-tile-label {
  color: rgba(255, 255, 255, 0.82);
}

.collection-tile.has-photo .collection-tile-name,
.collection-tile.has-photo .collection-tile-arrow {
  color: #fff;
}

.collection-tile:hover .collection-tile-arrow {
  transform: translateX(4px);
}

/* In the scrolling row the tile is normally one more track, the same width as a
   product card. A category holding fewer than two products is the exception:
   left at one track it reads as a lone card stranded in an empty row, so it
   takes two and becomes the banner it already is on the desktop grid. */
@media (max-width: 61.9375rem) {
  .home-products .collection-tile:is([data-span-lg="2"], [data-span-lg="3"]) {
    grid-column: span 2;
  }
}

/* Leftover columns only exist where the row is a grid. */
@media (min-width: 62rem) {
  .collection-tile[data-span-lg="2"] {
    grid-column: span 2;
  }

  .collection-tile[data-span-lg="3"] {
    grid-column: span 3;
  }

  /* Wider than a card, the name can carry more weight without crowding. */
  .collection-tile:is([data-span-lg="2"], [data-span-lg="3"]) .collection-tile-name {
    font-size: clamp(1.5rem, 1.2rem + 1vw, 2rem);
  }
}

.home-empty,
.category-empty,
.cart-empty {
  /* Span the whole track: these render as children of the product grid, and a
     single-column empty state stranded in the first column reads as a broken
     layout rather than as "nothing here yet". */
  grid-column: 1 / -1;
  display: grid;
  place-items: center;
  gap: var(--s-2);
  text-align: center;
  padding: var(--block) var(--s-5);
  background: var(--paper-panel);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-md);
}

.home-empty .button,
.category-empty .button,
.cart-empty .button {
  margin-top: var(--s-3);
}

.home-empty h2,
.category-empty h2 {
  margin-bottom: 0;
}

.home-empty p,
.category-empty p,
.cart-empty p {
  margin: 0;
}

/* ==========================================================================
   10. Category pages
   ========================================================================== */

.category-index {
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding: var(--block) var(--gutter) var(--page);
}

/* Less headroom than a normal page: this one opens on a breadcrumb, which needs
   almost no air under the header, and every pixel above the banner is a pixel
   the product grid starts lower. */
.category-page {
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding: var(--s-4) var(--gutter) var(--page);
}

.category-hero,
.page-header {
  max-width: 42rem;
  margin-bottom: var(--block);
}

.category-breadcrumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: var(--s-3);
  font-size: 0.85rem;
  color: var(--ink-soft);
}

/* 24px minimum height: WCAG 2.2 "Target Size (Minimum)". Breadcrumb links sat
   at 22px, which is a miss on a rule that exists precisely for small
   secondary navigation like this. */
.category-breadcrumbs .text-link {
  display: inline-flex;
  align-items: center;
  min-height: 1.5rem;
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: none;
  color: var(--ink-soft);
}

.category-breadcrumbs .text-link:hover {
  color: var(--brand-deep);
  text-decoration: underline;
}

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

.breadcrumb-separator {
  color: var(--ink-decor);
}

/* Compact page banner for a category: title over its own photograph, or a
   tinted band when the shop has not set one. */
.category-banner {
  position: relative;
  display: flex;
  align-items: stretch;
  min-height: 6rem;
  margin-bottom: var(--s-4);
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--brand-tint);
}

.category-banner-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.category-banner-body {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  padding: var(--s-4);
}

.category-banner h1 {
  margin-bottom: 0.1rem;
}

.category-banner-meta {
  margin: 0;
  font-size: 0.88rem;
  color: var(--ink-soft);
}

/* Tall enough to read as a photograph and no taller. This banner sits between
   the shopper and the grid they came for, so it gets the smallest band that
   still shows the image. */
.category-banner.has-photo {
  min-height: 9rem;
}

/* The scrim runs left-to-right, not bottom-to-top like the home-row banner.
   On a band this short a vertical fade covers nearly the whole image, leaving a
   dark bar with a sliver of photo above it; horizontally, the title gets its
   contrast on the left and the photograph still reads on the right. It is a
   gradient of the shop's secondary colour either way, so the title holds up
   over any image. */
.category-banner.has-photo .category-banner-body {
  padding-inline: var(--s-5);
  background: linear-gradient(
    to right,
    color-mix(in oklab, var(--secondary) 92%, transparent),
    color-mix(in oklab, var(--secondary) 74%, transparent) 40%,
    transparent 85%
  );
}

.category-banner.has-photo h1 {
  color: #fff;
}

.category-banner.has-photo .category-banner-meta {
  color: rgba(255, 255, 255, 0.85);
}

.category-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
  gap: clamp(var(--s-3), 1.6vw, var(--s-4));
}

/* Wider tracks than the product grid on purpose: a subcategory card is a
   signpost, and giving it a different shape is most of what stops the two
   grids on a category page from reading as one undifferentiated wall. */
.subcategory-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(19rem, 100%), 1fr));
  gap: clamp(var(--s-3), 1.6vw, var(--s-4));
}

.subcategory-photo {
  aspect-ratio: 16 / 9;
}

.subcategory-card-body {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
}

.subcategory-card-body h3 {
  font-size: 1.1rem;
  margin-bottom: 0;
}

.subcategory-arrow {
  flex: none;
  font-size: 1.1rem;
  color: var(--brand-deep);
  transition: transform var(--dur) var(--ease);
}

.subcategory-card:hover .subcategory-arrow {
  transform: translateX(4px);
}

.category-section + .category-section {
  margin-top: var(--block);
}

/* A section label, not a page title. The banner above already carries the
   page's h1 at full display size; these only have to separate two grids. */
.category-section .section-heading h2 {
  font-size: clamp(1.2rem, 1.1rem + 0.5vw, 1.5rem);
}

.category-card,
.subcategory-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--shadow-xs);
  transition:
    transform var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.category-card:hover,
.subcategory-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
  border-color: var(--line-brand);
}

.category-card-photo,
.subcategory-photo {
  width: 100%;
  object-fit: cover;
  background: var(--paper-sunken);
}

.category-card-photo {
  aspect-ratio: 3 / 2;
}

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

.category-card-body :is(h2, h3) {
  font-size: 1.15rem;
  margin-bottom: 0.25rem;
}

.category-card-body p {
  font-size: 0.88rem;
  color: var(--ink-soft);
  margin: 0;
}

/* ==========================================================================
   11. Product detail
   ========================================================================== */

.product-detail {
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding: var(--s-4) var(--gutter) var(--page);
}

/* Named areas because the heading has to sit in two different places: laid over
   the foot of the photo on a phone, and at the top of the buy column on a
   desktop. Areas move one element between the two without a second copy of it
   in the markup. */
.product-layout {
  display: grid;
  gap: var(--block);
  align-items: start;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "media"
    "buy";
}

/* Scoped to this grid's own children. .checkout-form is shared with the cart
   and checkout pages, and a bare `grid-area: buy` followed it there: the cart's
   own grid has no such area, so it invented a track and left the summary and
   the items each squeezed against opposite edges of the phone. */
.product-layout > .product-visual { grid-area: media; }
.product-layout > .product-headline { grid-area: headline; }
.product-layout > .checkout-form { grid-area: buy; }

/* Not rendered as anything on a desktop: nothing lies over the photo there, so
   there is nothing to clear away. */
.product-photo-toggle {
  display: none;
}

/* Phone: the heading and the opening lines of the description lie on the foot
   of the photo, so the sizes follow the picture directly.

   The band is only as tall as its contents and sits at the bottom of the frame,
   so opening the description pulls it up the picture rather than covering the
   whole thing. Its gradient goes with it — no second layer fading in over the
   photo, which is what was flashing. */
@media (max-width: 59.999rem) {
  /* Hugs the photo rather than the column, so the toggle lands on the picture's
     own corner even on a phone wide enough that the frame stops growing. */
  .product-gallery {
    position: relative;
    width: 100%;
    max-width: calc(34rem * 5 / 6);
    margin-inline: auto;
  }

  /* A pull tab beside the caption it hides: thin enough to take almost none of
     the picture, and labelled, because a tab has to say which way it goes. The
     text runs down the tab rather than the tab widening to hold it.

     Anchored to the foot of the frame so it stays with the caption whether that
     is two lines or twenty, and above it so it is still the way back once the
     caption has gone. */
  .product-photo-toggle {
    position: absolute;
    left: 0;
    bottom: var(--s-3);
    z-index: 4;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.85rem 0.35rem;
    border: 0;
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
    background: rgb(0 0 0 / 0.5);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    color: #fff;
    cursor: pointer;
    writing-mode: vertical-rl;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  /* Fades rather than vanishes, so the picture is not interrupted by a jump. */
  .product-layout > .product-headline {
    transition: opacity var(--dur) var(--ease);
  }

  .product-layout > .product-headline.is-hidden {
    opacity: 0;
    pointer-events: none;
  }

  .product-layout > .product-headline {
    grid-area: media;
    align-self: end;
    z-index: 2;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    width: 100%;
    max-width: calc(34rem * 5 / 6);
    margin: 0 auto;
    /* Generous at the top so the gradient has room to reach nothing before the
       picture proper begins, and clear on the left so the copy starts beside
       the tab rather than underneath it. */
    padding: 3rem var(--s-3) var(--s-3) 2.6rem;
    border-radius: 0 0 var(--r-md) var(--r-md);
    background: linear-gradient(
      to top,
      rgb(0 0 0 / 0.78),
      rgb(0 0 0 / 0.58) 45%,
      rgb(0 0 0 / 0) 100%
    );
    /* Decoration over a photo: taps belong to the picture and to the one
       control inside this panel, nothing else. */
    pointer-events: none;
  }

  .product-headline h1 {
    margin: 0;
    color: #fff;
    font-size: clamp(1.3rem, 1.05rem + 2.2vw, 1.9rem);
    text-shadow: 0 1px 3px rgb(0 0 0 / 0.35);
  }

  /* Tucked close under the title: they are one block of copy, and a gap between
     them read as two unrelated things stacked on the picture. */
  .product-headline .product-description-block {
    margin-top: 0.1rem;
  }

  .product-headline .product-description {
    margin: 0;
    color: rgb(255 255 255 / 0.92);
    font-size: 0.92rem;
    line-height: 1.5;
  }

  /* Two lines: the first plain, the second dissolving into the fade. */
  .product-headline .product-description-text {
    max-height: 2.76rem;
    transition:
      max-height var(--dur-slow) var(--ease),
      --desc-fade var(--dur-slow) var(--ease);
  }

  /* The height it grows to is set inline by the controller — the measured
     height of the copy, capped — so the band stops rising exactly when the last
     line arrives. This is the fallback for a page whose script has not run. */
  .product-headline.is-open .product-description-text {
    max-height: 55vh;
  }

  .product-headline .product-description-toggle {
    pointer-events: auto;
    color: #fff;
    text-shadow: 0 1px 3px rgb(0 0 0 / 0.4);
  }
}

@media (min-width: 60rem) {
  .product-layout {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    grid-template-areas:
      "media headline"
      "media buy";
    /* The right column was one block with its own margins before it became
       three rows; a block-sized row gap would tear it apart. */
    row-gap: 0;
  }

  .product-headline {
    margin-bottom: var(--s-3);
  }

  .product-description-block {
    margin-bottom: var(--s-5);
  }

  /* The gallery scrolls with the page while the buy box stays put, so "Add to
     cart" is reachable no matter how long the add-on list runs. */
  .product-visual {
    position: sticky;
    top: calc(var(--header-h) + var(--s-4));
  }
}

/* 5/6 to match the photography, so cover fills the frame without throwing the
   top blooms or the vase base away.

   Capped by max-WIDTH rather than max-height: a portrait at the full column
   width ran to ~750px and left the buy box floating beside empty page, but
   capping the height alone would let the box grow wider than 5/6 and start
   cropping again. Bounding the width holds the ratio exactly and puts the tall
   edge at 34rem. */
.product-photo {
  width: 100%;
  max-width: calc(34rem * 5 / 6);
  margin-inline: auto;
  aspect-ratio: 5 / 6;
  object-fit: cover;
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);

  /* The view sets an inline background-image: the card variant the shopper
     already downloaded, painted under the full-size photo while that loads.
     cover and centre because it is the same 5:6 crop of the same picture —
     anything else and the preview would shift as the real file lands on it. */
  background: var(--paper-sunken) center / cover no-repeat;
}

/* Set by product_controller.js when the chosen variant has no photo: the <img>
   stays in place holding its box, wearing the placeholder wash. */
.product-photo.is-empty {
  background:
    radial-gradient(120% 90% at 25% 15%, var(--brand-tint-strong), transparent 60%),
    linear-gradient(150deg, var(--paper-panel), var(--paper-sunken));
}

/* Fieldsets carry a default border, padding and margin in every browser; these
   group radios semantically and should be invisible. */
.field-group {
  border: 0;
  padding: 0;
  margin: 0 0 var(--s-5);
  min-width: 0;
}

.field-group > legend {
  padding: 0;
  margin-bottom: 0.6rem;
}

.product-gallery {
  display: grid;
  gap: var(--s-3);
}

.product-headline h1 {
  font-size: clamp(1.9rem, 1.4rem + 2vw, 2.9rem);
  margin-bottom: var(--s-3);
}

.product-sku {
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: var(--s-3);
}

.product-description {
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--ink-muted);
  margin: var(--s-3) 0 0;
}

/* Collapsed, the copy is clamped to two lines and masked so the first reads
   plainly and the second dissolves — a preview, which tells a shopper whether
   opening it is worth a tap in a way a "Description" label cannot.

   The clamp is the mask's partner: without it the mask would fade a paragraph
   of any length, and a long one would be a wall of half-legible text. */
/* Registered so it can be transitioned: a raw custom property in a gradient is
   a string swap, and the fade would switch off in a single frame. */
@property --desc-fade {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}

/* Two lines' worth of height with the overflow hidden, rather than a line
   clamp: the clamp's ellipsis appears and vanishes in one frame, which is one
   more thing blinking on a box that is otherwise moving smoothly.

   Opaque through the whole first line, dissolving across the second. Started
   any earlier and the fade eats the line it is meant to be introducing. */
.product-description-text {
  --desc-fade: 50%;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 0 var(--desc-fade), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0 var(--desc-fade), transparent 100%);
}

/* Pushing the fade's start to the foot of the box leaves nothing to fade. */
.product-description-block.is-open .product-description-text {
  --desc-fade: 100%;
}

/* Overflow stays hidden while the height animates: a box mid-transition is
   shorter than its contents, so `auto` put a scrollbar on screen for exactly as
   long as the animation lasted and then took it away again. The controller adds
   this once the copy has finished opening, and only when it genuinely overruns
   the cap. */
.product-description-text.is-scrollable {
  overflow-y: auto;
  overscroll-behavior: contain;
}

.product-description-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.15rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--brand-deep);
  font: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  cursor: pointer;
}

.product-description-toggle::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-1px) rotate(45deg);
}

.product-description-block.is-open .product-description-toggle::after {
  transform: translateY(2px) rotate(-135deg);
}

/* A desktop column has room for the paragraph, so none of this applies there:
   no clamp, no fade, and no button offering to reveal what is already visible. */
@media (min-width: 60rem) {
  .product-description-text {
    max-height: none;
    overflow: visible;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .product-description-toggle {
    display: none;
  }
}

/* Variants as a pill grid rather than a <select>. Sizes are the single most
   important choice on this page and there are rarely more than four; a dropdown
   hides all of them behind a tap and hides the prices entirely.

   A grid, not a wrapping flex line: sized to their own text the pills came out
   different widths and a trio wrapped 2 + 1 on a phone, which read as two rows
   of unrelated controls rather than one choice. */
.variant-options {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-2);
}

/* Quantity queries so one or two sizes fill the row instead of leaving an
   orphan third empty. Three or more take three columns and wrap. */
.variant-options:has(.variant-option:nth-child(2)) {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.variant-options:has(.variant-option:nth-child(3)) {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.variant-option {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.15rem;
  min-width: 0;
  padding: 0.7rem 0.6rem;
  text-align: center;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--paper-raised);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.variant-option:hover {
  border-color: var(--line-brand);
}

.variant-option input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
}

.variant-option:has(input:checked) {
  border-color: var(--brand);
  background: var(--brand-tint);
  box-shadow: inset 0 0 0 1px var(--brand);
}

.variant-option:has(input:focus-visible) {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* A sold-out size stays on the shelf but can't be picked up: the disabled
   input already refuses the click, this just makes the pill look like it. */
.variant-option--unavailable {
  cursor: not-allowed;
  opacity: 0.55;
}

.variant-option--unavailable:hover {
  border-color: var(--line-strong);
}

/* A column is roughly a third of a phone screen, so long names have to break
   inside the pill rather than push it wider than its grid track. */
.variant-option-name {
  font-size: 0.92rem;
  font-weight: 600;
  line-height: 1.25;
  overflow-wrap: break-word;
  color: var(--ink);
}

.variant-option-sku {
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.variant-option:has(input:checked) .variant-option-sku {
  color: var(--brand-deep);
}

.variant-option-price {
  font-size: 0.85rem;
  line-height: 1.25;
  font-variant-numeric: tabular-nums;
  overflow-wrap: break-word;
  color: var(--ink-soft);
}

.variant-option:has(input:checked) .variant-option-sku {
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.variant-option:has(input:checked) .variant-option-sku {
  color: var(--brand-deep);
}

.variant-option-price {
  color: var(--brand-deep);
}

/* --- Add-ons ------------------------------------------------------------ */

.addon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));
  gap: var(--s-3);
}

.addon-card {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "thumb info"
    "fields fields";
  gap: 0.75rem;
  padding: 0.85rem;
  background: var(--paper-panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  transition: border-color var(--dur) var(--ease);
}

.addon-card:has(input[type="number"]:not([value="0"])) {
  border-color: var(--line-brand);
}

.addon-thumb {
  grid-area: thumb;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--r-xs);
  overflow: hidden;
  background: var(--paper-sunken);
}

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

.addon-info {
  grid-area: info;
  align-self: center;
  min-width: 0;
}

.addon-name {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
}

.addon-price {
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  margin: 0;
}

.addon-fields {
  grid-area: fields;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.6rem;
}

.addon-fields .field {
  margin-bottom: 0;
}

.addon-fields label {
  font-size: 0.75rem;
  color: var(--ink-soft);
}

.addon-fields input,
.addon-fields select {
  min-height: 2.6rem;
  padding: 0.4rem 0.6rem;
  font-size: 0.95rem;
}

/* The add-on stepper is the same control as the arrangement's, one size down:
   two of them sit inside a card that is itself one of a grid. The rules above
   would otherwise pad the number out of its pill. */
.addon-fields .quantity-control {
  align-self: flex-start;
}

.addon-fields .quantity-control .quantity-btn {
  width: 2.1rem;
  height: 2.4rem;
}

.addon-fields .quantity-control input[type="number"] {
  width: 2.4rem;
  min-height: 2.4rem;
  padding: 0 0.15rem;
  font-size: 0.95rem;
}

.addon-fields select {
  padding-right: 2rem;
  background-position: calc(100% - 0.95rem) calc(50% + 2px), calc(100% - 0.65rem) calc(50% + 2px);
}

.addon-editor {
  margin-top: var(--s-4);
  border-top: 1px solid var(--line);
  padding-top: var(--s-3);
}

.addon-editor > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9rem;
}

.addon-editor > summary::-webkit-details-marker {
  display: none;
}

.addon-editor > summary::before {
  content: "＋";
  font-size: 0.9em;
  transition: transform var(--dur) var(--ease);
}

.addon-editor[open] > summary::before {
  content: "−";
}

.addon-editor-form {
  margin-top: var(--s-4);
}

/* ==========================================================================
   12. Cart
   ========================================================================== */

.cart-items {
  display: grid;
  gap: var(--s-3);
}

.cart-item {
  display: grid;
  gap: var(--s-3);
  padding: var(--s-3);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}

.cart-item-media {
  width: 4rem;
  height: 4rem;
  flex: none;
  border-radius: var(--r-xs);
  overflow: hidden;
  background: var(--paper-sunken);
}

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

.cart-item-main {
  display: flex;
  gap: var(--s-3);
  align-items: center;
}

.cart-item-titles {
  flex: 1;
  min-width: 0;
}

.cart-item-name {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 600;
  color: var(--ink);
}

.cart-item-variant {
  margin: 0.1rem 0 0;
  font-size: 0.88rem;
  color: var(--ink-soft);
}

.cart-item-addons {
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.5rem;
  margin: 0.25rem 0 0;
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.cart-item-addons-total {
  font-variant-numeric: tabular-nums;
}

.cart-item-meta {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--s-3) var(--s-5);
  flex-wrap: wrap;
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
}

.cart-item-meta .summary-label {
  margin-bottom: 0.2rem;
}

.cart-remove {
  margin-left: auto;
}

.cart-remove form {
  margin: 0;
}

.cart-remove .text-link {
  border: 0;
  background: none;
  padding: 0.35rem 0;
  font-family: inherit;
  font-size: 0.85rem;
  color: var(--ink-soft);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.22em;
}

.cart-remove .text-link:hover {
  color: var(--danger);
}

.summary-label {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0 0 0.15rem;
}

.summary-value {
  font-size: 0.98rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  margin: 0;
}

.quantity-control {
  display: inline-flex;
  align-items: center;
  /* .field is a column flexbox, which stretches its children; without this the
     stepper spans the full form width with the number stranded on the left. */
  align-self: flex-start;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--paper-raised);
  overflow: hidden;
}

.quantity-btn {
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.quantity-btn:hover {
  background: var(--brand-tint);
  color: var(--brand-deep);
}

.quantity-control input[type="number"] {
  width: 3.25rem;
  min-height: 2.5rem;
  padding: 0 0.25rem;
  border: 0;
  border-inline: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  -moz-appearance: textfield;
}

.quantity-control input[type="number"]::-webkit-outer-spin-button,
.quantity-control input[type="number"]::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}

.quantity-control input:focus {
  box-shadow: none;
  background: var(--brand-tint);
}

/* ==========================================================================
   13. Checkout
   ========================================================================== */

.checkout {
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding: var(--block) var(--gutter) var(--page);
}

.checkout-header {
  max-width: 44rem;
  margin-bottom: var(--s-5);
}

/* Standalone links are navigation targets, not running text, so they take the
   24px WCAG 2.2 minimum. Links inside a paragraph are deliberately excluded —
   growing those would open up the line height around them. */
.checkout-header > .text-link,
.home-category-header .text-link,
.section-heading .text-link {
  min-height: 1.5rem;
}

.checkout-header > .text-link {
  font-size: 0.85rem;
  margin-bottom: var(--s-4);
}

.checkout-header h1 {
  margin-bottom: var(--s-3);
}

.checkout-grid {
  display: grid;
  gap: var(--s-5);
  align-items: start;
}

/* Summary before form in the DOM so a screen reader and a phone both meet the
   order total before the fields; on wide screens it moves to the right rail.
   Scoped to .checkout-grid, NOT to .checkout-form on its own — the product page
   reuses .checkout-form as its buy column, and an unscoped `grid-column: 1`
   dragged the buy box into the gallery's track. */
@media (min-width: 64rem) {
  .checkout-grid {
    grid-template-columns: minmax(0, 1fr) minmax(20rem, 24rem);
  }

  .checkout-grid > .checkout-form {
    grid-column: 1;
    grid-row: 1;
  }

  .checkout-grid > .order-summary {
    grid-column: 2;
    grid-row: 1;
    position: sticky;
    top: calc(var(--header-h) + var(--s-4));
  }
}

.checkout-split-grid {
  display: grid;
  gap: var(--s-5);
}

@media (min-width: 48rem) {
  .checkout-split-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Cart items, Occasion and Order summary are three answers to the same
   question, so they get one shell: same surface, same border, same heading.
   They used to be a form-section, a second form-section and a card-inside-an-
   aside, which read as three unrelated widgets stacked up. */
.cart-panel {
  padding: clamp(1rem, 2vw, 1.25rem);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-xs);
}

.cart-page .checkout-form {
  display: grid;
  gap: var(--s-4);
  align-content: start;
}

.cart-page .checkout-grid {
  gap: var(--s-4);
}

.cart-panel > h2 {
  font-size: 1.15rem;
  margin-bottom: var(--s-3);
}

.cart-panel > :last-child {
  margin-bottom: 0;
}

/* Inside a panel the items are rows of one list, not cards of their own: a
   bordered card inside a bordered card is two frames around one thing. */
.cart-page .cart-items {
  gap: 0;
}

.cart-page .cart-item {
  padding: var(--s-3) 0;
  background: none;
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
}

.cart-page .cart-item:first-child {
  padding-top: 0;
  border-top: 0;
}

.cart-page .cart-item:last-child {
  padding-bottom: 0;
}

.summary-count {
  display: block;
  font-size: 0.82rem;
  font-weight: 400;
  color: var(--ink-soft);
}

.order-summary h2 {
  font-size: 1.25rem;
  margin-bottom: var(--s-3);
}

/* Kept in the DOM as a Turbo Stream target; see cart/_summary.html.erb. */
.order-summary.is-empty {
  display: none;
}

.summary-card {
  padding: clamp(1rem, 2vw, 1.25rem);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-xs);
}

.summary-card h3 {
  margin-bottom: var(--s-3);
}

.summary-rows {
  display: grid;
  gap: 0.55rem;
}

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

.summary-row dt,
.summary-row .summary-label {
  margin: 0;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--ink-soft);
}

.summary-row dd,
.summary-row .summary-value {
  margin: 0;
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.summary-row.is-total {
  padding-top: 0.7rem;
  margin-top: 0.35rem;
  border-top: 1px solid var(--line-strong);
}

.summary-row.is-total dt,
.summary-row.is-total .summary-label {
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}

.summary-row.is-total dd,
.summary-row.is-total .summary-value {
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--ink);
}

.summary-line-items {
  display: grid;
  gap: var(--s-3);
  padding-bottom: var(--s-4);
  margin-bottom: var(--s-4);
  border-bottom: 1px solid var(--line);
}

.summary-line-item {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
}

.summary-line-item-name {
  flex: 1;
  min-width: 0;
  font-size: 0.92rem;
  color: var(--ink);
  margin: 0;
}

.summary-line-item-note {
  display: block;
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.summary-line-item-amount {
  font-size: 0.92rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  margin: 0;
}

.summary-card .summary-label {
  margin-top: var(--s-3);
}

.summary-card .summary-label:first-child {
  margin-top: 0;
}

.summary-cta {
  margin-top: var(--s-4);
}

.summary-cta + .form-note {
  margin-top: var(--s-3);
}

/* --- Date pickers ------------------------------------------------------- */

.week-picker {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.5rem;
}

.week-nav {
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--paper-raised);
  color: var(--ink);
  font-size: 1rem;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.week-nav:hover:not(:disabled) {
  border-color: var(--brand);
  color: var(--brand-deep);
}

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

.week-grid {
  display: grid;
  grid-template-columns: repeat(var(--week-days, 7), minmax(0, 1fr));
  gap: 0.3rem;
}

/* Cells are rendered by delivery_date_controller.js / pickup_date_controller.js
   as .week-day with .week-day-name / .week-day-date / .week-day-fee inside. */
.week-grid button,
.week-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
  padding: 0.4rem 0.1rem;
  min-height: 3.6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  background: var(--paper-raised);
  color: var(--ink);
  font-family: var(--font-body);
  line-height: 1.15;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}

.week-day-name {
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.week-day-date {
  font-size: 0.82rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.week-day-fee {
  font-size: 0.62rem;
  font-variant-numeric: tabular-nums;
  color: var(--brand-deep);
}

.week-grid button:hover:not(:disabled) {
  border-color: var(--line-brand);
  background: var(--brand-tint);
}

.week-grid button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  background: var(--paper-sunken);
}

/* Today gets a ring rather than a fill, so it never competes with the
   selected day for "this is the one you picked". */
.week-grid button.today:not(.selected) {
  border-color: var(--line-brand);
  box-shadow: inset 0 0 0 1px var(--line-brand);
}

.week-grid button.selected,
.week-grid button[aria-pressed="true"] {
  border-color: var(--brand);
  background: var(--brand);
  color: var(--on-brand);
}

.week-grid button.selected .week-day-name,
.week-grid button.selected .week-day-fee {
  color: color-mix(in oklab, #fff 78%, var(--brand));
}

/* Seven columns inside a 390px phone leaves ~40px a cell. The nav arrows and
   the type shrink so "Aug 11" still fits on one line instead of wrapping into
   an unreadable stack. */
@media (max-width: 30rem) {
  .week-picker {
    gap: 0.3rem;
  }

  .week-nav {
    width: 2rem;
    height: 2rem;
    font-size: 0.85rem;
  }

  .week-grid {
    gap: 0.2rem;
  }

  .week-grid button {
    min-height: 3.2rem;
  }

  .week-day-name {
    font-size: 0.6rem;
  }

  .week-day-date {
    font-size: 0.72rem;
  }
}

.pickup-slots {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
  gap: 0.5rem;
  margin-top: var(--s-3);
}

.pickup-slot {
  padding: 0.6rem 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--paper-raised);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.pickup-slot:hover {
  border-color: var(--brand);
  color: var(--brand-deep);
}

.pickup-slot.selected {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-brand);
  font-weight: 600;
}

/* ==========================================================================
   14. Payment fields
   ========================================================================== */

.checkout-payment {
  border-color: var(--line-brand);
  background: var(--paper-raised);
}

.checkout-payment__heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  margin-bottom: var(--s-4);
}

.checkout-payment__heading h3 {
  margin-bottom: 0;
}

.checkout-payment__step {
  display: block;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brand-deep);
  margin-bottom: 0.3rem;
}

.checkout-payment__secure {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.75rem;
  border-radius: var(--r-pill);
  background: var(--success-tint);
  color: color-mix(in oklab, var(--success) 85%, var(--ink));
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
}

.checkout-payment__intro {
  font-size: 0.92rem;
  color: var(--ink-soft);
  margin-bottom: var(--s-4);
}

.clover-card-fields {
  display: grid;
  gap: var(--s-3);
}

.clover-card-fields__details {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(8rem, 100%), 1fr));
  gap: var(--s-3);
}

.clover-card-fields__label {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink);
}

/* Clover injects a cross-origin iframe here. Its own stylesheet gives the
   iframe a much taller intrinsic height than a text input, so the height is
   pinned rather than left to the iframe — otherwise each card field renders as
   a ~120px box that looks nothing like the inputs above it. Fixing the wrapper
   height also reserves the space before the iframes arrive, so the payment
   section does not jump. !important is required: the values Clover sets are
   inline on the iframe element. */
.clover-field {
  display: flex;
  align-items: center;
  width: 100%;
  height: 3rem;
  padding: 0 0.85rem;
  overflow: hidden;
  background: var(--paper-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.clover-field:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand) 20%, transparent);
}

.clover-field iframe {
  display: block;
  width: 100% !important;
  height: 2.6rem !important;
  min-height: 0;
  border: 0;
}

.clover-card-fields__security-note {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.clover-card-fields__security-note svg {
  flex: none;
  color: var(--success);
}

.clover-payment-error {
  padding: 0.6rem 0.85rem;
  border-radius: var(--r-xs);
  background: var(--danger-tint);
}

.tax-breakdown {
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.tax-breakdown summary {
  cursor: pointer;
}

/* ==========================================================================
   15. Confirmation
   ========================================================================== */

.confirmation {
  width: min(var(--shell-narrow), 100%);
  margin-inline: auto;
  padding: var(--block) var(--gutter) var(--page);
}

.confirmation-card {
  padding: clamp(1.25rem, 2.5vw, 1.9rem);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-md);
}

.card-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.85rem;
  margin-bottom: var(--s-4);
  border-radius: var(--r-pill);
  background: var(--success-tint);
  color: color-mix(in oklab, var(--success) 85%, var(--ink));
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.confirmation-card.is-pending .card-tag {
  background: color-mix(in oklab, #b8791f 15%, var(--paper));
  color: #7a4f0c;
}

.confirmation-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: var(--s-4) var(--s-5);
  padding: var(--s-5) 0;
  margin: var(--s-5) 0;
  border-block: 1px solid var(--line);
}

.confirmation-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}

/* ==========================================================================
   16. Account, subscriptions, recipients
   ========================================================================== */

.account-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  margin-bottom: var(--s-4);
}

.account-header h3 {
  margin-bottom: 0;
}

.account-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-4);
}

.recipient-list {
  display: grid;
  gap: var(--s-3);
}

.recipient-card {
  display: grid;
  gap: 0.5rem;
  padding: var(--s-4);
  background: var(--paper-panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}

.recipient-title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  font-size: 1.02rem;
}

.recipient-badge {
  padding: 0.15rem 0.6rem;
  border-radius: var(--r-pill);
  background: var(--brand-tint-strong);
  color: var(--brand-deep);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.recipient-meta {
  display: grid;
  gap: 0.15rem;
  font-size: 0.9rem;
  color: var(--ink-soft);
}

.recipient-actions {
  display: flex;
  gap: var(--s-4);
  font-size: 0.88rem;
}

/* ==========================================================================
   17. Blog, pages, inquiry forms
   ========================================================================== */

.blog-index,
.blog-show,
.page-show {
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding: var(--block) var(--gutter) var(--page);
}

.blog-show,
.page-show {
  width: min(var(--shell-narrow), 100%);
}

.blog-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(19rem, 100%), 1fr));
  gap: clamp(var(--s-3), 1.6vw, var(--s-4));
}

.blog-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: var(--s-5);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-xs);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.blog-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
}

.blog-card h2 {
  font-size: 1.35rem;
  margin-bottom: 0;
}

.blog-card .card-tag {
  align-self: flex-start;
  margin-bottom: 0;
  background: var(--brand-tint-strong);
  color: var(--brand-deep);
}

.blog-meta {
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0;
}

.blog-excerpt {
  flex: 1;
  font-size: 0.95rem;
  margin: 0;
}

.blog-card .button {
  align-self: flex-start;
  margin-top: var(--s-2);
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.tag {
  padding: 0.2rem 0.65rem;
  border-radius: var(--r-pill);
  background: var(--secondary-tint);
  color: color-mix(in oklab, var(--secondary) 85%, var(--ink));
  font-size: 0.75rem;
  font-weight: 500;
}

.page-body {
  font-size: 1.05rem;
  line-height: 1.7;
}

.visit-page {
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding: var(--block) var(--gutter) var(--page);
}

/* Map first on a phone, where it is the thing being scrolled to; side by side
   from the point two columns stop squeezing the hours into a narrow strip. */
.visit-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--block);
}

@media (min-width: 48rem) {
  .visit-grid {
    grid-template-columns: 1.4fr 1fr;
    align-items: start;
  }
}

.visit-map {
  border-radius: var(--r-md);
  overflow: hidden;
  line-height: 0;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
}

/* The height is set here rather than trusted from the shop's pasted markup: the
   iframe arrives with Google's own width/height attributes, which are whatever
   the embed builder happened to emit. */
.visit-map iframe {
  width: 100%;
  height: clamp(18rem, 42vh, 28rem);
  border: 0;
  display: block;
}

.visit-details {
  display: grid;
  gap: var(--block);
}

.visit-block h2 {
  margin-bottom: var(--s-3);
  font-size: 1.05rem;
}

.visit-block address p {
  margin: 0;
}

.visit-block .button {
  margin-top: var(--s-3);
}

.visit-hours {
  display: grid;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.95rem;
}

.visit-hours li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  margin: 0;
}

/* Same dotted leader as the footer's hours, in ink rather than white — without
   it the two columns read as two unrelated lists. */
.visit-hours li::before {
  content: "";
  order: 1;
  flex: 1;
  height: 0;
  margin-bottom: 0.28em;
  border-bottom: 1px dotted var(--line);
}

.visit-hours li span:last-child {
  order: 2;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.visit-hours li[data-today="true"] {
  font-weight: 600;
  color: var(--ink);
}

.inquiry-form-block {
  margin: var(--s-6) 0;
  padding: clamp(1rem, 2vw, 1.4rem);
  background: var(--paper-panel);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

.inquiry-form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: 0 var(--s-4);
}

.inquiry-form-field {
  margin-bottom: var(--s-4);
}

/* ==========================================================================
   18. Footer
   ========================================================================== */

.storefront-footer {
  margin-top: var(--page);
  padding: var(--page) 0 var(--s-5);
  background: var(--paper-inverse);
  color: color-mix(in oklab, #fff 88%, var(--secondary));
}

.storefront-footer h2,
.storefront-footer h3 {
  color: #fff;
}

.storefront-footer h3 {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin-bottom: var(--s-4);
  opacity: 0.75;
}

.storefront-footer p,
.storefront-footer li,
.storefront-footer a {
  color: color-mix(in oklab, #fff 82%, var(--secondary));
}

.storefront-footer a:hover {
  color: #fff;
  text-decoration: underline;
  text-underline-offset: 0.22em;
}

.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--block);
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.footer-brand h2 {
  font-size: 1.5rem;
  margin-bottom: var(--s-3);
}

.footer-brand p {
  margin-bottom: 0.35rem;
  font-size: 0.92rem;
}

.footer-logo {
  width: 52px;
  height: 52px;
  border-radius: var(--r-sm);
  object-fit: cover;
  margin-bottom: var(--s-3);
  background: rgba(255, 255, 255, 0.1);
}

.footer-hours {
  display: grid;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.9rem;
}

.footer-hours li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  margin: 0;
}

/* A dotted leader ties the day to its hours across the gap — without it the
   two columns read as two unrelated lists on a wide footer. */
.footer-hours li::before {
  content: "";
  order: 1;
  flex: 1;
  height: 0;
  margin-bottom: 0.28em;
  border-bottom: 1px dotted color-mix(in oklab, #fff 28%, transparent);
}

.footer-hours li span:last-child {
  order: 2;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.footer-hours li[data-today="true"] {
  font-weight: 600;
  color: #fff;
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}

.footer-links a {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.9rem;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--r-pill);
  font-size: 0.85rem;
}

.footer-links a:hover {
  background: rgba(255, 255, 255, 0.1);
  text-decoration: none;
}

.footer-seo {
  width: min(var(--shell), 100%);
  margin: var(--s-5) auto 0;
  padding: var(--s-5) var(--gutter) 0;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  font-size: 0.88rem;
  line-height: 1.65;
  opacity: 0.8;
}

.footer-legal {
  width: min(var(--shell), 100%);
  margin: var(--s-5) auto 0;
  padding: var(--s-4) var(--gutter) 0;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  font-size: 0.8rem;
  opacity: 0.7;
}

/* ==========================================================================
   19. Toasts, modals, misc chrome
   ========================================================================== */

.toast-stack {
  position: fixed;
  top: var(--s-4);
  right: var(--s-4);
  z-index: 250;
  display: grid;
  gap: var(--s-2);
  width: min(24rem, calc(100vw - 2 * var(--s-4)));
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: 0.85rem 1rem;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-left: 3px solid var(--ink-decor);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-lg);
  font-size: 0.92rem;
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.toast.show {
  opacity: 1;
  transform: translateY(0);
}

.toast--success {
  border-left-color: var(--success);
}

.toast--error {
  border-left-color: var(--danger);
}

.toast span {
  flex: 1;
}

.toast__close {
  flex: none;
  width: 1.75rem;
  height: 1.75rem;
  border: 0;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--ink-soft);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.toast__close:hover {
  background: var(--paper-sunken);
  color: var(--ink);
}

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 240;
  display: none;
  place-items: center;
  padding: var(--s-4);
  background: color-mix(in oklab, var(--ink) 45%, transparent);
}

/* .is-visible is what cart_controller.js toggles. */
.modal-overlay.is-visible,
.modal-overlay.show,
.modal-overlay[open] {
  display: grid;
}

.modal-card {
  width: min(30rem, 100%);
  padding: clamp(1.1rem, 2.5vw, 1.5rem);
  background: var(--paper-raised);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
}

.modal-title {
  font-family: var(--font-heading);
  font-size: 1.35rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 var(--s-2);
}

.modal-body {
  margin-bottom: var(--s-5);
}

.modal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  justify-content: flex-end;
}

/* --- Delivery check popover ---------------------------------------------
   The first question a flower shopper has ("do you even deliver to my mother
   in law?"), answered before they build a cart. Fixed to the bottom-left corner
   so it never sits under the cart CTA on a phone, and gone from the page once
   it has said its piece.

   Below the toast stack (250) and modal overlay (240), above the sticky header
   (130): a toast about an item added to the cart must still land on top. */
.delivery-check {
  position: fixed;
  left: var(--s-4);
  bottom: var(--s-4);
  z-index: 200;
  width: min(21rem, calc(100vw - 2 * var(--s-4)));
}

.delivery-check-panel {
  position: relative;
  width: 100%;
  padding: var(--s-4);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  animation: delivery-check-in var(--dur) var(--ease) both;
}

@keyframes delivery-check-in {
  from { opacity: 0; transform: translateY(0.5rem); }
  to { opacity: 1; transform: none; }
}

/* Its exit, once the answer has had its four seconds. 300ms here is the
   FADE_DURATION the controller waits before hiding the card outright. */
.delivery-check.is-leaving .delivery-check-panel {
  opacity: 0;
  transform: translateY(0.5rem);
  transition: opacity 300ms var(--ease), transform 300ms var(--ease);
}

.delivery-check-close {
  position: absolute;
  top: 0.35rem;
  right: 0.35rem;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  font-size: 1.35rem;
  line-height: 1;
  color: var(--ink-soft);
  background: none;
  border: 0;
  border-radius: var(--r-pill);
  cursor: pointer;
}

.delivery-check-close:hover {
  color: var(--ink);
  background: var(--paper-sunken);
}

.delivery-check-eyebrow {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brand-deep);
}

.delivery-check-title {
  margin: 0.15rem 0 var(--s-3);
  padding-right: 1.5rem;
  font-family: var(--font-heading);
  font-size: 1.2rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--ink);
}

.delivery-check-form {
  display: flex;
  gap: var(--s-2);
}

.delivery-check-form input {
  flex: 1 1 auto;
  min-width: 0;
}

.delivery-check-form .button {
  flex: none;
  padding-inline: 1.1rem;
}

/* The answer stands where the form was, so the card neither grows nor leaves a
   spent field under the reply it is about to disappear with. */
.delivery-check-result {
  padding: 0.7rem 0.85rem;
  border-radius: var(--r-sm);
  background: var(--paper-panel);
  border-left: 3px solid var(--ink-decor);
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--ink);
}

/* Only while the form is still up — an invalid ZIP is answered above the field
   it needs corrected in, not in place of it. */
.delivery-check-form:not([hidden]) + .delivery-check-result {
  margin-top: var(--s-3);
}

.delivery-check-result.is-same_day,
.delivery-check-result.is-next_available {
  background: var(--success-tint);
  border-left-color: var(--success);
}

.delivery-check-result.is-out_of_area {
  background: var(--brand-tint);
  border-left-color: var(--brand);
}

.delivery-check-result-headline {
  margin: 0 0 0.2rem;
  font-weight: 700;
}

.delivery-check-result-message {
  margin: 0;
  color: var(--ink-muted);
}

/* On a phone the popover spans the gutter and hangs from just under the header
   rather than from the bottom edge. A card sitting at the bottom is exactly
   where the software keyboard opens: tapping the ZIP field made the browser
   shove the page up to keep the field in view, and the whole screen lurched
   under the shopper's thumb. Anchored in the top half, the field is already
   well clear of the keyboard and nothing has to move. */
@media (max-width: 640px) {
  .delivery-check {
    left: var(--s-3);
    right: var(--s-3);
    top: calc(var(--header-h) + var(--s-3));
    bottom: auto;
    width: auto;
  }

  /* Arriving and leaving by the edge it now hangs from. */
  .delivery-check-panel {
    animation-name: delivery-check-in-top;
  }

  .delivery-check.is-leaving .delivery-check-panel {
    transform: translateY(-0.5rem);
  }

  /* Under the header is also exactly where the open nav drawer keeps its
     links, and the popover outranks it (200 against the drawer's 120), so it
     floated above the scrim and swallowed the taps aimed at the top of the
     menu. The drawer is a modal surface for as long as it is open; the card
     waits its turn and comes back when the menu closes. */
  .storefront-body.nav-open .delivery-check {
    display: none;
  }
}

@keyframes delivery-check-in-top {
  from { opacity: 0; transform: translateY(-0.5rem); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .delivery-check-panel,
  .delivery-check.is-leaving .delivery-check-panel {
    animation: none;
    transition: none;
  }
}

/* Impersonation works in production, so the banner is loud, sticky, and
   follows the admin onto every screen until they explicitly stop. Duplicated
   from application.css because the storefront no longer loads that file. */
.impersonation-banner {
  position: sticky;
  top: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1rem;
  flex-wrap: wrap;
  background: #8f3a22;
  color: #fff;
  padding: 0.6rem 1rem;
}

.impersonation-banner__label {
  font-weight: 600;
  line-height: 1.35;
}

.impersonation-banner form {
  margin: 0;
}

.impersonation-banner__stop {
  background: #fff;
  color: #8f3a22;
  border: none;
  border-radius: 999px;
  padding: 0.4rem 1.1rem;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
}

.impersonation-banner__stop:hover,
.impersonation-banner__stop:focus {
  background: #ffe9e0;
}

/* ==========================================================================
   20. Rich text (Action Text bodies authored in the admin)
   ==========================================================================
   actiontext.css is deliberately NOT loaded here. Nine tenths of it styles the
   Trix *editor*, which no shopper ever sees, and its `.trix-content * { margin:
   0; padding: 0 }` reset would flatten every paragraph and list in an authored
   page body. These are the display-side rules a rendered Action Text body
   actually needs. */

.trix-content {
  line-height: 1.7;
  overflow-wrap: break-word;
}

.trix-content h1,
.trix-content h2,
.trix-content h3 {
  margin-top: var(--s-6);
}

.trix-content > :first-child {
  margin-top: 0;
}

.trix-content a {
  color: var(--brand-deep);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.trix-content img,
.trix-content figure {
  border-radius: var(--r-sm);
  margin-block: var(--s-5);
}

.trix-content blockquote {
  margin: var(--s-5) 0;
  padding-left: var(--s-4);
  border-left: 3px solid var(--line-brand);
  color: var(--ink-muted);
  font-style: italic;
}

.trix-content pre {
  display: block;
  padding: var(--s-4);
  border-radius: var(--r-sm);
  background: var(--paper-sunken);
  font-size: 0.9em;
  overflow-x: auto;
}

.trix-content .attachment {
  display: inline-block;
  position: relative;
  max-width: 100%;
}

.trix-content .attachment a {
  color: inherit;
  text-decoration: none;
}

.trix-content .attachment__caption {
  text-align: center;
  font-size: 0.85em;
  color: var(--ink-soft);
}

.trix-content .attachment__caption .attachment__name + .attachment__size::before {
  content: " • ";
}

.trix-content .attachment--preview {
  width: 100%;
  text-align: center;
}

.trix-content .attachment--file {
  display: inline-flex;
  gap: 0.4em;
  padding: 0.4em 1em;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  line-height: 1.3;
}

.trix-content .attachment-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
  gap: var(--s-3);
  margin-block: var(--s-5);
}

.trix-content .attachment-gallery > action-text-attachment,
.trix-content .attachment-gallery > .attachment {
  max-width: 100%;
  margin: 0;
}

.trix-content action-text-attachment .attachment {
  padding: 0;
  max-width: 100%;
}

/* ==========================================================================
   21. Page builder sections (section-based pages authored in the admin)
   ==========================================================================
   A sectioned page skips the narrow .page-show shell entirely: .page-sections
   is full width and every section contains its own copy via
   .page-section-inner, so full-bleed media and edge-to-edge background bands
   need no 100vw tricks. */

.page-sections {
  padding-bottom: var(--page);
}

.page-sections-header {
  padding-top: var(--block);
}

.page-section {
  padding-block: var(--block);
}

.page-section.is-spacing-spacious {
  padding-block: var(--page);
}

.page-section-inner {
  width: min(var(--shell-narrow), 100%);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.page-section.is-align-center {
  text-align: center;
}

/* A centered list otherwise strands its bullets at the far left of the
   column while the text sits in the middle. */
.page-section.is-align-center .trix-content ul,
.page-section.is-align-center .trix-content ol {
  list-style-position: inside;
  padding-left: 0;
}

/* Background tones always span the full viewport width so a dark or tinted
   section reads as a band, whatever the media width. */
.page-section.is-bg-brand {
  background: var(--brand-tint);
}

.page-section.is-bg-dark {
  background: var(--paper-inverse);
  color: color-mix(in oklab, #fff 88%, var(--secondary));
}

.page-section.is-bg-dark h1,
.page-section.is-bg-dark h2 {
  color: #fff;
}

.page-section.is-bg-dark .page-section-subheading,
.page-section.is-bg-dark .trix-content {
  color: color-mix(in oklab, #fff 80%, var(--secondary));
}

.page-section-head {
  margin-bottom: var(--s-5);
}

.page-section-head h2 {
  margin: 0;
}

.page-section-subheading {
  margin-top: var(--s-2);
  color: var(--ink-muted);
}

/* --- Media width: contained media sits in the wide shell; is-width-full
   spans edge to edge. Copy stays in the narrow shell either way. ---------- */
.page-slideshow,
.page-gallery,
.page-video {
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.page-section.is-width-full .page-slideshow,
.page-section.is-width-full .page-gallery,
.page-section.is-width-full .page-video {
  width: 100%;
  padding-inline: 0;
}

/* --- Hero ---------------------------------------------------------------- */
.page-section--hero {
  display: grid;
  padding-block: 0;
}

.page-section--hero.is-width-contained {
  width: min(var(--shell), 100%);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: var(--block);
}

.page-hero-media,
.page-section--hero .page-hero-copy {
  grid-area: 1 / 1;
}

.page-hero-media {
  position: relative;
  min-height: clamp(320px, 55vh, 560px);
}

.page-hero-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.page-section--hero.is-width-contained .page-hero-media,
.page-section--hero.is-width-contained .page-hero-media img {
  border-radius: var(--r-lg);
}

/* Legibility scrim over the photo; copy on it is always light. */
.page-hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(to bottom, rgba(20, 17, 14, 0.2), rgba(20, 17, 14, 0.5));
}

.page-section--hero .page-hero-copy {
  align-self: center;
  position: relative;
  padding-block: var(--page);
}

.page-hero-media ~ .page-hero-copy,
.page-hero-media ~ .page-hero-copy h1 {
  color: #fff;
}

.page-section--hero h1 {
  margin: 0;
}

.page-hero-subheading {
  margin-top: var(--s-3);
  font-size: 1.1rem;
}

.page-hero-media ~ .page-hero-copy .page-hero-subheading {
  color: rgba(255, 255, 255, 0.88);
}

/* --- Slideshow ----------------------------------------------------------- */
.page-slideshow {
  position: relative;
}

.page-slideshow-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
  border-radius: var(--r-md);
}

.page-slideshow-track::-webkit-scrollbar {
  display: none;
}

.page-section.is-width-full .page-slideshow-track {
  border-radius: 0;
}

.page-slideshow-slide {
  flex: 0 0 100%;
  margin: 0;
  scroll-snap-align: start;
}

.page-slideshow-slide img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

@media (max-width: 640px) {
  .page-slideshow-slide img {
    aspect-ratio: 4 / 5;
  }
}

/* Arrows and dots appear once the Stimulus controller connects; without JS
   the slideshow is a plain swipeable scroll-snap strip. */
.page-slideshow-arrow,
.page-slideshow-dots {
  display: none;
}

.page-slideshow.is-ready .page-slideshow-arrow {
  display: grid;
  place-items: center;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 2.6rem;
  height: 2.6rem;
  border: 0;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.85);
  color: #1f1d1a;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}

.page-slideshow.is-ready .page-slideshow-arrow.is-prev {
  left: calc(var(--gutter) + var(--s-3));
}

.page-slideshow.is-ready .page-slideshow-arrow.is-next {
  right: calc(var(--gutter) + var(--s-3));
}

.page-section.is-width-full .page-slideshow.is-ready .page-slideshow-arrow.is-prev {
  left: var(--s-3);
}

.page-section.is-width-full .page-slideshow.is-ready .page-slideshow-arrow.is-next {
  right: var(--s-3);
}

.page-slideshow.is-ready .page-slideshow-dots {
  display: flex;
  justify-content: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
}

.page-slideshow-dot {
  width: 0.55rem;
  height: 0.55rem;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--ink-decor);
  cursor: pointer;
}

.page-slideshow-dot.is-active {
  background: var(--brand);
}

/* --- Gallery ------------------------------------------------------------- */
.page-gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s-3);
}

.page-gallery.is-layout-grid_2 {
  grid-template-columns: repeat(2, 1fr);
}

@media (max-width: 720px) {
  .page-gallery,
  .page-gallery.is-layout-grid_2 {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--s-2);
  }
}

.page-gallery-tile {
  margin: 0;
}

.page-gallery-tile img {
  display: block;
  width: 100%;
  aspect-ratio: 5 / 6;
  object-fit: cover;
  border-radius: var(--r-sm);
}

.page-section.is-width-full .page-gallery {
  gap: var(--s-1);
}

.page-section.is-width-full .page-gallery-tile img {
  border-radius: 0;
}

/* --- Video --------------------------------------------------------------- */
.page-video video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-md);
  background: #000;
}

.page-section.is-width-full .page-video video {
  border-radius: 0;
}

/* --- CTA ----------------------------------------------------------------- */
.page-cta h2 {
  margin: 0;
}

.page-cta-action {
  margin-top: var(--s-4);
}

/* ==========================================================================
   22. Motion & print
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .storefront-body *,
  .storefront-body *::before,
  .storefront-body *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .home-product-card:hover,
  .category-card:hover,
  .subcategory-card:hover,
  .blog-card:hover {
    transform: none;
  }

  .home-product-card:hover .home-product-photo {
    transform: none;
  }
}

@media print {
  .site-header,
  .storefront-footer,
  .toast-stack,
  .nav-scrim,
  .skip-link,
  .confirmation-actions,
  .report-problem-button {
    display: none !important;
  }

  .storefront-body {
    background: #fff;
    color: #000;
  }

  .confirmation-card,
  .summary-card {
    border: 1px solid #999;
    box-shadow: none;
  }
}
