/* components.css — reusable pieces shared across every page:
   buttons, badges, product cards, nav, footer, form inputs, loading states. */

/* Buttons */
.btn-primary, .btn-secondary, .btn-danger {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 15px;
  padding: 14px 28px;
  border-radius: var(--radius-button);
  cursor: pointer;
  transition: transform var(--motion-base) var(--ease), background var(--motion-base) var(--ease), border-color var(--motion-base) var(--ease);
  border: 1px solid transparent;
  display: inline-block;
  text-align: center;
}
.btn-primary { background: var(--indigo); color: var(--paper); }
.btn-primary:hover { background: #5C4BE0; transform: translateY(-1px); }
.btn-secondary { background: transparent; color: var(--paper); border-color: var(--border-default); }
.btn-secondary:hover { border-color: var(--paper); transform: translateY(-1px); }
.btn-danger { background: transparent; color: var(--danger); border-color: var(--danger-border); }
.btn-danger:hover { background: var(--danger-bg); border-color: var(--danger); }
.btn-primary:disabled, .btn-secondary:disabled, .btn-danger:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
  background: var(--surface-2);
  color: var(--text-tertiary);
  border-color: var(--border-subtle);
}

/* Same treatment as :disabled above, for <a>-tag buttons — anchors can't
   carry a real disabled attribute, so this is the class-based equivalent
   (used by the product page's Buy button when there's no payment link yet). */
.btn-primary.btn-disabled, .btn-secondary.btn-disabled, .btn-danger.btn-disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
  background: var(--surface-2);
  color: var(--text-tertiary);
  border-color: var(--border-subtle);
  pointer-events: none;
}

/* Small inline text button — no border, no background. Started in
   admin.css (Products tab's "copy download link"), moved here once the
   reseller dashboard's marketing kit needed the same thing. */
.link-btn {
  background: none;
  border: none;
  color: var(--signal-blue);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  padding: 0;
}
.link-btn:hover { text-decoration: underline; }

/* Badges */
.badge { font-size: 12px; font-weight: 600; letter-spacing: 0.03em; padding: 6px 12px; border-radius: 100px; display: inline-block; }
.badge-featured { background: rgba(79, 63, 208, 0.18); color: #9C8CFF; }
.badge-bestseller { background: rgba(46, 107, 255, 0.18); color: #7FA6FF; }
.badge-new { background: rgba(15, 174, 121, 0.18); color: #4FDBA6; }

/* Product card — used by the homepage teaser and the full Product Hub.
   Grid uses explicit column counts per breakpoint rather than auto-fit:
   auto-fit collapses empty tracks and stretches existing ones to fill the
   row, which is exactly why a single test product was rendering as one
   giant full-width card. Explicit repeat(N, 1fr) never does that. */
/* minmax(0, 1fr), not 1fr: a bare `1fr` track has min-width:auto, so any
   card whose content can't shrink below a certain width pushes the track
   wider than its share and the grid overflows the page. Measured at
   320px, the two-up grid rendered 160px cards in 140px of space and the
   page scrolled sideways by 11px. minmax(0, …) lets the track shrink and
   the content wrap, which is what the rest of this codebase already uses
   (.admin-stats, .analytics-cards). Still explicit column counts — never
   auto-fit, per the frontend-conventions rule. */
.card-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }

/* Small phones stay 2-up rather than dropping to a single full-width
   card. One card per screen makes a 5-product store feel like an endless
   scroll and hides the fact that there's anything else to buy.
   Two-up at 360px leaves ~115px of inner card width, which the default
   card sizing overflows -- so everything inside tightens here. Measured,
   not guessed: .btn-primary alone carries 56px of horizontal padding. */
@media (max-width: 419px) {
  .card-grid { gap: var(--space-2); }
  .product-card { padding: var(--space-2); border-radius: 16px; }
  .card-thumb { margin-bottom: var(--space-2); border-radius: 10px; }
  .card-title { font-size: 15px; line-height: 1.3; }
  .card-desc { font-size: 12px; -webkit-line-clamp: 2; }
  .card-price { font-size: 18px; }
  .card-grid .btn-primary { padding: 11px 10px; font-size: 14px; }
  .badge { font-size: 10px; padding: 4px 8px; }
}
@media (min-width: 768px) {
  .card-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-4); }
}
@media (min-width: 1024px) {
  .card-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1280px) {
  .card-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

.product-card {
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-card);
  padding: var(--space-3);
  box-shadow: var(--shadow-soft);
  transition: border-color var(--motion-base) var(--ease), transform var(--motion-base) var(--ease), box-shadow var(--motion-base) var(--ease);
  display: flex;
  flex-direction: column;
  position: relative;
}
.product-card:hover {
  border-color: var(--border-default);
  transform: translateY(-4px);
  box-shadow: var(--shadow-lift, 0 4px 8px rgba(0, 0, 0, 0.4), 0 20px 48px rgba(0, 0, 0, 0.32));
}
/* Keyboard users get the same affordance as mouse users — without this,
   tabbing through a grid of cards gives no indication of position. */
