/* ===== Only the custom bits Bootstrap doesn't already give us ===== */
body {
  background: var(--bs-tertiary-bg);
  padding-bottom: 70px; /* room for mobile bottom nav */
}
/* Header brand text on mobile (2026-07) — the single-line SITE_NAME at Bootstrap's
   default .navbar-brand font-size (20px) was wide enough that the hamburger+logo+text
   left no room for the search/bell/theme icons on the right, so the flex row wrapped
   onto a second line. Splitting the name across two short, smaller lines (first word,
   then the rest — see header.php) keeps everything on one row on any phone width.
   Desktop keeps the original single-line span untouched. */
.site-name-stacked {
  line-height: 1.15;
}
.site-name-stacked span:first-child {
  font-size: .95rem;
}
.site-name-stacked span:last-child {
  font-size: .75rem;
  font-weight: 500;
}
/* Header icon touch targets (2026-07) — our audience skews 30-60, so the bare ~20px
   icon glyphs (no padding at all before this) were too small/tight to tap reliably.
   Padding pushes each icon's real clickable box to ~44px (the usual accessibility-
   recommended minimum), and combined with the header's own flex gap, keeps the icons
   visually farther apart too — not just individually bigger. Applies at every screen
   size on purpose (desktop mouse users aren't hurt by a bigger target; touch-screen
   laptops/tablets benefit the same as phones do). */
.header-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: .75rem;
  border-radius: .5rem;
  line-height: 1;
}
.header-icon-btn:hover {
  background: var(--bs-secondary-bg);
}
.sidebar-link.active {
  background: var(--bs-primary-bg-subtle);
  color: var(--bs-primary);
  font-weight: 600;
}
/* Sidebar mobile/desktop polish (2026-07). Desktop: the nav column sticks in place while
   the main content scrolls, instead of scrolling away with the page. Mobile (the
   offcanvas-md breakpoint Bootstrap itself uses): bigger tap targets on every nav link
   and — the actual reported bug — a bigger, unmistakable close ("X") button. Bootstrap's
   own default .btn-close hit area is only ~1em, easy to miss on a touchscreen; this
   enlarges both the glyph and its padding, and the explicit z-index is a defensive
   belt-and-braces guard (Bootstrap's own default is already 1045, above
   .mobile-bottom-nav's 1030) so the close button can never end up rendered under
   another fixed element on some as-yet-unseen page. */
@media (min-width: 768px) {
  .sidebar-column {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
  }
}
@media (max-width: 767.98px) {
  #sidebarOffcanvas.offcanvas-md {
    z-index: 1045;
  }
  .sidebar-link {
    padding-top: .65rem;
    padding-bottom: .65rem;
    font-size: 1.05rem;
  }
  .sidebar-link .bi {
    font-size: 1.15rem;
  }
}
.sidebar-close-btn {
  --bs-btn-close-width: 1.5em;
  padding: .75rem;
}
/* 3:2 matches the real product photos (media-shared/item-box-v1/, mostly 1536x1024)
   — aspect-ratio instead of a fixed px height keeps that ratio exact at every card
   width (col-6/col-md-4/col-lg-3 all size differently), where a fixed height used to
   crop by a different, arbitrary amount at each breakpoint. */
.product-thumb {
  background: var(--bs-secondary-bg);
  border-radius: .5rem;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  width: 100%;
}
/* Product card description: clamped to 2 lines by default so every card
   stays the same height regardless of how long the description text is.
   "More" expands it in place; card height grows only for that one card. */
.product-desc {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.product-desc.expanded {
  -webkit-line-clamp: unset;
  overflow: visible;
}
.desc-more {
  cursor: pointer;
  white-space: nowrap;
}
/* Responsive ratio (2026-07): 4:1 alone (previously used at every width) renders too
   thin/hard-to-read on a phone-width screen (~100px tall at 390px wide) — but a taller
   ratio at desktop width dominates the fold instead (an earlier, since-reverted change).
   Mobile (<768px) now gets its own taller 2:1 box + a separate, optional mobile image
   (see index.php's <picture> + admin/hero-slides.php's second upload field) instead of
   forcing one compromise ratio/image everywhere. aspect-ratio (not a fixed height) keeps
   the box tracking its fluid container width at every size. */
.hero-slide-box {
  width: 100%;
  aspect-ratio: 2 / 1;
  background: var(--bs-secondary-bg);
}
@media (min-width: 768px) {
  .hero-slide-box {
    aspect-ratio: 4 / 1;
  }
}
.hero-slide-box img,
.hero-slide-box iframe {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
  display: block;
}
/* Same 3:2 ratio as .product-thumb, kept exact at every width via aspect-ratio
   instead of the old per-breakpoint fixed-height overrides (320/260/220px) — those
   produced a different crop ratio at each screen size; this produces the same one
   everywhere, scaling smoothly instead of jumping between three fixed heights. */
.product-detail-img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  display: block;
  border-radius: .375rem;
}
.product-detail-img-empty {
  background: var(--bs-secondary-bg);
}
.mobile-bottom-nav {
  display: none;
}
@media (max-width: 767.98px) {
  .mobile-bottom-nav {
    display: flex;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    background: var(--bs-body-bg);
    border-top: 1px solid var(--bs-border-color);
    z-index: 1030;
  }
}
/* Lazy-image loader (2026-07) — a CSS-only shimmer shown on any <img class="lazy-img">
   until it actually finishes loading (assets/js/main.js adds .loaded on the image's own
   'load'/'error' event, or immediately if it was already cached). Pure CSS/background,
   not a JS-drawn spinner overlay, so it still shows something reasonable even if JS is
   disabled or hasn't run yet — the shimmer is just the element's own background,
   painted over by the real image content once it loads. */
