/*
 * This is a manifest file that'll be compiled into application.css.
 *
 * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
 * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
 * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
 * depending on specificity.
 *
 * Consider organizing styles into separate files for maintainability.
 */

body {
  /* ...existing code... */
}

/* ── Semantic tokens: static per-context defaults ──────
   Two tokens, both declared with indigo defaults in the @theme block
   (app/assets/tailwind/application.css):
     --color-primary-* = IDENTITY/chrome (navbar, sidebar, brand surfaces)
     --color-accent-*  = ACTIONS (buttons, links, form focus, badges)
   [data-context] is stamped on <body> by each layout (merchant / shipper /
   store / courier). Merchant and store need no static block (indigo
   defaults; runtime brand tags override). Shipper and courier get static
   blocks that re-point the tokens to their literal palettes.

   SPECIFICITY / INHERITANCE TRAP — read before touching:
   These blocks match <body> ([data-context], specificity 0,1,0). A custom
   property declared ON body always beats one inherited from :root, no
   matter the :root rule's position. Runtime brand style tags therefore
   MUST NOT emit their overrides at bare :root only — a :root-only
   `--color-accent-600: <brand>` would lose to the static
   `--color-accent-600: var(--color-teal-600)` below on the whole body
   subtree. The brand tag helpers in app/helpers/brand_colors_helper.rb
   emit under `:root, body[data-context]` — the body[data-context]
   selector (0,1,1) out-specifies these blocks, and the inline tags are
   unlayered so they also beat Tailwind's @layer theme.

   Shipper: primary = teal (identity/chrome), accent = teal (actions,
   runtime brand tag overrides). Both default to the same teal ramp so an
   unbranded shipper looks identical to before the primary/accent split;
   a branded shipper's runtime tag overrides only accent (actions) unless
   it also supplies a primary/chrome color. */