.product-card:focus-within {
  border-color: var(--border-default);
  box-shadow: var(--shadow-lift, 0 20px 48px rgba(0, 0, 0, 0.32));
}

/* Portrait thumbnail — the card's primary visual element.
   aspect-ratio locks every card to the same shape (reserves space before
   the image loads too, avoiding layout shift). The image itself sits on
   an absolutely-positioned inner layer so it can zoom on hover without
   the fixed-size box around it moving — and background-size: contain
   shows the complete cover rather than cropping it to fill the box. */
.card-thumb {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: 12px;
  /* A soft mat rather than flat grey. `contain` (below) is deliberate --
     it shows the whole cover instead of cropping the title off -- but it
     leaves bands of empty space on either side. Flat grey makes those
     read as a broken image; a gentle gradient makes them read as a
     deliberate mount, which is the difference between "cheap" and
     "considered" at a glance. */
  background-color: var(--surface-2);
  background-image: var(--thumb-mat, linear-gradient(160deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0) 60%));
  margin-bottom: var(--space-3);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--border-subtle);
}
.card-thumb-img {
  position: absolute;
  inset: 0;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  transition: transform var(--motion-base) var(--ease);
}
.product-card:hover .card-thumb-img { transform: scale(1.04); }

/* Plain block-level link wrappers around the thumbnail and title — reset
   so the anchor doesn't reintroduce underline/blue-link styling, since
   these exist purely for navigation, not as text links. */
.card-thumb-link, .card-title-link { display: block; text-decoration: none; color: inherit; }