.lazy-img {
  background: linear-gradient(110deg, var(--bs-secondary-bg) 8%, var(--bs-tertiary-bg, #f5f5f5) 18%, var(--bs-secondary-bg) 33%);
  background-size: 200% 100%;
  animation: lazyImgShimmer 1.4s linear infinite;
}
.lazy-img.loaded {
  animation: none;
  background: none;
}
@keyframes lazyImgShimmer {
  to { background-position-x: -200%; }
}
/* Notifications offcanvas (2026-07 redesign — see header.php) — width capped to the
   viewport itself on very narrow phones via the CSS var Bootstrap's own offcanvas reads,
   so it's never wider than the screen (the old dropdown-menu's fixed min-width had no
   such cap); a comfortable fixed width on anything wider. */
.notif-offcanvas {
  --bs-offcanvas-width: min(400px, 100vw);
}
.notif-icon-circle {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
}
.notif-item {
  color: inherit;
}
.notif-item:hover {
  background: var(--bs-secondary-bg);
}
.notif-item.notif-unread {
  background: var(--bs-primary-bg-subtle);
}
.notif-group-header {
  background: var(--bs-tertiary-bg);
  position: sticky;
  top: 0;
  z-index: 1;
}
/* Cart summary/download card (cart.php) — stacked on narrow screens so the download
   button never squeezes against the icon+text next to it (a plain d-flex row let the
   text-nowrap button crowd out the label on phones); side-by-side again from the sm
   breakpoint up, where there's room. */
.cart-summary-card {
  display: flex;
  flex-direction: column;
  gap: .75rem;
}
.cart-summary-card .cart-summary-info {
  display: flex;
  align-items: center;
  gap: .75rem;
  min-width: 0;
}
.cart-summary-card > .btn {
  width: 100%;
}
@media (min-width: 576px) {
  .cart-summary-card {
    flex-direction: row;
    align-items: center;
  }
  .cart-summary-card .cart-summary-info {
    flex: 1 1 auto;
  }
  .cart-summary-card > .btn {
    width: auto;
  }
}
/* Anti-fraud disclaimer ticker (2026-08, header.php) — a permanent, site-wide notice
   (no authorized reseller for JNV STEM kits) as a scrolling marquee so it stays
   noticeable without taking much vertical space. Standard "duplicate + translateX"
   marquee technique: padding-left:100% starts the text off-screen right, the keyframe
   slides it fully to the left. Close button is absolutely positioned so it never moves
   with the scrolling text and stays reachable regardless of scroll position. */
.ccl-notice-ticker {
  position: relative;
  overflow: hidden;
  white-space: nowrap;
  padding-inline-end: 2.75rem;
}
.ccl-notice-ticker-track {
  overflow: hidden;
  width: 100%;
}
.ccl-notice-ticker-text {
  display: inline-block;
  padding-left: 100%;
  animation: ccl-notice-scroll 32s linear infinite;
}
.ccl-notice-ticker:hover .ccl-notice-ticker-text {
  animation-play-state: paused;
}
@keyframes ccl-notice-scroll {
  0%   { transform: translateX(0); }
  100% { transform: translateX(-100%); }
}
.ccl-notice-ticker-close {
  position: absolute;
  top: 50%;
  right: 1rem;
  transform: translateY(-50%);
}
@media (prefers-reduced-motion: reduce) {
  .ccl-notice-ticker,
  .ccl-notice-ticker-text {
    white-space: normal;
    animation: none;
    padding-left: 0;
    display: block;
  }
}