[data-context="shipper"] {
  --color-accent-50:  var(--color-teal-50);
  --color-accent-100: var(--color-teal-100);
  --color-accent-200: var(--color-teal-200);
  --color-accent-300: var(--color-teal-300);
  --color-accent-400: var(--color-teal-400);
  --color-accent-500: var(--color-teal-500);
  --color-accent-600: var(--color-teal-600);
  --color-accent-700: var(--color-teal-700);
  --color-accent-800: var(--color-teal-800);
  --color-accent-900: var(--color-teal-900);
  --color-accent-950: var(--color-teal-950);
  --color-accent-rgb: 0, 127, 67; /* teal-600 oklch(52% 0.14 155) */
  --color-accent-400-fg: var(--color-teal-400-fg, var(--color-teal-400));
  --color-accent-500-fg: var(--color-teal-500-fg, var(--color-teal-500));
  --color-accent-600-fg: var(--color-teal-600-fg, var(--color-teal-600));
  --color-accent-700-fg: var(--color-teal-700-fg, var(--color-teal-700));
  --color-accent-800-fg: var(--color-teal-800-fg, var(--color-teal-800));
  --color-accent-900-fg: var(--color-teal-900-fg, var(--color-teal-900));
  --color-accent-600-text: var(--color-teal-600-text, #fff);
  --color-primary-50:  var(--color-teal-50);
  --color-primary-100: var(--color-teal-100);
  --color-primary-200: var(--color-teal-200);
  --color-primary-300: var(--color-teal-300);
  --color-primary-400: var(--color-teal-400);
  --color-primary-500: var(--color-teal-500);
  --color-primary-600: var(--color-teal-600);
  --color-primary-700: var(--color-teal-700);
  --color-primary-800: var(--color-teal-800);
  --color-primary-900: var(--color-teal-900);
  --color-primary-950: var(--color-teal-950);
  --color-primary-rgb: 0, 127, 67; /* teal-600 oklch(52% 0.14 155) */
  --color-primary-400-fg: var(--color-teal-400-fg, var(--color-teal-400));
  --color-primary-500-fg: var(--color-teal-500-fg, var(--color-teal-500));
  --color-primary-600-fg: var(--color-teal-600-fg, var(--color-teal-600));
  --color-primary-700-fg: var(--color-teal-700-fg, var(--color-teal-700));
  --color-primary-800-fg: var(--color-teal-800-fg, var(--color-teal-800));
  --color-primary-900-fg: var(--color-teal-900-fg, var(--color-teal-900));
  --color-primary-600-text: var(--color-teal-600-text, #fff);
}

/* Courier: amber identity chrome, teal (shipper-accent) actions. The
   courier layout stamps data-context="courier"; the runtime
   courier_semantic_brand_style_tag overrides these per shipper. */
[data-context="courier"] {
  --color-primary-50:  var(--color-amber-50);
  --color-primary-100: var(--color-amber-100);
  --color-primary-200: var(--color-amber-200);
  --color-primary-300: var(--color-amber-300);
  --color-primary-400: var(--color-amber-400);
  --color-primary-500: var(--color-amber-500);
  --color-primary-600: var(--color-amber-600);
  --color-primary-700: var(--color-amber-700);
  --color-primary-800: var(--color-amber-800);
  --color-primary-900: var(--color-amber-900);
  --color-primary-950: var(--color-amber-950);
  --color-primary-rgb: 205, 120, 0; /* amber-600 oklch(65% 0.16 68) */
  --color-primary-400-fg: var(--color-amber-400-fg, var(--color-amber-400));
  --color-primary-500-fg: var(--color-amber-500-fg, var(--color-amber-500));
  --color-primary-600-fg: var(--color-amber-600-fg, var(--color-amber-600));
  --color-primary-700-fg: var(--color-amber-700-fg, var(--color-amber-700));
  --color-primary-800-fg: var(--color-amber-800-fg, var(--color-amber-800));
  --color-primary-900-fg: var(--color-amber-900-fg, var(--color-amber-900));
  --color-primary-600-text: var(--color-amber-600-text, #fff);
  --color-accent-50:  var(--color-teal-50);
  --color-accent-100: var(--color-teal-100);
  --color-accent-200: var(--color-teal-200);
  --color-accent-300: var(--color-teal-300);
  --color-accent-400: var(--color-teal-400);
  --color-accent-500: var(--color-teal-500);
  --color-accent-600: var(--color-teal-600);
  --color-accent-700: var(--color-teal-700);
  --color-accent-800: var(--color-teal-800);
  --color-accent-900: var(--color-teal-900);
  --color-accent-950: var(--color-teal-950);
  --color-accent-rgb: 0, 127, 67; /* teal-600 */
  --color-accent-400-fg: var(--color-teal-400-fg, var(--color-teal-400));
  --color-accent-500-fg: var(--color-teal-500-fg, var(--color-teal-500));
  --color-accent-600-fg: var(--color-teal-600-fg, var(--color-teal-600));
  --color-accent-700-fg: var(--color-teal-700-fg, var(--color-teal-700));
  --color-accent-800-fg: var(--color-teal-800-fg, var(--color-teal-800));
  --color-accent-900-fg: var(--color-teal-900-fg, var(--color-teal-900));
  --color-accent-600-text: var(--color-teal-600-text, #fff);
}

/* Touch-friendly targets for mobile */
.touch-target {
  min-width: 44px;
  min-height: 44px;
}

/* Hide scrollbar while allowing scroll */
.scrollbar-hide {
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
  display: none;
}

/* Required field indicators — auto-appends red asterisk to labels
   associated with required inputs, selects, and textareas */
label:has(~ input[required])::after,
label:has(~ textarea[required])::after,
label:has(~ select[required])::after,
label:has(~ .relative select[required])::after,
label:has(~ .iti input[required])::after {
  content: " *";
  color: oklch(0.637 0.237 25.331); /* red-500 */
}

@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: fade 0.3s ease both;
}

@keyframes fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes float {
  0%,
  100% {
    transform: translateY(0px) rotate(0deg);
  }
  33% {
    transform: translateY(-10px) rotate(1deg);
  }
  66% {
    transform: translateY(5px) rotate(-1deg);
  }
}

@keyframes float-reverse {
  0%,
  100% {
    transform: translateY(0px) rotate(0deg);
  }
  33% {
    transform: translateY(10px) rotate(-1deg);
  }
  66% {
    transform: translateY(-5px) rotate(1deg);
  }
}

@keyframes float-slow {
  0%,
  100% {
    transform: translateY(0px) scale(1);
  }
  50% {
    transform: translateY(-20px) scale(1.05);
  }
}

@keyframes spin-slow {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@keyframes bounce-slow {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

@keyframes pulse-slow {
  0%,
  100% {
    opacity: 0.3;
    transform: scale(1);
  }
  50% {
    opacity: 0.6;
    transform: scale(1.1);
  }
}

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes shimmer {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(100%);
  }
}

@keyframes gradient-x {
  0%,
  100% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
}

@keyframes count-up {
  from {
    opacity: 0;
    transform: scale(0.5);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* The only confirmation an instantly-saving switch gives the eye: the row it
   sits in flashes and fades. shared/_toggle_switch is the one consumer, and ten
   surfaces render that partial.

   Defined unconditionally, with the MOTION scoped by the query rather than the
   whole rule. It used to sit entirely inside `prefers-reduced-motion:
   no-preference`, so a merchant who asks for reduced motion got no keyframes at
   all, the animation resolved to nothing, and the save was silent. Reduced
   motion means "do not move things", not "tell me less". */
@keyframes highlight-saved {
  0% {
    background-color: rgb(199 210 254 / 0.7);
  }
  100% {
    background-color: transparent;
  }
}

@media (prefers-reduced-motion: reduce) {
  @keyframes highlight-saved {
    /* A step rather than a fade: the tint holds, then goes. Same information,
       nothing easing. */
    0%,
    70% {
      background-color: rgb(199 210 254 / 0.7);
    }
    71%,
    100% {
      background-color: transparent;
    }
  }
}

.animate-float {
  animation: float 6s ease-in-out infinite;
}
.animate-float-reverse {
  animation: float-reverse 8s ease-in-out infinite;
}
.animate-float-slow {
  animation: float-slow 10s ease-in-out infinite;
}
.animate-spin-slow {
  animation: spin-slow 20s linear infinite;
}
.animate-bounce-slow {
  animation: bounce-slow 4s ease-in-out infinite;
}
.animate-pulse-slow {
  animation: pulse-slow 4s ease-in-out infinite;
}
.animate-fade-in-up {
  /* `both` lets the keyframe supply the opacity:0 start (incl. during
     animation-delay) without statically hiding the element. Renderers that
     don't run animations (crawlers, some link-preview/headless captures) keep
     the default opacity:1 and show the content instead of a blank hero. */
  animation: fade-in-up 0.8s ease-out both;
}
.animate-shimmer {
  animation: shimmer 2s ease-in-out infinite;
}
.animate-gradient-x {
  background-size: 200% 200%;
  animation: gradient-x 3s ease infinite;
}
.animate-count-up {
  animation: count-up 1s ease-out 0.8s forwards;
  opacity: 0;
}

/* ── Dialog fade-in ── */
@keyframes fade-in {
  from { opacity: 0; transform: scale(0.95); }
  to   { opacity: 1; transform: scale(1); }
}
.animate-fade-in {
  animation: fade-in 0.15s ease-out;
}

@keyframes fade-out {
  0%   { opacity: 1; }
  70%  { opacity: 1; }
  100% { opacity: 0; }
}
.animate-fade-out {
  animation: fade-out 3s ease-out forwards;
}

/* ── Turbo Stream panel highlight ── */
@media (prefers-reduced-motion: no-preference) {
  @keyframes highlight-panel {
    0%   { background-color: rgb(199 210 254 / 0.4); }
    100% { background-color: transparent; }
  }
  .animate-highlight {
    animation: highlight-panel 1.5s ease-out;
  }
}

/* ── Disabled-until-connect JS controls ──
   JS-only buttons (searchable-select toggles, smart-slot chips/toggles) render
   disabled until their Stimulus controller is listening, so a pre-boot tap
   can't silently no-op. Only show the disabled look when boot is actually
   slow (>300ms) — fast connections never see a flash. */
@keyframes enable-on-connect-pending {
  to { opacity: 0.5; }
}
[data-enable-on-connect][disabled] {
  animation: enable-on-connect-pending 1ms linear 300ms forwards;
}

/* ── Searchable-select bottom-sheet (mobile) ── */
@keyframes ss-slide-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
@keyframes ss-slide-down {
  from { transform: translateY(0); }
  to   { transform: translateY(100%); }
}

.ss-sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9998;
  background: rgb(107 114 128 / 0.75);
  opacity: 0;
  transition: opacity 300ms ease-in-out;
}

.ss-sheet-backdrop--visible {
  opacity: 1;
}

.ss-sheet {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 9999;
  max-height: 85vh;
  background: white;
  border-radius: 0.75rem 0.75rem 0 0;
  box-shadow: 0 -4px 24px rgb(0 0 0 / 0.12);
  display: flex;
  flex-direction: column;
  animation: ss-slide-up 300ms ease-out forwards;
  will-change: transform;
  padding-bottom: env(safe-area-inset-bottom);
}

.ss-sheet--closing {
  animation: ss-slide-down 300ms ease-in forwards;
}

.ss-sheet-handle {
  display: flex;
  justify-content: center;
  padding: 0.5rem 0 0.25rem;
  flex-shrink: 0;
}

.ss-sheet-handle::after {
  content: "";
  width: 2rem;
  height: 0.25rem;
  border-radius: 9999px;
  background: #d1d5db;
}

.ss-sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 1rem 0.75rem;
  border-bottom: 1px solid #e5e7eb;
  flex-shrink: 0;
}

.ss-sheet-header h3 {
  font-size: 1rem;
  font-weight: 600;
  color: #111827;
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.ss-sheet-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 9999px;
  color: #6b7280;
  background: transparent;
  border: none;
  cursor: pointer;
}

.ss-sheet-close:hover {
  background: #f3f4f6;
  color: #374151;
}

.ss-sheet-search {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid #e5e7eb;
  flex-shrink: 0;
}

.ss-sheet-body {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.ss-sheet-body > button,
.ss-sheet-body > [role="option"] {
  min-height: 2.75rem;
  border-bottom: 1px solid #f3f4f6;
}

.ss-sheet-body > button:last-child,
.ss-sheet-body > [role="option"]:last-child {
  border-bottom: none;
}

.ss-sheet-body > button > span,
.ss-sheet-body > [role="option"] > span {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: normal;
}

.ss-sheet-body > button:active,
.ss-sheet-body > [role="option"]:active {
  background: #f3f4f6;
}

/* Color stripe variants — overflow visible so border-top isn't clipped by radius */
.ss-sheet--indigo { border-top: 3px solid #6366f1; }
.ss-sheet--teal { border-top: 3px solid #14b8a6; }
.ss-sheet--accent { border-top: 3px solid var(--color-accent-600, #6366f1); }

/* Sheet swipe tracking */
.ss-sheet--dragging {
  animation: none;
  transition: none;
}

/* Hero entrance animations */
@keyframes slide-in-left {
  from {
    opacity: 0;
    transform: translateX(-40px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes scale-in {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.animate-slide-in-left {
  animation: slide-in-left 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  opacity: 0;
}

.animate-scale-in {
  animation: scale-in 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  opacity: 0;
}

/* Shared inline spinner for buttons and loading states */
.btn-spinner {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 9999px;
  animation: spin 1s linear infinite;
  margin-right: 0.5rem;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* Dot grid decoration — warm tinted */
.dot-grid {
  background-image: radial-gradient(
    circle,
    oklch(80% 0.06 55) 1px,
    transparent 1px
  );
  background-size: 20px 20px;
}

/* Additional advanced animations */
@keyframes gradient-shift {
  0%,
  100% {
    background-position: 0% 50%;
    transform: scale(1);
  }
  50% {
    background-position: 100% 50%;
    transform: scale(1.02);
  }
}

.animate-gradient-shift {
  background-size: 200% 200%;
  animation: gradient-shift 8s ease-in-out infinite;
}

/* Hover shadow enhancements */
.hover\:shadow-4xl:hover {
  box-shadow:
    0 25px 50px -12px rgba(0, 0, 0, 0.25),
    0 0 0 1px rgba(255, 255, 255, 0.05);
}

/* Performance: only promote actively animating elements to compositor */
.animate-fade-in-up {
  will-change: auto;
}

/* Smooth transitions for all interactive elements */
.group:hover .transition-all {
  transition-duration: 0.3s;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* ----------------------------------------------------------------
   Disclosure expand/collapse animation
   ---------------------------------------------------------------- */
.disclosure-content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.2s ease-out;
}
.disclosure-content.disclosure-open {
  overflow: visible;
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
  .disclosure-content {
    transition: none;
  }
}

/* ----------------------------------------------------------------
   Form micro-interactions
   ---------------------------------------------------------------- */

/* Gentle shake for validation errors (2-frame, subtle) */
@keyframes form-shake {
  0%,
  100% {
    transform: translateX(0);
  }
  25% {
    transform: translateX(-4px);
  }
  75% {
    transform: translateX(4px);
  }
}

.form-error-shake {
  animation: form-shake 0.3s ease-out;
}

/* Rails wraps errored fields in .field_with_errors — style without breaking layout */
.field_with_errors {
  display: contents;
}
.field_with_errors > input,
.field_with_errors > select,
.field_with_errors > textarea {
  border-color: #f14e46 !important;
  box-shadow: 0 0 0 3px rgba(241, 78, 70, 0.15) !important;
  animation: form-shake 0.3s ease-out;
}

/* form_select_wrapper overlays the chevron with `absolute inset-y-0 … items-center`,
   i.e. centred on everything the wrapper contains. That is the select alone until a
   field is errored: .field_with_errors is display:contents, so field_error_proc's
   message becomes a child of the same wrapper and the chevron re-centres over
   select + message. It stays inside the box for a one-line message and crosses out
   over the text once the message wraps — which is the 375px case, on the very screen
   telling the merchant to fix that control.

   Anchor it to the top instead, and centre it on the select's FIRST line box. The
   offset is derived from the select's own metrics, not measured: border (1px) + its
   py-3 (0.75rem) + half the leading (1.5rem line box, 1em glyph → 0.25rem). No total
   height is assumed, so this holds however long the message runs.

   :has() is progressive — where it is unsupported the rule simply does not apply and
   the chevron behaves exactly as it does today. */
.form-select-wrapper:has(.field_with_errors) > .form-select-chevron {
  align-items: flex-start;
  padding-top: calc(1rem + 1px);
}

/* ── Tinted neutrals ─────────────────────────────────
   Subtle warm-gray backgrounds further tinted toward the area's brand color.
   Since the base gray palette is now warm (hue 55), these add
   a slight brand-hue shift on top.
   Merchant = indigo tint, Shipper = teal tint.
   ------------------------------------------------------------------ */
.admin-tint-50--indigo {
  background-color: oklch(97.5% 0.012 260);
} /* warm gray + bazin blue */
.admin-tint-100--indigo {
  background-color: oklch(95.5% 0.01 260);
}
.admin-tint-50--teal {
  background-color: oklch(97.5% 0.01 175);
} /* warm gray + teal   */
.admin-tint-100--teal {
  background-color: oklch(95.5% 0.008 175);
}

/* ------------------------------------------------------------------
   Legacy-shaped component button kept because JS toggles it at runtime
   (variant_selector_controller.js, frame_fallback_controller.js) and its
   unlayered specificity over Tailwind utilities is load-bearing. It now
   reads the semantic accent token variables, which the runtime brand
   style tags override per merchant/shipper.
   ------------------------------------------------------------------ */
.brand-accent-button {
  background-color: var(--color-accent-600, #0d53bd);
  border-color: var(--color-accent-700, #03409c);
  /* -text picks white/dark against the branded 600 shade; falls back to
     white for the static indigo/teal defaults. */
  color: var(--color-accent-600-text, #fff);
  transition: all 0.2s ease;
}

.brand-accent-button:hover {
  filter: brightness(0.9);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(var(--color-accent-rgb, 13, 83, 189), 0.3);
}

.brand-accent-button:active {
  filter: brightness(0.85);
  transform: translateY(0);
  box-shadow: none;
}

.brand-accent-button:disabled {
  filter: brightness(1);
  opacity: 0.5;
  transform: none;
  box-shadow: none;
}

/* Semantic-token button text — adaptive text on bg-{token}-600 buttons.
   Reads the runtime -text variable (white/dark chosen against the branded
   600 shade), falling back to white for the static indigo/teal/amber
   defaults, which all carry white text. Used by the
   "primary"/"accent" entries of PRIMARY_BUTTON_COLORS. */
.token-button-text-primary {
  color: var(--color-primary-600-text, #fff);
}
.token-button-text-accent {
  color: var(--color-accent-600-text, #fff);
}

/* ── Foreground-safe text overrides ────────────────────
   Override Tailwind's text-{token}-{shade} utilities for the semantic
   tokens (primary, accent) to use -fg CSS variables when present.
   Falls back to the raw shade when no brand override is active.
   This ensures text on white/light backgrounds stays readable even when
   a shipper/merchant picks a very light brand color. */

/* Base text overrides — shades 400–900 */
.text-primary-400 {
  color: var(--color-primary-400-fg, var(--color-primary-400));
}
.text-primary-500 {
  color: var(--color-primary-500-fg, var(--color-primary-500));
}
.text-primary-600 {
  color: var(--color-primary-600-fg, var(--color-primary-600));
}
.text-primary-700 {
  color: var(--color-primary-700-fg, var(--color-primary-700));
}
.text-primary-800 {
  color: var(--color-primary-800-fg, var(--color-primary-800));
}
.text-primary-900 {
  color: var(--color-primary-900-fg, var(--color-primary-900));
}
.text-accent-400 {
  color: var(--color-accent-400-fg, var(--color-accent-400));
}
.text-accent-500 {
  color: var(--color-accent-500-fg, var(--color-accent-500));
}
.text-accent-600 {
  color: var(--color-accent-600-fg, var(--color-accent-600));
}
.text-accent-700 {
  color: var(--color-accent-700-fg, var(--color-accent-700));
}
.text-accent-800 {
  color: var(--color-accent-800-fg, var(--color-accent-800));
}
.text-accent-900 {
  color: var(--color-accent-900-fg, var(--color-accent-900));
}

/* Border overrides — same approach for borders that need 3:1 contrast on white.
   Only overrides shades 400–900 where -fg variables exist. */
.border-accent-500 {
  border-color: var(--color-accent-500-fg, var(--color-accent-500));
}
.border-accent-700 {
  border-color: var(--color-accent-700-fg, var(--color-accent-700));
}

/* Hover text overrides */
.hover\:text-accent-500:hover {
  color: var(--color-accent-500-fg, var(--color-accent-500));
}
.hover\:text-accent-600:hover {
  color: var(--color-accent-600-fg, var(--color-accent-600));
}
.hover\:text-accent-700:hover {
  color: var(--color-accent-700-fg, var(--color-accent-700));
}
.hover\:text-accent-800:hover {
  color: var(--color-accent-800-fg, var(--color-accent-800));
}

/* Group-hover text overrides */
.group:hover .group-hover\:text-accent-600 {
  color: var(--color-accent-600-fg, var(--color-accent-600));
}
.group:hover .group-hover\:text-accent-700 {
  color: var(--color-accent-700-fg, var(--color-accent-700));
}

/* Hover border overrides */
.hover\:border-accent-500:hover {
  border-color: var(--color-accent-500-fg, var(--color-accent-500));
}

/* Focus border overrides */
.focus\:border-accent-400:focus {
  border-color: var(--color-accent-400-fg, var(--color-accent-400));
}

/* Navbar overlay — adapts hover/active backgrounds on brand-colored navbars.
   Uses --navbar-overlay (rgb triplet) set inline on the <header>. */
.navbar-overlay-hover:hover {
  background-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.1);
}
.navbar-overlay-active {
  background-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.15);
}
.navbar-overlay-border {
  border-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.1);
}

/* Navbar search — adapts to light/dark navbar backgrounds */
.navbar-search {
  background-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.15);
  border-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.3);
  color: inherit;
}
.navbar-search::placeholder {
  color: rgba(var(--navbar-overlay, 255, 255, 255), 0.6);
}
.navbar-search:hover {
  background-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.2);
}
.navbar-search:focus {
  background-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.25);
  border-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.6);
  box-shadow:
    0 0 0 2px rgba(var(--navbar-overlay, 255, 255, 255), 0.3),
    0 10px 15px -3px rgba(0, 0, 0, 0.1);
  outline: none;
}
.navbar-search-icon {
  color: rgba(var(--navbar-overlay, 255, 255, 255), 0.6);
}
.navbar-search-icon-focus {
  color: inherit;
}

/* Mobile search pill — compact inline input */
.navbar-search-pill {
  background-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.12);
  border: 1px solid transparent;
  color: inherit;
  outline: none;
  transition:
    background-color 0.15s,
    border-color 0.15s,
    box-shadow 0.15s;
}
.navbar-search-pill::placeholder {
  color: rgba(var(--navbar-overlay, 255, 255, 255), 0.55);
}
.navbar-search-pill:hover {
  background-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.18);
}
.navbar-search-pill:focus {
  background-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.28);
  border-color: rgba(var(--navbar-overlay, 255, 255, 255), 0.45);
  box-shadow: 0 0 0 2px rgba(var(--navbar-overlay, 255, 255, 255), 0.25);
  outline: none;
}
.navbar-search-pill:focus::placeholder {
  color: rgba(var(--navbar-overlay, 255, 255, 255), 0.45);
}

/* Peer-checked pill cards (personalization configurator) */
.peer:checked ~ .peer-checked\:font-medium {
  font-weight: 500;
}

/* ----------------------------------------------------------------
   Add-to-cart button pulse animation
   ---------------------------------------------------------------- */
@keyframes cart-pulse {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(0.95);
  }
  100% {
    transform: scale(1);
  }
}

.cart-added-feedback {
  animation: cart-pulse 0.3s ease-out;
}

/* ----------------------------------------------------------------
   Cart count badge bounce animation
   ---------------------------------------------------------------- */
@keyframes cart-count-bounce {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.4);
  }
  100% {
    transform: scale(1);
  }
}

.cart-count-bounce {
  animation: cart-count-bounce 0.4s ease-out;
}

/* ----------------------------------------------------------------
   Price crossfade transition
   ---------------------------------------------------------------- */
.price-transition {
  transition: opacity 0.2s ease;
}

@keyframes price-pulse {
  0% { background-color: transparent; }
  20% { background-color: rgb(237, 240, 255); background-color: oklch(0.95 0.02 250); }
  100% { background-color: transparent; }
}

.price-changed-pulse {
  animation: price-pulse 0.8s ease-out;
  border-radius: 0.25rem;
}

@media (prefers-reduced-motion: reduce) {
  .price-changed-pulse {
    animation: none;
  }
}

/* Validation shake animation for invalid fields */
@keyframes validation-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  40% { transform: translateX(4px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
}

/* Icon spacing inside the button helpers comes from `gap-2` on BUTTON_BASE, not
   from a margin each call site has to remember. See ButtonsHelper::BUTTON_BASE
   for why the two ship together: 422 icons across 223 views still carry the old
   `mr-2`/`mr-1.5`, and with a gap in place those would render at double spacing.
   Zeroing them here retires the margin as the mechanism in one move, instead of
   a 223-file regex sweep whose false positives strip spacing from icons that are
   NOT inside a button and have no gap to replace it.

   Unlayered, so it beats Tailwind's `mr-*` in @layer utilities — the same
   mechanism the store-context colour overrides below already rely on. Direct
   child only: an <i> nested inside a <span> is not a flex item of the button, so
   the gap never applies to it and its margin is still load-bearing.

   The `mr-*` left in the markup is now inert. It can be removed file by file
   whenever those files are touched for other reasons; nothing depends on it. */
.ui-btn > i {
  margin-left: 0;
  margin-right: 0;
}

.validation-shake {
  animation: validation-shake 0.3s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .validation-shake {
    animation: none;
  }
}

/* ----------------------------------------------------------------
   Toast notification animations
   ---------------------------------------------------------------- */
@keyframes toast-slide-in {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes toast-slide-out {
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(100%);
    opacity: 0;
  }
}

.toast-enter {
  animation: toast-slide-in 0.3s ease-out forwards;
}

.toast-exit {
  animation: toast-slide-out 0.2s ease-in forwards;
}

/* ----------------------------------------------------------------
   Bottom notice (bottom-center, minimal)
   ---------------------------------------------------------------- */
.bottom-notice {
  position: fixed;
  bottom: 1.5rem;
  left: 50%;
  transform: translateX(-50%) translateY(1rem);
  opacity: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.625rem 1rem;
  border-radius: 0.5rem;
  background: oklch(0.25 0.02 250);
  color: white;
  font-size: 0.8125rem;
  font-weight: 500;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transition:
    opacity 0.3s ease,
    transform 0.3s ease;
  pointer-events: none;
}

.bottom-notice.visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
  .bottom-notice,
  .bottom-notice.visible {
    transform: translateX(-50%);
    transition: opacity 0.15s ease;
  }
}

.bottom-notice-action {
  margin-left: 0.5rem;
  padding: 0.25rem 0.625rem;
  border-radius: 0.25rem;
  background: rgba(255, 255, 255, 0.2);
  color: white;
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
  border: none;
  transition: background 0.15s ease;
}

.bottom-notice-action:hover {
  background: rgba(255, 255, 255, 0.35);
}

.bottom-notice-close {
  margin-left: 0.25rem;
  padding: 0.25rem;
  color: rgba(255, 255, 255, 0.7);
  background: transparent;
  border: none;
  font-size: 0.75rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.15s ease;
}

.bottom-notice-close:hover {
  color: white;
}

/* Transient field injected by clipboard_controller on copy failure so the
   user can see and select the value when there is no on-screen source. */
.clipboard-fallback-field {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin-top: 0.375rem;
  padding: 0.25rem 0.5rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  color: oklch(0.3 0.02 250);
  background: #fff;
  border: 1px solid oklch(0.85 0.02 250);
  border-radius: 0.375rem;
}

@keyframes bottom-notice-spin {
  to {
    transform: rotate(360deg);
  }
}

.bottom-notice-spinner {
  width: 0.875rem;
  height: 0.875rem;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: white;
  border-radius: 50%;
  animation: bottom-notice-spin 0.6s linear infinite;
  flex-shrink: 0;
}

/* Destructive confirm cancel — demotes the cancel button to a text link */
.bulk-confirm-cancel-link {
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  color: var(--color-gray-500);
  text-decoration: underline;
}

/* ── Global focus-visible ─────────────────────────────
   Keyboard-only focus ring for elements that don't have
   their own focus styling via Tailwind helpers.
   ---------------------------------------------------------------- */
a:focus-visible,
button:focus-visible,
[role="button"]:focus-visible {
  outline: 2px solid var(--color-accent-600, oklch(55% 0.22 270));
  outline-offset: 2px;
}

/* ── Focus-revealed sr-only controls (skip links, list reorder buttons) ──
   DO NOT MOVE THIS INTO AN @layer, AND DO NOT DELETE IT: doing so silently
   breaks keyboard accessibility (WCAG 2.4.1 / 2.4.7) with no visual symptom
   for mouse users.

   Every layout loads the Font Awesome CDN stylesheet, which ships an
   UNLAYERED `.sr-only` rule:
     .sr-only { position:absolute; width:1px; height:1px; padding:0;
                margin:-1px; overflow:hidden; clip:rect(0,0,0,0);
                white-space:nowrap; border-width:0 }
   Tailwind 4 emits its utilities inside `@layer utilities`, and an unlayered
   declaration beats ANY layered one regardless of specificity or source
   order. So `focus:not-sr-only` never won, and elements marked
   `sr-only focus:not-sr-only` stayed clipped to 1x1 even while focused:
   the six layout skip links and the move-up/move-down buttons in
   account/pages/_page_row and account/menu_items/_menu_item_node were
   permanently invisible to keyboard users.

   Winning the layer battle is necessary but not sufficient: Tailwind's
   `not-sr-only` resets `clip-path`, not the legacy `clip` property that Font
   Awesome sets, so `clip` has to be reset here explicitly.

   The rules below are unlayered on purpose (this file is unlayered and is
   linked in every layout), and out-specify `.sr-only` at 3 classes to 1.

   Padding is restated rather than left to the markup because Font Awesome's
   `padding: 0` still beats the layered `focus:px-4 focus:py-2` /
   `px-2.5 py-1.5` utilities: without a value here the revealed control paints
   with zero padding. The base rule restates the compact-button padding
   (px-2.5 py-1.5, matching compact_button_classes, the only in-flow user);
   the skip-link rule restates its own focus:px-4 focus:py-2. `revert-layer`
   was tried first and measured to fall through to `0`, so the values are
   literal. */
.sr-only.focus\:not-sr-only:focus {
  /* Shared reset. Position stays IN FLOW here: the reorder buttons live
     inside a table row and must not be pulled out of it. */
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  clip: auto;
  clip-path: none;
  white-space: normal;
  padding: 0.375rem 0.625rem; /* px-2.5 py-1.5 */
}

/* The menu-item reorder buttons hide with `sm:sr-only` (Tailwind's own
   sr-only, which Font Awesome's `.sr-only` selector never matches), so they
   do reveal on focus without help. They still paint with zero padding
   because Tailwind's `not-sr-only` itself sets `padding: 0`. Restore the
   compact-button padding so both reorder sites look the same when focused.
   Below the sm breakpoint the buttons are always visible and already carry
   px-2.5/py-1.5, so this changes nothing there. */
.sm\:sr-only.focus\:not-sr-only:focus {
  padding: 0.375rem 0.625rem; /* px-2.5 py-1.5 */
}

/* Skip links only — they carry `focus:fixed` in the markup and must overlay
   the page at top-left. */
.sr-only.focus\:not-sr-only.focus\:fixed:focus {
  position: fixed;
  padding: 0.5rem 1rem;
}

/* Accent-aware focus ring for custom radio/checkbox label patterns
   (hidden input + visible label span using peer-focus-visible) */
.peer:focus-visible + span,
.peer:focus-visible + .brand-peer-focus {
  outline: 2px solid var(--color-accent-600, oklch(55% 0.22 270));
  outline-offset: 2px;
  border-radius: 0.5rem;
}

/* ── Text truncation utilities ──────────────────────── */
.text-truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ----------------------------------------------------------------
   Scroll-triggered fade-in (respect reduced motion)
   ---------------------------------------------------------------- */
/* Even line lengths on big display headings */
.display-hero,
.display-title {
  text-wrap: balance;
}

.scroll-animate {
  transition:
    opacity 0.5s ease-out,
    transform 0.5s ease-out;
}

/* Hidden start state applies only when JS is active (html.js). Without JS
   (no-JS, crawlers, failed/slow scripts) content stays visible — never blank. */
.js .scroll-animate:not(.animate-in) {
  opacity: 0;
  transform: translateY(16px);
}

/* Stat number pop — scales in when parent scroll-animate triggers */
.scroll-animate-stat {
  display: inline-block;
  transition:
    transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    opacity 0.4s ease-out;
  transition-delay: 0.15s;
}
.js .scroll-animate-stat {
  transform: scale(0.7);
  opacity: 0;
}

.scroll-animate.animate-in .scroll-animate-stat {
  transform: scale(1);
  opacity: 1;
}

/* Ascending bar chart — bars grow upward when parent triggers */
.scroll-animate-bar {
  transform-origin: bottom;
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.js .scroll-animate-bar {
  transform: scaleY(0);
}
.scroll-animate-bar:nth-child(1) {
  transition-delay: 0.2s;
}
.scroll-animate-bar:nth-child(2) {
  transition-delay: 0.3s;
}
.scroll-animate-bar:nth-child(3) {
  transition-delay: 0.4s;
}
.scroll-animate-bar:nth-child(4) {
  transition-delay: 0.5s;
}

.scroll-animate.animate-in .scroll-animate-bar {
  transform: scaleY(1);
}

/* ── Product-mockup motion ───────────────────────────────
   Layer 1 (universal): list rows / chat bubbles fade-rise in
   sequence when their .scroll-animate ancestor gets .animate-in.
   Per-item order via --i; per-container pace via --row-stagger.
   ──────────────────────────────────────────────────────── */
.scroll-animate-row {
  transition:
    opacity 0.45s ease-out,
    transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * var(--row-stagger, 0.09s));
}
.js .scroll-animate-row {
  opacity: 0;
  transform: translateY(10px);
}
.scroll-animate.animate-in .scroll-animate-row {
  opacity: 1;
  transform: none;
}

/* Horizontal bar grow — scaleX variant of .scroll-animate-bar.
   The inner fill element carries the class; parent reserves width. */
.scroll-animate-bar--x {
  transform-origin: left;
  transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * 0.12s);
}
.js .scroll-animate-bar--x {
  transform: scaleX(0);
}
.scroll-animate.animate-in .scroll-animate-bar--x {
  transform: scaleX(1);
}

/* ── Layer 2 flagship: order status advances through its lifecycle.
   All four badges overlap in one inline-grid cell (auto-sized to the
   widest label, locale-safe). Default / no-JS / reduced-motion show
   only the first badge; under .animate-in the lifecycle cross-fades
   once and holds on the last state. ──────────────────────────────── */
.status-advance {
  display: inline-grid;
}
.status-advance > * {
  grid-area: 1 / 1;
  opacity: 0;
}
.status-advance > *:first-child {
  opacity: 1;
}
.scroll-animate.animate-in .status-advance > .status-s1 {
  animation: statusS1 5.5s ease-in-out both;
}
.scroll-animate.animate-in .status-advance > .status-s2 {
  animation: statusS2 5.5s ease-in-out both;
}
.scroll-animate.animate-in .status-advance > .status-s3 {
  animation: statusS3 5.5s ease-in-out both;
}
.scroll-animate.animate-in .status-advance > .status-s4 {
  animation: statusS4 5.5s ease-in-out both;
}
@keyframes statusS1 { 0%, 20% { opacity: 1; } 25%, 100% { opacity: 0; } }
@keyframes statusS2 { 0%, 20% { opacity: 0; } 25%, 45% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes statusS3 { 0%, 45% { opacity: 0; } 50%, 70% { opacity: 1; } 75%, 100% { opacity: 0; } }
@keyframes statusS4 { 0%, 70% { opacity: 0; } 75%, 100% { opacity: 1; } }

/* Chat typing beat — the "Tama is replying" indicator that appears
   briefly, then collapses as the next bubble reveals (homepage chat). */
.scroll-animate.animate-in .chat-typing {
  animation: chatTypingBeat 1.05s ease-out calc(var(--i, 0) * var(--row-stagger, 0.45s)) both;
}
.js .chat-typing {
  opacity: 0;
}
@keyframes chatTypingBeat {
  0% { opacity: 0; transform: translateY(6px); }
  20% { opacity: 1; transform: none; }
  78% { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translateY(-3px); }
}
.chat-typing-dot {
  animation: typing-dot 1.2s ease-in-out infinite;
}
.chat-typing-dot:nth-child(2) { animation-delay: 0.15s; }
.chat-typing-dot:nth-child(3) { animation-delay: 0.3s; }

@media (prefers-reduced-motion: reduce) {
  .scroll-animate {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .scroll-animate-stat {
    transform: none;
    opacity: 1;
    transition: none;
  }

  .scroll-animate-bar {
    transform: none;
    transition: none;
  }

  /* Product-mockup motion → instant final/visible state, no movement */
  .scroll-animate-row {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .scroll-animate-bar--x {
    transform: none;
    transition: none;
  }

  .status-advance > * {
    animation: none !important;
  }

  .chat-typing {
    opacity: 0 !important;
    animation: none !important;
  }

  .chat-typing-dot {
    animation: none;
  }

  .field_with_errors > input,
  .field_with_errors > select,
  .field_with_errors > textarea {
    animation: none;
  }
}

/* ----------------------------------------------------------------
   Image blur-up placeholder
   ---------------------------------------------------------------- */
.blur-up-wrapper {
  position: relative;
  overflow: hidden;
}

.blur-up-placeholder {
  position: absolute;
  inset: 0;
  background-color: #e5e7eb;
  z-index: 1;
  transition: opacity 0.4s ease-out;
}

.blur-up-placeholder.loaded {
  opacity: 0;
  pointer-events: none;
}

/* Shimmer effect on placeholder */
.blur-up-placeholder::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255, 255, 255, 0.4) 50%,
    transparent 100%
  );
  animation: shimmer 1.5s ease-in-out infinite;
}

.blur-up-placeholder.loaded::after {
  animation: none;
}

/* Turbo Frame skeleton loading: show skeletons and hide content while busy */
.skeleton-grid {
  display: none;
}

turbo-frame[busy] .product-grid {
  display: none;
}

turbo-frame[busy] .skeleton-grid {
  display: grid;
}

/* Reports: dim content while Turbo frame is loading */
turbo-frame#reports-content {
  transition: opacity 0.15s ease-out;
}

turbo-frame#reports-content[busy] {
  opacity: 0.5;
  pointer-events: none;
}

/* Reduced motion: disable decorative animations for vestibular disorder users */
@media (prefers-reduced-motion: reduce) {
  .animate-float,
  .animate-float-reverse,
  .animate-float-slow,
  .animate-spin-slow,
  .animate-bounce-slow,
  .animate-pulse-slow,
  .animate-shimmer,
  .animate-gradient-x,
  .animate-gradient-shift {
    animation: none;
  }

  .animate-fade-in-up,
  .animate-slide-in-left,
  .animate-scale-in {
    animation: none;
    opacity: 1;
  }

  .animate-count-up {
    animation: none;
    opacity: 1;
  }

  .blur-up-placeholder::after {
    animation: none;
  }

  .form-error-shake {
    animation: none;
  }

  .ss-sheet {
    animation: none;
  }

  .ss-sheet--closing {
    animation: none;
  }

  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* GPS location success entrance */
@keyframes gps-fade-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ----------------------------------------------------------------
   Smart Slot Picker — Day cards & slot chips
   ---------------------------------------------------------------- */

/* Day card base */
.day-card {
  border-color: #d1d5db; /* gray-300 */
  background-color: #f9fafb; /* gray-50 */
  color: #374151; /* gray-700 */
}

.day-card:hover {
  border-color: var(--color-accent-600, #6366f1);
  background-color: var(--color-accent-50, #eef2ff);
}

.day-card:focus-visible {
  outline: 2px solid var(--color-accent-600, #6366f1);
  outline-offset: 2px;
}

.day-card-selected {
  border-color: var(--color-accent-600, #6366f1);
  background-color: var(--color-accent-50, #eef2ff);
  box-shadow: 0 0 0 2px var(--color-accent-600, #6366f1);
  color: var(--color-accent-600, #4338ca);
}

.day-card-disabled {
  border-color: #f3f4f6; /* gray-100 */
  background-color: #f9fafb; /* gray-50 */
  color: #d1d5db; /* gray-300 */
  cursor: not-allowed;
}

/* Today indicator: "Auj." text label is sufficient — no dot needed
   (avoids collision with availability density dots at bottom-1) */

/* Slot chip base */
.slot-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  border: 1px solid #e5e7eb;
  background-color: #fff;
  color: #374151;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}

.slot-chip:hover {
  border-color: var(--color-accent-600, #6366f1);
  background-color: var(--color-accent-50, #eef2ff);
}

.slot-chip:focus-visible {
  outline: 2px solid var(--color-accent-600, #6366f1);
  outline-offset: 2px;
}

.slot-chip-selected {
  border-color: var(--color-accent-600, #6366f1);
  background-color: var(--color-accent-50, #eef2ff);
  box-shadow: 0 0 0 2px var(--color-accent-600, #6366f1);
  color: var(--color-accent-600, #4338ca);
}

.slot-chip:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  border-color: #f3f4f6;
  background-color: #f9fafb;
}

/* Admin collapsed picker: a BORDERLESS accordion row. The parent order-item card
   already provides the container — a bordered disclosure inside it would double the
   border (nested cards). The summary carries the amber "to schedule" signal and a
   hover affordance so the row reads as tappable; expanding reveals the picker inline,
   aligned with the card's other content (variant, quantity, personalization). */
.smart-slot-disclosure > summary {
  padding: 0.5rem;
  margin-inline: -0.5rem; /* hover pill breathes, text stays aligned to card content */
  border-radius: 0.375rem;
  transition: background-color 0.15s ease;
}
.smart-slot-disclosure > summary:hover {
  background-color: #f9fafb; /* gray-50 */
}
.smart-slot-disclosure > div {
  padding: 0.5rem 0 0;
}

/* Time-of-day toggle group (two-step appointment picker). A segmented pill on a
   gray track — deliberately a different shape from the answer chips so a chosen
   PERIOD never looks like a chosen TIME. */
.smart-slot-periods {
  display: inline-flex;
  gap: 0.25rem;
  padding: 0.25rem;
  background-color: #f3f4f6; /* gray-100 track */
  border-radius: 9999px;
  max-width: 100%;
}
.smart-slot-period {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 44px;
  padding: 0.375rem 0.875rem;
  border: 0;
  border-radius: 9999px;
  background-color: transparent;
  color: #4b5563; /* gray-600 */
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}
.smart-slot-period:focus-visible {
  outline: 2px solid var(--color-accent-600, #6366f1);
  outline-offset: 2px;
}
.smart-slot-period-active {
  background-color: var(--color-accent-600, #4f46e5);
  color: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
}
.smart-slot-period-count {
  /* gray-600, not gray-400: the count is the only thing separating a busy période
     from an empty one before you tap it, and at gray-400 on the gray-100 track it
     sat near 2:1 — unreadable on a phone outdoors. The lighter weight keeps it
     recessive against the période label without leaning on colour to do it. */
  color: #4b5563; /* gray-600 */
  font-weight: 500;
}
.smart-slot-period-active .smart-slot-period-count {
  color: rgba(255, 255, 255, 0.8);
}

/* Smart Slot summary card states */
.smart-slot-summary {
  background-color: #f9fafb; /* gray-50 */
  border-color: #e5e7eb; /* gray-200 */
}

.smart-slot-summary-confirmed {
  background-color: var(--color-accent-50, #eef2ff);
  border-color: var(--color-accent-600, #6366f1);
}

/* Scroll affordance fade (right edge gradient) */
.smart-slot-scroll-fade {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0.75rem; /* pb-3 offset */
  width: 2.5rem;
  pointer-events: none;
  background: linear-gradient(to right, transparent, var(--smart-slot-bg, #fff) 85%);
  border-radius: 0 0.5rem 0.5rem 0;
}

/* Smart Slot scrollbar hide (cross-browser) */
.smart-slot-scroller {
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none;
}
.smart-slot-scroller::-webkit-scrollbar {
  display: none;
}

/* Confirmation pulse animation (desktop) */
@keyframes smart-slot-pulse {
  0% { transform: scale(1); }
  50% { transform: scale(1.25); }
  100% { transform: scale(1); }
}

.smart-slot-pulse {
  animation: smart-slot-pulse 0.3s ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  .smart-slot-pulse {
    animation: none;
  }
}

/* FontAwesome spinners (.fa-spin) carry their own keyframes and ignore the
   per-class rules above. Stop them when the user asks to reduce motion; the
   static glyph still reads as a loading indicator. */
@media (prefers-reduced-motion: reduce) {
  .fa-spin {
    animation: none;
  }
}

/* Booking Turbo Frame loading indicator — shown while the slot picker is fetching.
   Scoped to booking frames only via [data-booking-url]. Reuses the existing @keyframes spin.
   Skeleton placeholder divs (data-frame-placeholder) are shown during loading. */
turbo-frame[data-booking-url][aria-busy="true"] [data-frame-placeholder],
turbo-frame[id^="personalization_"][aria-busy="true"] [data-frame-placeholder] {
  display: block;
}
turbo-frame[data-booking-url][aria-busy="true"],
turbo-frame[id^="personalization_"][aria-busy="true"] {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 3.5rem;
}
turbo-frame[data-booking-url][aria-busy="true"]::before,
turbo-frame[id^="personalization_"][aria-busy="true"]::before {
  content: "";
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  border: 2px solid #e5e7eb;
  border-top-color: var(--color-accent-600, #4f46e5);
  animation: spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  turbo-frame[data-booking-url][aria-busy="true"]::before,
  turbo-frame[id^="personalization_"][aria-busy="true"]::before {
    animation: none;
    border-top-color: #6366f1;
    opacity: 0.5;
  }
}

/* Report Turbo Frame loading state — shown while a new period is fetched.

   Changing the range swaps #reports-content, and until the response lands the
   merchant reads the OLD figures with nothing saying they are stale. On mobile
   data that wait is long enough to write a wrong number into a declaration.

   Deliberately a dim rather than the spinner the booking frames use: those
   frames replace a small control, this one replaces a whole page of figures,
   and collapsing it to a centred spinner would drop the page height and throw
   the scroll position. Dimming keeps the layout and says "not current yet".

   cursor: progress rather than not-allowed — the content is not disabled, it is
   superseded — and pointer-events stay on, so a link tapped mid-swap still
   works. */
turbo-frame#reports-content[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
  transition: opacity 120ms ease-in;
}
@media (prefers-reduced-motion: reduce) {
  turbo-frame#reports-content[aria-busy="true"] {
    transition: none;
  }
}

/* ── Raw neutrals inside a storefront ─────────────────────────────────────
   A safety net, not the mechanism. app/views/store was migrated to
   store-neutral-* wholesale, but the storefront also renders partials that live
   in app/views/shared and are written for the ADMIN, where a literal grey is
   correct: shared/_page_header supplies every store page's h1, subtitle and
   breadcrumb, and it painted them text-gray-900 on a dark theme at 1.16:1.

   Chasing those one partial at a time is whack-a-mole — the category nav and the
   page header were each found only by looking at a screenshot. This remaps the
   raw utilities to the themed ramp for anything rendered inside
   [data-context="store"], so a shared partial is themed by being on a storefront
   rather than by being edited.

   Unlayered and 0,2,0, so it beats Tailwind's own utilities in @layer utilities
   — the same mechanism the .text-accent-* overrides above already rely on.
   Scoped to the store context, so the admin, shipper and courier keep literal
   greys everywhere. */
[data-context="store"] .text-gray-400 { color: var(--color-store-neutral-400); }
/* 600, not 500. gray-500 on the storefront's own panel measures 4.41 — under
   AA, and it was under it on the light themes long before any theming. The
   admin keeps literal gray-500; only the storefront is held to the bar. */
[data-context="store"] .text-gray-500 { color: var(--color-store-neutral-600); }
[data-context="store"] .text-gray-600 { color: var(--color-store-neutral-600); }
[data-context="store"] .text-gray-700 { color: var(--color-store-neutral-700); }
[data-context="store"] .text-gray-800 { color: var(--color-store-neutral-800); }
[data-context="store"] .text-gray-900 { color: var(--color-store-neutral-900); }
[data-context="store"] .bg-white { background-color: var(--color-store-surface); }
[data-context="store"] .bg-gray-50 { background-color: var(--color-store-neutral-50); }
[data-context="store"] .bg-gray-100 { background-color: var(--color-store-neutral-100); }
[data-context="store"] .border-gray-100 { border-color: var(--color-store-neutral-100); }
[data-context="store"] .border-gray-200 { border-color: var(--color-store-neutral-200); }
[data-context="store"] .border-gray-300 { border-color: var(--color-store-neutral-300); }

/* A band that is deliberately dark on EVERY theme keeps literal neutrals.
   shared/_powered_by is bg-gray-900 with text-gray-300 on it: the band does not
   follow the page, so its text must not either. The remap above themed the text
   and left the background literal, which took the footer from about 5.9:1 to
   3.82:1 — a regression the remap introduced rather than a bug it found.
   0,3,0, so it wins against the 0,2,0 rules above. */
[data-context="store"] :is(.bg-gray-800, .bg-gray-900, .bg-gray-950, .bg-black) .text-gray-300 { color: var(--color-gray-300); }
[data-context="store"] :is(.bg-gray-800, .bg-gray-900, .bg-gray-950, .bg-black) .text-gray-400 { color: var(--color-gray-400); }
[data-context="store"] :is(.bg-gray-800, .bg-gray-900, .bg-gray-950, .bg-black) .text-gray-500 { color: var(--color-gray-500); }

/* ── Semantic colours on a dark storefront ────────────────────────────────
   Green means green on every theme, so these are NOT tokens the server
   computes and NOT a mirrored ramp: the hue is the meaning and must not move.
   Only the LIGHTNESS follows the page. A text-green-700 that reads as success
   on white is 2.18:1 on a dark checkout — unreadable, which is not a dimmer
   version of the same signal, it is no signal.

   Keyed off data-surface, which the store layouts stamp from the theme. CSS
   cannot ask whether a surface is dark, and unlike the neutrals this cannot be
   expressed as a variable the server fills in, because the value depends on the
   ROLE (text lightens, tinted backgrounds darken) rather than on the colour.

   Only the shades app/views/store actually uses, so this stays reviewable.
   Backgrounds move the other way from text on purpose: a bg-amber-50 chip is a
   pale wash on a light page and must become a deep one, not a bright slab. */
[data-surface="dark"] .text-green-500 { color: var(--color-green-400); }
[data-surface="dark"] .text-green-600 { color: var(--color-green-400); }
[data-surface="dark"] .text-green-700 { color: var(--color-green-400); }
[data-surface="dark"] .text-green-800 { color: var(--color-green-300); }
[data-surface="dark"] .text-green-900 { color: var(--color-green-300); }
[data-surface="dark"] .text-green-950 { color: var(--color-green-200); }
[data-surface="dark"] .text-green-400 { color: var(--color-green-300); }
[data-surface="dark"] .text-green-300 { color: var(--color-green-300); }
[data-surface="dark"] .text-red-500 { color: var(--color-red-400); }
[data-surface="dark"] .text-red-600 { color: var(--color-red-400); }
[data-surface="dark"] .text-red-700 { color: var(--color-red-400); }
[data-surface="dark"] .text-red-800 { color: var(--color-red-300); }
[data-surface="dark"] .text-red-900 { color: var(--color-red-300); }
[data-surface="dark"] .text-red-950 { color: var(--color-red-200); }
[data-surface="dark"] .text-red-400 { color: var(--color-red-300); }
[data-surface="dark"] .text-red-300 { color: var(--color-red-300); }
[data-surface="dark"] .text-amber-500 { color: var(--color-amber-400); }
[data-surface="dark"] .text-amber-600 { color: var(--color-amber-400); }
[data-surface="dark"] .text-amber-700 { color: var(--color-amber-400); }
[data-surface="dark"] .text-amber-800 { color: var(--color-amber-300); }
[data-surface="dark"] .text-amber-900 { color: var(--color-amber-300); }
[data-surface="dark"] .text-amber-950 { color: var(--color-amber-200); }
[data-surface="dark"] .text-amber-400 { color: var(--color-amber-300); }
[data-surface="dark"] .text-amber-300 { color: var(--color-amber-300); }
[data-surface="dark"] .text-yellow-500 { color: var(--color-yellow-400); }
[data-surface="dark"] .text-yellow-600 { color: var(--color-yellow-400); }
[data-surface="dark"] .text-yellow-700 { color: var(--color-yellow-400); }
[data-surface="dark"] .text-yellow-800 { color: var(--color-yellow-300); }
[data-surface="dark"] .text-yellow-900 { color: var(--color-yellow-300); }
[data-surface="dark"] .text-yellow-950 { color: var(--color-yellow-200); }
[data-surface="dark"] .text-yellow-400 { color: var(--color-yellow-300); }
[data-surface="dark"] .text-yellow-300 { color: var(--color-yellow-300); }
[data-surface="dark"] .text-orange-500 { color: var(--color-orange-400); }
[data-surface="dark"] .text-orange-600 { color: var(--color-orange-400); }
[data-surface="dark"] .text-orange-700 { color: var(--color-orange-400); }
[data-surface="dark"] .text-orange-800 { color: var(--color-orange-300); }
[data-surface="dark"] .text-orange-900 { color: var(--color-orange-300); }
[data-surface="dark"] .text-orange-950 { color: var(--color-orange-200); }
[data-surface="dark"] .text-orange-400 { color: var(--color-orange-300); }
[data-surface="dark"] .text-orange-300 { color: var(--color-orange-300); }
[data-surface="dark"] .text-blue-500 { color: var(--color-blue-400); }
[data-surface="dark"] .text-blue-600 { color: var(--color-blue-400); }
[data-surface="dark"] .text-blue-700 { color: var(--color-blue-400); }
[data-surface="dark"] .text-blue-800 { color: var(--color-blue-300); }
[data-surface="dark"] .text-blue-900 { color: var(--color-blue-300); }
[data-surface="dark"] .text-blue-950 { color: var(--color-blue-200); }
[data-surface="dark"] .text-blue-400 { color: var(--color-blue-300); }
[data-surface="dark"] .text-blue-300 { color: var(--color-blue-300); }
[data-surface="dark"] .bg-green-50 { background-color: var(--color-green-950); }
[data-surface="dark"] .bg-green-100 { background-color: var(--color-green-900); }
[data-surface="dark"] .bg-green-500 { background-color: var(--color-green-600); }
[data-surface="dark"] .bg-green-600 { background-color: var(--color-green-600); }
[data-surface="dark"] .bg-red-50 { background-color: var(--color-red-950); }
[data-surface="dark"] .bg-red-100 { background-color: var(--color-red-900); }
[data-surface="dark"] .bg-red-500 { background-color: var(--color-red-600); }
[data-surface="dark"] .bg-red-600 { background-color: var(--color-red-600); }
[data-surface="dark"] .bg-amber-50 { background-color: var(--color-amber-950); }
[data-surface="dark"] .bg-amber-100 { background-color: var(--color-amber-900); }
[data-surface="dark"] .bg-amber-500 { background-color: var(--color-amber-600); }
[data-surface="dark"] .bg-amber-600 { background-color: var(--color-amber-600); }
[data-surface="dark"] .bg-yellow-50 { background-color: var(--color-yellow-950); }
[data-surface="dark"] .bg-yellow-100 { background-color: var(--color-yellow-900); }
[data-surface="dark"] .bg-yellow-500 { background-color: var(--color-yellow-600); }
[data-surface="dark"] .bg-yellow-600 { background-color: var(--color-yellow-600); }
[data-surface="dark"] .bg-orange-50 { background-color: var(--color-orange-950); }
[data-surface="dark"] .bg-orange-100 { background-color: var(--color-orange-900); }
[data-surface="dark"] .bg-orange-500 { background-color: var(--color-orange-600); }
[data-surface="dark"] .bg-orange-600 { background-color: var(--color-orange-600); }
[data-surface="dark"] .bg-blue-50 { background-color: var(--color-blue-950); }
[data-surface="dark"] .bg-blue-100 { background-color: var(--color-blue-900); }
[data-surface="dark"] .bg-blue-500 { background-color: var(--color-blue-600); }
[data-surface="dark"] .bg-blue-600 { background-color: var(--color-blue-600); }
[data-surface="dark"] .border-green-200 { border-color: var(--color-green-800); }
[data-surface="dark"] .border-green-300 { border-color: var(--color-green-700); }
[data-surface="dark"] .border-red-200 { border-color: var(--color-red-800); }
[data-surface="dark"] .border-red-300 { border-color: var(--color-red-700); }
[data-surface="dark"] .border-amber-200 { border-color: var(--color-amber-800); }
[data-surface="dark"] .border-amber-300 { border-color: var(--color-amber-700); }
[data-surface="dark"] .border-yellow-200 { border-color: var(--color-yellow-800); }
[data-surface="dark"] .border-yellow-300 { border-color: var(--color-yellow-700); }
[data-surface="dark"] .border-orange-200 { border-color: var(--color-orange-800); }
[data-surface="dark"] .border-orange-300 { border-color: var(--color-orange-700); }
[data-surface="dark"] .border-blue-200 { border-color: var(--color-blue-800); }
[data-surface="dark"] .border-blue-300 { border-color: var(--color-blue-700); }