.card-badges { position: absolute; top: var(--space-2); left: var(--space-2); z-index: 2; display: flex; gap: 6px; }
.card-title {
  font-family: var(--font-display); font-weight: 600; font-size: 17px; margin: 0 0 4px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.card-desc {
  font-size: 13px; color: var(--text-secondary); margin: 0 0 var(--space-2); flex-grow: 1;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Price gets real weight. At 18px it sat below the 17px title in the
   visual order, which buries the single number a buyer is scanning for. */
.card-price {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 22px;
  line-height: 1.2;
  margin: 0;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}

/* "Base Price" caption on Zyentora's own catalog cards (never on a
   reseller storefront, which shows the selling price and no label).
   Stacked above the number so the price stays dominant and a long label
   can't fight the figure for space on a narrow card. */
.card-price-label {
  display: block;
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .product-card, .card-thumb-img { transition: none; }
  .product-card:hover { transform: none; }
}

/* Filter chips — shared by the admin Applications filter and the public
   category filter, same visual language either place. */
.filter-chip {
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  color: var(--text-secondary);
  padding: 6px 14px;
  border-radius: 100px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
}
.filter-chip:hover { color: var(--text-primary); }
.filter-chip.active { color: var(--text-primary); border-color: var(--signal-blue); background: rgba(46, 107, 255, 0.1); }
.chip-count {
  font-size: 11px; font-weight: 700; color: var(--text-tertiary);
  background: rgba(250, 250, 250, 0.08); padding: 1px 7px; border-radius: 100px;
}
.filter-chip.active .chip-count { color: var(--signal-blue); background: rgba(46, 107, 255, 0.15); }

/* Loading + empty + error states */
.skeleton { background: var(--surface-2); border-radius: var(--radius-card); animation: pulse 1.4s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.state-message { grid-column: 1 / -1; text-align: center; padding: var(--space-6) var(--space-4); color: var(--text-secondary); }

/* Nav */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(11, 11, 15, 0.7);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border-subtle);
}
.nav-inner { max-width: 1120px; margin: 0 auto; padding: var(--space-3) var(--space-4); display: flex; align-items: center; justify-content: space-between; position: relative; }
.nav-logo { font-family: var(--font-display); font-weight: 700; font-size: 20px; color: var(--text-primary); }
.nav-links { display: none; align-items: center; gap: var(--space-4); }
.nav-links a:not(.btn-primary) { color: var(--text-secondary); font-size: 15px; }
.nav-links a:not(.btn-primary):hover { color: var(--text-primary); }
.nav-links a.active:not(.btn-primary) { color: var(--text-primary); }
.nav-cta { padding: 10px 20px; font-size: 14px; }
.nav-toggle { display: flex; background: none; border: none; color: var(--text-primary); font-size: 24px; cursor: pointer; }

.nav-links.open {
  display: flex;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  flex-direction: column;
  align-items: flex-start;
  background: var(--surface-1);
  padding: var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  gap: var(--space-3);
}

@media (min-width: 768px) {
  .nav-links { display: flex; }
  .nav-toggle { display: none; }
}

/* Footer */
.site-footer { border-top: 1px solid var(--border-subtle); padding: var(--space-6) 0 var(--space-4); }
.footer-full-inner {
  max-width: 1120px; margin: 0 auto; padding: 0 var(--space-4);
  display: grid; grid-template-columns: 1.4fr 1fr 1fr 1.2fr; gap: var(--space-5);
}
@media (max-width: 767px) { .footer-full-inner { grid-template-columns: 1fr 1fr; } }
@media (max-width: 479px) { .footer-full-inner { grid-template-columns: 1fr; } }
.footer-brand-col .footer-logo { font-family: var(--font-display); font-weight: 700; font-size: 20px; display: block; margin-bottom: var(--space-2); }
.footer-tagline { font-size: 14px; color: var(--text-tertiary); line-height: 1.5; max-width: 260px; margin: 0; }
.footer-col h4 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-tertiary); margin-bottom: var(--space-3); font-weight: 600; }
.footer-col a { display: block; color: var(--text-secondary); font-size: 14px; margin-bottom: var(--space-2); }
.footer-col a:hover { color: var(--paper); }
.footer-full-bottom {
  max-width: 1120px; margin: var(--space-5) auto 0; padding: var(--space-4) var(--space-4) 0;
  border-top: 1px solid var(--border-subtle); font-size: 13px; color: var(--text-tertiary);
}

/* Forms */
label { display: block; font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; }
input, textarea, select {
  width: 100%;
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-input);
  padding: 12px 16px;
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 15px;
  margin-bottom: var(--space-3);
  transition: border-color var(--motion-fast) var(--ease);
}
input:focus, textarea:focus, select:focus { border-color: var(--signal-blue); outline: none; }

/* One height for every single-line control.
   A <select> renders 2px taller than an <input> with identical padding,
   border, font and box-sizing — Chrome sizes it from an internal button
   box, so line-height has no effect on it (measured both ways). Rather
   than chase that, every single-line control gets the same floor, which
   is what makes a select and an input sitting next to each other line up.
   Checkbox, radio and file are excluded: the global `input` selector
   catches them too, and a 46px floor would turn the terms checkbox into
   a large empty box. */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
select { min-height: 46px; }

/* Password show/hide toggle — wrapper and button are injected by
   wirePasswordToggle() in utils.js, not written by hand in any page's
   HTML. Only the input's own right padding + these two rules need to
   exist for it to work correctly wherever that function gets called. */
.password-field { position: relative; margin-bottom: var(--space-3); }
.password-field input { margin-bottom: 0; padding-right: 44px; }
.password-toggle-btn {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-tertiary);
  cursor: pointer;
  font-size: 17px;
  border-radius: 8px;
}
.password-toggle-btn:hover { color: var(--text-primary); background: var(--surface-2); }

/* Modals — shared base, used by the Product Hub detail view, every admin
   modal, and the Contact modal. Previously duplicated between products.css
   and admin.css; consolidated here now that index.html needs it too. */
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(11, 11, 15, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  animation: fadeIn var(--motion-base) var(--ease);
  overflow-y: auto;
}
.modal-backdrop[hidden] { display: none; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

.modal {
  position: relative;
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-dialog);
  padding: var(--space-5);
  max-width: 480px;
  width: 100%;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: var(--shadow-soft);
  animation: scaleIn var(--motion-slow) var(--ease);
  margin: auto;
}
.modal.modal-sm { max-width: 400px; }
.modal.modal-wide { max-width: 720px; }
@keyframes scaleIn { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
.modal h3 { font-size: 20px; margin-bottom: var(--space-4); padding-right: var(--space-6); }

/* Sticky, not absolute. The modal is its own scroll container, so an
   absolutely-positioned close button scrolls away with the content:
   measured on the Payouts modal at 320px, once you scrolled to the
   request button the X sat 410px above the viewport. You could still
   close by scrolling back up or tapping the backdrop, but on a phone the
   modal covers nearly the whole screen and there is very little backdrop
   left to tap.

   float + sticky keeps it out of normal flow (so it doesn't push the
   heading down) while still being positioned by the scroll container.
   The negative bottom margin reclaims the vertical space it would
   otherwise occupy. .modal h3 keeps its padding-right so a long heading
   still clears the button. */
.modal-close {
  position: sticky;
  float: right;
  top: 0;
  margin: 0 0 -32px var(--space-3);
  z-index: 5;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--border-default);
  color: var(--text-primary);
  font-size: 16px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal-footer-actions { display: flex; gap: var(--space-3); justify-content: flex-end; margin-top: var(--space-4); flex-wrap: wrap; }
.modal-footer-actions .btn-primary, .modal-footer-actions .btn-secondary { border: none; padding: 12px 24px; font-size: 14px; }
.modal-footer-actions .btn-secondary { border: 1px solid var(--border-default); }

/* Login shell — originally admin-only, promoted here once the reseller
   login page needed the identical layout. Same pattern as every other
   "promote once a second context needs it" move in this codebase. */
.login-shell {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
}
.login-card {
  width: 100%;
  max-width: 380px;
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-card);
  padding: var(--space-6) var(--space-5);
  box-shadow: var(--shadow-soft);
}
.login-logo { font-family: var(--font-display); font-weight: 700; font-size: 22px; margin: 0 0 var(--space-4); }
.login-heading { font-size: 24px; margin-bottom: var(--space-4); }
.login-card .btn-primary { width: 100%; margin-top: var(--space-1); border: none; }
.login-error {
  background: var(--danger-bg);
  border: 1px solid var(--danger-border);
  color: var(--danger);
  font-size: 14px;
  padding: var(--space-3);
  border-radius: var(--radius-input);
  margin-bottom: var(--space-3);
}
.login-error[hidden] { display: none; }
.login-note { font-size: 13px; color: var(--text-tertiary); text-align: center; margin-top: var(--space-4); }
.login-note a { color: var(--text-secondary); }
.login-toggle { background: none; border: none; padding: 0; color: var(--signal-blue); cursor: pointer; font: inherit; }
.login-success {
  background: rgba(15, 174, 121, 0.1);
  border: 1px solid rgba(15, 174, 121, 0.3);
  color: var(--text-primary);
  font-size: 14px;
  padding: var(--space-3);
  border-radius: var(--radius-input);
  margin-bottom: var(--space-3);
}
.login-success[hidden] { display: none; }

/* Promoted here now that a third context (reseller login) needs it —
   was duplicated between admin.css and become-a-reseller.css before
   this. Those two are left as-is rather than retroactively cleaned up;
   identical values, harmless redundancy, not worth touching
   already-verified files without a stronger reason. */
.form-hint { font-size: 12px; color: var(--text-tertiary); margin: -10px 0 var(--space-3); }

/* ---------- Inline field feedback ----------
   Live per-field verdicts: store-name availability, phone shape, social
   URL match. Sits directly under its input so it reads as belonging to
   that field, rather than as a page-level message like .form-error.
   Moved here from become-a-reseller.css once the reseller dashboard's
   store-rename field needed it too.

   Colour is never the only signal — every message is prefixed with a
   ✓ or ✕ in store-name.js, so this still works for a colour-blind user. */
.field-status {
  font-size: 12.5px;
  line-height: 1.5;
  margin: -10px 0 var(--space-3);
  min-height: 1em;
}
.field-status[hidden] { display: none; }
.field-status-ok { color: var(--emerald); }
.field-status-error { color: var(--danger); }
.field-status-checking { color: var(--text-tertiary); }
.field-status-idle { color: var(--text-tertiary); }

/* Label that exists for screen readers only. The store-rename field has
   a visible heading above it, so a second visible label would be noise —
   but the input still needs a programmatic one. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Generic version of admin.css's .admin-loading-gate, for any
   session-gated page that isn't the admin dashboard. Deliberately not
   touching the admin one — it's already tested and working. */
.loading-gate { min-height: 100vh; display: flex; align-items: center; justify-content: center; color: var(--text-tertiary); }
.loading-gate[hidden] { display: none; }

/* Promoted here now that a third context (reseller dashboard's add-more-
   products modal) needs it — was duplicated between admin.css and
   become-a-reseller.css before. Those two left as-is, same reasoning as
   every other promotion note in this file. */
.form-error {
  background: var(--danger-bg);
  border: 1px solid var(--danger-border);
  color: var(--danger);
  font-size: 14px;
  padding: var(--space-3);
  border-radius: var(--radius-input);
  margin-bottom: var(--space-4);
}
.form-error[hidden] { display: none; }

/* ---------- Product picker ----------
   Originally built for the public application form (become-a-reseller.css).
   Promoted here — actually moved, not duplicated, since this block is
   large — now that the reseller dashboard's "Add More Products" needs
   the identical checkbox+price-reveal interaction. */

.picker-hint { margin-top: -8px; }

.picker-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}
@media (max-width: 419px) {
  .picker-grid { grid-template-columns: 1fr; }
}
@media (min-width: 768px) {
  .picker-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-4); }
}

/* ---------- Picker search + category filter ----------
   Sticky at the top of the modal's own scroll area so the search field
   stays reachable while scrolling 26 products. */
.picker-toolbar {
  position: sticky;
  top: calc(var(--space-5) * -1);
  z-index: 4;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-3) 0;
  margin-bottom: var(--space-3);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-subtle);
}

.picker-search { position: relative; display: flex; align-items: center; flex: 1 1 200px; min-width: 0; }
/* Explicitly centred, not left to the flex container's static position —
   see the .admin-search note in admin.css for why that indirection is
   what made the admin search icon sit low. */
.picker-search .ph {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-tertiary);
  font-size: 16px;
  pointer-events: none;
}
.picker-search input {
  width: 100%;
  min-height: 42px;
  padding: 10px 12px 10px 36px;
  margin-bottom: 0;
  font-size: 14px;
}
.picker-search input::-webkit-search-cancel-button { -webkit-appearance: none; }

/* Scrolls sideways rather than wrapping into a tall stack, same choice
   as .admin-tabs. */
.picker-categories {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  padding-bottom: 2px;
  flex: 1 1 100%;
}
.picker-categories .filter-chip { white-space: nowrap; flex-shrink: 0; }

.picker-empty { padding: var(--space-5) var(--space-3); }
.picker-empty[hidden] { display: none; }

/* Sticky action bar. Without this the button sat below all 26 cards and
   you had to scroll the whole catalog back down to save. */
.picker-footer {
  position: sticky;
  bottom: calc(var(--space-5) * -1);
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  margin-top: var(--space-3);
  background: var(--surface-1);
  border-top: 1px solid var(--border-subtle);
}
.picker-selected-count { font-size: 13px; color: var(--text-secondary); }
@media (max-width: 480px) {
  .picker-footer { flex-direction: column; align-items: stretch; }
  .picker-footer .btn-primary { width: 100%; }
  .picker-selected-count { text-align: center; }
}

.picker-card {
  position: relative;
  transition: border-color var(--motion-base) var(--ease), box-shadow var(--motion-base) var(--ease), transform var(--motion-base) var(--ease);
}
/* Load-bearing: .product-card sets `display: flex`, which beats the
   browser's default `[hidden] { display: none }`. Without this rule the
   filter would set .hidden on cards and nothing would visibly change. */
.picker-card[hidden] { display: none; }
.picker-card.selected {
  border-color: var(--signal-blue);
  box-shadow: 0 0 0 1px var(--signal-blue), var(--shadow-soft);
}
.picker-card:hover { transform: none; }
.picker-card.already-added {
  opacity: 0.6;
}
/* At the product limit. Dimmed rather than hidden, so it's clear the
   product still exists and the limit is what's blocking it. */
.picker-card-disabled { opacity: 0.45; }
.picker-card-disabled .picker-toggle { cursor: not-allowed; }

/* Cap notice — shared, because the apply page and the reseller
   dashboard both need it and only one of them loads
   reseller-dashboard.css. */
.cap-notice {
  background: rgba(46, 107, 255, 0.1);
  border: 1px solid rgba(46, 107, 255, 0.3);
  border-radius: var(--radius-input);
  padding: var(--space-3);
  margin-bottom: var(--space-3);
  font-size: 14px;
  color: var(--text-primary);
}
.cap-notice[hidden] { display: none; }

/* ---------- Messaging (admin tab + reseller modal) ---------- */

.msg-layout { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: var(--space-4); align-items: start; }
@media (max-width: 760px) { .msg-layout { grid-template-columns: 1fr; } }

.msg-threads {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 460px;
  overflow-y: auto;
}
@media (max-width: 760px) { .msg-threads { flex-direction: row; overflow-x: auto; max-height: none; } }

.msg-thread-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: 10px var(--space-3);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-input);
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: 14px;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
}
.msg-thread-item:hover { background: var(--surface-2); }
.msg-thread-item.active { background: var(--surface-2); border-color: var(--border-default); color: var(--text-primary); }
/* Two lines: display name, then the store slug beneath it. Two resellers
   can share a name, so the name alone can't identify a conversation. */
.msg-thread-name {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  min-width: 0;
  overflow: hidden;
}
.msg-thread-top { display: inline-flex; align-items: center; gap: 8px; }
.msg-thread-id {
  font-size: 11px;
  font-weight: 400;
  color: var(--text-tertiary);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-thread-count { color: var(--text-tertiary); font-size: 12px; flex-shrink: 0; }
/* Small dot: this reseller's last message was theirs, so they're waiting. */
.msg-awaiting { width: 7px; height: 7px; border-radius: 50%; background: var(--emerald); flex-shrink: 0; }

.msg-thread-broadcast .msg-thread-top { color: var(--emerald); font-weight: 600; }
/* Archived: the reseller has no active store. Kept legible rather than
   greyed into unreadability -- it's still a record the admin may need. */
.msg-thread-archived { opacity: 0.7; }
.msg-thread-archived .msg-thread-id { font-style: italic; }
.msg-thread-group {
  margin: var(--space-2) 0 2px;
  padding: 0 var(--space-3);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
}

/* The scrolling message list. Fixed height so the composer stays put
   rather than being pushed off-screen as a thread grows. */
.msg-thread {
  height: 340px;
  overflow-y: auto;
  padding: var(--space-3);
  background: var(--surface-0);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-card);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.msg-row { display: flex; }
.msg-mine { justify-content: flex-end; }
.msg-theirs { justify-content: flex-start; }

.msg-bubble {
  max-width: 78%;
  padding: 10px var(--space-3);
  border-radius: 14px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
}
.msg-mine .msg-bubble { background: rgba(79, 63, 208, 0.16); border-color: rgba(79, 63, 208, 0.32); }

.msg-sender { font-size: 12px; font-weight: 600; color: var(--signal-blue); margin: 0 0 2px; }

/* A broadcast has to be unmistakable from a private reply -- in both
   directions. The admin needs to see they're addressing everyone; the
   reseller needs to know the message wasn't written to them personally.
   Colour alone wouldn't carry that, so it's a labelled banner. */
.msg-kind {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 6px;
  padding-bottom: 5px;
  border-bottom: 1px dashed var(--border-subtle);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.msg-kind-broadcast { color: var(--emerald); }
.msg-is-broadcast .msg-bubble {
  background: rgba(16, 185, 129, 0.10);
  border-color: rgba(16, 185, 129, 0.35);
}
.msg-body { margin: 0; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
.msg-meta {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: 6px 0 0;
  font-size: 11px;
  color: var(--text-tertiary);
}
.msg-expiry-saved { color: var(--emerald); font-weight: 600; }

.msg-composer { margin-top: var(--space-3); }
.msg-composer textarea { width: 100%; resize: vertical; }
.msg-composer-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-2);
}
.msg-composer-actions .btn-primary,
.msg-composer-actions .btn-secondary { padding: 10px 20px; font-size: 14px; }
.msg-composer-actions .btn-primary { margin-left: auto; }
.msg-expiry-label { font-size: 13px; color: var(--text-secondary); }
/* Inline beside the Send button — same leaked-margin fix as .admin-search. */
.msg-composer-actions select { width: auto; min-height: 0; padding: 8px var(--space-2); margin-bottom: 0; font-size: 13px; }

/* Names the audience before you type, so a reply can't accidentally
   become a broadcast. */
.msg-recipient-label { font-size: 13px; color: var(--text-secondary); margin: 0 0 var(--space-2); }
.msg-recipient-broadcast { color: var(--emerald); font-weight: 600; }
.msg-recipient-archived { color: var(--text-tertiary); font-style: italic; }

/* The reseller-side modal is a chat surface: its own scroll region
   already handles overflow, so the modal shouldn't add a second one. */
.msg-modal { max-height: 92vh; }
.msg-modal .msg-thread { height: 300px; }

.card-price { display: block; }

.picker-toggle {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 3;
  width: 28px;
  height: 28px;
  cursor: pointer;
}
.picker-checkbox {
  position: absolute;
  inset: 0;
  width: 28px;
  height: 28px;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.picker-toggle-box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: rgba(11, 11, 15, 0.55);
  border: 1.5px solid var(--border-default);
  color: transparent;
  font-size: 16px;
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.picker-checkbox:checked + .picker-toggle-box {
  background: var(--indigo);
  border-color: var(--indigo);
  color: var(--paper);
}
.picker-checkbox:focus-visible + .picker-toggle-box {
  outline: 2px solid var(--signal-blue);
  outline-offset: 2px;
}
.picker-checkbox:disabled + .picker-toggle-box {
  background: var(--surface-2);
  border-color: var(--border-subtle);
  cursor: not-allowed;
}

.picker-price-row {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-subtle);
  animation: fadeIn var(--motion-fast) var(--ease);
}
.picker-price-row[hidden] { display: none; }
.picker-price-row label { font-size: 12px; }
.picker-price-input { margin-bottom: 0; }
.picker-earn {
  font-size: 13px;
  font-weight: 600;
  color: var(--emerald);
  margin: var(--space-2) 0 0;
}
.picker-earn[hidden] { display: none; }
.picker-price-error-text {
  font-size: 12px;
  color: var(--danger);
  margin: var(--space-2) 0 0;
}
.picker-price-error-text[hidden] { display: none; }
.picker-already-label {
  font-size: 12px;
  color: var(--text-tertiary);
  margin: var(--space-2) 0 0;
}

@media (prefers-reduced-motion: reduce) {
  .modal-backdrop, .modal { animation: none; }
}

/* ---------- Dashboard tabs ----------
   Shared by the admin dashboard and the reseller dashboard. Lives here
   rather than admin.css because the reseller page does not load that
   file. The strip scrolls sideways on narrow screens (overflow-x plus
   nowrap) instead of wrapping into a tall stack of buttons. */
/* ---------- Tabs ---------- */
.admin-tabs {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
  border-bottom: 1px solid var(--border-subtle);
  overflow-x: auto;
}
.admin-tab {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 15px;
  background: none;
  border: none;
  color: var(--text-secondary);
  padding: var(--space-3);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
  white-space: nowrap;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.admin-tab:hover { color: var(--text-primary); }
.admin-tab.active { color: var(--text-primary); border-bottom-color: var(--indigo); }
.tab-count {
  background: var(--indigo);
  color: var(--paper);
  font-size: 11px;
  font-weight: 700;
  min-width: 18px;
  height: 18px;
  border-radius: 100px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
}
.tab-count[hidden] { display: none; }
.admin-panel[hidden] { display: none; }

/* Reseller variant: a little tighter, since that page has no stats
   strip above the bar to separate it from the hero. */
.dashboard-tabs { margin-top: var(--space-4); }
@media (max-width: 560px) {
  .dashboard-tabs .admin-tab { font-size: 14px; padding: var(--space-2) var(--space-3); }
}
