﻿/* ============================ BRAND FONTS ============================
   Self-hosted, not Google-hosted. Two reasons that matter here:

   1. A <link> to fonts.googleapis.com is a render-blocking round trip to a
      THIRD party before the browser has even seen a font URL to fetch — that
      stylesheet has to arrive before the font request can start. Serving the
      files ourselves lets index.html preload them alongside the HTML.
   2. Only the `latin` subset ships, and Nunito is a variable font, so the
      whole brand is two files (15KB + 39KB) instead of a stylesheet plus six
      weight-specific downloads.

   IMPORTANT: /fonts/ is carved out of the site-wide no-store rule in
   vercel.json alongside /images/. Without that, these would be re-downloaded
   on every single page view and the flash would come back every load — worse
   than what Google Fonts was doing, since its CDN at least allowed caching.

   unicode-range is Google's own latin block, kept so a character outside it
   makes the browser skip the file rather than render tofu. */
@font-face {
  font-family: 'Fredoka One';
  src: url('/fonts/fredoka-one-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Nunito';
  src: url('/fonts/nunito-latin-var.woff2') format('woff2');
  /* Variable (wght 200-1000). The whole declared range comes out of one file,
     so 600/700/800/900 cost no extra request and never get a fake-bold. */
  font-weight: 200 1000;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

/* ---- Metric-matched fallbacks -------------------------------------------
   THIS is what the "flash of a cursive font" actually was. Every heading and
   price was declared `font-family: 'Fredoka One', cursive`, and `cursive` is
   a real generic family: Comic Sans MS on Windows, Apple Chancery on macOS,
   Dancing Script on Android. With font-display: swap the browser paints that
   generic for the swap period, so the site really did render in Comic Sans
   for a moment. The fix is not only to load the font sooner — it is to make
   whatever shows during the swap a deliberate choice.

   These two faces wrap real system fonts and correct their metrics to the
   brand fonts', measured off the woff2 files themselves:

     Fredoka One   asc 97.4 / desc 23.6   vs Trebuchet MS Bold
     Nunito        asc 101.1 / desc 35.3  vs Segoe UI

   The size-adjust values are not the raw glyph-advance ratio — that estimate
   (104.3 / 99.5) was measured per-lowercase-letter and came out 3.4% wide and
   6.3% narrow once real mixed-case strings were rendered in a browser. These
   are the corrected numbers, measured on canvas against the loaded faces.

   ascent/descent-override make the fallback occupy the same vertical box as
   the real font on every platform, so the swap moves no text. size-adjust is
   tuned to the first local() that will resolve. Trebuchet Bold is the pick
   for Fredoka because it is the roundest face present on both Windows and
   macOS, and Fredoka One is a heavy display face — a regular weight would
   read as noticeably lighter. */
@font-face {
  font-family: 'Fredoka Fallback';
  src: local('Trebuchet MS Bold'), local('TrebuchetMS-Bold'), local('Trebuchet MS'),
       local('Arial Bold'), local('Arial-BoldMT'), local('Helvetica Bold');
  /* Spans the range so a heading asking for 700 maps here without the browser
     synthesizing bold on top of already-bold Trebuchet. */
  font-weight: 400 900;
  font-style: normal;
  ascent-override: 97.4%;
  descent-override: 23.6%;
  line-gap-override: 0%;
  size-adjust: 100.8%;
}
@font-face {
  font-family: 'Nunito Fallback';
  src: local('Segoe UI'), local('Helvetica Neue'), local('Arial'), local('Liberation Sans');
  font-weight: 200 1000;
  font-style: normal;
  ascent-override: 101.1%;
  descent-override: 35.3%;
  line-gap-override: 0%;
  size-adjust: 106.2%;
}

:root {
  /* Brand type. Never put a bare `cursive`/`fantasy` generic in a stack —
     those resolve to Comic Sans / Apple Chancery and are exactly what the
     font flash was. The *-Fallback families above are metric-matched; the
     plain names after them are the last resort if no local() resolves. */
  --font-display: 'Fredoka One', 'Fredoka Fallback', 'Trebuchet MS', system-ui, sans-serif;
  --font-body: 'Nunito', 'Nunito Fallback', 'Segoe UI', system-ui, -apple-system, sans-serif;

  --orange: #E8630A;
  --orange-light: #FF7A2E;
  --orange-dark: #C95608;
  --orange-pale: #FCEADD;
  --black: #1a1a2e;        /* repurposed as primary navy */
  --navy: #1a1a2e;
  --navy-2: #16213e;
  --navy-deep: #0d0d1a;
  --gray: #6B6B6B;
  --light-gray: #f8f6f2;   /* off-white for light sections */
  --offwhite: #f8f6f2;
  --white: #FFFFFF;
  --green: #22C55E;

  /* Warm neutrals for borders/dividers — tinted toward the off-white so
     light UI chrome feels cohesive instead of cool-gray. */
  --line: #E9E4DB;
  --line-soft: #F1EEE7;

  /* Shown behind every <img> until it decodes, so a slow connection reveals
     a soft neutral placeholder instead of whatever's behind the element
     (which on mobile can be the deep-navy body — read as a "black flash"). */
  --img-placeholder: #f0ede6;

  /* Fill for the product-card loading skeletons (see the very bottom of this
     file) — one notch darker than --line-soft so the placeholder bars read as
     content-shaped, not as empty card. */
  --skel: #ECE6DC;

  --radius: 20px;
  --radius-sm: 12px;

  /* Layered navy-tinted shadows: soft, diffuse, never muddy. */
  --shadow-xs: 0 1px 3px rgba(26,26,46,0.06);
  --shadow-sm: 0 2px 10px rgba(26,26,46,0.06), 0 1px 3px rgba(26,26,46,0.04);
  --shadow: 0 8px 30px rgba(26,26,46,0.09), 0 2px 8px rgba(26,26,46,0.04);
  --shadow-hover: 0 24px 48px -12px rgba(26,26,46,0.18), 0 8px 20px -8px rgba(26,26,46,0.1);
  --shadow-orange: 0 8px 24px rgba(232,99,10,0.28);
  --shadow-orange-hover: 0 14px 32px rgba(232,99,10,0.38);

  /* Signature easing — snappy start, silky landing. */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* Drawer/burger motion. --ease (quint-out) is right for something ARRIVING;
     it is wrong for something leaving, where a long soft tail just reads as
     lag — so the panel and scrim close on --ease-exit instead. --ease-burger
     carries a small overshoot so the X snaps into place. */
  --ease-exit: cubic-bezier(0.4, 0.02, 0.9, 0.55);
  --ease-burger: cubic-bezier(0.34, 1.3, 0.5, 1);
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; overflow-x: hidden; }

/* Every <img> on the site is a real photo (hero slides, lifestyle shots,
   reviewer avatars, product shots) — never an icon — so this is safe
   to apply universally. The gradient sweeps continuously; once the photo
   decodes it's opaque and covers the whole box via object-fit:cover, so
   the shimmer is simply hidden behind it — no load-event JS required. */
@keyframes imgShimmer {
  0% { background-position: 150% 0; }
  100% { background-position: -50% 0; }
}
img {
  background-color: var(--img-placeholder);
  background-image: linear-gradient(100deg, rgba(255,255,255,0) 20%, rgba(255,255,255,0.8) 50%, rgba(255,255,255,0) 80%);
  background-size: 250% 100%;
  animation: imgShimmer 1.6s ease-in-out infinite;
}

/* ---- PHOTO LOCK (long-press / right-click) -------------------------------
   Long-pressing any photo on a phone or tablet pops the native sheet — Save
   to Photos, Copy, Share, Look Up — and right-clicking does the same on a
   desktop. That hands the product photography out one tap at a time, so it
   is switched off site-wide rather than per page.

   Three separate mechanisms, because they guard three different routes to
   the same menu:
     -webkit-touch-callout  the iOS long-press sheet itself;
     user-select            the iOS 16+ Live Text path, which selects INSIDE
                            the photo and offers Copy/Share from there;
     -webkit-user-drag      dragging the file out to the desktop or a tab.
   app.js blocks contextmenu and dragstart as a backup for the engines that
   ignore the CSS — Android Chrome's long-press menu is the notable one.

   None of this touches normal interaction: taps still open the gallery and
   the carousels still swipe, because neither is a selection or a drag. */
img, picture, svg {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* A long-press on a photo that sits inside a link (blog cards, the review
   photo lightbox) targets the LINK, so iOS shows its link preview — which
   carries Save to Photos of its own. Deliberately a rule by itself: an
   engine without :has() must drop this one only, never the block above. */
a:has(img) { -webkit-touch-callout: none; }

/* Large lifestyle/background photos (About/Contact hero, home mission
   banner, 10%-off popup — hero slides excluded, they
   already crossfade via their own .active opacity transition above) fade in
   once actually decoded instead of snapping straight from shimmer to photo.
   app.js adds .img-ready on load/error, with a hard fallback timer so an
   image can never get stuck invisible if that JS somehow doesn't run. */
.lifestyle-img { opacity: 0; transition: opacity 0.3s ease; }
.lifestyle-img.img-ready { opacity: 1; }

body {
  font-family: var(--font-body);
  background: white;
  color: var(--black);
  cursor: default;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

::selection { background: var(--orange); color: #fff; }

/* ==================== FORM FIELD BASE (iOS AUTO-ZOOM) ====================
   iOS Safari zooms the whole viewport in whenever you focus a field whose
   computed font-size is under 16px — the page visibly jumps and stays
   zoomed after the keyboard closes. 16px is therefore a FUNCTIONAL floor
   for every text field on this site, not a stylistic choice: if a field
   needs to look smaller, change its padding/width, never its font-size.
   This element-level rule catches any field that has no class of its own
   (or a future one whose class forgets to set a size); the per-component
   rules below (.email-input, .form-input/.form-textarea/.form-select,
   .offer-input, .nav-search-input, .chat-input) each restate 16px so the
   floor survives their higher specificity. */
input, textarea, select { font-size: 16px; }

/* HERO VIDEO (task 121) — single looping background video, replacing the old
   4-photo crossfade. Light placeholder fallback so a slow-loading poster
   never flashes black/navy before it decodes. No shimmer animation here
   (unlike the universal `img` rule at the top of this file): the poster
   attribute itself already shows instantly, and a <video> isn't an <img> so
   it never picked up that rule in the first place. */
#heroSection { background: var(--img-placeholder); }
#heroSection .hero-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
}

/* CUSTOM CURSOR */
.cursor { display: none; }

/* PAGE SYSTEM */
.page { display: none; }
.page.active { display: block; }
/* showPage() (products.js) still adds/removes a "page-transition" class on
   every JS-driven navigation — harmless no-op now, left alone rather than
   touching JS for a pure CSS fix. It used to drive a translateY(16px)->0
   slide-up entrance (see git history / project memory for why: an earlier
   opacity-based version let the mobile-only navy body bg bleed through
   during the fade). That transform version had its own bug — the incoming
   page rendered at its OFFSET (translateY 16px, i.e. visibly lower) for
   the animation's very first frame, then animated back to translateY(0),
   which read as the whole page "dropping down" and snapping back up on
   every single navigation. Removed outright per explicit user request
   (no entrance motion at all) rather than just shrinking the offset —
   navigation should feel instant. This does NOT reintroduce the opacity
   black-flash bug: no opacity, no transform, nothing here composites
   against whatever's behind the page. */

/* ANNOUNCE BAR — slim single line sitting above the nav: message left,
   secondary note right (task 117 reference proportions; wording unchanged).
   .announce-bar is a plain wrapper here (desktop needs no safe-area strip
   or fixed positioning); .announce-bar-inner (task 120) carries the actual
   coloured bar so a mobile-only neutral strip can be added to the OUTER
   element without a single box needing two different background colours —
   see the mobile rule's own comment for why. */
.announce-bar-inner {
  background: var(--orange);
  color: white;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 20px;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.2px;
  position: relative;
  overflow: hidden;
  white-space: nowrap;
}

.announce-bar-inner::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
  animation: shimmer 3s infinite;
}

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

.announce-main, .announce-secondary { overflow: hidden; text-overflow: ellipsis; }
.announce-dot { margin: 0 8px; opacity: 0.6; }
.announce-secondary { opacity: 0.85; font-weight: 700; }
/* task 122's rotating single-message bar is mobile-only (≤900px below) — on
   desktop/tablet this stays hidden and the two spans above are what render. */
.announce-rotator { display: none; }
/* The notch-safe-area strip is mobile-only too — desktop's announce bar was
   never position:fixed (so it never needed this), and --safe-top itself is
   only defined inside the mobile :root block below, so without this rule
   the div would render at `height:auto` on desktop (harmlessly collapsing
   to 0 in practice, but relying on that rather than saying so explicitly). */
.safe-top-strip { display: none; }

/* NAVBAR */
nav {
  background: var(--navy);
  padding: 14px 5%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: sticky;
  top: 0;
  z-index: 500;
  box-shadow: 0 2px 24px rgba(0,0,0,0.25);
}
/* Explicit desktop order (task 123) — index.html's DOM order is now
   hamburger, search, logo, nav-center, nav-right (mobile reads top-to-bottom
   that way), so desktop needs its own order back to logo / links / search /
   cart. Without this, .nav-search-btn — NOT display:none on desktop, unlike
   the hamburger — would visually jump to the front there too just because it
   comes earlier in the DOM now. */
nav > .logo { order: 1; }
nav > .nav-center { order: 2; }
nav > .nav-search-btn { order: 3; }
nav > .nav-right { order: 4; }
nav > .hamburger { order: 5; } /* display:none on desktop anyway */

.logo {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  text-decoration: none; /* now a real <a> (routing) — was a div, never needed this before */
}

/* Paw badge + wordmark on every screen size (desktop, iPad and mobile).
   No tilt/rotation on hover or tap; the logo stays completely still. */
/* task 123: real <img> now (the new logo is its own finished circular badge
   — orange ring, cream interior, no wrapper background/padding needed the
   way the old flat paw-on-black-circle icon did). border-radius stays as a
   safety clip against any stray edge pixels; object-fit:cover keeps it from
   stretching if the box and image aspect ratio ever drift. */
.logo-circle {
  display: block;
  width: 40px; height: 40px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

.logo-text {
  font-family: var(--font-display);
  font-size: 24px;
  color: #fff;
  -webkit-font-smoothing: antialiased;
}

.logo-text span { color: var(--orange); }

.nav-links {
  display: flex;
  gap: 28px;
  list-style: none;
}

.nav-links a {
  text-decoration: none;
  color: #fff;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  position: relative;
  transition: color 0.2s;
}

.nav-links a::after {
  content: '';
  position: absolute;
  bottom: -4px; left: 0;
  width: 0; height: 2px;
  background: var(--orange);
  transition: width 0.3s ease;
}

.nav-links a:hover { color: var(--orange); }
.nav-links a:hover::after { width: 100%; }

.nav-right { display: flex; align-items: center; gap: 14px; }

.cart-btn {
  display: inline-flex; /* was a <button>, default inline-block; now a real <a href> (routing) */
  align-items: center;
  background: var(--orange);
  color: white;
  text-decoration: none;
  border: none;
  padding: 10px 22px;
  border-radius: 50px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s var(--ease);
  position: relative;
}

.cart-btn:hover {
  background: var(--orange-light);
  transform: translateY(-2px);
  box-shadow: var(--shadow-orange);
}

.cart-count {
  position: absolute;
  top: -6px; right: -6px;
  background: var(--black);
  color: white;
  width: 20px; height: 20px;
  border-radius: 50%;
  font-size: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 900;
}

/* BUTTONS */
.btn-primary {
  background: var(--orange);
  color: white;
  padding: 15px 34px;
  border-radius: 50px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  border: none;
  cursor: pointer;
  transition: all 0.25s var(--ease);
  box-shadow: var(--shadow-orange);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
}

.btn-primary:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-orange-hover);
  background: var(--orange-light);
}

.btn-secondary {
  background: white;
  color: var(--black);
  padding: 15px 34px;
  border-radius: 50px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  border: 2px solid var(--line);
  cursor: pointer;
  transition: all 0.25s var(--ease);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.btn-secondary:hover {
  border-color: var(--orange);
  color: var(--orange);
  transform: translateY(-3px);
}

.btn-black {
  background: var(--black);
  color: white;
  padding: 11px 20px;
  min-height: 40px;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 13.5px;
  border: none;
  cursor: pointer;
  transition: all 0.2s var(--ease);
  width: 100%;
}

/* The product card's "Add To Cart". It used to go ORANGE on hover, which is
   also what a touch device paints when a finger lands on it (a tap fires
   :hover), so tapping a card flashed orange before the toast — the thing
   reported in task 83. It now deepens into the secondary navy instead, and
   the whole rule sits behind `hover: hover` so a touch paints no hover state
   at all. Same reasoning as the back button in task 78.
   NOTE: this is scoped to .btn-black (the card button). Orange remains the
   brand's interaction accent everywhere else on the site — see the audit in
   task 83's report; it is the live design system, not leftover styling. */
@media (hover: hover) {
  .btn-black:hover { background: var(--navy-2); transform: translateY(-2px); }
}
/* Touch press feedback for the same button, since it no longer gets one from
   :hover: a slightly lighter navy and the site's standard 0.97 press. */
.btn-black:active { background: var(--navy-2); }

/* SECTION HELPERS */
.section-tag {
  display: inline-block;
  background: var(--orange-pale);
  color: var(--orange);
  font-weight: 800;
  font-size: 11px;
  padding: 6px 16px;
  border-radius: 50px;
  margin-bottom: 16px;
  text-transform: uppercase;
  letter-spacing: 2.2px;
}

.section-title {
  font-family: var(--font-display);
  font-size: clamp(27px, 4vw, 46px);
  color: var(--black);
  margin-bottom: 12px;
  line-height: 1.12;
  letter-spacing: 0.01em;
  -webkit-font-smoothing: antialiased;
}

.section-title span { color: var(--orange); }
.section-sub { font-size: 16.5px; color: var(--gray); font-weight: 600; line-height: 1.7; letter-spacing: 0.1px; }

/* PAW DIVIDER — subtle signature motif marking the top of each major section */
.paw-divider { display: flex; justify-content: center; pointer-events: none; margin-bottom: 38px; }
.paw-divider svg { display: block; opacity: 0.14; }
.paw-divider--on-light svg { fill: var(--navy); }
.paw-divider--on-dark svg { fill: #fff; }

/* ==================== HOME PAGE ==================== */

/* HERO */
.hero {
  background: radial-gradient(ellipse at 25% 45%, #FFE4CC 0%, #FFDBBE 45%, #FFD0AD 100%);
  padding: 70px 5% 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: end;
  gap: 40px;
  overflow: hidden;
  position: relative;
  min-height: 560px;
}

.hero::after {
  content: '';
  position: absolute;
  width: 330px; height: 330px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' fill='%23ffffff'%3E%3Cellipse cx='50' cy='67' rx='20' ry='16'/%3E%3Cellipse cx='27' cy='47' rx='9' ry='12'/%3E%3Cellipse cx='42' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='58' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='73' cy='47' rx='9' ry='12'/%3E%3C/svg%3E") center / contain no-repeat;
  opacity: 0.06;
  top: -30px; right: -30px;
  pointer-events: none;
  transform: rotate(12deg);
}

.hero-content { padding-bottom: 16px; }

.hero-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: white;
  border: 2px solid var(--orange);
  color: var(--orange);
  padding: 5px 14px;
  border-radius: 50px;
  font-size: 12px;
  font-weight: 800;
  margin-bottom: 20px;
  animation: fadeUp 0.5s ease both;
}

.hero h1 {
  font-family: var(--font-display);
  font-size: clamp(36px, 5vw, 60px);
  line-height: 1.1;
  margin-bottom: 18px;
  animation: fadeUp 0.6s ease both 0.1s;
}

.hero h1 span { color: var(--orange); }

.hero p {
  font-size: 17px;
  color: var(--gray);
  line-height: 1.6;
  margin-bottom: 32px;
  max-width: 420px;
  font-weight: 600;
  animation: fadeUp 0.6s ease both 0.2s;
}

.hero-btns {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  animation: fadeUp 0.6s ease both 0.3s;
}

.hero-visual {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  animation: fadeUp 0.8s ease both 0.2s;
}

.hero-img-box {
  position: relative;
  width: 100%;
  max-width: 400px;
}

.hero-dog {
  width: 100%;
  height: 360px;
  background: linear-gradient(135deg, #FFD4B8 0%, #FFBF99 100%);
  border-radius: 24px 24px 0 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 110px;
  position: relative;
  overflow: hidden;
}

.hero-dog::before {
  content: '';
  position: absolute;
  width: 200px; height: 200px;
  background: rgba(255,255,255,0.2);
  border-radius: 50%;
  top: -50px; right: -50px;
}

.floating-tag {
  position: absolute;
  background: white;
  border-radius: var(--radius-sm);
  padding: 12px 16px;
  box-shadow: var(--shadow);
  font-weight: 800;
  animation: float 3s ease-in-out infinite;
}

.tag-price {
  top: 24px; right: -16px;
}

.tag-price .now { font-size: 22px; color: var(--orange); font-family: var(--font-display); }

.tag-stars {
  bottom: 24px; left: -16px;
  font-size: 12px;
  animation-delay: 1s;
}

/* TRUST BAR */
.trust-bar {
  background: var(--black);
  padding: 18px 5%;
  display: flex;
  justify-content: center;
  gap: 40px;
  flex-wrap: wrap;
}

.trust-item {
  display: flex;
  align-items: center;
  gap: 8px;
  color: white;
  font-weight: 700;
  font-size: 13px;
}


.products-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 22px;
}

/* Photography-first card: the image owns 70-80% of the card, text below is
   kept to name/rating/price/swatches/button only — no borders, a soft
   floating shadow instead, consistent height regardless of name length. */
.product-card {
  background: white;
  border-radius: var(--radius);
  overflow: hidden;
  border: none;
  transition: all 0.3s var(--ease);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  box-shadow: 0 1px 2px rgba(26,26,46,0.04), 0 6px 20px -4px rgba(26,26,46,0.10);
  display: flex;
  flex-direction: column;
  height: 100%;
}

.product-card:active {
  transform: scale(0.98);
  box-shadow: 0 1px 2px rgba(26,26,46,0.04), 0 2px 8px -2px rgba(26,26,46,0.10);
}

.product-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(26,26,46,0.06), 0 16px 32px -8px rgba(26,26,46,0.16);
}

.product-img-wrap { position: relative; overflow: hidden; flex-shrink: 0; }

.product-img {
  width: 100%;
  height: 300px;
  background: linear-gradient(145deg, #FFE8D5 0%, #FFD9C0 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 80px;
  transition: transform 0.4s var(--ease);
  overflow: hidden;
}

/* Same trick as .det-slide:has(img): white behind a real photo so the gutter
   `contain` opens up is invisible, gradient behind an emoji card. Without it
   the card wears a peach band above the photo and nothing below (the photo's
   own white runs into the card body), which reads as a misaligned image. */
.product-img:has(img) { background: #fff; }

/* CONTAIN for the same reason as the detail carousel: the card's photo area
   is 300x~280 (and 220 / 190 tall further down), never square, so `cover`
   cropped a band off every product photo — the one thing a shopper is here
   to look at. width:auto + background:none keep the shimmer out of the
   letterbox gutters; the card's own gradient shows there instead. */
.product-img img {
  width: auto;
  max-width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  background: none;
  animation: none;
}

.product-card:hover .product-img { transform: scale(1.06); }


/* Small + subtle — a soft translucent circle just big enough to tap, not a
   bold solid-white disc competing with the photo. */
.wishlist-btn {
  position: absolute;
  top: 10px; right: 10px;
  width: 28px; height: 28px;
  background: rgba(255,255,255,0.85);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  font-size: 14px;
  color: #FF0000; /* hearts always solid red */
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 1px 4px rgba(26,26,46,0.18);
  transition: all 0.2s var(--ease);
  opacity: 0;
}

.product-card:hover .wishlist-btn { opacity: 1; }
.wishlist-btn:hover { transform: scale(1.15); }

.product-info {
  padding: 14px 16px 16px;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  gap: 2px;
}
/* Pin the Add To Cart button to the bottom so every card aligns,
   regardless of name length or how many variant-picker rows it has. */
.product-info .btn-black { margin-top: auto; }
.product-name {
  font-weight: 800;
  font-size: 14px;
  line-height: 1.3;
  margin-bottom: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* overflow:hidden (needed for the ellipsis) strips flexbox's normal
     min-height:auto shrink protection inside a flex column — without this,
     a too-tight fixed-height card silently collapses the name to 0 height
     (invisible) instead of visibly overflowing. Keep it un-shrinkable. */
  flex-shrink: 0;
}
/* Star row + "4.8 · 322 ratings", on one line. gap (not a margin on the text)
   so the row stays centred as a unit in the carousel's centred card layout. */
/* BASELINE row, not a flex row. align-items:center lines up the three BOXES,
   which is not the same as lining up the ink inside them: "4.8" has no
   descender so it sat 0.7px high, while "ratings" has one so it sat 0.5px
   low — visibly out of line against the stars at card size. Laying the parts
   out inline instead puts the number and the count on ONE shared text
   baseline automatically, and leaves the star box as the single thing that
   needs positioning (see .product-stars .rv-stars below). */
.product-stars {
  display: block;
  color: #FFB800;
  /* Scales with the viewport so the single line survives a 2-up grid at
     320px and still reads at full size once the grid goes 3- and 4-up.
     See .product-stars-num for why this is a clamp and not a fixed size. */
  font-size: clamp(7px, 2.1vw, 11px);
  line-height: 1.6;
  margin-bottom: 8px;
  flex-shrink: 0;
  min-height: 15px;
  white-space: nowrap;
}
.product-stars .rv-star { width: 1.25em; height: 1.25em; }
/* Drops the star box so the stars' OPTICAL centre lands on the text's cap
   band. Derived, not eyeballed, and every term is measured rather than
   assumed (task 106 — the previous pass guessed two of the four and landed
   0.64px low here, which is what read as the stars sagging):

     cap height   0.71875em  — Nunito's digit height, measured off the shipped
                               woff2, not the 0.705 that was assumed before
     star box     1.25em     — set just above
     optical centre = the box centre, because starSvg() now ships a viewBox
                               recentred on the star's centroid (see there)

   The number is 1.05em and the count is 1em, so they have cap-band midlines
   0.37734em and 0.35938em above the shared baseline. The stars sit BETWEEN
   those two, so they target the mean, 0.36836em — splitting a difference of
   0.2px rather than picking a side and being 0.2px out against the other.
   The -0.15em on .rv-star has already dropped the box, so the transform only
   carries the remainder:

     translateY = 0.5 x 1.25 - 0.36836 - 0.15 = 0.10664em

   A TRANSFORM and not more vertical-align, because a transform is applied
   after layout: it moves the ink without touching the line box, so tuning it
   can never resize a card. Being unrounded, one value also holds across the
   whole clamp() range. */
.product-stars .rv-stars {
  vertical-align: baseline;
  transform: translateY(0.10664em);
  margin: 0 0.45em;
}
.product-stars span { color: var(--gray); font-weight: 600; }

/* Past the 4-up breakpoint the columns stop growing, so pin the row at its
   intended size instead of letting vw keep scaling it with the window. */
@media (min-width: 1100px) { .product-stars { font-size: 11px; } }

.product-price {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-bottom: 10px;
}

.price-now { font-family: var(--font-display); font-size: 19px; color: var(--orange); }
/* No .price-was rule, deliberately: compare-at pricing is never displayed on
   this site. See variantPriceHtml in products.js. */
.price-from { font-size: 11px; color: var(--gray); font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; }

/* BEST PRODUCT — spotlight section right under the trust bar, before WHY PAWHAUL.
   Orange background like the rest of the brand's CTA sections; the product shot
   is a transparent cutout (images/products/water-bottle-cutout*) so it reads as a
   floating hero object rather than a boxed-in product photo. */
.best-product-section {
  background: var(--orange);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 64px;
  padding: 90px 6%;
  overflow: hidden;
}

.bp-image-wrap { flex: 0 0 auto; display: flex; justify-content: center; }
.bp-image {
  width: 340px;
  max-width: 100%;
  height: auto;
  filter: drop-shadow(0 30px 40px rgba(0,0,0,0.25));
  transform: rotate(-4deg);
  /* This is a real cutout with transparent pixels (not a photo that fills its
     box once decoded), so the sitewide `img` shimmer-placeholder background
     would otherwise show through it forever instead of just until it loads. */
  background: none;
  animation: none;
}
/* task 127: stand-in for the real cutout shot until the new 2-in-1 Portable
   Bottle has one — a dashed-border card with an emoji and an explicit "Photo
   coming soon" tag, not a silent blank box, so the gap is obvious on the live
   site and not just in a code comment. Remove this class (keep `.bp-image`
   as a real <img>, same as the old cutout markup) once a real photo exists. */
.bp-image-placeholder {
  width: 260px;
  height: 320px;
  max-width: 70vw;
  border-radius: 28px;
  background: rgba(255, 255, 255, 0.14);
  border: 2px dashed rgba(255, 255, 255, 0.55);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
}
.bp-image-placeholder-emoji { font-size: 96px; line-height: 1; }
.bp-image-placeholder-tag {
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.9);
  background: rgba(13, 13, 26, 0.3);
  padding: 6px 12px;
  border-radius: 50px;
}

.bp-content { max-width: 520px; color: white; }
.bp-title {
  font-family: var(--font-display);
  font-size: clamp(26px, 3.6vw, 42px);
  line-height: 1.15;
  margin-bottom: 18px;
  letter-spacing: 0.01em;
}
.bp-desc {
  font-size: 16.5px;
  color: rgba(255,255,255,0.9);
  font-weight: 600;
  line-height: 1.7;
  margin-bottom: 30px;
}
.bp-actions { display: flex; gap: 14px; flex-wrap: wrap; }

.btn-outline-white {
  display: inline-block;
  background: transparent;
  color: white;
  border: 2px solid rgba(255,255,255,0.7);
  text-decoration: none;
  padding: 12px 28px;
  border-radius: 50px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  cursor: pointer;
  transition: all 0.25s var(--ease);
}
.btn-outline-white:hover { background: rgba(255,255,255,0.12); border-color: white; transform: translateY(-3px); }

@media (max-width: 900px) {
  .best-product-section { flex-direction: column; text-align: center; gap: 36px; padding: 64px 6%; }
  .bp-content { max-width: 100%; }
  .bp-actions { justify-content: center; }
  .bp-image { width: 230px; transform: rotate(-3deg); }
}

/* WHY SECTION */
.why-section {
  /* Subtle depth: barely-there navy gradient instead of a flat fill.
     Stays well within readable contrast for the white text on top. */
  background: linear-gradient(160deg, #1f2040 0%, var(--navy) 55%, #14152b 100%);
  padding: 140px 5%;
  color: white;
}

.why-header { text-align: center; margin-bottom: 56px; }
.why-header .section-title { color: white; }
.why-header .section-sub { color: rgba(255,255,255,0.55); margin: 0 auto; max-width: 480px; }

.why-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
}

.why-card {
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius);
  padding: 30px 22px;
  text-align: center;
  transition: all 0.3s var(--ease);
}

.why-card:hover {
  background: rgba(232,99,10,0.12);
  border-color: var(--orange);
  border-top: 3px solid var(--orange);
  transform: translateY(-6px);
}

.why-icon { font-size: 42px; margin-bottom: 16px; display: block; }
.why-title { font-family: var(--font-display); font-size: 18px; margin-bottom: 8px; }
.why-desc { font-size: 13px; color: rgba(255,255,255,0.55); line-height: 1.6; font-weight: 600; }

/* MISSION */
.mission-section {
  background: var(--orange);
  padding: 96px 5%;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 60px;
  align-items: center;
}

.mission-section h2 {
  font-family: var(--font-display);
  font-size: clamp(26px, 4vw, 42px);
  color: white;
  line-height: 1.2;
  margin-bottom: 18px;
}

.mission-section p {
  color: rgba(255,255,255,0.85);
  font-size: 16px;
  line-height: 1.7;
  font-weight: 600;
  margin-bottom: 26px;
}

.btn-white {
  display: inline-block; /* was a <button>, default inline-block; now a real <a href> (routing) */
  background: white;
  color: var(--orange);
  text-decoration: none;
  padding: 14px 30px;
  border-radius: 50px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  border: none;
  cursor: pointer;
  transition: all 0.25s var(--ease);
}

.btn-white:hover { transform: translateY(-3px); box-shadow: 0 10px 28px rgba(0,0,0,0.15); }

.stats-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}

.stat-card {
  background: rgba(255,255,255,0.15);
  border-radius: var(--radius);
  padding: 26px 22px;
  text-align: center;
  border: 1px solid rgba(255,255,255,0.2);
  transition: all 0.3s var(--ease);
}

.stat-card:hover { background: rgba(255,255,255,0.25); transform: translateY(-4px); }

.stat-num { font-family: var(--font-display); font-size: 38px; color: white; display: block; }
.stat-label { color: rgba(255,255,255,0.8); font-size: 13px; font-weight: 700; }

/* REVIEWS (navy section, white cards) */
.reviews-section { padding: 140px 5%; background: var(--navy); }
.reviews-header { text-align: center; margin-bottom: 56px; }
.reviews-section .section-title { color: #fff; }
.reviews-section .section-title span { color: var(--orange); }

.reviews-section .reviews-grid-wrap {
  position: relative;
}

/* ── 10% OFF POPUP ─────────────────────────────────────────── */
.offer-overlay {
  position: fixed; inset: 0; background: rgba(13,13,26,0.62);
  z-index: 1000; opacity: 0; pointer-events: none;
  transition: opacity 0.4s ease;
}
.offer-overlay.active { opacity: 1; pointer-events: auto; }

/* Centered modal: lifestyle photo + offer body. Desktop = side-by-side,
   mobile (≤900px below) = photo stacked on top, near full-screen. */
.offer-popup {
  position: fixed; top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(0.92);
  width: min(820px, calc(100% - 40px));
  background: #1a1a2e; border-radius: 24px;
  overflow: hidden; display: flex; box-sizing: border-box;
  box-shadow: 0 40px 90px -20px rgba(0,0,0,0.55);
  z-index: 1001;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.45s var(--ease), transform 0.45s cubic-bezier(0.34, 1.3, 0.4, 1),
              visibility 0s linear 0.45s;
}
.offer-popup.active {
  opacity: 1; visibility: visible; pointer-events: auto;
  transform: translate(-50%, -50%) scale(1);
  transition: opacity 0.45s var(--ease), transform 0.45s cubic-bezier(0.34, 1.3, 0.4, 1);
}

.offer-photo {
  position: relative; width: 45%; min-height: 480px; flex-shrink: 0;
  background: #1a1a2e; /* same as the card: a raster gap can never flash a lighter color */
  overflow: hidden;
}
.offer-photo img {
  /* 1px overscan on every edge: dvh/percentage heights land on fractional
     pixels, and during scroll repaints the image can rasterize a hairline
     short of its box, flashing a seam at the photo/body boundary */
  position: absolute; inset: -1px; width: calc(100% + 2px); height: calc(100% + 2px);
  object-fit: cover; object-position: center 62%; /* keep the dog in frame on wide/short crops */
  /* Override the universal <img> shimmer (top of file): this img fills
     .offer-photo exactly, so its own background would otherwise hide the
     navy fallback behind it — reintroducing the light-flash-on-navy-card
     mismatch this component was already built to avoid. This image also
     reuses the already-preloaded hero slide, so it's never really "loading". */
  background: #1a1a2e;
  animation: none;
}
/* Blend the photo edge into the navy body so the two halves read as one card */
.offer-photo::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(to right, transparent 62%, #1a1a2e 100%);
}

.offer-close {
  position: absolute; top: 14px; right: 14px; z-index: 5;
  background: rgba(13,13,26,0.55); border: none; color: #fff;
  width: 38px; height: 38px; border-radius: 50%; font-size: 16px;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: background 0.2s, transform 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.offer-close:hover { background: rgba(13,13,26,0.8); }
.offer-close:active { transform: scale(0.92); }

.offer-body {
  flex: 1; display: flex; flex-direction: column; justify-content: center;
  padding: 48px 44px; min-width: 0;
  position: relative; background: #1a1a2e; /* paint over any seam at the photo edge */
}
.offer-kicker {
  color: var(--orange); font-size: 12px; font-weight: 900;
  letter-spacing: 2.5px; text-transform: uppercase; margin-bottom: 12px;
}
.offer-headline {
  color: #fff; font-size: clamp(26px, 3.2vw, 36px); line-height: 1.15;
  margin: 0 0 12px; font-family: var(--font-display); font-weight: 400;
}
.offer-headline span { color: var(--orange); }
.offer-sub { color: rgba(255,255,255,0.68); font-size: 15px; font-weight: 600; line-height: 1.6; margin: 0 0 24px; }

.offer-input {
  width: 100%; padding: 14px 16px; border: 1.5px solid rgba(255,255,255,0.35);
  border-radius: 12px; background: rgba(255,255,255,0.06); color: #fff;
  font-size: 16px; box-sizing: border-box; margin-bottom: 12px;
  outline: none; font-family: inherit; font-weight: 600;
  transition: border-color 0.2s, background 0.2s;
}
.offer-input::placeholder { color: rgba(255,255,255,0.35); }
.offer-input:focus { border-color: var(--orange); background: rgba(255,255,255,0.1); }

/* Inline format error under the email field. Sits between the input and the
   submit button; [hidden] keeps it out of the layout entirely when clean, so
   the popup doesn't reserve a gap for an error that usually never shows. */
.offer-error {
  color: #FF8A80; font-size: 13px; font-weight: 700; line-height: 1.45;
  margin: -4px 0 12px; display: block;
}
.offer-error[hidden] { display: none; }
.offer-input[aria-invalid="true"] { border-color: #FF8A80; }
.offer-input[aria-invalid="true"]:focus { border-color: #FF8A80; }

.offer-btn {
  width: 100%; padding: 16px; background: var(--orange); color: #fff;
  border: none; border-radius: 12px; font-size: 16px; font-weight: 800;
  cursor: pointer; font-family: inherit; transition: background 0.2s, transform 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.offer-btn:hover { background: #cf5800; }
.offer-btn:disabled { opacity: 0.65; cursor: default; }

.offer-dismiss {
  background: none; border: none; margin-top: 16px; align-self: center;
  color: rgba(255,255,255,0.45); font-size: 13px; font-weight: 700;
  font-family: inherit; cursor: pointer; text-decoration: underline;
  text-underline-offset: 3px; -webkit-tap-highlight-color: transparent;
  transition: color 0.2s;
}
.offer-dismiss:hover { color: rgba(255,255,255,0.75); }

/* Code-reveal panel shown in place of the form after a successful signup. */
.offer-success-msg {
  color: rgba(255,255,255,0.78); font-size: 15px; font-weight: 600;
  line-height: 1.6; margin: 0 0 18px;
}
.offer-code-row {
  display: flex; align-items: center; gap: 10px;
  border: 2px dashed rgba(232,99,10,0.55);
  background: rgba(232,99,10,0.10);
  border-radius: 12px; padding: 14px 16px; margin-bottom: 18px;
}
.offer-code {
  flex: 1; font-family: var(--font-display); font-size: 20px;
  letter-spacing: 1.5px; color: var(--orange); text-align: center;
}
.offer-copy-btn {
  background: var(--orange); color: #fff; border: none;
  padding: 10px 16px; border-radius: 8px; font-weight: 800; font-size: 13px;
  cursor: pointer; font-family: inherit; white-space: nowrap;
  transition: background 0.2s; -webkit-tap-highlight-color: transparent;
}
.offer-copy-btn:hover { background: #cf5800; }
.offer-continue {
  width: 100%; padding: 16px; background: transparent; color: #fff;
  border: 1.5px solid rgba(255,255,255,0.35); border-radius: 12px;
  font-size: 15px; font-weight: 800; cursor: pointer; font-family: inherit;
  transition: border-color 0.2s, background 0.2s;
  -webkit-tap-highlight-color: transparent;
}
.offer-continue:hover { border-color: var(--orange); background: rgba(232,99,10,0.08); }

/* GET 10% OFF SIDE TAB (task 122, restyled per "make it look more
   appealing" feedback) — vertical tab protruding from the left edge,
   reference-matched structure (their "Spin to Win" tab) but in the site's
   own brand colours. Switched from plain navy to the orange/orange-light
   gradient the rest of the offer UI already uses (popup button, discount
   badges) so it actually reads as a deal worth tapping, plus a tag icon and
   a slow attention-pulse on the shadow — subtle enough not to be annoying
   (6s cycle), present enough to catch a passing eye. Hidden by default and
   shown only via .show (dismissOffer()/markOfferClaimed() in app.js);
   mobile-only, same scope as the rest of task 122's reference-match work —
   see the ≤900px block below for the part that actually reveals it. */
.offer-side-tab {
  display: none;
  position: fixed;
  top: 58%;
  left: 0;
  transform: translateY(-50%) translateX(-100%);
  z-index: 480;
  background: linear-gradient(160deg, var(--orange-light), var(--orange));
  color: #fff;
  border-radius: 0 16px 16px 0;
  box-shadow: 4px 0 24px rgba(232,99,10,0.45);
  flex-direction: column;
  align-items: center;
  padding: 14px 8px 12px;
  transition: transform 0.3s var(--ease);
  animation: offerTabPulse 6s ease-in-out infinite;
}
.offer-side-tab.show { transform: translateY(-50%) translateX(0); }
@keyframes offerTabPulse {
  0%, 60%, 100% { box-shadow: 4px 0 24px rgba(232,99,10,0.45); }
  80% { box-shadow: 4px 0 30px 6px rgba(232,99,10,0.75); }
}
.offer-side-tab-icon {
  width: 22px; height: 22px;
  margin-bottom: 6px;
  flex-shrink: 0;
}
/* The icon stays upright; only the text itself is rotated into a vertical
   column — writing-mode/rotate on the whole button would also sideways-flip
   the icon, since it isn't text. */
.offer-side-tab-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  background: none;
  border: none;
  color: #fff;
  cursor: pointer;
  padding: 2px;
}
.offer-side-tab-label span {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 15px;
  letter-spacing: 0.4px;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  text-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.offer-side-tab-label:hover span { color: var(--navy-2); }
.offer-side-tab-close {
  background: rgba(255,255,255,0.22);
  border: none;
  color: #fff;
  width: 20px; height: 20px;
  border-radius: 50%;
  font-size: 10px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 8px;
  -webkit-tap-highlight-color: transparent;
}
.offer-side-tab-close:hover { background: rgba(255,255,255,0.38); }

@media (max-width: 900px) {
  .offer-side-tab.show { display: flex; }
}
@media (prefers-reduced-motion: reduce) {
  .offer-side-tab { animation: none; }
}

/* Near full-screen on mobile: photo banner on top, content below.
   Internal scroll (rare, tiny screens) instead of overflowing the viewport. */
@media (max-width: 900px) {
  .offer-popup {
    flex-direction: column;
    width: calc(100% - 24px);
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
    overscroll-behavior: contain;
    border-radius: 20px;
  }
  .offer-photo { width: 100%; height: min(40dvh, 360px); min-height: 220px; }
  .offer-photo::after { background: linear-gradient(to bottom, transparent 55%, #1a1a2e 100%); }
  /* overlap the body 1px onto the photo so no hairline can open between them */
  .offer-body { padding: 9px 22px 28px; margin-top: -1px; }
  .offer-headline { font-size: clamp(27px, 7.5vw, 34px); }
  .offer-sub { font-size: 14px; margin-bottom: 20px; }
}

@media (max-width: 900px) {
  .reviews-section .reviews-grid-wrap::after {
    content: '';
    position: absolute;
    top: 0; right: 0; bottom: 12px;
    width: 48px;
    background: linear-gradient(to left, var(--navy) 0%, transparent 100%);
    pointer-events: none;
    z-index: 1;
  }
}

.reviews-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
}

.review-card {
  background: white;
  border-radius: var(--radius);
  padding: 30px;
  border: 2px solid var(--line-soft);
  transition: all 0.3s var(--ease);
  position: relative;
  overflow: hidden;
}

.review-card::before {
  content: '\201C';
  position: absolute;
  top: -8px; left: 16px;
  font-size: 120px;
  color: var(--orange);
  opacity: 0.07;
  font-family: Georgia, serif;
  line-height: 1;
  pointer-events: none;
}

.review-card:hover {
  border-color: var(--orange);
  transform: translateY(-4px);
  box-shadow: var(--shadow-hover);
}

.review-stars { color: #FFB800; font-size: 16px; margin-bottom: 12px; }
.review-text { font-size: 14px; line-height: 1.7; color: var(--black); font-weight: 600; margin-bottom: 18px; font-style: italic; }

.reviewer { display: flex; align-items: center; gap: 10px; }

.reviewer-av {
  width: 40px; height: 40px;
  border-radius: 50%;
  background: var(--orange-pale);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  font-weight: 800;
  color: var(--orange);
  font-family: var(--font-display);
}
img.reviewer-av {
  object-fit: cover;
  flex-shrink: 0;
  border: 2px solid var(--orange-pale);
}

.reviewer-name { font-weight: 800; font-size: 14px; }
.reviewer-loc { font-size: 11px; color: var(--gray); font-weight: 600; }
.verified { margin-left: auto; font-size: 11px; color: var(--green); font-weight: 700; }

/* FAQ */
/* MEET OUR PORTABLE BOTTLES (task 126) — comparison section for the two
   hydration products added in task 127. Same section rhythm as .faq-section
   just below (white background, 140px vertical padding, centered header). */
.bottle-compare-section { padding: 140px 5%; background: white; }
.bottle-compare-header { text-align: center; margin-bottom: 56px; }
.bottle-compare-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 32px;
  max-width: 940px;
  margin: 0 auto;
}
.bottle-compare-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 32px 28px 36px;
  border: 2px solid var(--line-soft);
  border-radius: var(--radius-sm);
}
/* Placeholder media — no real product photo for either item yet (see the
   HTML comment above this section). Same idea as .bp-image-placeholder on
   the homepage hero, restyled for a white background instead of orange. */
.bottle-compare-media {
  width: 100%;
  max-width: 220px;
  aspect-ratio: 1 / 1;
  border-radius: 20px;
  background: var(--light-gray);
  border: 2px dashed #D8D2C8;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-bottom: 20px;
}
.bottle-compare-emoji { font-size: 72px; line-height: 1; }
.bottle-compare-soon {
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 11px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--gray);
  background: white;
  padding: 5px 10px;
  border-radius: 50px;
}
.bottle-compare-name { font-family: var(--font-display); font-size: 21px; color: var(--navy); margin-bottom: 6px; }
.bottle-compare-price { font-weight: 800; color: var(--orange); font-size: 15px; margin-bottom: 14px; }
.bottle-compare-desc { font-size: 14px; color: var(--gray); line-height: 1.6; font-weight: 600; margin-bottom: 14px; }
.bottle-compare-best-for { font-size: 13.5px; color: var(--navy); margin-bottom: 20px; }
.bottle-compare-bundle-note {
  text-align: center;
  margin-top: 40px;
  font-weight: 700;
  color: var(--gray);
}
.bottle-compare-bundle-note a { color: var(--orange); font-weight: 800; }
@media (max-width: 760px) {
  .bottle-compare-grid { grid-template-columns: 1fr; }
  .bottle-compare-section { padding: 60px 5%; }
}

.faq-section { padding: 140px 5%; background: white; }
.faq-header { text-align: center; margin-bottom: 56px; }
.faq-wrap { max-width: 700px; margin: 0 auto; }

.faq-item {
  border: 2px solid var(--line-soft);
  border-radius: var(--radius-sm);
  margin-bottom: 10px;
  overflow: hidden;
  transition: border-color 0.2s, border-left-width 0.2s;
}

.faq-item:hover { border-color: var(--orange); }
.faq-item.open { border-color: var(--orange); border-left: 4px solid var(--orange); }

.faq-q {
  padding: 18px 22px;
  font-weight: 800;
  font-size: 15px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: white;
  user-select: none;
}

.faq-icon {
  font-size: 22px;
  color: var(--orange);
  transition: transform 0.3s var(--ease);
  font-weight: 400;
  line-height: 1;
}

.faq-item.open .faq-icon { transform: rotate(45deg); }

.faq-a {
  max-height: 0;
  overflow: hidden;
  transition: all 0.3s var(--ease);
  background: white;
  color: var(--gray);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.7;
  padding: 0 22px;
}

.faq-item.open .faq-a { max-height: 200px; padding: 0 22px 18px; }

/* EMAIL SIGNUP */
.email-section {
  background: linear-gradient(135deg, #FFEEE3 0%, #FFE0CC 100%);
  padding: 130px 5%;
  text-align: center;
  position: relative;
  overflow: hidden;
}

.email-section::before {
  content: '';
  position: absolute;
  width: 230px; height: 230px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' fill='%23E8630A'%3E%3Cellipse cx='50' cy='67' rx='20' ry='16'/%3E%3Cellipse cx='27' cy='47' rx='9' ry='12'/%3E%3Cellipse cx='42' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='58' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='73' cy='47' rx='9' ry='12'/%3E%3C/svg%3E") center / contain no-repeat;
  opacity: 0.08;
  bottom: -40px; left: -30px;
  pointer-events: none;
  transform: rotate(-20deg);
}

.email-section::after {
  content: '';
  position: absolute;
  width: 170px; height: 170px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' fill='%23E8630A'%3E%3Cellipse cx='50' cy='67' rx='20' ry='16'/%3E%3Cellipse cx='27' cy='47' rx='9' ry='12'/%3E%3Cellipse cx='42' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='58' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='73' cy='47' rx='9' ry='12'/%3E%3C/svg%3E") center / contain no-repeat;
  opacity: 0.07;
  top: -20px; right: -20px;
  pointer-events: none;
  transform: rotate(15deg);
}

.email-section h2 { font-family: var(--font-display); font-size: clamp(24px, 4vw, 40px); margin-bottom: 10px; }
.email-section p { font-size: 16px; color: var(--gray); font-weight: 600; margin-bottom: 28px; }

.email-form {
  display: flex;
  gap: 10px;
  max-width: 460px;
  margin: 0 auto;
}

.email-input {
  flex: 1;
  padding: 14px 20px;
  border: 2px solid var(--line);
  border-radius: 50px;
  font-family: var(--font-body);
  font-size: 16px; /* >=16px so iOS never auto-zooms on focus — see form field base */
  font-weight: 600;
  outline: none;
  transition: border-color 0.2s;
}

.email-input:focus { border-color: var(--orange); }

/* The claimed state of the home page's 10% off box — the standing "your code"
   panel that replaces the form for good once an email is on file, from either
   entry point. Which of the two states renders is decided pre-paint in the
   <head> block, not here. The code box itself reuses the popup's
   .offer-code-row/.offer-code/.offer-copy-btn so both entry points show an
   identical code; only the message needs its own rule, since the popup's is
   tuned for white-on-navy and this section sits on the light cream band. */
.email-claimed { max-width: 520px; margin: 0 auto; }
/* Scoped to .email-section so it outranks `.email-section p` (0,1,1), which
   would otherwise win on specificity and quietly grey this back out. */
.email-section .email-success-msg {
  color: var(--black);
  font-size: 17px;
  font-weight: 700;
  line-height: 1.6;
  margin: 0 0 18px;
}
.email-claimed .offer-code-row { margin-bottom: 0; }

.email-submit {
  background: var(--orange);
  color: white;
  border: none;
  padding: 14px 24px;
  border-radius: 50px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s var(--ease);
  white-space: nowrap;
}

.email-submit:hover { background: var(--orange-light); transform: translateY(-2px); box-shadow: var(--shadow-orange); }

/* ==================== SHOP PAGE ==================== */
.shop-page { padding: 50px 5%; background: white; }


.shop-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 22px;
}

/* ==================== PRODUCT DETAIL PAGE ==================== */
.product-detail { padding: 0 0 50px 0; overflow: visible; background: white; }

.product-detail-grid {
  display: flex;
  flex-direction: column;
  align-items: stretch;
}

.product-gallery {
  position: relative;
  top: 0;
  align-self: auto;
  width: 100%;
}

.main-img {
  width: 100%;
  height: 380px;
  background: var(--orange-pale);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 130px;
  margin-bottom: 0;
  /* clip, not hidden — same reason as .det-carousel below: hidden would make
     this a second dead scroll port wrapped around the gallery scroller, and
     the mobile override further down changes this element's height, display,
     padding and background but never its overflow. All it has to do is clip
     the hover scale to the rounded corner, which clip does. */
  overflow: hidden;
  overflow: clip;
  transition: transform 0.3s var(--ease);
  border-radius: 0;
}

/* ── DETAIL PAGE IMAGE CAROUSEL (structure, works on all sizes) ──
   Arrows + dots are real navigation (not just a mobile affordance), so they
   stay visible at every width — previously desktop had no visible way to
   move the carousel at all. */
/* `overflow: clip` rather than `hidden`: hidden makes this a real scroll
   PORT (a scroll container that simply cannot be scrolled), nested directly
   around the .det-track scroller. On iOS a touch landing inside a dead
   scroll port is offered to it first, and the gesture only chains out to the
   page once it has been refused — which is felt as a vertical drag that does
   nothing until the second or third attempt. `clip` does the same clipping
   (which is all this needs it for — the border radius and the arrows) while
   creating no scroll container at all. `hidden` stays first as the fallback
   value for any engine that does not know `clip`. */
.det-carousel {
  width: 100%; align-self: stretch; position: relative;
  overflow: hidden;
  overflow: clip;
}
/* Stated here rather than inherited from the blanket `* { touch-action:
   manipulation }` in the mobile block: this element IS the horizontal
   scroller, so the axes it allows belong next to the overflow that makes it
   one. Both pans, and let the browser disambiguate the direction — it does
   that better than any axis-locking code, which is what this carousel used
   to carry. */
.det-track {
  display: flex; width: 100%; height: 100%;
  overflow: hidden;
  touch-action: pan-x pan-y pinch-zoom;
}
.det-slide { flex: 0 0 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: var(--orange-pale); }
/* A slide holding a real photo goes white, because `contain` (below) leaves a
   gutter where `cover` used to leave none, and every product photo is shot on
   white — so a white gutter is invisible and the slide looks exactly as it did
   before the crop was removed. The peach stays for the emoji-only fallback
   slide, which is the only thing it was ever really for. */
.det-slide:has(img) { background: #fff; }
/* CONTAIN, not cover. The carousel box is
   NEVER the photo's aspect ratio (546x520 at desktop, 390x380 on a phone),
   so `cover` was silently clipping every slide: 4.8% off a square studio
   shot and 9.2% off the taller feature-callout panel, whose text runs to
   the edge. `width: auto` shrink-wraps the <img> BOX to the picture, which
   is what stops the global `img { background: var(--img-placeholder) }`
   shimmer from painting animated cream bars in the letterbox gutters.
   SELECTOR: `.det-carousel` is in there to out-specify `.main-img img`
   (further down at "main-img img", equal specificity but LATER in the file,
   so it won and kept every slide on cover). The carousel is rendered INTO
   #detailImg, which is a .main-img — both rules match every slide. */
.det-carousel .det-slide img {
  width: auto;
  max-width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  background: none;
  animation: none;
}
.det-prev, .det-next {
  display: flex;
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 40px; height: 40px; border-radius: 50%;
  background: rgba(255,255,255,0.92); border: none;
  box-shadow: 0 2px 10px rgba(26,26,46,0.18);
  align-items: center; justify-content: center;
  font-size: 24px; color: var(--black); z-index: 5;
  cursor: pointer; padding: 0; line-height: 1; transition: opacity 0.2s, background 0.2s;
}
.det-prev:hover, .det-next:hover { background: white; }
.det-prev { left: 14px; }
.det-next { right: 14px; }
.det-prev.disabled, .det-next.disabled { opacity: 0.3; cursor: default; }
/* min-height is a STRUCTURAL BACKSTOP, not decoration: this row is padding-only,
   so its height would otherwise collapse by exactly one dot (8px) whenever it is
   momentarily empty — which is what made the page jolt on every colour tap while
   the dots were being injected a paint later. Keep it equal to padding + dot
   (14 + 8 + 14, border-box) and update it alongside the padding. */
.det-dots { display: flex; justify-content: center; gap: 8px; padding: 14px 0; min-height: 36px; background: #fff; }
.det-dot { width: 8px; height: 8px; border-radius: 50%; background: #ccc; border: 1.5px solid #aaa; transition: background 0.2s; }
.det-dot.active { background: var(--orange); border-color: var(--orange); }

.product-detail-info {
  padding: 24px 5%;
}

@media (min-width: 900px) {
  .product-detail { padding: 50px 5% 100px; }
  .product-detail-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 60px;
    align-items: start;
  }
  .product-gallery {
    position: sticky;
    top: 90px;
    align-self: flex-start;
  }
  .main-img {
    border-radius: var(--radius);
    margin-bottom: 0;
    height: 520px;
  }
  /* padding-top rises to 16 here, so the backstop rises with it (16 + 8 + 14). */
  .det-dots { background: transparent; padding-top: 16px; min-height: 38px; }
  .product-detail-info {
    padding: 0;
  }
}

.main-img:hover { transform: scale(1.02); }

.main-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: inherit;
}

.product-detail-info h1 { font-family: var(--font-display); font-size: 36px; margin-bottom: 12px; line-height: 1.2; }

/* SHARE CONTROL — sits on the title's right, the way Amazon places it.
   flex-start (not center) so a title that wraps to two or three lines keeps
   the button beside the FIRST line instead of drifting to the middle; the
   small margin-top optically centres it on that line against the 36px cap
   height. The h1 keeps its own margin-bottom, so the row needs none. */
.detail-title-row { display: flex; align-items: flex-start; gap: 14px; }
.detail-title-row h1 { flex: 1 1 auto; min-width: 0; }
.detail-share-btn {
  flex: 0 0 auto;
  margin-top: 6px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 15px;
  border-radius: 999px;
  border: 1.5px solid var(--line);
  background: #fff;
  color: var(--black);
  font-family: inherit;
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease), background 0.2s var(--ease), transform 0.2s var(--ease);
}
.detail-share-btn svg { width: 17px; height: 17px; display: block; flex-shrink: 0; }
/* Exactly one icon shows; is-copy is set by initShareControl() when the
   device has no Web Share API and the button becomes "Copy Link". */
.detail-share-btn .icon-link,
.detail-share-btn.is-copy .icon-share { display: none; }
.detail-share-btn.is-copy .icon-link { display: block; }
.detail-share-btn:hover { color: var(--orange); border-color: var(--orange); background: rgba(232, 99, 10, 0.06); }
.detail-share-btn:active { transform: scale(0.95); }
.detail-share-btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }

/* Product-page rating line beside the price. min-height reserves the row so
   the price and Add To Cart below it do not jolt when renderDetailRating()
   fills it on product open. */
/* Same three parts, same order and the same baseline alignment as the card
   row — see the .product-stars block for why this is an inline baseline row
   with a transform on the stars rather than a centred flex row. Only the
   sizes differ: this one is not fighting a 2-up grid for width, so it is set
   at a fixed, larger size instead of the cards' clamp. min-height reserves
   the row so the price and Add To Cart below do not jolt when
   renderDetailRating() fills it on product open. */
.detail-stars {
  display: block;
  color: #FFB800;
  font-size: 13px;
  line-height: 1.6;
  margin-bottom: 16px;
  min-height: 18px;
  white-space: nowrap;
}
.detail-stars .rv-star { width: 1.3em; height: 1.3em; }
/* 0.09570em, not the cards' 0.10664em, and from the same formula — only the
   two inputs differ. The star box is 1.3em here (set just above) and the
   number is 1.25em against the cards' 1.05em, so its cap-band midline sits
   0.44922em above the baseline; the count is still 1em, so its midline is
   0.35938em. The stars take the mean, 0.40430em:

     translateY = 0.5 x 1.3 - 0.40430 - 0.15 = 0.09570em

   See the .product-stars block for where 0.71875em, the recentred viewBox and
   the -0.15em come from; if any of them changes, both numbers move together. */
.detail-stars .rv-stars {
  vertical-align: baseline;
  transform: translateY(0.09570em);
  margin: 0 0.5em;
}
.detail-stars .product-stars-num { color: var(--black); font-weight: 800; font-size: 1.25em; }
.detail-stars .product-stars-txt { color: var(--gray); font-size: 1em; font-weight: 600; }
.detail-stars span { color: var(--gray); font-weight: 600; }
.detail-stars:empty { display: none; }

.detail-price {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 22px;
}

.detail-price .now { font-family: var(--font-display); font-size: 36px; color: var(--orange); }
/* .was and .save rules removed with the markup they styled — a struck-through
   compare-at price and a "Save N%" pill derived from it. Same rule as
   .price-was above. (.option-btn-oos further down still uses line-through:
   that marks a sold-out SIZE, nothing to do with pricing.) */

.detail-desc { font-size: 15px; color: var(--gray); line-height: 1.7; font-weight: 600; margin-bottom: 20px; }
.detail-desc p + p { margin-top: 12px; }

/* Short one-line value statement — sits between price and the selectors,
   above the fold, so shoppers get the "why buy this" before the long
   description (which now lives in the Description accordion below). */
.detail-tagline {
  font-size: 15.5px;
  font-weight: 700;
  color: var(--black);
  line-height: 1.5;
  margin-bottom: 22px;
  padding-left: 14px;
  border-left: 3px solid var(--orange);
}

.option-label { font-weight: 800; font-size: 14px; margin-bottom: 10px; }

.options-row { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }

/* Size selector only (not colors, which still flow naturally) — a fixed 2x2
   grid so 4-option sizes (like the LED Dog Collar's S/M/L/XL) always
   read as two even rows instead of wrapping 3-then-1. */
#detailSizes.options-row {
  display: grid;
  grid-template-columns: repeat(2, auto);
  justify-content: start;
  gap: 10px 14px;
}

.option-btn {
  padding: 8px 18px;
  border-radius: 50px;
  border: 2px solid var(--line);
  background: white;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 13px;
  cursor: pointer;
  transition: all 0.2s var(--ease);
}

.option-btn:hover, .option-btn.active { border-color: var(--orange); color: var(--orange); }

.option-btn-oos {
  color: var(--gray);
  background: var(--light-gray);
  text-decoration: line-through;
  cursor: not-allowed;
  opacity: 0.65;
}
.option-btn-oos:hover { border-color: var(--line); color: var(--gray); }
.option-btn-oos.active { border-color: var(--line); color: var(--gray); }

.detail-case-note {
  display: inline-block;
  background: rgba(232, 99, 10, 0.10);
  color: var(--orange);
  font-size: 12px;
  font-weight: 800;
  padding: 6px 12px;
  border-radius: 999px;
  margin: -8px 0 16px;
}

.qty-row {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 22px;
}

.qty-label { font-weight: 800; font-size: 14px; }

.qty-control {
  display: flex;
  align-items: center;
  border: 2px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.qty-btn {
  width: 40px; height: 40px;
  border: none;
  background: white;
  font-size: 18px;
  cursor: pointer;
  transition: background 0.2s;
  font-weight: 700;
}

.qty-btn:hover { background: var(--orange-pale); }
.qty-num { padding: 0 18px; font-weight: 800; font-size: 16px; }

.detail-btns { display: flex; gap: 12px; margin-bottom: 24px; }

.btn-add-cart {
  flex: 1;
  background: var(--black);
  color: white;
  padding: 16px;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  border: none;
  cursor: pointer;
  transition: all 0.2s var(--ease);
}

.btn-add-cart:hover { background: var(--orange); transform: translateY(-2px); }

.btn-buy-now {
  flex: 1;
  background: var(--orange);
  color: white;
  padding: 16px;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  border: none;
  cursor: pointer;
  transition: all 0.2s var(--ease);
  box-shadow: var(--shadow-orange);
}

.btn-buy-now:hover { background: var(--orange-light); transform: translateY(-2px); }

.detail-features { list-style: none; margin-bottom: 24px; }
.detail-features li {
  padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 14px;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 10px;
}
.detail-features li::before { content: '✓'; color: var(--green); font-weight: 900; font-size: 16px; }

/* PRODUCT DETAIL EXTRA SECTIONS */
.detail-section {
  margin-bottom: 14px;
  padding: 16px 16px 20px;
  background: var(--light-gray);
  border-radius: var(--radius-sm);
}
.detail-section-title {
  font-weight: 800;
  font-size: 13px;
  margin-bottom: 8px;
  color: var(--black);
}
.detail-section-text {
  font-size: 13px;
  color: var(--gray);
  font-weight: 600;
  line-height: 1.6;
  margin: 0;
}

.detail-info-cards {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid var(--line-soft);
}
.detail-info-card {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 16px;
  background: white;
  border-radius: var(--radius-sm);
  border: 2px solid var(--line-soft);
  transition: border-color 0.2s;
}
.detail-info-card:hover { border-color: var(--orange); }
.detail-info-icon { font-size: 24px; flex-shrink: 0; }
.detail-info-label { font-weight: 800; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 3px; color: var(--gray); }
.detail-info-value { font-size: 13px; color: var(--black); font-weight: 700; line-height: 1.4; }

/* Small trust row right under the main Add To Cart / Buy It Now buttons —
   the decision point — distinct from the fuller cards inside the Shipping
   & Returns accordion panel below. */
.detail-trust-mini {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  margin-bottom: 22px;
}
.detail-trust-mini span {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--gray);
}
.detail-trust-mini svg { width: 16px; height: 16px; color: var(--orange); flex-shrink: 0; }

/* ==================== DESCRIPTION/REVIEWS/SHIPPING ACCORDION ==================== */
.pd-accordion { margin-top: 28px; border-top: 2px solid var(--line-soft); }
.pd-acc-item { border-bottom: 2px solid var(--line-soft); }
.pd-acc-head {
  width: 100%;
  padding: 18px 2px;
  background: none;
  border: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  color: var(--black);
  cursor: pointer;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.pd-acc-icon {
  font-size: 20px;
  color: var(--orange);
  font-weight: 400;
  line-height: 1;
  transition: transform 0.3s var(--ease);
  flex-shrink: 0;
}
.pd-acc-item.open .pd-acc-icon { transform: rotate(45deg); }
.pd-acc-body {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.35s var(--ease);
}
.pd-acc-item.open .pd-acc-body { max-height: 2000px; }
.pd-acc-body .detail-desc,
.pd-acc-body .detail-features { padding-bottom: 0; }
.pd-acc-body > *:first-child { margin-top: 2px; }
.pd-acc-body > *:last-child { margin-bottom: 20px; }

.pd-reviews-summary {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
  padding-top: 4px;
}
.pd-reviews-stars { display: flex; gap: 2px; }
.pd-reviews-count { font-weight: 800; font-size: 14px; color: var(--black); }

/* ==================== STICKY ADD TO CART BAR ==================== */
.sticky-atc {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 900;
  background: white;
  border-top: 2px solid var(--line-soft);
  box-shadow: 0 -6px 24px rgba(26,26,46,0.10);
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 14px;
  transform: translateY(100%);
  transition: transform 0.3s var(--ease);
}
.sticky-atc.show { transform: translateY(0); }
.sticky-atc-info { flex: 1; min-width: 0; }
.sticky-atc-name {
  font-weight: 800;
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sticky-atc-price { font-family: var(--font-display); font-size: 16px; color: var(--orange); }
.sticky-atc-btn {
  flex-shrink: 0;
  background: var(--black);
  color: white;
  border: none;
  padding: 12px 22px;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.2s;
}
.sticky-atc-btn:hover { background: var(--orange); }

/* Lift the floating chat launcher clear of the sticky bar while it's showing.
   #chatWidget's bottom:24px is an inline style — !important is required to
   win over it. */
#chatWidget { transition: bottom 0.3s var(--ease); }
#chatWidget.chat-lifted { bottom: 84px !important; }

/* ==================== CART PAGE ==================== */
.cart-page { padding: 50px 5%; background: white; }
.cart-page h1 { font-family: var(--font-display); font-size: 44px; margin-bottom: 32px; }

.cart-layout {
  display: grid;
  grid-template-columns: 1fr 380px;
  gap: 40px;
  align-items: start;
}

.cart-items { display: flex; flex-direction: column; gap: 16px; }

.cart-item {
  background: white;
  border: 2px solid var(--line-soft);
  border-radius: var(--radius);
  padding: 20px;
  display: flex;
  gap: 20px;
  align-items: center;
  transition: border-color 0.2s;
}

.cart-item:hover { border-color: var(--orange); }

.cart-item-img {
  width: 90px; height: 90px;
  background: var(--orange-pale);
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40px;
  flex-shrink: 0;
  overflow: hidden;
}

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

.cart-item-info { flex: 1; }
.cart-item-name { font-weight: 800; font-size: 16px; margin-bottom: 4px; }
.cart-item-variant { font-size: 13px; color: var(--gray); font-weight: 600; margin-bottom: 10px; }

.cart-item-price { font-family: var(--font-display); font-size: 22px; color: var(--orange); }

.cart-item-right { display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }

/* ---- A cart line whose colour was discontinued --------------------------
   Deliberately not styled as an error: nothing has gone wrong for the
   customer, one choice they made is simply no longer for sale. So the line
   keeps its normal shape and just loses its price (it is excluded from the
   subtotal in renderCart, since it cannot be bought) and gains the one
   action that resolves it. The photo is dimmed rather than hidden — it is
   still the thing they picked, and removing it would make the line hard to
   recognise. See retiredColorFor() in products.js. */
.cart-item-gone { border-color: var(--line); background: var(--light-gray); }
.cart-item-gone:hover { border-color: var(--line); }
.cart-item-gone .cart-item-img { opacity: 0.55; }

.cart-item-gone-note {
  font-size: 13px;
  font-weight: 700;
  color: var(--orange-dark);
  line-height: 1.45;
  margin-bottom: 10px;
}

.cart-item-gone-btn {
  font-family: inherit;
  font-size: 13px;
  font-weight: 800;
  color: var(--white);
  background: var(--orange);
  border: none;
  border-radius: 999px;
  padding: 9px 16px;
  cursor: pointer;
  transition: background 0.2s;
}
.cart-item-gone-btn:hover { background: var(--orange-dark); }


.remove-btn {
  background: none;
  border: none;
  color: var(--gray);
  font-size: 20px;
  cursor: pointer;
  transition: color 0.2s;
}

.remove-btn:hover { color: #EF4444; }

.cart-summary {
  background: var(--light-gray);
  border-radius: var(--radius);
  padding: 28px;
  position: sticky;
  top: 100px;
}

.cart-summary h3 { font-family: var(--font-display); font-size: 24px; margin-bottom: 20px; }

.summary-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 12px;
  font-size: 15px;
  font-weight: 600;
}

.summary-row.total {
  font-size: 18px;
  font-weight: 800;
  padding-top: 14px;
  border-top: 2px solid var(--line);
  margin-top: 14px;
}

.summary-row.total span:last-child { color: var(--orange); font-family: var(--font-display); font-size: 22px; }

.checkout-btn {
  width: 100%;
  background: var(--orange);
  color: white;
  border: none;
  padding: 18px;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 16px;
  cursor: pointer;
  margin-top: 20px;
  transition: all 0.2s var(--ease);
  box-shadow: var(--shadow-orange);
}

.checkout-btn:hover { background: var(--orange-light); transform: translateY(-2px); }

/* Pressed state for every orange CTA — a shade deeper than the hover orange,
   settled back down so the click reads as a real press. Placed after all the
   hover rules so it wins the cascade mid-press. */
.btn-primary:active, .btn-buy-now:active, .checkout-btn:active,
.email-submit:active, .cart-btn:active {
  background: var(--orange-dark);
  transform: translateY(0);
  box-shadow: var(--shadow-orange);
}

.empty-cart { text-align: center; padding: 64px 20px; }
.empty-cart .empty-icon { font-size: 56px; margin-bottom: 16px; display: block; }
.empty-cart h2 { font-family: var(--font-display); font-size: 24px; margin-bottom: 8px; }
.empty-cart p { color: var(--gray); font-weight: 600; font-size: 15px; margin-bottom: 24px; }

/* ==================== ABOUT PAGE ==================== */
.about-hero {
  background: var(--orange-pale);
  padding: 80px 5%;
  text-align: center;
}

.about-hero h1 { font-family: var(--font-display); font-size: clamp(36px, 5vw, 60px); margin-bottom: 18px; }
.about-hero h1 span { color: var(--orange); }
.about-hero p { font-size: 18px; color: var(--gray); font-weight: 600; line-height: 1.7; max-width: 600px; margin: 0 auto 32px; }

.about-story { padding: 80px 5%; background: white; }

/* The founder story as a flowing editorial narrative: a centered reading
   column, numbered subheads, and one big serif pull-quote in the middle. */
.story-flow { max-width: 720px; margin: 0 auto; }
.story-sec { margin-bottom: 46px; }
.story-sec:last-child { margin-bottom: 0; }
.story-sec h3 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 27px;
  line-height: 1.25;
  color: var(--black);
  margin-bottom: 14px;
  display: flex;
  align-items: baseline;
  gap: 13px;
}
.story-num {
  font-size: 15px;
  color: var(--orange);
  letter-spacing: 1px;
  flex-shrink: 0;
}
.story-sec p { font-size: 16.5px; color: var(--gray); line-height: 1.85; font-weight: 600; margin-bottom: 14px; }
.story-sec p:last-child { margin-bottom: 0; }

.story-pullquote {
  margin: 54px 0;
  text-align: center;
  background: var(--orange-pale);
  border-radius: 24px;
  padding: 44px 40px 36px;
}
.pq-mark {
  display: block;
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 88px;
  line-height: 0.55;
  color: var(--orange);
  margin-bottom: 6px;
}
.story-pullquote blockquote {
  font-family: Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-size: clamp(20px, 2.6vw, 28px);
  line-height: 1.5;
  color: var(--black);
  margin: 0 0 18px;
}
.story-pullquote figcaption {
  font-size: 12.5px;
  font-weight: 800;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--orange);
}

.about-values { background: var(--black); padding: 80px 5%; }
.about-values-header { text-align: center; margin-bottom: 50px; }
.about-values-header .section-title { color: white; }
.about-values-header .section-sub { color: rgba(255,255,255,0.55); margin: 0 auto; max-width: 460px; }

/* Four provable promises as alternating icon/text rows */
.values-flow {
  max-width: 860px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.value-row {
  display: flex;
  align-items: center;
  gap: 30px;
  background: rgba(255,255,255,0.045);
  border: 1px solid rgba(255,255,255,0.09);
  border-radius: 20px;
  padding: 28px 32px;
  transition: all 0.3s var(--ease);
}
.value-row:nth-child(even) { flex-direction: row-reverse; }
.value-row:hover {
  background: rgba(232,99,10,0.07);
  border-color: rgba(232,99,10,0.55);
  transform: translateY(-3px);
  box-shadow: 0 14px 34px rgba(0,0,0,0.35);
}
.value-visual {
  flex: 0 0 96px;
  width: 96px;
  height: 96px;
  border-radius: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, rgba(232,99,10,0.24), rgba(232,99,10,0.07));
  border: 1px solid rgba(232,99,10,0.42);
  color: var(--orange);
}
.value-visual svg { width: 42px; height: 42px; display: block; }
.value-text { flex: 1; min-width: 0; }
.value-row-title { font-family: var(--font-display); font-weight: 400; font-size: 22px; color: white; margin-bottom: 8px; }
.value-row-desc { font-size: 14.5px; color: rgba(255,255,255,0.62); line-height: 1.7; font-weight: 600; }

/* Phones: every value row reads icon-left (alternation is a wide-screen
   rhythm — tablets 701px+ keep it), smaller icon tile so text gets the width */
@media (max-width: 700px) {
  .value-row, .value-row:nth-child(even) { flex-direction: row; }
  .value-row { align-items: flex-start; gap: 16px; padding: 20px 18px; }
  .value-visual { flex-basis: 58px; width: 58px; height: 58px; border-radius: 17px; }
  .value-visual svg { width: 27px; height: 27px; }
  .value-row-title { font-size: 18px; }
  .value-row-desc { font-size: 13.5px; }
}

/* ==================== CONTACT PAGE ==================== */
/* Hero photo + intro text reuse the About page's exact pattern (.about-hero)
   for visual consistency between the two "premium" content pages. */
.contact-hero-wrap { height: 280px; background: var(--img-placeholder); }
.contact-hero-img { object-position: center 45%; } /* keeps the leashed dog centered and in frame on narrow/tall crops */

.contact-page { padding: 60px 5% 70px; background: white; }

.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 60px;
  align-items: start;
}

/* 2x2 icon cards — same soft-shadow/rounded-corner language as the product cards */
.contact-methods { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

.contact-method {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  padding: 22px 20px;
  background: white;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: all 0.25s var(--ease);
}
.contact-method:hover { box-shadow: var(--shadow); transform: translateY(-3px); border-color: rgba(232,99,10,0.3); }

.contact-method-icon-tile {
  width: 46px;
  height: 46px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  background: linear-gradient(135deg, rgba(232,99,10,0.16), rgba(232,99,10,0.05));
  border: 1px solid rgba(232,99,10,0.28);
  color: var(--orange);
}
.contact-method-icon-tile svg { width: 21px; height: 21px; display: block; }
.contact-method-label { font-size: 11px; color: var(--gray); font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; }
.contact-method-value { font-weight: 800; font-size: 14px; line-height: 1.4; word-break: break-word; }

/* Quick answers — instant links for the most common questions, no form needed */
.quick-answers { margin-top: 28px; padding: 24px; background: var(--light-gray); border-radius: var(--radius); }
.quick-answers-title { font-family: var(--font-display); font-weight: 400; font-size: 16px; margin-bottom: 12px; color: var(--black); }
.quick-answer-links { display: flex; flex-direction: column; }
.quick-answer-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 13px 2px;
  font-weight: 700;
  font-size: 14.5px;
  color: var(--black);
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: color 0.15s ease;
}
.quick-answer-link:last-child { border-bottom: none; padding-bottom: 2px; }
.quick-answer-link:first-child { padding-top: 2px; }
.quick-answer-link:hover { color: var(--orange); }
.quick-answer-link svg { width: 16px; height: 16px; color: var(--orange); flex-shrink: 0; }

.contact-form-wrap {
  background: var(--light-gray);
  border-radius: var(--radius);
  padding: 36px;
}

.contact-form-wrap h2 { font-family: var(--font-display); font-size: 28px; margin-bottom: 24px; }

.form-group { margin-bottom: 20px; }

.form-label { display: block; font-weight: 800; font-size: 13px; margin-bottom: 8px; }

.form-input, .form-textarea, .form-select {
  width: 100%;
  padding: 13px 16px;
  border: 2px solid var(--line);
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-size: 16px; /* >=16px so iOS never auto-zooms on focus — see form field base */
  font-weight: 600;
  outline: none;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
  background: white;
}

.form-input:focus, .form-textarea:focus, .form-select:focus {
  border-color: var(--orange);
  box-shadow: 0 0 0 4px rgba(232, 99, 10, 0.14);
}
.form-textarea { height: 120px; resize: vertical; }

.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

/* ==================== FOOTER ==================== */
footer {
  /* Subtle navy gradient. Bottom stays --navy-deep (#0d0d1a) to match the
     body background so the mobile overscroll bounce never flashes a seam. */
  background: linear-gradient(180deg, var(--navy) 0%, var(--navy-deep) 100%);
  color: white;
  padding: 90px 5% 42px;
}

.footer-grid {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1fr;
  gap: 44px;
  padding-bottom: 44px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
  margin-bottom: 28px;
}

.footer-brand-logo {
  font-family: var(--font-display);
  font-size: 26px;
  color: white;
  margin-bottom: 12px;
  display: block;
}

.footer-brand-logo span { color: var(--orange); }

.footer-brand p { color: rgba(255,255,255,0.45); font-size: 13px; line-height: 1.7; font-weight: 600; margin-bottom: 18px; }

.social-row { display: flex; gap: 10px; }

.social-btn {
  width: 38px; height: 38px;
  background: rgba(255,255,255,0.08);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  cursor: pointer;
  transition: all 0.2s var(--ease);
  text-decoration: none;
}

.social-btn:hover { background: var(--orange); transform: translateY(-3px); }

.footer-col h4 { font-family: var(--font-display); font-size: 17px; margin-bottom: 16px; }

.footer-col ul { list-style: none; display: flex; flex-direction: column; gap: 9px; }

.footer-col ul li a {
  color: rgba(255,255,255,0.45);
  text-decoration: none;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: color 0.2s;
}

.footer-col ul li a:hover { color: var(--orange); }

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.footer-bottom p { color: rgba(255,255,255,0.28); font-size: 12px; font-weight: 600; }
.footer-bottom p span { color: var(--orange); }

/* TOAST */
.toast {
  position: fixed;
  bottom: 30px;
  right: 30px;
  background: var(--black);
  color: white;
  padding: 14px 22px;
  border-radius: var(--radius-sm);
  font-weight: 800;
  font-size: 14px;
  z-index: 1000;
  transform: translateY(100px);
  opacity: 0;
  transition: all 0.3s var(--ease);
  display: flex;
  align-items: center;
  /* Purely a passive notification (no click handler of its own) — opacity:0
     alone doesn't stop it from capturing clicks, so without this its full
     layout box silently blocks taps on anything underneath it even while
     "hidden" (found via the new sticky Add To Cart bar sitting in the same
     bottom-of-screen region). */
  pointer-events: none;
  gap: 10px;
  box-shadow: var(--shadow-hover);
}

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

/* BACK BTN */
.back-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--gray);
  text-decoration: none; /* now real <a href> (routing) — was a <button>, never needed this before */
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  margin-bottom: 28px;
  transition: color 0.2s;
  background: none;
  border: none;
  font-family: var(--font-body);
  /* THE "FLASH ON PRESS" (task 78). This element was the one tappable thing
     in the file that never zeroed the native tap highlight — measured on the
     live site it was rgba(51,181,229,0.4), i.e. the browser painted its own
     highlight box over the whole link the instant it was pressed, a frame or
     two before the page swapped. Everything else here (product cards, nav
     buttons, search rows...) already sets this; .back-btn was
     simply missed when it stopped being a <button> and became an <a>. */
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

/* The other half of the same flash: a touch device fires :hover on press, so
   tapping ran grey -> orange through the 0.2s transition above and you saw
   the label change colour in the instant before navigating. Behind
   `hover: hover` the recolour is a mouse affordance again — where it is a
   no-op on click anyway, because the pointer is already hovering. Touch gets
   no press state at all, deliberately: this link navigates immediately, and
   the new page IS the feedback. */
@media (hover: hover) {
  .back-btn:hover { color: var(--orange); }
}

/* ANIMATIONS */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(24px); }
  to { opacity: 1; transform: translateY(0); }
}

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

/* Subtle fade-up reveal as sections enter the viewport.
   The .reveal class is added by JS at load, so if JS fails nothing is hidden. */
.reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.6s var(--ease), transform 0.6s var(--ease); }
.reveal.reveal-in { opacity: 1; transform: none; }


/* ==================== NAV CENTER & SEARCH ==================== */
.nav-center {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
}

.nav-search-btn {
  display: flex;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
  color: #fff;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
  border-radius: 50%;
  transition: background 0.15s;
}
.nav-search-btn:hover,
.nav-search-btn:active { background: rgba(255,255,255,0.12); }

/* The floating chat paw (inline z-index 9999) would ride on top of the
   search overlay and the offer popup — fade it out while either is up. */
#chatWidget { transition: opacity 0.25s ease; }
body.overlay-up #chatWidget { opacity: 0; pointer-events: none; }

/* Dim the page behind the open search panel. Fixed, so the page itself
   never shifts, reflows or loses its scroll position. */
.search-scrim {
  position: fixed; inset: 0; background: rgba(13,13,26,0.45);
  z-index: 895; opacity: 0; pointer-events: none;
  transition: opacity 0.28s ease;
  touch-action: none;
}
.search-scrim.open { opacity: 1; pointer-events: auto; }

/* Search panel. Desktop/tablet: dropdown pinned below the nav (JS sets the
   exact top). Mobile (≤900px block further down): full-screen overlay.
   Always in the layer tree (visibility, not display) so it can transition. */
.nav-search-bar {
  position: fixed;
  top: 60px; left: 0; right: 0;
  background: white;
  z-index: 905;
  padding: 16px 5% 10px;
  box-shadow: 0 24px 48px -16px rgba(13,13,26,0.28);
  display: flex;
  flex-direction: column;
  gap: 0;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-14px);
  transition: opacity 0.28s var(--ease), transform 0.28s var(--ease),
              visibility 0s linear 0.28s;
  overscroll-behavior: contain;
}
.nav-search-bar.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: none;
  transition: opacity 0.28s var(--ease), transform 0.28s var(--ease);
}

.nav-search-inner {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--light-gray);
  border: 1.5px solid #E7E3DD;
  border-radius: 50px;
  padding: 11px 18px;
  width: 100%;
  max-width: 680px;
  margin: 0 auto;
  box-sizing: border-box;
  /* Closed state: the pill sits collapsed; opening the panel expands it
     (see .open below). Origin near the nav's search icon so it feels like
     it grows out of the tap. */
  transform: scaleX(0.55);
  transform-origin: 88% 50%;
  opacity: 0;
  transition: transform 0.3s var(--ease), opacity 0.24s ease,
              border-color 0.25s ease, box-shadow 0.25s ease;
}
.nav-search-bar.open .nav-search-inner {
  transform: scaleX(1);
  opacity: 1;
}
/* Soft orange glow while the input is active */
.nav-search-inner:focus-within {
  border-color: var(--orange);
  box-shadow: 0 0 0 4px rgba(232, 99, 10, 0.14);
  background: #fff;
}
.nav-search-paw {
  flex-shrink: 0;
  color: #B9B2A9;
  transition: color 0.25s ease;
}
.nav-search-inner:focus-within .nav-search-paw { color: var(--orange); }

.nav-search-field {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
}
.nav-search-input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  font-family: var(--font-body);
  font-size: 16px; /* ≥16px so iOS never auto-zooms (zooming = the page "jumps") */
  font-weight: 600;
  color: var(--black);
  outline: none;
}
/* Rotating placeholder: a real element (native placeholders can't fade).
   Sits over the input, ignores pointer events, fades between phrases. */
.nav-search-ph {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 600;
  color: #aaa;
  pointer-events: none;
  white-space: nowrap;
  overflow: hidden;
  transition: opacity 0.24s ease;
}
.nav-search-ph.is-fading { opacity: 0; }
.nav-search-ph.ph-hidden { opacity: 0; transition: none; }
.nav-search-close {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 16px;
  color: var(--gray);
  padding: 6px 8px;
  border-radius: 50%;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.15s;
}
.nav-search-close:hover { color: var(--black); }

.search-results {
  border-top: 1px solid transparent;
  margin-top: 0;
  width: 100%;
  max-width: 680px;
  margin-left: auto;
  margin-right: auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  max-height: min(430px, calc(100vh - 220px));
}
.search-results:not(:empty) { margin-top: 10px; border-top-color: var(--line-soft); }

.search-result-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 6px;
  cursor: pointer;
  border-bottom: 1px solid #F5F5F5;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s;
  border-radius: 12px;
}
.search-result-item:last-child { border-bottom: none; }
.search-result-item:hover, .search-result-item:active { background: var(--orange-pale); }
.search-result-thumb {
  width: 46px; height: 46px; border-radius: 12px;
  background: var(--orange-pale);
  display: flex; align-items: center; justify-content: center;
  font-size: 26px; flex-shrink: 0; overflow: hidden;
}
.search-result-thumb img { width: 100%; height: 100%; object-fit: cover; }
.search-result-info { flex: 1; min-width: 0; }
.search-result-name { font-family: var(--font-body); font-weight: 800; font-size: 14px; color: var(--black); }
.search-result-name b { color: var(--orange); font-weight: 900; }
.search-result-price { font-family: var(--font-display); font-size: 14px; color: var(--orange); margin-top: 2px; }
.search-no-results {
  padding: 30px 8px 24px;
  font-family: var(--font-body);
  font-size: 14px; font-weight: 700; color: var(--gray);
  text-align: center;
}
.search-no-results .snr-emoji { display: block; font-size: 34px; margin-bottom: 8px; }
.search-no-results .snr-hint { display: block; margin-top: 4px; font-size: 12.5px; font-weight: 600; color: #aaa; }


/* Mobile: the search panel drops from the very top of the screen (its own
   close X), still position:fixed so the page behind keeps its exact scroll
   position. It is only as tall as what it holds — just the input until there
   are results — so an idle panel never paints a blank white screen; the
   results list scrolls inside a viewport-high cap. */
@media (max-width: 900px) {
  .nav-search-bar {
    top: 0 !important; /* JS positions desktop only */
    padding: 14px 5% 10px;
    transform: translateY(-18px);
  }
  .nav-search-bar.open { transform: none; }
  .search-results { flex: 0 1 auto; max-height: calc(100dvh - 110px); }
}

/* ==================== HAMBURGER & MOBILE MENU ==================== */
.hamburger {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  -webkit-tap-highlight-color: transparent;
  width: 40px;
  height: 40px;
  transition: transform 0.18s var(--ease);
}
/* The three bars morph into an X in TWO overlapping stages rather than all at
   once: opening, the middle bar collapses first (fast, no delay) and the outer
   two start rotating 70ms later, so the X forms into empty space instead of
   sweeping through the bar it is replacing; closing runs the reverse, with the
   middle bar waiting for the outer two to lie flat again. Two-stage timing is
   the whole difference between this reading as a morph and reading as three
   lines that happen to move at the same time.
   The outer bars use a gentle overshoot so they settle into the X rather than
   stopping dead on it. */
.hamburger span {
  display: block;
  width: 22px; height: 2px;
  background: #fff;
  border-radius: 2px;
  transform-origin: center;
  transition: transform 0.34s var(--ease-burger), opacity 0.16s ease;
}
/* Closing: outer bars unfold immediately, middle bar comes back after them. */
.hamburger span:nth-child(2) { transition-delay: 0.14s; }

.hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.hamburger.open span:nth-child(2) { opacity: 0; transform: scaleX(0.2); transition-delay: 0s; }
.hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.hamburger.open span:nth-child(1),
.hamburger.open span:nth-child(3) { transition-delay: 0.07s; }

/* The button itself acknowledges the press — without it the only feedback on
   a tap is the icon animation, which starts a frame or two later. */
.hamburger:active { transform: scale(0.9); }

/* Right-slide panel — enabled via mobile media query */
/* Nothing in this panel scrolls (explicit request) — no overflow property
   anywhere in the header/.mob-menu-nav/footer chain. The link list is short
   enough to always fit a real phone screen at its natural height, and the
   footer below it pins to the bottom with its own `margin-top: auto` (see
   .mob-menu-footer).

   The footer-overlapping-the-last-row bug reported here traced to something
   simpler than it first looked: .mob-menu-nav's own rule (further down this
   file, in the mobile media query) never re-declared `height`, so it was
   quietly inheriting `height: 60px` from the site's bare `nav {}` rule —
   see that rule's own comment for the full story. Fixed there, not here;
   this element's `margin-top: auto` was correct the whole time. */
.mob-menu {
  display: none;
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: 280px;
  background: white;
  z-index: 1000;
  flex-direction: column;
  box-shadow: -8px 0 32px rgba(0,0,0,0.15);
  visibility: hidden;
  transform: translateX(100%);
  transition: transform 0.28s ease, visibility 0.28s;
  pointer-events: none;
}
.mob-menu.open {
  visibility: visible;
  transform: translateX(0);
  pointer-events: all;
}
.mob-menu-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 20px 14px;
  border-bottom: 2px solid var(--line-soft);
}
.mob-menu-title {
  font-family: var(--font-display);
  font-size: 18px;
  color: var(--black);
}
.mob-menu-close {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 20px;
  color: var(--gray);
  padding: 4px 8px;
  -webkit-tap-highlight-color: transparent;
  border-radius: 50%;
  transition: background 0.15s;
}
.mob-menu-close:active { background: var(--light-gray); }
.mob-menu a {
  display: block;
  padding: 18px 20px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 16px;
  color: var(--black);
  text-decoration: none; /* now real <a href> (routing) — was href-less before, so this never mattered */
  cursor: pointer;
  border-bottom: 1px solid #F5F5F5;
  transition: color 0.15s ease, background 0.15s ease;
  -webkit-tap-highlight-color: transparent;
}
.mob-menu a:last-child { border-bottom: none; }
.mob-menu a:active { color: var(--orange); background: var(--orange-pale); }

.mob-menu-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 22px 20px 18px;
  border-bottom: 1px solid #F5F5F5;
}
/* task 123: real <img> now — see .logo-circle's comment, same reasoning. */
.mob-menu-brand-icon {
  display: block;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}
.mob-menu-brand-text {
  font-family: var(--font-display);
  font-size: 19px;
  color: var(--black);
}
.mob-menu-brand-text span { color: var(--orange); }

.mob-menu-footer {
  /* Pinned to the bottom of the (non-scrolling) panel: all the leftover
     vertical space becomes one clean gap above the footer, rather than a
     fixed number that would look cramped on a tall phone or too roomy on a
     short one. (The bug that made this look broken was .mob-menu-nav's
     height, not this margin — see that rule's comment.) */
  margin-top: auto;
  flex-shrink: 0;
  padding: 16px 20px 22px;
  text-align: center;
  border-top: 1px solid #F5F5F5;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.mob-menu-footer .paw-divider { margin: 0; }
.mob-menu-tagline {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.3px;
  color: var(--gray);
}

.mob-menu-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.4);
  z-index: 999;
  /* The blur that used to be here is gone too — see the note on the second
     .mob-menu-overlay rule near the end of this file. Removing it there and
     leaving it here would just have re-enabled it. */
}
.mob-menu-overlay.open { display: block; }
.carousel-btn { display: none; }
/* bindCarousel() sets `hidden` on an arrow whose track has nothing left to
   scroll (see sync() in app.js). Every per-carousel block below re-declares
   `display: flex`, so this needs !important to actually win. */
.carousel-btn[hidden] { display: none !important; }
/* ==================== MOBILE (≤900px) ==================== */
@media (max-width: 900px) {

  /* ── SMOOTH SCROLLING & GPU ACCELERATION ──────────────── */
  html { scroll-behavior: smooth; }
  nav { transform: translateZ(0); will-change: transform; }

  /* nav is `position: sticky` on mobile (per direct feedback: only nav stays
     pinned while scrolling, the announce bar above it scrolls away — see the
     "NAV: the ONLY thing that stays pinned" rule further down this block).
     Sticky/static elements occupy their own real space in the document, so
     `.page`/`#page-home` need NO compensating padding-top the way a
     position:fixed nav would have required — do not add one here. The
     mobile-menu overlay (opened by the hamburger) is a separate, still-fixed,
     full-screen element with its own iOS visual-viewport handling — see its
     rules near the end of this file — unrelated to nav's own positioning. */

  /* `.page { -webkit-overflow-scrolling: touch }` USED TO BE HERE. Removed —
     it is the cause of the "first tap on a card does nothing until you scroll"
     bug (task 54), and it was doing no work in exchange.

     .page is `display:block` and has NO overflow property anywhere in this
     file, so it is not a scroll container — and this property only affects
     overflow:auto/scroll elements. Worse, on iOS it forces the element into
     its own accelerated compositing layer, and content inside such a layer is
     a known source of taps landing nowhere until a scroll forces the layer to
     resync. That is exactly the reported symptom, on exactly the element that
     wraps EVERY page, which is why it showed up on the shop grid, the home
     carousel and product pages alike.

     It also explains why this was never reproducible in any local tool: neither
     Chromium nor Playwright's Windows WebKit implements the property, so the
     layer is never created and the bug cannot appear there.

     Nothing is lost by deleting it: since iOS 13 momentum scrolling is the
     default everywhere and the property is deprecated. The two REMAINING
     uses in this file are all on real overflow:auto containers (nav search
     results, trust bar) and are left alone. Do not reintroduce
     this on a non-scrolling element. */

  /* ── DISABLE DOUBLE-TAP ZOOM ───────────────────────────── */
  * { touch-action: manipulation; }

  /* ── PREVENT HORIZONTAL SCROLL ─────────────────────────── */
  /* html ONLY, not body — this used to be `html, body { overflow-x: hidden }`,
     and that extra `body` is what was silently breaking `position: sticky`
     for EVERYTHING on mobile (nav included), discovered chasing why nav
     wasn't staying pinned once the announce bar became non-fixed. Setting a
     non-visible overflow-x on an element forces its overflow-y to `auto` per
     the CSS overflow spec (not just on html, which already carries this same
     overflow-x rule at the top of this file with no ill effect — confirmed
     desktop's already-sticky nav keeps working fine with it) — but doing
     that on BODY as well turns body into its own competing scroll container
     that isn't actually what `window.scrollTo`/touch-scrolling moves, which
     is what broke sticky's "nearest scrolling ancestor" tracking. html alone
     already clips horizontal overflow from every descendant; body doesn't
     need its own copy of the same rule. */
  html { overflow-x: hidden; max-width: 100%; }

  /* ── OVERSCROLL / RUBBER-BAND ─────────────────────────── */
  /* iOS Safari paints the html/body background — the canvas — in the strip the
     rubber band opens up when you pull past the top or the bottom. It is ONE
     colour for both ends, so it has to be a colour that works against the fixed
     nav above the strip at the top AND against the footer below it at the bottom.

     This used to be --navy-deep (#0d0d1a), chosen to match the footer's gradient
     end so a bottom pull showed no white gap. That left the TOP pull opening a
     #0d0d1a strip directly under a #1a1a2e nav — against the nav and the hero it
     read as a black band flashing in and out (task 91).

     It is now --navy, the EXACT nav background, so the top pull just looks like
     the header stretching. The footer's gradient end is brought to the same navy
     on mobile (see the footer rule further down) so the bottom pull stays seamless
     too — both ends now agree with the one canvas colour. Keep the three in step:
     nav background, this canvas, and the mobile footer gradient end. */
  html, body { background-color: var(--navy); }

  /* ── PAGE SCROLLBAR (visual only) ─────────────────────── */
  /* Hide the thin indicator that rides down the right edge while the page
     scrolls. Same three-property idiom the carousels in this file already
     use, just pointed at the document scroller: scrollbar-width for Firefox
     and Safari 18.2+, -ms-overflow-style for old Edge, ::-webkit-scrollbar
     for the rest of WebKit/Blink.

     Both html AND body are listed because either one can end up being the
     element that scrolls the page, and the rule has to land on whichever it
     is. This is cosmetic only — nothing here touches overflow, so every
     scroll gesture, momentum fling and scrollTo still behaves exactly as
     before; the bar is drawn, or not drawn, over the same scroller.

     MOBILE ONLY, deliberately. On mobile the indicator is a transient
     overlay that costs no layout width, so hiding it is purely visual. On
     desktop the scrollbar is a real control people grab and read position
     from, and on a classic (non-overlay) scrollbar hiding it would also give
     the page back ~15px of width, reflowing every desktop breakpoint. Do not
     promote this out of the mobile block.

     One caveat this cannot cover: on iOS the MAIN-FRAME indicator is drawn
     by the system rather than by the page, so older WebKit may keep showing
     it. Hiding it there would mean moving the whole page into an inner
     overflow container, which is not worth what it would cost the fixed nav
     and the overscroll work above. */
  html, body {
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  html::-webkit-scrollbar,
  body::-webkit-scrollbar { display: none; }

  /* sections with decorative elements that extend beyond their bounds */
  .email-section { overflow: hidden; }

  /* ── ANNOUNCE BAR (task 118; safe-area saga tasks 120-124; now scrolls
     away, per direct feedback — only nav stays pinned) ─────────────────
     Sits ABOVE nav in normal document flow, matching the reference's mobile
     layout (bar -> header/logo -> hero) on first paint, then scrolls off
     screen like any other content once you scroll past it — it is NOT
     position:fixed or sticky (see that rule's own comment).

     .announce-bar (outer) is a neutral (--offwhite) shell: `padding-top`
     reserves space above `.announce-bar-inner` (the actual orange bar) so
     the status bar sits on a clean neutral strip instead of directly on the
     orange while the bar is still visible at the top. That padding-top is
     `min(env(safe-area-inset-top, 0px), 59px)` — a real per-device safe-area
     value, clamped to stop Safari's live toolbar-animation spike from ever
     visibly moving it. Full reasoning (why flat numbers were tried and
     abandoned, then env() was dropped entirely over the shake bug, then
     brought back clamped) lives on that padding-top declaration itself —
     not repeated here to avoid the two explanations drifting apart.

     --mob-announce-h is the outer box's total height (padding + inner bar) —
     read by the hero's height calc and by app.js's hero-height lock (which
     measures the real rendered element rather than parsing this custom
     property, since getComputedStyle() on a calc()/env() property returns
     its literal text, never a resolved number). Must track
     .announce-bar-inner's height + this padding-top exactly or the hero
     ends up assuming a different bar height than what's actually rendered. */
  :root {
    /* The clamped safe-area inset (see .announce-bar's padding-top for the
       full reasoning) — one source of truth, read by .announce-bar,
       --mob-announce-h, and .safe-top-strip below, so all three can never
       drift to slightly different heights. */
    --safe-top: min(env(safe-area-inset-top, 0px), 59px);
    /* Must track .announce-bar-inner's height + .announce-bar's own
       padding-top exactly (44px inner; padding-top is now --safe-top — see
       that rule's comment for why — so this variable is itself a live,
       per-device value, not a flat number) or nav/page/hero all end up
       assuming a shorter bar than what's actually rendered. This is fine for
       CSS consumers (anything using var()) since the browser resolves it
       like any other value; it's NOT fine to parseFloat() in JS (a custom
       property's computed value is always the literal text, calc()/env()
       included) — which is exactly why the mobile hero-height lock in
       app.js measures the real rendered #announceBar element instead of
       reading this variable. */
    --mob-announce-h: calc(44px + var(--safe-top));
    --mob-nav-h: 68px;
  }
  /* SAFE-AREA NOTCH STRIP — see the matching comment on this element in
     index.html for why this exists (nav, now sticky instead of fixed, has
     no safe-area awareness of its own and would otherwise sit navy right up
     under the status bar once the announce bar has scrolled away). Always
     fixed at the literal top of the viewport, regardless of scroll position
     or what's rendered beneath it — when the announce bar is still visible
     underneath, this just harmlessly overlays the same colour its own
     padding-top already paints there. */
  .safe-top-strip {
    display: block;
    position: fixed;
    top: 0; left: 0; right: 0;
    height: var(--safe-top);
    background: var(--offwhite);
    z-index: 950; /* above nav (900) and the announce bar (normal flow, no z-index) */
    pointer-events: none;
  }
  .announce-bar {
    display: block;
    /* NOT fixed/sticky (per direct feedback) — this bar should scroll away
       with the page and never be seen again once you've scrolled past it;
       only `nav` below stays pinned. Normal static flow does exactly that on
       its own: it just sits at the top of the document like any other
       element and scrolls off naturally. height/z-index/the GPU-compositing
       hint (transform/will-change) that used to live here were all specific
       to being position:fixed and are gone along with it — a static element
       needs none of them. */
    height: var(--mob-announce-h);
    background: var(--offwhite);
    /* env(safe-area-inset-top) IS back, but CAPPED with min() — a real photo
       on an actual notch/Dynamic-Island device showed the orange bar (set to
       a flat 0px per the previous round's "no white strip" feedback) running
       straight up through the status bar and into the camera cutout itself.
       A flat number can't know how big any given device's notch is, so "0"
       was simply wrong on that hardware, not just a style choice. The
       feedback since has been explicit: go right up to the real safe-area
       boundary — "the limit" — automatically, per device, but never past it,
       and never with a manually-chosen white gap.

       The reason this was ripped out before (task 124) still stands: with
       viewport-fit=cover engaged, Safari RENEGOTIATES env(safe-area-inset-
       top) LIVE as its own address bar slides in and out on scroll, spiking
       to roughly 200px mid-animation on top of whatever the device's real
       resting inset is — traced frame-by-frame in a screen recording at the
       time. The old fix (previous round, and task 124 before it) was to
       stop reading the value at all. The fix THIS round is narrower: clamp
       it instead. min(env(...), 59px) — 59px covers every current notch/
       Dynamic-Island inset (typically 44–59px) — means the live spike can
       shoot up to 200px internally and the clamped OUTPUT never moves past
       59px regardless, since min() only ever lets the SMALLER of the two
       numbers through. On any device whose resting inset is already at or
       above the cap (every current notch/Dynamic-Island iPhone), the clamped
       value never changes during the spike at all — there is nothing left
       to shake. The cost is scoped to devices with NO notch (inset normally
       0): there the clamped value can still move between 0 and 59 during
       the animation, a real but far smaller version of the original 200px
       jump, and only on hardware that's now several generations old. Flat
       numbers (including flat 0) remain wrong on every device whose actual
       inset isn't exactly that number — which is the bug a real screenshot
       just caught — so this is the trade being made going forward. */
    padding-top: var(--safe-top);
  }
  /* .announce-bar::before (the 200px neutral-colour "bleed up" above the bar)
     is gone — it existed purely to cover the transient gap iOS's overscroll
     rubber-banding could open above a FIXED topmost element. Now that the
     bar scrolls away with the page like any other static content, that gap
     can't occur above it in the first place; the rule had nothing left to
     fix. */
  .announce-bar-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 44px; /* skinnier per direct feedback — was 56px (task 125) */
    box-sizing: border-box;
    padding: 0 4%;
    font-size: 12px;
    gap: 8px;
    animation: none; /* mobile gets no shimmer-sweep */
  }
  .announce-bar-inner::before { display: none; } /* desktop-only shimmer sweep */
  /* task 122: the desktop two-message layout is replaced on mobile by a
     single centered rotating message (.announce-rotator below), matching the
     reference's single-line bar. */
  .announce-main, .announce-secondary { display: none; }
  .announce-dot { margin: 0 5px; }
  /* The trust bar's messages ALSO rotate through the announce bar above
     (paired up two-per-slide — see the slide markup) — per direct follow-up
     feedback, that's in addition to this section, not instead of it. Stays
     fully visible on mobile, same as desktop. */

  /* ── ANNOUNCE ROTATOR (task 122, mobile only) ─────────────────────────
     Slides are stacked via absolute positioning inside a relatively-
     positioned, fixed-height box (height = one line of this font, matching
     .announce-bar-inner's own font-size) and crossfaded by toggling .active
     — same fade pattern as the old hero crossfade, just on text instead of
     photos. JS (announceRotate() in app.js) only ever toggles .active; it
     never adds/removes slides from the DOM. */
  .announce-rotator {
    display: block; /* overrides the desktop default's display:none */
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    height: 16px; /* ~1 line at this bar's 12px font-size + line-height */
    text-align: center;
  }
  .announce-slide {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    opacity: 0;
    color: inherit;
    text-decoration: none;
    transition: opacity 0.5s ease-in-out;
    pointer-events: none;
  }
  .announce-slide.active { opacity: 1; pointer-events: auto; }
  /* The one slide that's a real link (not just text) gets an underline so it
     reads as tappable, same as any other inline text link on the site. */
  .announce-slide--offer { text-decoration: underline; text-underline-offset: 2px; }
  /* Permanent once an email's been submitted (html.offer-claimed — set
     pre-paint from localStorage and live by markOfferClaimed()): this slide
     is simply never a candidate again, on this device, ever. */
  html.offer-claimed .announce-slide--offer { display: none; }

  /* ── NAV: the ONLY thing that stays pinned while scrolling ─────────
     position:sticky, not fixed — per direct feedback, the announce bar above
     should scroll away and disappear, while nav keeps riding along at the
     top. Sticky is exactly this: nav sits in normal flow right after the
     (now also normal-flow) announce bar on first paint — orange above navy,
     same as before — and only "sticks" to top:0 once scrolling would
     otherwise carry it past the top of the viewport. No left/right/z-index
     needed the way position:fixed required them: a sticky element doesn't
     need to be told its horizontal bounds the way a fixed one did — sticky
     keeps the width/position it already has from normal flow. z-index kept
     as-is (still matters for sticky, same as any positioned element — e.g.
     .nav-search-bar's dropdown is explicitly z-index:905 to render above
     this). */
  nav {
    position: sticky;
    top: 0;
    height: var(--mob-nav-h);
    padding: 0 4%;
    z-index: 900;
    flex-wrap: nowrap;
    box-shadow: 0 2px 16px rgba(0,0,0,0.08);
    /* task 124: was the desktop base rule's justify-content:space-between,
       inherited unchanged. That only ever worked here by accident, back
       when .logo was still a real flex item splitting the row into even
       gaps — now that .logo is taken out of flow and centred absolutely
       (below), space-between would spread the 3 remaining items (hamburger,
       search, cart) evenly across the FULL width instead of keeping
       hamburger+search grouped together on the left, which visually
       collides with the now-centred logo. flex-start + nav-right's own
       margin-left:auto (below) keeps the left pair grouped and pins cart to
       the right edge instead. */
    justify-content: flex-start;
    gap: 10px;
  }
  /* Paw badge/logo sized to fit the --mob-nav-h bar (task 120: bumped up from
     36px/21px to sit closer to the reference's scale). */
  .logo-circle { width: 40px; height: 40px; }
  .logo-text { font-size: 23px; }
  .nav-links { display: none; }
  .nav-full { display: none; }
  .nav-search-btn { display: flex; }
  /* Header row (task 123): hamburger+search / centered logo / cart — was
     hamburger / centered logo / search+cart (task 122), moved again per
     direct feedback: the search icon sits immediately right of the hamburger
     instead of paired with the cart. index.html now puts the hamburger AND
     the search button as nav's first two DOM children; `order` here just
     keeps that explicit and self-documenting rather than relying on DOM
     order alone, since .nav-search-btn is NOT display:none on desktop (the
     hamburger is) and would otherwise visually jump to the front there too. */
  /* `nav > .X` (not bare `.X`) to MATCH the desktop order rules' specificity
     (nav > .hamburger etc., near the top of this file) — a bare class
     selector here would lose to that compound one regardless of this block
     coming later in the file, since specificity beats source order. Learned
     this the hard way: without the `nav >`, every icon rendered in the
     desktop order (search, cart, hamburger) even inside this ≤900px block. */
  nav > .hamburger { order: 1; }
  nav > .nav-search-btn { order: 2; }
  /* task 124: true centering, not flex:1-within-its-own-box centering. The
     left group (hamburger+search, two icons) and right group (cart alone,
     one icon) are different widths, so giving .logo flex:1 and centering
     its OWN content only centers it within the leftover space AFTER those
     two groups — which is itself off-center, since one side group is wider
     than the other. Measured: the logo sat visibly left of true centre,
     more empty space on the right (toward cart) than the left (toward
     search), exactly as reported.
     Fix: take .logo out of the flex flow entirely and centre it on nav's
     own box directly (nav is position:sticky, which — like relative/fixed/
     absolute — establishes the containing block absolute positioning needs,
     so no extra wrapper is required). left:50% + translateX(-50%) centres
     on nav's actual width regardless of what either side group's combined
     width is, so this holds at any viewport width or icon-count asymmetry,
     not just the one size it was eyeballed against. */
  nav > .logo {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
  .nav-center { display: none; }
  /* margin-left:auto consumes all the free space left of nav-right, pinning
     it to the right edge while hamburger+search stay grouped at the start
     (flex-start, above) — the classic flex trick for "two groups, opposite
     ends" without space-between's even-spread-everything behaviour. */
  nav > .nav-right { order: 4; margin-left: auto; }
  /* Logo itself (task 123): wordmark first, paw icon after — reversed from
     desktop's icon-then-wordmark. Still one flex row (.logo, unchanged),
     just re-ordering its two existing children rather than touching the DOM,
     so the link's accessible name/reading order ("PawHaul") doesn't change. */
  .logo-text { order: 1; }
  .logo-circle { order: 2; }
  .cart-btn-text { display: none; }
  /* Cart becomes a plain white icon with an orange count badge (Ruffwear-style) */
  .cart-btn { background: none; color: #fff; padding: 8px; font-size: 22px; box-shadow: none; }
  .cart-btn:hover, .cart-btn:active { background: none; transform: none; box-shadow: none; }
  .cart-count { background: var(--orange); top: -2px; right: -2px; }
  .nav-right { gap: 6px; }
  .hamburger { display: flex; }
  .mob-menu { display: flex; }

  /* ── PAGE SYSTEM ──────────────────────────────────────── */
  /* No padding-top needed anymore: the announce bar is normal static flow
     and nav is sticky (not fixed) — both now occupy real space in the
     document, so .page naturally starts right after them with no gap, the
     same as it would after any other normal-flow element. The old
     calc(--mob-announce-h + --mob-nav-h) padding-top existed specifically to
     compensate for BOTH of them being taken OUT of flow by position:fixed;
     with neither one fixed anymore, reserving that same space twice would
     leave an empty gap between nav and the page content. padding-bottom:0 so
     a white page never shows a strip below its footer — the footer sits
     flush and the navy body (var(--navy), which the mobile footer now ends
     on) extends below it on overscroll with no gap. */
  .page { padding-top: 0; padding-bottom: 0; }
  #page-home { padding-top: 0; }
  /* Footer-less pages fill the viewport so their own light bg (not the deep-navy body)
     covers the screen on a short page like an empty cart. */
  #page-cart, #page-product { min-height: 100vh; }

  /* ── HERO (slideshow background) — task 118: fills essentially the whole
     screen below the announce-bar+nav stack on first load, text/CTAs
     sitting near the bottom of the image (reference layout), not centered.

     100svh, NOT 100dvh (task 119 fix) — 100dvh tracks Safari's toolbar live,
     so the very act of scrolling (which is what collapses the toolbar) grew
     the hero taller mid-scroll: object-fit:cover then reveals more of the
     same image into the newly-taller box, which reads as the photo zooming/
     jumping while you scroll. 100svh is the SMALL viewport height — the
     size with the toolbar fully shown — so it's one fixed number that never
     changes after first paint, whatever the toolbar does later. The only
     cost is the hero is very slightly shorter than the absolute max once
     the toolbar auto-hides, which is invisible next to a scroll-triggered
     resize. min-height is a floor for short-viewport landscape phones,
     where 100svh minus the announce-bar+nav stack could otherwise get
     uncomfortably short. */
  /* -44px per direct feedback: "a little shorter" than the full remaining
     viewport height. */
  #heroSection { height: calc(100svh - var(--mob-announce-h) - var(--mob-nav-h) - 44px) !important; min-height: 420px; }
  #heroSection .hero-media { object-position: center center !important; width: 100% !important; height: 100% !important; object-fit: cover !important; }
  .hero-copy {
    align-items: center !important;
    text-align: center !important;
    /* task 122: headline/sub now live at the TOP of the hero and .hero-cta at
       the BOTTOM (reference layout) — supersedes task 118's bottom-clustered
       layout. .hero-text and .hero-cta are .hero-copy's only two flex
       children (see the wrapper div in index.html), so space-between puts
       one at each end instead of drifting the subtext to the middle. */
    justify-content: space-between !important;
    padding: 0 6% !important;
    /* task 123: headline sat too close to the top edge, right under the nav —
       nudged down from 28px per direct feedback. */
    padding-top: 64px !important;
    /* Clears the floating chat launcher (#chatToggle: 58px tall, 24px off the
       bottom — index.html — so its top edge sits 82px up from the screen
       bottom) with a 14px buffer: the button's bottom edge must stay at or
       above (viewport bottom - 96px).
       NOTE: a BIGGER number here pushes the button further UP, away from the
       bottom edge — more padding-bottom on the flex container means less
       room for the last child before it, not more.
       #heroSection's own height is `100svh - stack - 44px` (the hero-shrink
       feedback), and its top sits right at the bottom of the announce-bar+nav
       stack — so the HERO's bottom edge always lands ~44px above the true viewport
       bottom, regardless of device height. That means the floor this
       padding actually needs is only 96 - 44 = 52px, not 96 — 96 was the
       right number before the hero got shorter, and kept the button sitting
       well short of the hero's own bottom edge afterward ("in the middle of
       empty space" per feedback) even though it was still technically clear
       of the chat button. 56px keeps a small margin over that 52px floor. */
    padding-bottom: 56px !important;
  }
  #heroSection .hero-overlay {
    /* Dark at BOTH ends now (task 122 moved the headline to the top, the CTA
       stays at the bottom) — a vignette rather than the old bottom-only
       gradient built for copy clustered at the bottom. */
    background: linear-gradient(to bottom, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.12) 32%, rgba(0,0,0,0.12) 58%, rgba(0,0,0,0.72) 100%) !important;
  }
  #heroSection h1 { font-size: clamp(26px, 8vw, 40px) !important; }
  #heroSection p { max-width: 290px !important; text-align: center !important; margin-left: auto !important; margin-right: auto !important; }
  .hero-cta { justify-content: center !important; flex-direction: column !important; width: 100% !important; }
  #heroSection .btn-primary, #heroSection .btn-white { width: 100% !important; justify-content: center !important; }
  /* task 123: "See Bundles" removed from the mobile hero per direct feedback
     — just Shop Walk Gear now. Scoped to #heroSection .hero-cta specifically
     so this doesn't touch .btn-white anywhere else on the site. Hidden via
     CSS rather than removed from the DOM so desktop (which still shows both
     buttons) needs no markup fork. */
  #heroSection .hero-cta .btn-white { display: none !important; }

  /* ── HERO (gradient/grid fallback) ───────────────────── */
  .hero { grid-template-columns: 1fr; padding: 36px 5% 0; min-height: auto; }
  .hero-content { padding-bottom: 0; }
  .hero-visual { margin-top: 20px; }
  .hero h1 { font-size: clamp(30px, 8vw, 44px); }
  .hero p { font-size: 15px; }
  .hero-btns { flex-direction: column; gap: 10px; }
  .hero-btns .btn-primary,
  .hero-btns .btn-secondary { width: 100%; justify-content: center; }
  .hero-dog { height: 240px; font-size: 80px; }

  /* ── TRUST BAR ────────────────────────────────────────── */
  .trust-bar {
    justify-content: flex-start;
    overflow-x: auto;
    flex-wrap: nowrap;
    gap: 20px;
    padding: 14px 5%;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .trust-bar::-webkit-scrollbar { display: none; }
  .trust-item { white-space: nowrap; flex-shrink: 0; }

  
  .why-section { padding: 66px 5%; }
  .reviews-section { padding: 60px 5%; }
  .faq-section { padding: 60px 5%; }
  .email-section { padding: 66px 5%; }
  .about-story { padding: 44px 5%; }
  .about-values { padding: 44px 5%; }
  .mission-section { padding: 44px 5%; grid-template-columns: 1fr; gap: 28px; }
  .contact-page { padding: 28px 5%; }
  .cart-page { padding: 24px 5%; }
  .shop-page { padding: 24px 5%; }

  /* ── SECTION HEADERS ──────────────────────────────────── */
  .section-title { font-size: clamp(22px, 6vw, 32px); }
  .reviews-header { margin-bottom: 24px; }
  .faq-header { margin-bottom: 28px; }
  .why-header { margin-bottom: 32px; }
  .paw-divider { margin-bottom: 18px; }
  .paw-divider svg { width: 18px; height: 18px; }

  /* ── PRODUCT CARDS ────────────────────────────────────── */
  .products-grid, .shop-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  .product-img { height: 220px; }
  .product-info { padding: 12px 12px 14px; }
  .product-name { font-size: 13px; margin-bottom: 2px; }
  /* margin only — the font-size is the clamp() up top, which is what keeps
     "4.8 * * * * * . 1,749 ratings" on one line in this 2-up grid. Setting a
     fixed 11px here overrode it and pushed the count out of the card. */
  .product-stars { margin-bottom: 6px; }
  .price-now { font-size: 17px; }
  .price-was { font-size: 12px; }
  .btn-black { min-height: 40px; font-size: 13px; }
  .wishlist-btn { opacity: 1; }


  /* ── WHY SECTION ──────────────────────────────────────── */
  .why-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  .why-card { padding: 20px 14px; }
  .why-icon { font-size: 34px; margin-bottom: 12px; }
  .why-title { font-size: 15px; }
  .why-desc { font-size: 12px; }

  /* ── MISSION / STATS ──────────────────────────────────── */
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  .stat-card { padding: 20px 16px; }
  .stat-num { font-size: 30px; }

  /* ── REVIEWS ──────────────────────────────────────────── */
  .reviews-section .reviews-grid-wrap::after { display: none; }
  .reviews-grid {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }
  .review-card {
    min-width: unset;
    width: 100%;
    flex-shrink: unset;
    padding: 23px;
  }

  /* ── FAQ ──────────────────────────────────────────────── */
  .faq-q { padding: 16px 18px; font-size: 14px; }
  .faq-item.open .faq-a { padding: 0 18px 16px; }

  /* ── EMAIL FORM ───────────────────────────────────────── */
  .email-form { flex-direction: column; }
  .email-section h2 { font-size: clamp(22px, 6vw, 32px); }

  /* ── BUTTONS ──────────────────────────────────────────── */
  .btn-primary, .btn-secondary { padding: 15px 28px; font-size: 15px; min-height: 52px; }

  /* ── PRODUCT DETAIL ───────────────────────────────────── */
  .product-detail { padding-top: 18px; }
  .back-btn { padding-left: 16px; }
  .product-detail-grid { grid-template-columns: 1fr; }
  /* main-img becomes auto-height wrapper; det-carousel provides the image area,
     given more vertical room now that the thumbnail row is gone. */
  .main-img { height: auto !important; display: block !important; padding: 0 !important; background: transparent !important; }
  /* touch-action deliberately NOT set here. This rule used to also declare
     `touch-action: pan-y`, from the era when a JS handler owned horizontal
     dragging; pan-y forbids the native horizontal scroll that replaced it.
     It was only harmless because the later `.det-carousel` rule at the
     bottom of this file re-allows both axes on equal specificity — i.e. one
     cascade edit away from killing gallery swipe outright. Removed. */
  .det-carousel { height: 380px; }
  .det-prev, .det-next { width: 36px; height: 36px; font-size: 22px; }
  .det-prev { left: 10px; }
  .det-next { right: 10px; }
  .product-detail-info h1 { font-size: 26px; }
  /* Titles here run long ("LED Flashlight Retractable Dog Leash"), so the
     button drops its label and becomes a 40px tap target — comfortably over
     the 44px-with-padding minimum once the row gap is counted, and it frees
     the width the wrapped title needs. Mobile is also where the Web Share API
     is essentially always present, so the "Copy Link" wording it would lose is
     the branch that almost never renders here. */
  .detail-title-row { gap: 10px; }
  .detail-share-btn { padding: 0; width: 40px; height: 40px; justify-content: center; margin-top: 2px; }
  .detail-share-btn span { display: none; }
  .detail-price .now { font-size: 28px; }
  .detail-btns { flex-direction: column; gap: 10px; }
  .btn-add-cart, .btn-buy-now { padding: 16px; min-height: 52px; }
  /* Leave room above so content isn't hidden behind the sticky bar. */
  .product-detail { padding-bottom: 90px; }

  /* ── CART ─────────────────────────────────────────────── */
  .cart-layout { grid-template-columns: 1fr; }
  .cart-page h1 { font-size: 26px; margin-bottom: 18px; }
  .cart-item { padding: 14px; gap: 12px; }
  .cart-item-img { width: 68px; height: 68px; font-size: 30px; }
  .cart-item-name { font-size: 14px; }
  .cart-summary { position: static; }

  /* ── ABOUT PAGE ───────────────────────────────────────── */
  .story-sec { margin-bottom: 36px; }
  .story-sec h3 { font-size: 21px; gap: 10px; }
  .story-num { font-size: 13px; }
  .story-sec p { font-size: 15px; line-height: 1.8; }
  .story-pullquote { margin: 40px 0; padding: 32px 22px 28px; border-radius: 20px; }
  .pq-mark { font-size: 64px; }
  .about-hero h1 { font-size: clamp(28px, 8vw, 44px); }
  .about-hero p { font-size: 15px; margin-bottom: 24px; }

  /* ── CONTACT PAGE ─────────────────────────────────────── */
  .contact-grid { grid-template-columns: 1fr; gap: 28px; }
  .contact-form-wrap { padding: 24px; }
  .form-row { grid-template-columns: 1fr; }

  /* ── FOOTER ───────────────────────────────────────────── */
  /* Flat --navy rather than the desktop navy->navy-deep fade: the overscroll
     canvas can only be one colour, and it has to be the nav's navy for the top
     pull (see OVERSCROLL / RUBBER-BAND above), so the footer has to end on that
     same navy or a bottom pull opens a lighter seam under it. */
  footer { padding: 54px 5% 30px; background: var(--navy); }
  .footer-grid { grid-template-columns: 1fr 1fr; gap: 20px; padding-bottom: 24px; }
  .footer-bottom { flex-direction: column; text-align: center; gap: 6px; }

  /* ── TOAST ────────────────────────────────────────────── */
  .toast { left: 16px; right: 16px; bottom: 82px; }

  /* ── HOME PRODUCTS CAROUSEL ───────────────────────────── */
  .home-carousel-wrap { position: relative; }

  /* ── REVIEWS CAROUSEL ──────────────────────────────────── */
  #revCarousel {
    display: flex !important;
    flex-direction: row !important;
    overflow-x: hidden;
    scrollbar-width: none;
    gap: 16px;
    padding: 0;
    margin: 0;
  }
  #revCarousel::-webkit-scrollbar { display: none; }
  #revCarousel .review-card {
    flex: 0 0 calc(100% / 1.2 - 8px);
    flex-shrink: 0;
    min-width: unset !important;
    width: auto !important;
  }

  /* ── CAROUSEL ARROW BUTTONS ────────────────────────────── */
  .carousel-btn {
    display: flex;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: #fff;
    border: none;
    box-shadow: 0 2px 10px rgba(0,0,0,0.2);
    align-items: center;
    justify-content: center;
    font-size: 22px;
    color: #1a1a2e;
    cursor: pointer;
    z-index: 10;
    transition: opacity 0.2s;
    padding: 0;
    line-height: 1;
  }
  .carousel-prev { left: -6px; }
  .carousel-next { right: -6px; }
  .carousel-btn.disabled { opacity: 0.3; cursor: default; }
  #backToTop { display: none !important; }

}

/* ==================== EXTRA SMALL (≤480px / iPhone 14) ==================== */
@media (max-width: 480px) {
  .footer-grid { grid-template-columns: 1fr !important; }
  .products-grid, .shop-grid { grid-template-columns: 1fr 1fr; }
  .why-grid { grid-template-columns: 1fr; gap: 10px; }
  .logo-text { font-size: 20px; }
  .cart-btn { padding: 9px 14px; font-size: 13px; }
  .product-img { height: 190px; }
  .why-section { padding: 54px 5%; }
  .reviews-section { padding: 48px 5%; }
  .faq-section { padding: 48px 5%; }
  .email-section { padding: 54px 5%; }
  .mission-section { padding: 36px 5%; }
  .stat-num { font-size: 28px; }
  .about-hero { padding: 56px 5%; }
}

/* task 124: the header's centered logo group (absolutely centered on nav —
   see the nav > .logo rule in the ≤900px block) is a FIXED width regardless
   of viewport, while the space available to its left (between it and the
   search icon) shrinks as the viewport narrows. Measured exactly where that
   runs out: clear down to ~340px, a few px of overlap below that. Only
   really hits old/small phones (iPhone SE 1st-gen class, 320px) — nothing
   in the regular modern range (375px+) — but shrinking the logo slightly
   below that width removes the overlap with room to spare rather than
   leaving it merely untested. */
@media (max-width: 360px) {
  .logo-text { font-size: 17px; }
  .logo-circle { width: 32px; height: 32px; }
  .logo { gap: 6px; }
}

/* Disable hover transform effects on touch devices */
@media (hover: none) {
  .product-card:hover { transform: none; box-shadow: none; border-color: var(--line-soft); }
  .why-card:hover { transform: none; }
  .value-row:hover { transform: none; box-shadow: none; }
  /* THE PRODUCT GALLERY'S OWN HOVER, and the reason tasks 100/101 existed.
     Every other hover transform on this site was already listed here; this
     one was missed, and it is the worst possible place to miss one. iOS
     applies :hover to whatever you touch, so landing a finger on the gallery
     started a 300ms scale on .main-img — the ANCESTOR of the .det-track
     scroller. A transform animating on a scroller's ancestor moves it into a
     different compositing/coordinate space mid-gesture, and the in-flight
     touch is dropped rather than scrolled. That is one cause with two
     symptoms: the first horizontal swipe did nothing (task 101), and a
     vertical drag starting on the photo needed several tries before the page
     moved (task 100). Once hover stuck, the transform was stable and later
     gestures behaved — exactly what both reports describe. */
  .main-img:hover { transform: none; }
  .review-card:hover { transform: none; }
  .btn-primary:hover { transform: none; }
  .btn-secondary:hover { transform: none; }
  .btn-black:hover { transform: none; }
  /* Keep color changes, just remove transforms.
     The product card's orange border-color is gone (task 83) — the card has
     `border: none`, so it never painted anything anyway, but leaving an
     orange value on a press state the user was already suspicious of was not
     worth keeping. */
  .product-card:active { transform: scale(0.97); }
}

/* ============================================================
   VISUAL POLISH PASS — additive only, no layout/structure change
   ============================================================ */

/* Subtle press-down on tap/click for the primary action buttons.
   Applies on touch too (press feedback) since it's :active, not :hover.
   Each of these already declares a transition, so the scale eases smoothly. */
.btn-primary:active,
.btn-secondary:active,
.btn-black:active,
.btn-white:active,
.btn-add-cart:active,
.btn-buy-now:active,
.checkout-btn:active,
.email-submit:active,
.offer-btn:active,
.cart-btn:active {
  transform: scale(0.97);
}

/* Balance multi-line section headings so line breaks look even.
   Only affects auto-wrapping (the hero h1 uses explicit <br>, so it's
   untouched); ignored gracefully by browsers without support. */
.section-title { text-wrap: balance; }

/* Respect users who prefer reduced motion: drop the hover/press transforms
   and the scroll-reveal slide so nothing animates, but content stays visible. */
@media (prefers-reduced-motion: reduce) {
  .product-card:hover,
  .product-card:active,
  .btn-primary:active,
  .btn-secondary:active,
  .btn-black:active,
  .btn-white:active,
  .btn-add-cart:active,
  .btn-buy-now:active,
  .checkout-btn:active,
  .email-submit:active,
  .offer-btn:active,
  .cart-btn:active {
    transform: none;
  }
  .reveal { transition: opacity 0.2s ease; transform: none; }
  /* Search panel + offer popup: fade only, no slide/scale */
  .nav-search-bar { transform: none; transition: opacity 0.2s ease, visibility 0s linear 0.2s; }
  .nav-search-bar.open { transition: opacity 0.2s ease; }
  .nav-search-inner { transform: none; transition: opacity 0.2s ease, border-color 0.25s ease, box-shadow 0.25s ease; }
  .offer-popup { transform: translate(-50%, -50%); transition: opacity 0.2s ease, visibility 0s linear 0.2s; }
  .offer-popup.active { transform: translate(-50%, -50%); transition: opacity 0.2s ease; }
  .search-scrim, .offer-overlay { transition: opacity 0.2s ease; }
  img { animation: none; }
}

/* ============================================================
   TABLET / DESKTOP / LARGE — min-width queries ONLY.
   Mobile (≤767px) is final and must never be affected by these.
   ============================================================ */

/* ── TABLET (768px+) ─────────────────────────────────────────
   Phones show ~1.5 cards per carousel view; tablets have room
   for 2. Product grids step up from 2 to 3 columns. */
@media (min-width: 768px) {
  #revCarousel .review-card { flex: 0 0 calc(50% - 8px); }
  .products-grid, .shop-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ── DESKTOP (901px+) ────────────────────────────────────────
   Grids back to their 4-column desktop layout. */
@media (min-width: 901px) {
  .products-grid, .shop-grid { grid-template-columns: repeat(4, 1fr); }

  
}

/* ── LARGE DESKTOP (1440px+) ─────────────────────────────────
   Cap content width so nothing stretches edge-to-edge on big
   monitors (full-bleed section backgrounds stay full-bleed);
   give the hero a little more height so it doesn't look squat. */
@media (min-width: 1440px) {
  .why-header, .walk-route,
  .reviews-header, .reviews-grid-wrap,
  .shop-page, .product-detail,
  .footer-grid, .footer-bottom {
    max-width: 1400px;
    margin-left: auto;
    margin-right: auto;
  }
  #heroSection { height: 600px !important; }
}


/* ============================================================
   TABLET/DESKTOP RESPONSIVE FIX PASS — min-width queries ONLY.
   Mobile (≤767px) is final and untouched by construction.
   ============================================================ */
@media (min-width: 768px) {
  /* IMAGE CROPS */
  .lifestyle-banner img { object-position: center 38%; }  /* dogs' heads sit above center */
  .about-hero-img { object-position: center 60%; }        /* running puppy sits below center */

  /* EQUAL-HEIGHT CAROUSEL CARDS — base .product-card{height:100%} resolves
     as auto inside the flex carousel (no definite container height), so
     short-name cards rendered shorter and Add To Cart buttons misaligned.
     height:auto lets the default align-items:stretch equalize them. */
  .product-card { height: auto; }
}

/* Hero CTAs in the 768–900 overlap: the mobile block stacks them full-width
   (phone style). Side-by-side auto-width reads as intentional tablet layout.
   Scoped with an upper bound so the 901+ inline flex-end alignment is kept. */
@media (min-width: 768px) and (max-width: 900px) {
  #heroSection > div:last-child > div:last-child {
    flex-direction: row !important;
    width: auto !important;
    justify-content: center !important;
  }
  #heroSection > div:last-child > div:last-child button { width: auto !important; }
  #heroSection .btn-primary { width: auto !important; }
}


@media (min-width: 1440px) {
  /* Mission section stretched edge-to-edge on large monitors while every
     other section was capped at 1400px — cap it the same way. */
  .mission-section {
    padding-left: max(5%, calc((100% - 1400px) / 2));
    padding-right: max(5%, calc((100% - 1400px) / 2));
  }
}


/* ============================================================
   WHY SECTION v3 — "THE WALK ROUTE". No photo: the 4 points are
   stops along a dashed dog-walk path, each with an orange number
   badge on the line and a glassy card. Background = navy gradient
   + two soft orange glows + a whisper-faint tiled paw pattern
   (inline SVG — no external image). Mobile: path down the left.
   Desktop: path down the center, stops zigzag left/right.
   ============================================================ */
.why-section {
  position: relative;
  overflow: hidden;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='120' height='120'%3E%3Cg fill='%23ffffff' fill-opacity='0.022' transform='rotate(-18 50 50)'%3E%3Cellipse cx='50' cy='67' rx='20' ry='16'/%3E%3Cellipse cx='27' cy='47' rx='9' ry='12'/%3E%3Cellipse cx='42' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='58' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='73' cy='47' rx='9' ry='12'/%3E%3C/g%3E%3C/svg%3E") repeat,
    radial-gradient(640px 340px at 88% 6%, rgba(232, 99, 10, 0.10), transparent 70%),
    radial-gradient(520px 340px at 4% 96%, rgba(232, 99, 10, 0.07), transparent 70%),
    linear-gradient(160deg, #1f2040 0%, #1a1a2e 55%, #14152b 100%);
  background-size: 150px, auto, auto, auto;
}
.why-content { position: relative; z-index: 1; }

.walk-route {
  position: relative;
  max-width: 940px;
  margin: 0 auto;
}
/* The dashed walk path */
.walk-route::before {
  content: '';
  position: absolute;
  top: 12px;
  bottom: 30px;
  left: 25px;
  border-left: 2px dashed rgba(232, 99, 10, 0.45);
}
/* ...and the route ends at a paw. You arrived. */
.walk-route::after {
  content: '';
  position: absolute;
  bottom: -4px;
  left: 26px;
  transform: translateX(-50%);
  width: 22px; height: 22px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' fill='%23E8630A'%3E%3Cellipse cx='50' cy='67' rx='20' ry='16'/%3E%3Cellipse cx='27' cy='47' rx='9' ry='12'/%3E%3Cellipse cx='42' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='58' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='73' cy='47' rx='9' ry='12'/%3E%3C/svg%3E") center / contain no-repeat;
}

.route-stop { position: relative; padding-left: 78px; }
.route-stop + .route-stop { margin-top: 36px; }

.route-badge {
  position: absolute;
  left: 0;
  top: 0;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 19px;
  color: white;
  background: linear-gradient(135deg, #F58138 0%, var(--orange) 55%, var(--orange-dark) 100%);
  box-shadow: 0 0 0 5px rgba(232, 99, 10, 0.16), 0 8px 20px rgba(232, 99, 10, 0.35);
}

.route-card {
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 16px;
  padding: 20px 22px;
  transition: all 0.3s var(--ease);
}
.route-card:hover {
  background: rgba(232, 99, 10, 0.07);
  border-color: rgba(232, 99, 10, 0.55);
  transform: translateY(-3px);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.35);
}
.route-card-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.route-icon {
  flex: 0 0 38px;
  width: 38px;
  height: 38px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(232, 99, 10, 0.14);
  border: 1px solid rgba(232, 99, 10, 0.38);
  color: var(--orange-light);
}
.route-icon svg { width: 20px; height: 20px; display: block; }
.route-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 20px;
  line-height: 1.2;
  color: #f8f6f2;
}
.route-desc {
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.7;
  color: rgba(255, 255, 255, 0.65);
}

/* Closing CTA — turn the trust points into a purchase decision */
.why-cta {
  text-align: center;
  margin-top: 60px;
}
.why-cta-line {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(20px, 3.2vw, 27px);
  line-height: 1.3;
  color: #f8f6f2;
  max-width: 520px;
  margin: 0 auto 22px;
}

/* Desktop: path down the middle, stops alternate sides */
@media (min-width: 768px) {
  .walk-route::before { left: 50%; margin-left: -1px; }
  .walk-route::after { left: 50%; width: 26px; height: 26px; bottom: -14px; }
  .route-stop {
    width: 50%;
    padding-left: 0;
    padding-right: 74px;
  }
  .route-stop:nth-child(even) {
    margin-left: 50%;
    padding-right: 0;
    padding-left: 74px;
  }
  /* Stagger: each stop starts alongside the previous one's lower half */
  .route-stop + .route-stop { margin-top: -44px; }
  .route-badge {
    width: 56px;
    height: 56px;
    font-size: 21px;
  }
  .route-stop:nth-child(odd) .route-badge { left: auto; right: -28px; }
  .route-stop:nth-child(even) .route-badge { left: -28px; }
  .route-card { padding: 24px 28px; }
  .route-title { font-size: 22px; }
  .route-desc { font-size: 15px; }
  .route-icon { flex-basis: 42px; width: 42px; height: 42px; }
  .route-icon svg { width: 22px; height: 22px; }
  .why-cta { margin-top: 76px; }
}

/* Contact page: bring the two-column layout (info+quick-answers | form) in at
   tablet width too, not just the 901px+ the shared ≤900 block leaves it at.
   The hero photo also gets taller + a re-centered crop here — the wider the
   band, the more a short crop clips the dog out of frame entirely, so give
   it more vertical room instead of losing the second subject. */
@media (min-width: 768px) {
  .contact-grid { grid-template-columns: 1fr 1fr; gap: 40px; }
  .contact-form-wrap { padding: 32px; }
  .form-row { grid-template-columns: 1fr 1fr; }
  .contact-hero-wrap { height: 400px; }
  .contact-hero-img { object-position: center 45%; }
}

/* ==================== CHAT WIDGET ==================== */
/* Everything else in the chat widget is inline (index.html/app.js) — this is
   the one rule that needs an actual class, since :focus can't be expressed
   as an inline style attribute. Navy border at rest, orange glow on focus —
   same pattern as the nav search bar and contact form inputs. */
/* The chat input also carries font-size:16px inline in index.html (which
   wins over this rule); it's restated here so the anti-zoom floor survives
   if that inline style is ever cleaned up. */
.chat-input { font-size: 16px; border: 2px solid rgba(26,26,46,0.22); transition: border-color 0.2s ease, box-shadow 0.2s ease; }
.chat-input:focus { border-color: var(--orange); box-shadow: 0 0 0 4px rgba(232,99,10,0.14); }

/* ==================== PRODUCT-GRID LOADING SKELETON ====================
   The shop grid is rendered by products.js, which can only run once it
   has downloaded — until then that container used to be EMPTY and
   therefore 0px tall, so the footer sat high up on the page and then
   jumped down as the real cards appeared. index.html now ships one static
   `.skel-card` per real product inside it. Each one reuses the real card's
   classes (and the real number of swatches / size pills), so it occupies
   exactly the height the real card
   will — nothing below the grid ever moves. The first innerHTML render
   replaces them, so none of this survives past load.
   These rules only change how the reserved boxes LOOK; they must never
   change a size, a margin or a border WIDTH, or the reservation stops
   matching the real thing. Colour-only, by design. */
.skel-card { pointer-events: none; cursor: default; }

/* The pulse is on the grey FILLS, never on the card itself: fading a whole
   white card on a dark band just turns it grey. */
.skel-card .product-img,
.skel-card .btn-black,
.skel-card .product-name::after,
.skel-card .product-stars::after,
.skel-card .product-price::after { animation: skelPulse 1.5s ease-in-out infinite; }
@keyframes skelPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@media (prefers-reduced-motion: reduce) {
  .skel-card .product-img,
  .skel-card .btn-black,
  .skel-card .product-name::after,
  .skel-card .product-stars::after,
  .skel-card .product-price::after { animation: none; }
}

.skel-card .product-img { background: var(--skel); }
/* Text is kept in the DOM (a non-breaking space, a star, a price span) so the
   line boxes are the same height as the real card's — it's just invisible. */
.skel-card .product-name,
.skel-card .product-stars,
/* the number and the count carry their own colours, so they need naming
   explicitly. The skeleton row ships the SAME three parts as the real row
   (number, star box, count) so the height it reserves matches to the pixel —
   a lighter placeholder was 1px shorter and jolted the grid on swap. */
.skel-card .product-stars-num,
.skel-card .product-stars-txt,
.skel-card .product-stars span,
.skel-card .product-price,
.skel-card .btn-black { color: transparent; }
.skel-card .product-stars svg { fill: transparent; }
.skel-card .btn-black { background: var(--skel); }

/* The grey bars. Drawn as absolutely-positioned pseudo-elements (zero effect
   on layout) rather than backgrounds, so they can be rounded and inset. */
.skel-card .product-name,
.skel-card .product-stars,
.skel-card .product-price { position: relative; }
.skel-card .product-name::after,
.skel-card .product-stars::after,
.skel-card .product-price::after {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  border-radius: 5px;
  background: var(--skel);
}
.skel-card .product-name::after  { width: 82%; height: 11px; }
.skel-card .product-stars::after { width: 54%; height: 8px; }
.skel-card .product-price::after { width: 44%; height: 15px; }

/* ============================================================
   DESKTOP LAYOUT PASS
   ------------------------------------------------------------
   Every rule below is min-width only, so none of it can reach
   the ≤900 / ≤767 / ≤480 blocks earlier in this file — the
   mobile layout is signed off and must stay byte-identical.
   Appended last on purpose: it supersedes earlier min-width
   rules it deliberately replaces (notably the 1440px hero
   height), which same-specificity source order settles.

   The through-line: several numbers here were mobile values
   that were never re-thought for a wide viewport. Held at
   1920px they don't read as "big" — they read as a phone
   layout stretched, with letterboxed photos and text columns
   marooned in whitespace.
   ============================================================ */

@media (min-width: 901px) {
  /* PRODUCT GALLERY — the dots were laying out BESIDE the photo.
     .main-img is display:flex in the base rules (it used to centre a 130px
     emoji placeholder) and .det-dots is its second child, so at desktop the
     dots became a flex ROW item: 56px stolen off the image and a bare
     --orange-pale strip down the right of every product photo. The ≤900
     block already restructured exactly this ("main-img becomes auto-height
     wrapper; det-carousel provides the image area") — desktop never got the
     same treatment. Radius moves to the carousel, which is what now clips. */
  .main-img { display: block; height: auto; background: transparent; }
  .det-carousel { height: 520px; border-radius: var(--radius); }
}

@media (min-width: 1024px) {
  /* ---------- HERO ---------- */
  /* Height was a flat inline 520px — a mobile number. Held at desktop widths
     it crops the two PORTRAIT hero photos (natural 2:3) to a 2.5-3.2:1
     letterbox, i.e. ~20% of the source height survives. A taller box keeps a
     taller slice, which is most of the "hero image isn't cropped right on
     desktop" problem. */
  #heroSection { height: 640px !important; }
  /* The copy block was still on mobile measurements too: a 180px-wide
     paragraph (four words a line) pinned against the right edge. */
  #heroSection > div:last-child { padding: 0 7% !important; }
  #heroSection > div:last-child p {
    max-width: 430px !important;
    font-size: 17px !important;
    line-height: 1.75 !important;
  }

  /* ---------- SECTION HEADERS ---------- */
  /* A 480px measure orphaned the last word of every section subtitle at
     desktop ("...walks with your / dog"). */
  .why-header .section-sub,
  .about-values-header .section-sub { max-width: 640px !important; }

  /* ---------- THE PAWHAUL PROMISE ---------- */
  /* A 940px column marooned in the middle of a wide screen. */
  .walk-route { max-width: 1080px; }

  /* ---------- LANDSCAPE / LIFESTYLE BANDS ---------- */
  /* All three held mobile heights at desktop widths, so they degraded into
     letterbox strips as the viewport grew — the mission banner was a 7.7:1
     sliver at 1920. Product photography is untouched; these are the
     full-bleed scenic bands only. */
  .lifestyle-banner { height: 360px !important; }
  .about-hero-wrap { height: 440px !important; }
  .contact-hero-wrap { height: 440px; }

  /* ---------- CART ---------- */
  /* The only main page with no width cap: at 1920 the item row ran the full
     1728px, leaving the price at the far left and the quantity stepper
     ~1000px away from it. Capped to match every other section. */
  .cart-page { max-width: 1400px; margin-left: auto; margin-right: auto; }
}

@media (min-width: 1440px) {
  #heroSection { height: 680px !important; }
  #heroSection > div:last-child h1 { font-size: 64px !important; }
  .lifestyle-banner { height: 400px !important; }
  .about-hero-wrap { height: 480px !important; }
  .contact-hero-wrap { height: 480px; }

}

/* Very wide monitors: the 1400px cap starts to look like a narrow ribbon
   down the middle of the screen, so it (and the things measured against it)
   step up once. Kept to a single step — beyond this, line lengths stop being
   comfortable to read and the layout would need a different design, not a
   bigger number. */
@media (min-width: 1728px) {
  .why-header, .reviews-header, .reviews-grid-wrap,
  .shop-page, .product-detail,
  .cart-page,
  .footer-grid, .footer-bottom {
    max-width: 1560px;
  }
  .mission-section {
    padding-left: max(5%, calc((100% - 1560px) / 2));
    padding-right: max(5%, calc((100% - 1560px) / 2));
  }
  #heroSection { height: 760px !important; }
  #heroSection > div:last-child h1 { font-size: 72px !important; }
  #heroSection > div:last-child p { max-width: 480px !important; font-size: 18px !important; }
  .lifestyle-banner { height: 440px !important; }
  .det-carousel { height: 600px; }
}

/* ============================================================
   BLOG
   Self-contained: every selector below is a new .blog-* class, so this block
   cannot collide with the existing cascade and is appended at the very end
   (after the min-width desktop passes) purely for locality. Written
   desktop-first with ≤900/≤600 overrides, matching the rest of the file.

   The measure is capped at 720px on purpose — long-form body text is the one
   place on this site where the wide content cap would hurt, since a 1400px
   line length is unreadable. See the note about .faq-wrap in the desktop
   layout pass: same reasoning, deliberate reading width, not a leftover.
   ============================================================ */

.blog-hero {
  background: var(--navy);
  color: white;
  padding: 110px 5% 70px;
  text-align: center;
}
.blog-hero .section-tag { color: var(--orange); }
.blog-hero h1 {
  font-family: var(--font-display);
  font-size: 46px;
  line-height: 1.15;
  margin: 10px 0 14px;
  color: white;
}
.blog-hero p {
  max-width: 640px;
  margin: 0 auto;
  font-size: 17px;
  line-height: 1.6;
  color: rgba(255,255,255,0.82);
}

.blog-wrap { padding: 70px 5% 90px; }

.blog-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 28px;
  max-width: 1000px;
  margin: 0 auto;
}

.blog-card {
  display: block;
  text-decoration: none;
  color: inherit;
  background: white;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease);
}
.blog-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-hover); }
.blog-card-img { height: 200px; background: var(--img-placeholder); overflow: hidden; }
.blog-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.blog-card-body { padding: 22px 24px 24px; }
.blog-card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.blog-card-tags span {
  background: var(--orange-pale);
  color: var(--orange-dark);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 50px;
}
.blog-card h2 {
  font-family: var(--font-display);
  font-size: 21px;
  line-height: 1.25;
  color: var(--navy);
  margin: 0 0 10px;
}
.blog-card p { font-size: 15px; line-height: 1.6; color: var(--gray); margin: 0 0 14px; }
.blog-card-meta {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; font-weight: 700; color: var(--gray);
}

/* ---------- article ---------- */

.blog-article { max-width: 720px; margin: 0 auto; }

/* GOTCHA: styles.css has a bare `nav { }` rule (the site navbar was written as
   an unclassed element) which applies to EVERY <nav> on the page — including a
   breadcrumb inside article content. Without these resets the crumbs render as
   a full-width navy sticky bar at z-index 500 floating over the article. Any
   future <nav> in page content needs the same treatment. */
.blog-crumbs {
  display: flex; align-items: center; gap: 8px;
  justify-content: flex-start;
  background: none;
  padding: 0;
  position: static;
  box-shadow: none;
  z-index: auto;
  font-size: 13px; font-weight: 700; color: var(--gray); margin-bottom: 18px;
}
.blog-crumbs a { color: var(--orange); text-decoration: none; }
.blog-crumbs a:hover { text-decoration: underline; }

.blog-article-title {
  font-family: var(--font-display);
  font-size: 42px;
  line-height: 1.15;
  color: var(--navy);
  margin: 0 0 14px;
}
.blog-article-meta {
  display: flex; align-items: center; gap: 8px;
  font-size: 14px; font-weight: 700; color: var(--gray); margin-bottom: 28px;
}
.blog-article-img {
  width: 100%;
  height: 380px;
  object-fit: cover;
  border-radius: var(--radius);
  display: block;
  margin-bottom: 36px;
  background: var(--img-placeholder);
}

.blog-body { font-size: 17px; line-height: 1.75; color: #333; }
.blog-body h2 {
  font-family: var(--font-display);
  font-size: 27px;
  line-height: 1.25;
  color: var(--navy);
  margin: 44px 0 14px;
}
.blog-body h3 {
  font-family: var(--font-body);
  font-weight: 900;
  font-size: 20px;
  color: var(--navy);
  margin: 30px 0 10px;
}
.blog-body p { margin: 0 0 18px; }
.blog-body ul, .blog-body ol { margin: 0 0 20px; padding-left: 24px; }
.blog-body li { margin-bottom: 10px; }
.blog-body strong { color: var(--navy); font-weight: 800; }
.blog-body a { color: var(--orange); font-weight: 700; text-decoration: underline; text-underline-offset: 2px; }
.blog-body a:hover { color: var(--orange-dark); }

/* In-article product callout. Visually distinct from the body copy so it
   reads as a recommendation rather than as smuggled-in body text. */
.blog-cta {
  background: var(--light-gray);
  border-left: 4px solid var(--orange);
  border-radius: var(--radius-sm);
  padding: 18px 22px;
  margin: 30px 0;
}
.blog-cta p { margin: 0; font-size: 16px; }

.blog-note {
  font-size: 14px;
  color: var(--gray);
  font-style: italic;
  border-top: 1px solid var(--line);
  padding-top: 16px;
  margin-top: 28px;
}

.blog-article-foot {
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
  margin-top: 44px; padding-top: 30px; border-top: 1px solid var(--line);
}
.blog-back { color: var(--gray); font-weight: 700; text-decoration: none; font-size: 15px; }
.blog-back:hover { color: var(--orange); }

@media (max-width: 900px) {
  .blog-hero { padding: 90px 5% 50px; }
  .blog-hero h1 { font-size: 34px; }
  .blog-hero p { font-size: 15px; }
  .blog-wrap { padding: 44px 5% 60px; }
  .blog-grid { gap: 20px; }
  .blog-article-title { font-size: 30px; }
  .blog-article-img { height: 240px; margin-bottom: 26px; }
  .blog-body { font-size: 16px; }
  .blog-body h2 { font-size: 23px; margin: 34px 0 12px; }
  .blog-body h3 { font-size: 18px; }
}

@media (max-width: 600px) {
  .blog-grid { grid-template-columns: minmax(0, 1fr); }
  .blog-card-img { height: 180px; }
  .blog-hero h1 { font-size: 28px; }
  .blog-article-title { font-size: 26px; }
  .blog-article-img { height: 200px; }
  .blog-cta { padding: 15px 17px; }
  .blog-article-foot { gap: 14px; }
}

/* ==========================================================================
   MENU REDESIGN (task 49) — mobile slide-out panel + desktop nav
   --------------------------------------------------------------------------
   Appended at the END on purpose: the older .mob-menu / .nav-links rules and
   the ≤900 mobile block both live earlier in this file, and several values
   here need to beat them without editing rules other pages depend on.
   Everything is scoped to a breakpoint so the two designs stay independent —
   the panel is mobile-only (.mob-menu is display:none above 900) and the
   inline nav is desktop-only.

   The active state on both is driven by markActiveNav() in products.js via
   [data-nav] + .is-active — NOT by the html.route-* classes, which are
   stripped as soon as client-side routing takes over.
   ========================================================================== */

/* ---------- shared: the scrim now fades instead of snapping ---------- */
/* It used to toggle display:none/block, so the dim appeared and vanished in a
   single frame while the panel took 280ms to slide — that mismatch is most of
   why the old menu felt abrupt. */
.mob-menu-overlay {
  display: block;
  background: rgba(13, 13, 26, 0.5);
  opacity: 0;

  /* NO `visibility` ON EITHER OVERLAY ANY MORE, AND THAT IS THE POINT.
     `visibility: hidden` at the end of the close is what lets the browser
     DESTROY the scrim's and the panel's compositing layers. On iOS the fixed
     header is re-composited a frame late when that happens: for that frame it
     is drawn attached to the layout viewport while the page behind is painted
     in the taller visual viewport Safari uses when its top bar is collapsed —
     so a strip of the page (the hero photo, the hero copy) appears ABOVE the
     header. That is the reported flash. It lands exactly at the end of every
     close, which is why no timing or easing change could reach it. The layers
     now stay alive, fully transparent and not interactive.
     `pointer-events` does the work `visibility` used to; `aria-hidden` and
     `inert` (set in app.js) keep the closed panel out of the accessibility
     tree and out of the tab order. Anything that reintroduces `visibility`
     here brings the flash back.
     Ruled out on the way here, do not retry: the scrim's `backdrop-filter`
     (removed — a full-viewport blur snapshots the page underneath, same class
     of teardown), `will-change` on either layer (removed — promoting them
     changes the teardown, and it is what the blur wanted), a `nav::before`
     navy strip (reverted — the nav is a composited layer on mobile and it
     painted over the OPEN DRAWER'S OWN HEADER on iOS), and a `theme-color`
     meta (kept, correct on its own terms, but Safari only tints chrome it
     owns). The header is already `position: fixed` on mobile, so this was
     never a sticky-positioning problem. */
  pointer-events: none;

  /* The scrim sits over the header, so its opacity IS the header's
     brightness: an accelerating exit un-dims the top of the page almost
     within a single frame. It leaves on the gentle decelerating curve even
     though the panel leaves quickly on --ease-exit. */
  transition: opacity 0.34s var(--ease);

  /* A drag that starts on the scrim used to scroll the page along underneath
     the open panel — the content sliding behind a still menu is the jankiest
     thing about the old one. This kills the gesture at the source instead of
     locking `body` (which iOS ignores for touch anyway, and which risks
     losing the scroll position). Beats the `* { touch-action: manipulation }`
     rule in the ≤900 block on specificity. */
  touch-action: none;
}
/* Opening: the dim leads the panel in — it starts under it and is nearly
   complete by the time the panel arrives, so the page darkens *because* the
   menu is coming rather than a beat after it. Paced against the panel's own
   0.55s fade: keep this shorter than that, never longer. */
.mob-menu-overlay.open {
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.42s var(--ease);
}

@media (max-width: 900px) {
  /* ---------- panel shell ---------- */
  /* task 125: full-page takeover, not a right-side drawer — left:0 on top of
     the base rule's top/right/bottom:0 is what makes this cover the whole
     viewport (that base rule never declared `left`, which is exactly how the
     drawer got its width: an implicit left edge at `100% - width`). No more
     border-radius/box-shadow: those read as a floating card, which a
     full-bleed page is not. */
  .mob-menu {
    left: 0;
    width: auto;
    background: #fff;
    box-shadow: none;
    border-radius: 0;

    /* THE PANEL FADES IN; it does not slide across. The base .mob-menu rule
       higher up this file parks it at translateX(100%) — a full panel-width
       slide — and this overrides that with a fade plus a gentle scale, which
       reads as an arrival without a drawer's sense of direction (there is no
       "from the right" any more — the panel covers the whole screen).
       (The base rule is left alone: it is what a browser that never reaches
        this block still gets, and off-screen is the safe closed state.) */
    opacity: 0;
    transform: scale(0.98);
    /* CLOSING timing — shorter, and accelerating away. A panel leaving on the
       same long quint-out it arrived on is what made the old menu feel
       sluggish: the tail of that curve creeps on after the motion looks over. */
    /* Overrides the base rule's `visibility: hidden` — see the scrim's note
       above for why neither overlay may flip visibility. opacity 0 plus the
       base rule's `pointer-events: none` do the hiding. */
    visibility: visible;
    transition: opacity 0.24s var(--ease-exit), transform 0.24s var(--ease-exit);
    overscroll-behavior: contain;
    /* See the scrim's note above for why there is no will-change /
       backface-visibility here. */
  }
  /* ARRIVING: slow and decelerating, with the soft landing --ease is for.
     Deliberately much longer than the exit (0.55s vs 0.24s) — the menu eases
     into view, but still gets out of the way promptly when dismissed. */
  .mob-menu.open {
    opacity: 1;
    transform: scale(1);
    transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
  }

  /* ---------- header: navy band carrying the brand and the close button ---------- */
  /* Replaces the old white "Menu" bar PLUS the separate centred brand block
     under it — two stacked headers for one panel, which is what left so much
     empty space at the top. Both pieces of content survive, in one band. */
  .mob-menu-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 18px 18px 17px;
    padding-top: calc(18px + env(safe-area-inset-top));
    background: var(--navy);
    border-bottom: none;
    flex-shrink: 0;
  }
  /* Now a <button> (tapping it goes home — see index.html) rather than a
     plain div, so it needs the usual button reset on top of its layout. */
  .mob-menu-brand {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 11px;
    padding: 0;
    border: none;
    border-bottom: none;
    background: none;
    font: inherit;
    text-align: left;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .mob-menu-brand-icon {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    box-shadow: 0 4px 14px rgba(232, 99, 10, 0.35);
    flex-shrink: 0;
  }
  .mob-menu-brand-copy { display: flex; flex-direction: column; gap: 1px; }
  .mob-menu-title {
    font-family: var(--font-body);
    font-weight: 800;
    font-size: 10px;
    letter-spacing: 1.4px;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.5);
    line-height: 1;
  }
  .mob-menu-brand-text { font-size: 20px; color: #fff; line-height: 1.15; }

  .mob-menu-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    padding: 0;
    border-radius: 50%;
    color: #fff;
    background: rgba(255, 255, 255, 0.12);
    transition: background 0.2s var(--ease), transform 0.25s var(--ease);
  }
  .mob-menu-close:active { background: rgba(255, 255, 255, 0.24); transform: rotate(90deg); }

  /* ---------- task 125: full-page image-grid menu ---------- */
  /* The scrollable middle of the panel — header and footer stay put, this is
     the part that grows past one screen on a short phone (a 2x2 image grid
     plus the Blog/Contact rows is taller than the old plain link list was).
     flex:1 + overflow-y:auto is what makes that scroll instead of overflowing
     the fixed-height panel or getting clipped. */
  .mob-menu-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  .mob-menu-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    padding: 16px 16px 6px;
  }
  /* `.mob-menu a.mob-grid-tile`, not bare `.mob-grid-tile`: the base
     `.mob-menu a` rule (class+tag, far above this file) sets display:block
     and padding:18px 20px, and beats a single-class selector on specificity
     regardless of source order — the exact trap `.mob-menu a.mob-link`
     elsewhere in this file exists to dodge. Skipping it here silently broke
     the flex layout (tiles rendered as blocks with the link-row padding,
     pushing the label to the top-left instead of flex-end at the bottom). */
  .mob-menu a.mob-grid-tile {
    position: relative;
    display: flex;
    align-items: flex-end;
    aspect-ratio: 1 / 1.05;
    padding: 0;
    border-bottom: none;
    border-radius: 16px;
    overflow: hidden;
    background-size: cover;
    background-position: center;
    background-color: var(--light-gray);
    -webkit-tap-highlight-color: transparent;
    transition: transform 0.15s var(--ease);
  }
  .mob-menu a.mob-grid-tile:active {
    transform: scale(0.97);
    color: #fff;
    background-color: var(--light-gray);
  }
  /* Darkens the bottom half so the white label stays legible over any photo
     (including a plain white product-studio shot, not just a dark lifestyle
     one), without a flat tint over the whole tile. */
  .mob-grid-tile-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, rgba(13, 13, 26, 0) 32%, rgba(13, 13, 26, 0.88) 100%);
    pointer-events: none;
  }
  .mob-grid-tile-label {
    position: relative;
    z-index: 1;
    padding: 14px;
    font-family: var(--font-body);
    font-weight: 800;
    font-size: 15px;
    color: #fff;
    text-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
  }
  /* Wishlist: no lifestyle photo fits "saved favorites", so this tile is
     navy/orange-gradient-and-icon instead of photo-and-caption.
     `.mob-menu a.mob-grid-tile.mob-grid-tile--brand`, not bare
     `.mob-grid-tile--brand`: align-items:center here would otherwise lose to
     `.mob-menu a.mob-grid-tile`'s align-items:flex-end above on specificity,
     the same trap documented on that rule. */
  .mob-menu a.mob-grid-tile.mob-grid-tile--brand {
    background-image: linear-gradient(145deg, var(--navy), #232340);
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
  }
  .mob-grid-tile--brand .mob-grid-tile-icon { color: var(--orange); }
  .mob-grid-tile--brand .mob-grid-tile-label { padding: 0; }
  /* Current page, same treatment as the .mob-link rows below. */
  .mob-menu a.mob-grid-tile.is-active { box-shadow: inset 0 0 0 3px var(--orange); }

  /* ---------- link list (Blog, Contact — everything else is a grid tile
     above, "Home" is the header's own logo/brand button) ---------- */
  /* This is a real <nav> element, and the NAVBAR rule near the top of this
     file selects bare `nav` — so without resetting it here the menu's list
     inherits the site header's sticky positioning, navy background, flex-row
     layout and z-index:500, and ends up laid out across the panel header
     swallowing clicks on the close button. Any future <nav> in this document
     hits the same trap — and it caught a NEW one: task 118 later gave the
     site's mobile nav bar `height: 60px` (for the fixed announce-bar+nav
     stack), and because this rule never re-declared `height`, the class
     selector here still lost that one property to the bare-tag rule (a
     class beats a tag on properties BOTH declare, but declares nothing for
     a property either). That silently clamped this whole link list's box
     to 60px tall while its 10 real children kept rendering at their full
     size past that boundary (overflow:visible doesn't clip, it just means
     the box's own geometry stops matching where its content visibly ends) —
     which is exactly what put the footer, positioned right after that
     60px box, up over the last couple of rows instead of below all of
     them. `height: auto` below is the actual fix; explicit `height`
     is why this needs restating even though `position`/`background`/etc.
     already guard the rest of the bare `nav` rule. */
  .mob-menu-nav {
    position: static;
    display: block;
    background: none;
    box-shadow: none;
    z-index: auto;
    height: auto;
    flex-shrink: 0;
    margin-top: 10px;
    padding: 14px 12px 32px;
    border-top: 1px solid var(--line-soft);
  }
  .mob-menu-group {
    font-family: var(--font-body);
    font-weight: 800;
    font-size: 10px;
    letter-spacing: 1.3px;
    text-transform: uppercase;
    color: #9A9A9A;
    padding: 0 10px;
    margin: 0 0 6px;
  }
  /* Separation between groups, but nothing above the first one. */
  .mob-link + .mob-menu-group {
    margin-top: 18px;
    padding-top: 16px;
    border-top: 1px solid var(--line-soft);
  }

  /* 60px rows — comfortably past the 44px minimum, and the whole row is the
     target, not just the text. The old list used full-bleed hairline dividers;
     these are inset rows with their own radius, which still reads as a list
     without the ruled-paper look. */
  .mob-menu a.mob-link {
    display: flex;
    align-items: center;
    gap: 13px;
    min-height: 60px;
    padding: 10px 12px;
    border-bottom: none;
    border-radius: 14px;
    font-family: var(--font-body);
    font-weight: 800;
    font-size: 15.5px;
    color: var(--navy);
    /* NO `transition` here on purpose — the row's transition list is declared
       once, in the staggered-entrance block below, because the per-row delays
       there are given PER PROPERTY and so have to match its property order.
       This rule used to carry its own list, and being one element-selector
       more specific than the entrance rule it silently won: the rows' opacity
       was never in the transition at all, so they popped in at full opacity
       while only the group labels animated. */
  }
  .mob-link-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    border-radius: 12px;
    /* A shade deeper than --light-gray: at #f8f6f2 the tiles all but vanished
       against the white panel and the icons read as floating. */
    background: #F1ECE3;
    color: var(--navy);
    transition: background 0.2s var(--ease), color 0.2s var(--ease);
  }
  .mob-link-label { flex: 1; min-width: 0; }
  .mob-link-chev {
    flex-shrink: 0;
    color: #C9C4BC;
    transition: transform 0.2s var(--ease), color 0.2s var(--ease);
  }

  /* `.mob-menu.open`, not `.mob-menu`: this now out-specifies .is-active too,
     so the row for the page you are on responds to a press like every other
     row instead of sitting there inert. */
  .mob-menu.open a.mob-link:active {
    background: var(--light-gray);
    color: var(--navy);
    transform: scale(0.985);
    transition-delay: 0s;
  }
  .mob-menu.open a.mob-link:active .mob-link-icon { background: var(--orange-pale); color: var(--orange); }
  .mob-menu.open a.mob-link:active .mob-link-chev { transform: translateX(2px); color: var(--orange); }

  /* Current page. The icon tile carries the orange so the row stays legible
     instead of turning into a block of colour. */
  .mob-menu a.mob-link.is-active { background: var(--orange-pale); color: var(--orange-dark); }
  .mob-menu a.mob-link.is-active .mob-link-icon {
    background: var(--orange);
    color: #fff;
    box-shadow: 0 4px 12px rgba(232, 99, 10, 0.3);
  }
  .mob-menu a.mob-link.is-active .mob-link-chev { color: var(--orange); }

  /* ---------- contents ---------- */
  /* NO per-row entrance. The panel itself fades in as one piece, so the rows
     arrive with it — a cascade on top of that gave the same content two
     separate arrivals and left the bottom of the panel visibly empty while
     the tail of it caught up. (The staggered version lived here until it was
     replaced; if it ever comes back, note the trap it had: this rule set is
     also the ONE place the rows' transition list is declared, because the
     row rule above it is more specific and its own list silently won.) */
  .mob-menu a.mob-link {
    transition: background 0.2s var(--ease), color 0.2s var(--ease), transform 0.2s var(--ease);
  }

  /* ---------- footer ---------- */
  /* Bottom padding: env(safe-area-inset-bottom) is the "proper" way to clear
     the home indicator / gesture bar, same pattern as the header's own
     safe-area-inset-top above — but it only ever returns a real, non-zero
     value on a page that opts into edge-to-edge layout via
     `viewport-fit=cover` in the viewport meta tag, which this site does not
     (a bigger, riskier, site-wide change: it would also affect how the
     fixed announce-bar/nav stack and the overscroll colour-matching sit
     against the notch and status bar, all tuned assuming the contained,
     non-cover viewport this site currently uses). Without it, env() here
     silently resolves to 0px, same as it already did on the header's
     padding-top. `max()` is what makes this actually fix the reported bug
     today: it guarantees a real 40px floor regardless of whether env()
     is doing anything, and would automatically prefer the more precise
     safe-area value over that floor if this page ever does adopt
     viewport-fit=cover later. */
  .mob-menu-footer {
    border-top: 1px solid var(--line-soft);
    background: var(--light-gray);
    padding: 18px 20px max(40px, calc(20px + env(safe-area-inset-bottom)));
  }
  .mob-menu-tagline { font-size: 11px; color: #8A8A8A; }

  /* The chat button is fixed to the same bottom-right corner the panel
     occupies, and used to sit on top of the tagline while the menu was open.
     Degrades to the old overlap where :has() is unsupported. */
  body:has(.mob-menu.open) #chatToggle {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s var(--ease);
  }
}

/* Reduced motion still gets the panel — just without the fade, the drift and
   the burger morph. (The scrim's blur is gone for everyone now.) */
@media (prefers-reduced-motion: reduce) {
  .mob-menu,
  .mob-menu.open,
  .hamburger span,
  .mob-menu-overlay,
  .mob-menu-overlay.open { transition-duration: 0.01ms !important; transition-delay: 0s !important; }
}

/* ==================== DESKTOP NAV (≥901px) ====================
   A hover dropdown was considered and deliberately not used: every entry is a
   single destination with no children, so a dropdown would either be empty or
   need sub-items invented — and this task is a visual redesign, not a
   restructure. What the desktop nav actually lacked was hit area and any
   sense of place: bare text with a 2px underline that existed only on hover,
   so nothing ever indicated the page you were already on.
   ============================================================== */
@media (min-width: 901px) {
  .nav-links { gap: 4px; align-items: center; }

  .nav-links a {
    display: block;
    padding: 9px 15px;
    border-radius: 11px;
    font-size: 14.5px;
    letter-spacing: 0.1px;
    transition: color 0.22s var(--ease), background 0.22s var(--ease);
  }

  /* The old rule grew a 2px bar under the text on hover. That bar now marks
     the CURRENT page, and hover gets a filled pill instead — so the two states
     are distinguishable rather than identical. */
  .nav-links a::after {
    bottom: 4px;
    left: 50%;
    width: 0;
    height: 2.5px;
    border-radius: 2px;
    transform: translateX(-50%);
    transition: width 0.28s var(--ease);
  }
  /* The two states are told apart by KIND, not by intensity: hover is a pill,
     current page is an orange label with a bar under it. An orange-tinted pill
     was tried first and is not an option — any orange at a low alpha over
     #1a1a2e mixes to a muddy brown that reads as a dirty box rather than as
     the brand colour. */
  .nav-links a:hover { color: #fff; background: rgba(255, 255, 255, 0.08); }
  .nav-links a:hover::after { width: 0; }

  .nav-links a.is-active { color: var(--orange); background: none; }
  .nav-links a.is-active::after { width: 22px; box-shadow: 0 1px 6px rgba(232, 99, 10, 0.55); }
  .nav-links a.is-active:hover { color: var(--orange-light); background: rgba(255, 255, 255, 0.08); }
  .nav-links a.is-active:hover::after { width: 22px; }

  /* Keyboard users were getting the browser default outline on a navy bar. */
  .nav-links a:focus-visible,
  .nav-search-btn:focus-visible,
  .cart-btn:focus-visible {
    outline: 2px solid var(--orange);
    outline-offset: 3px;
  }

  /* The search icon sat bare next to a solid orange cart pill, which made it
     read as a stray glyph rather than a control. */
  .nav-search-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    color: #fff;
    transition: background 0.22s var(--ease), color 0.22s var(--ease);
  }
  .nav-search-btn:hover { background: rgba(255, 255, 255, 0.11); color: #fff; }

  .nav-right { gap: 10px; }
}

/* ============================================================
   HERO TEXT CENTRED — DESKTOP ONLY (task 51)

   The hero copy block is 100% INLINE styles in index.html
   (align-items:flex-end + text-align:right on the container,
   margin-left:auto + text-align:right on the paragraph,
   justify-content:flex-end on the CTA row), so every rule here
   needs !important to reach it. Appended LAST on purpose: the
   min-1024 / min-1440 / min-1728 hero blocks above set some of
   these same properties, and same-specificity source order is
   what settles it.

   Scoped min-width:901px — the SAME boundary the mobile hero
   block (max-width:900px) ends at, so the two never overlap and
   the mobile hero, which was already centred and correct, is
   not touched by any of this.

   Target children by :last-child / :nth-child WITHIN the copy
   block only. Never position-match #heroSection's own children:
   the slideshow <img>s are its first children and adding them
   is what broke the old div:nth-child(2) overlay selector.
   ============================================================ */

@media (min-width: 901px) {
  /* The block itself: centred in the hero, with its own text centred to
     match. A left-aligned ragged edge inside a centred block reads as a
     mistake — the whole point of moving it off the right edge is that it
     now has no edge to align to. */
  #heroSection > div:last-child {
    align-items: center !important;
    text-align: center !important;
  }

  /* The paragraph was pinned right with margin-left:auto. It needs BOTH
     margins auto now, and its own inline text-align:right undone. The
     max-width set by the blocks above is kept — it's a reading measure,
     and it's what stops the sentence spanning the full 1920px. */
  #heroSection > div:last-child p {
    margin-left: auto !important;
    margin-right: auto !important;
    text-align: center !important;
  }

  /* CTA row: inline justify-content:flex-end. */
  #heroSection > div:last-child > div:last-child {
    justify-content: center !important;
  }

  /* The overlay was built FOR right-aligned text: a one-way gradient, 0.7
     black at the right edge fading to fully transparent at the left. Centred
     text sits where that gradient has thinned to ~0.35, and its left half
     runs out over bare photo — measurably worse contrast than the old
     right-aligned copy enjoyed.

     So: symmetric, with a FLAT 0.68 plateau across the middle 36% rather
     than a single midpoint stop. A plain 3-stop gradient peaks only at the
     exact centre, which leaves the outer thirds of a ~500px-wide text block
     on a visibly thinner scrim; the plateau is a little wider than the block
     at every desktop width, so the whole of it sits on peak darkness. The
     edges keep 0.32 so the photograph still reads instead of flattening to
     grey. Measured against the pre-change build: headline and body copy now
     score at or above what the right-aligned version had. */
  #heroSection .hero-overlay {
    background: linear-gradient(to right,
      rgba(0, 0, 0, 0.32) 0%,
      rgba(0, 0, 0, 0.68) 32%,
      rgba(0, 0, 0, 0.68) 68%,
      rgba(0, 0, 0, 0.32) 100%) !important;
  }
}

/* 901-1023 was never given desktop hero measurements: the earlier desktop
   pass starts its hero rules at min-width:1024, so this band still runs the
   mobile 180px paragraph — four words a line. Pinned to the right edge that
   merely looked cramped; centred under a 56px headline it reads as broken.

   Deliberately bounded at 1023 rather than folded into the block above:
   this file's LAST matching rule wins, and an unbounded max-width here would
   override the 430px (min-1024) and 480px (min-1728) measures set earlier. */
@media (min-width: 901px) and (max-width: 1023px) {
  #heroSection > div:last-child p {
    max-width: 340px !important;
  }
}

/* ============================================================
   TRUST / PAYMENT SIGNALS (task 52)

   Every mark in this row corresponds to a payment method the
   store genuinely accepts — verified against the Storefront
   API's shop.paymentSettings AND against the real checkout
   page (see the comment above PAYMENT_MARKS in products.js).
   Nothing here is decorative.

   Restraint is the point: a wall of saturated card logos reads
   as try-hard and fights the navy/orange palette, so the marks
   are small, on neutral chips, and capped at six.
   ============================================================ */

.trust-badges { margin-top: 16px; }

.trust-badges-line {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--gray);
  text-align: center;
  margin: 0;
}
.trust-badges-line svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--green); }
.trust-badges-line strong { color: var(--black); font-weight: 800; }

.pay-marks {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 10px;
}
/* Fixed box so the row's height never depends on how many marks render. */
.pay-mark {
  width: 34px;
  height: 22px;
  display: block;
  flex: 0 0 auto;
  border-radius: 3px;
}

.trust-badges-note {
  margin: 10px 0 0;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--gray);
  text-align: center;
  font-weight: 600;
}

/* Cart-only: the returns line that used to read "Secure checkout • 30 day
   returns". "Secure checkout" moved into the badge block above, where it now
   says who actually secures it, so repeating it here would be noise. */
.cart-returns-line {
  text-align: center;
  font-size: 12px;
  color: var(--gray);
  margin-top: 14px;
  font-weight: 600;
}

/* ---- Buy with Shop Pay ---- */
.shop-pay-or {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0 12px;
  color: var(--gray);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.shop-pay-or::before, .shop-pay-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* HEIGHT IS RESERVED ON PURPOSE. <shop-pay-button> is unknown to the browser
   until Shopify's module arrives from their CDN, and an unknown element is an
   inline 0-height box — without this the trust badges and everything under
   them would jump ~46px when it upgrades. 46px = the button's own rendered
   height (42px, measured) plus its internal margin. If Shopify ever changes
   that height, this number is what keeps the page still. */
.shop-pay-wrap {
  min-height: 46px;
  display: block;
}
/* The host element already spans the full summary width, but the real button
   lives in Shopify's SHADOW ROOT at a fixed 260px and sits flush LEFT inside
   it — so it drew ~32px left of the Checkout Securely button's centre. Shadow
   content still lays out inside the host's box, so making the host a centring
   flex container moves it without needing to reach into the shadow root
   (which CSS can't do anyway). Measured: centre offset 32px -> 0px. */
.shop-pay-wrap shop-pay-button {
  display: flex;
  justify-content: center;
  width: 100%;
}

/* ---- Footer policy links ----
   Previously only the HOME footer carried the full policy set: shop had
   privacy alone, and wishlist/about/contact/blog/blog-post had none at all.
   They now live in .footer-bottom, which is the one structure all seven
   footers share. */
/* THE BARE-<nav> TRAP, HIT AGAIN. styles.css styles the element selector
   `nav {}` near the top of this file as the site header — navy background,
   sticky, z-index 500, padding 14px 5%, justify-content:space-between. ANY
   <nav> added anywhere in this document inherits all of it: this row first
   rendered as a navy bar floating at the left of the footer, and collapsed
   to invisible on mobile. Task 49's mobile menu hit the identical thing.
   Every one of these declarations is undoing that rule, not styling choice. */
.footer-policies {
  background: none;
  padding: 0;
  position: static;
  top: auto;
  z-index: auto;
  box-shadow: none;

  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 6px 20px;
  margin-bottom: 12px;
}
.footer-policies a {
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.72);
  transition: color 0.2s var(--ease);
}
.footer-policies a:hover { color: var(--orange); }
.footer-policies a:focus-visible {
  outline: 2px solid var(--orange);
  outline-offset: 3px;
  border-radius: 3px;
}

/* Sits under the revealed WELCOME10 code in both the popup and the home
   10% off box, telling the customer where the code is actually used. */
.offer-where {
  margin: 10px 0 0;
  font-size: 11.5px;
  line-height: 1.5;
  font-weight: 600;
  color: var(--gray);
  text-align: center;
}
.offer-where strong { color: var(--black); font-weight: 800; }

/* Product page: quieter than the cart's. The detail column is narrow, so the
   marks get their own line under the existing shipping/returns pair. */
#detailTrustBadges .trust-badges { margin-top: 14px; }
#detailTrustBadges .pay-marks { justify-content: flex-start; }
#detailTrustBadges .trust-badges-line { justify-content: flex-start; text-align: left; }

@media (max-width: 900px) {
  #detailTrustBadges .pay-marks,
  #detailTrustBadges .trust-badges-line { justify-content: center; text-align: center; }
}

/* ============================================================
   SOCIAL LINKS IN THE MOBILE MENU (task 55)

   The footer's .social-btn is built for the DARK footer (a
   translucent-white disc that turns orange on hover). The menu
   panel's footer is light (#fff / #F5F5F5 border), so those
   values would render a near-invisible white disc on white.
   This is the same control re-tuned for a light surface.
   ============================================================ */
.mob-social-row {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 2px;
}
.mob-social-btn {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--black);
  background: #F1ECE3;          /* the menu's own icon-tile fill, from task 49 */
  text-decoration: none;
  transition: background 0.2s var(--ease), color 0.2s var(--ease), transform 0.2s var(--ease);
}
.mob-social-btn:hover,
.mob-social-btn:active {
  background: var(--orange);
  color: #fff;
}
.mob-social-btn:focus-visible {
  outline: 2px solid var(--orange);
  outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .mob-social-btn { transition: none; }
}

/* ============================================================
   MENU STACKING ABOVE THE CHAT WIDGET (task 55)

   The menu panel was z-index 1000 and the chat widget is
   z-index 9999 (inline, in index.html), so the widget sat ON
   TOP of the panel's bottom-right corner. Task 49 dealt with
   the VISIBLE half of that by fading #chatToggle out while the
   menu is open — but #chatWidget, the container, kept
   pointer-events:auto and went on silently swallowing taps in
   that corner. Nothing interactive lived there until the social
   icons were added, which is why it surfaced only now.

   Fixed at the stacking level rather than with another :has()
   rule: the menu is a modal (role="dialog" aria-modal="true"),
   so it belongs above a floating action button, and this also
   works in browsers without :has() support.
   ============================================================ */
.mob-menu { z-index: 10001; }
.mob-menu-overlay { z-index: 10000; }

/* .social-btn never set a colour. That was harmless while these were <a>
   elements with NO href — the UA's :link blue only applies to real links — but
   wiring up the TikTok/Facebook URLs turned them into links and the glyphs
   (fill="currentColor") started rendering in default browser blue on the dark
   footer. Same trap as the .logo anchor. */
.social-btn { color: #fff; }
.social-btn:hover, .social-btn:focus-visible { color: #fff; }
.social-btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 3px; }

/* ============================================================
   SUPPLIER RATINGS (task 103)

   Replaces the user-submitted review CSS. The star row is two
   stacked copies of the same five stars — a grey base and a gold
   overlay clipped to the exact fraction — because every rating in
   the catalogue is x.5-x.9 and rounding 4.5 up to five whole gold
   stars would overstate it.
   ============================================================ */

/* The -0.15em is a LAYOUT constant, not an optical nudge, and the two jobs
   must not be confused (task 106). Where this box sits in the line box is
   what sets the ROW's height, and the row's height is what sets the card's:
   parking it flat on the baseline instead grew every card by 0.75px and the
   shop grid by 2.25px. So this value stays put and stays the same everywhere,
   and all the OPTICAL tuning lives in the one transform per surface below.
   Change this and you resize cards; change the transform and you only move
   ink, because transforms are applied after layout. */
.rv-star { width: 15px; height: 15px; display: inline-block; vertical-align: -0.15em; }

.rv-stars { position: relative; display: inline-block; line-height: 0; white-space: nowrap; }
.rv-stars-base { display: block; }
/* The overlay is clipped by WIDTH, so it must not wrap or re-flow: it is the
   same five glyphs at the same size, laid exactly over the base row. */
.rv-stars-fill {
  position: absolute;
  inset: 0 auto 0 0;
  overflow: hidden;
  display: block;
  white-space: nowrap;
}

/* ---- product cards (shop grid, wishlist) ----
   ONE line, fixed order: number, stars, "· N ratings".
   nowrap + min-width:0 on the text is what stops "1,749 ratings" (the widest
   count in the catalogue) pushing the stars onto a second line in a narrow
   grid column; it ellipsises instead, which never happens at the sizes the
   grid actually uses but fails safe if a column gets narrower. */
/* The whole row scales from ONE font-size so the three parts always keep
   their proportions: the star box is sized in em, the number and count in
   em. The shop grid is 2-up on phones, which leaves a card only ~114px of
   text width at 320px, and "4.8 * * * * * . 1,749 ratings" (the widest count
   in the catalogue) does not fit there at desktop sizes — hence the clamp
   rather than a fixed size with an ellipsis, which truncated the count. */
.product-stars-num {
  color: var(--black);
  font-weight: 800;
  font-size: 1.05em;
}
.product-stars-txt {
  color: var(--gray);
  font-size: 1em;
  font-weight: 600;
  white-space: nowrap;
}

/* ---- compact rows (search results, frequently-bought-together) ----
   Same order and the same parts as the card row, one step smaller. */
/* Baseline row on the SAME geometry as the card row — number 1.05em, star
   box 1.25em, stars dropped 0.10664em — so one derived offset covers both and
   they cannot drift apart. It was an align-items:center flex row, which put
   the number 0.62px above the stars for exactly the reason the cards did.
   Only the container font-size differs, which scales the whole thing. */
.rating-compact {
  display: inline-block;
  font-size: 10px;
  line-height: 1.6;
  font-weight: 700;
  color: var(--gray);
  white-space: nowrap;
  flex-shrink: 0;
}
.rating-compact .rv-star { width: 1.25em; height: 1.25em; }
.rating-compact .rv-stars {
  vertical-align: baseline;
  transform: translateY(0.10664em);
  margin: 0 0.4em;
}
.rating-compact strong { color: var(--black); font-weight: 800; font-size: 1.05em; }
.rating-compact-count { font-weight: 600; }

/* nowrap: the price and the rating stay on one line together, or the rating
   drops whole — never mid-phrase. */
.search-result-meta { display: flex; align-items: center; gap: 10px; margin-top: 2px; flex-wrap: nowrap; min-width: 0; }

@media (max-width: 600px) { .search-result-meta { gap: 8px; } }

/* ---- SHOP PAY ON THE PRODUCT PAGE (task 58) ----
   Replaced the old "Buy It Now" button. Shopify's <shop-pay-button> renders a
   fixed ~260px button inside its own shadow root, which CSS here cannot
   stretch, so it gets its OWN full-width row underneath Add To Cart — the
   placement accelerated-checkout buttons normally take — rather than sharing
   the flex row, where it would have been squeezed at mobile widths and sat
   visually mismatched next to a full-width dark button.

   .detail-btns therefore has to be allowed to wrap; flex-basis:100% on the
   Shop Pay row is what forces Add To Cart to keep the first row to itself.

   :empty matters — renderDetailShopPay() writes '' when the selected variant
   is out of stock or can't be resolved, and without this the container would
   still contribute the flex gap and leave a dead 12px band. */
.detail-btns { flex-wrap: wrap; }
.detail-shop-pay { flex: 1 1 100%; }
.detail-shop-pay:empty { display: none; }

/* Match Add To Cart's width. Shopify's inner button is
   `width: var(--shop-pay-button-width, 260px)` inside its shadow root, and
   that custom property is the ONLY supported way in — the shadow root exposes
   no ::part and its styles come from adoptedStyleSheets.

   IT MUST BE SET FROM A STYLESHEET. Setting the same variable from JS after
   the element renders (or forcing an inline width on the inner button) leaves
   it stubbornly at 260px; declared here it just works.

   --shop-pay-button-border-radius is the only other variable the element
   reads, matched to --radius-sm so both buttons have the same corners. There
   is no height hook, so Shop Pay keeps its own 42px against Add To Cart's
   52px. The Shop Pay logo centres itself (mx-auto) at any width. */
.detail-shop-pay {
  --shop-pay-button-width: 100%;
  --shop-pay-button-border-radius: var(--radius-sm);
}
.detail-shop-pay .shop-pay-wrap { width: 100%; }


/* ============================================================
   PAW LOADER + OFFLINE SCREEN (task 81)
   ------------------------------------------------------------
   Both are built from the site's own paw mark and the live navy/orange
   palette. The brief asked for the "black & white design system (task 53)";
   that overhaul was reverted and the live brand is navy + orange, so these
   follow what is actually on screen. The animation is CSS-only — no library,
   no images, nothing that competes with a real load for bandwidth.
   ============================================================ */

/* Three paw prints stepping in sequence, like a trail being laid down.
   Each paw carries its own --paw-tilt so left/right prints angle outwards
   the way real tracks do. */
@keyframes pawTrot {
  0%, 62%, 100% { opacity: 0.16; transform: translateY(0) scale(0.9) rotate(var(--paw-tilt, 0deg)); }
  18%, 42%      { opacity: 1;    transform: translateY(-5px) scale(1) rotate(var(--paw-tilt, 0deg)); }
}

.paw-loader {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  color: var(--black);
  font-family: var(--font-body);
}

.paw-loader-paws { display: inline-flex; align-items: flex-end; gap: 5px; flex-shrink: 0; }

.paw-loader-paw {
  display: block;
  width: 20px; height: 20px;
  color: inherit;
  opacity: 0.16;
  animation: pawTrot 1.15s ease-in-out infinite;
  transform-origin: 50% 70%;
}
.paw-loader-paw svg { display: block; width: 100%; height: 100%; fill: currentColor; }
.paw-loader-paw:nth-child(1) { --paw-tilt: -16deg; animation-delay: 0s; }
.paw-loader-paw:nth-child(2) { --paw-tilt: 0deg;   animation-delay: 0.16s; }
.paw-loader-paw:nth-child(3) { --paw-tilt: 16deg;  animation-delay: 0.32s; }

.paw-loader-text {
  font-size: 14px;
  font-weight: 700;
  color: var(--gray);
  letter-spacing: 0.01em;
}

/* Block form: stacked and centred, for a region that is waiting on data
   (the reviews list, the product gallery before its photos are wired up). */
.paw-loader--block { flex-direction: column; gap: 14px; padding: 30px 16px; }
.paw-loader--block .paw-loader-paw { width: 26px; height: 26px; }
.paw-loader--block .paw-loader-text { font-size: 15px; }

/* Inline form: sits inside a button in place of its label while the button's
   own request is in flight. Inherits the button's colour, so it is white on
   the navy checkout button and navy on a light one. */
.paw-loader--inline { display: inline-flex; gap: 9px; color: inherit; }
.paw-loader--inline .paw-loader-paw { width: 15px; height: 15px; }
.paw-loader--inline .paw-loader-text { font-size: inherit; font-weight: inherit; color: inherit; }

/* The product gallery's pre-render state. #detailImg ships with a loader
   inside it, so a direct /product/<slug> load shows the brand doing something
   during the shell moment before products.js fills the gallery in, instead of
   a bare emoji on an empty box. renderDetailGallery() replaces it wholesale. */
.main-img .paw-loader--block { color: var(--black); }

/* Loading skeletons: a slow pulse of the paw mark in the card's photo area,
   so the placeholder reads as PawHaul loading rather than as generic grey
   bars. Purely decorative and behind the existing skeleton fill. */
@keyframes pawPulse {
  0%, 100% { opacity: 0.10; transform: scale(0.94); }
  50%      { opacity: 0.30; transform: scale(1); }
}
.skel-card .product-img { position: relative; }
.skel-card .product-img::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 54px; height: 54px;
  margin: -27px 0 0 -27px;
  background: no-repeat center/contain url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' fill='%231a1a2e'%3E%3Cellipse cx='50' cy='67' rx='20' ry='16'/%3E%3Cellipse cx='27' cy='47' rx='9' ry='12'/%3E%3Cellipse cx='42' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='58' cy='35' rx='9' ry='12'/%3E%3Cellipse cx='73' cy='47' rx='9' ry='12'/%3E%3C/svg%3E");
  animation: pawPulse 1.8s ease-in-out infinite;
  pointer-events: none;
}

/* ---- OFFLINE SCREEN ----------------------------------------------------
   Same overlay contract as the mobile drawer: opacity + pointer-events +
   aria-hidden/inert, and NEVER visibility — tearing a full-screen layer out
   of the compositor is what produced the iOS header flash this project spent
   five rounds on. */
.offline-screen {
  position: fixed;
  inset: 0;
  /* Above the mobile drawer (10001) and the chat widget (inline 9999): this
     is the one state that outranks everything else on the page. The chat paw
     is also faded out via body.overlay-up while it is up, since the chat is
     exactly the thing that cannot work without a connection. */
  z-index: 10002;
  background: var(--black);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 24px;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.32s var(--ease);
}
.offline-screen.active { opacity: 1; pointer-events: auto; }

.offline-inner { max-width: 420px; display: flex; flex-direction: column; align-items: center; }

/* task 123: real <img> now — see .logo-circle's comment, same reasoning. */
.offline-paw {
  width: 72px; height: 72px;
  display: block;
  border-radius: 50%;
  object-fit: cover;
  margin-bottom: 22px;
  flex-shrink: 0;
}

.offline-title {
  font-family: var(--font-display);
  font-size: clamp(24px, 6vw, 30px);
  line-height: 1.2;
  margin-bottom: 12px;
  color: #fff;
}
.offline-msg {
  font-size: 15px;
  line-height: 1.6;
  color: rgba(255,255,255,0.72);
  font-weight: 600;
  margin-bottom: 24px;
}
.offline-note {
  font-size: 13px;
  font-weight: 700;
  color: var(--orange);
  min-height: 18px;
  margin-bottom: 14px;
}
.offline-retry {
  background: var(--orange);
  color: #fff;
  border: none;
  border-radius: 50px;
  padding: 14px 34px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  cursor: pointer;
  transition: transform 0.2s var(--ease), opacity 0.2s;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  min-width: 190px;
}
.offline-retry:active { transform: scale(0.97); }
.offline-retry[disabled] { opacity: 0.75; cursor: default; }
.offline-dismiss {
  margin-top: 16px;
  background: none;
  border: none;
  color: rgba(255,255,255,0.55);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 13px;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
@media (hover: hover) {
  .offline-retry:hover { transform: translateY(-1px); }
  .offline-dismiss:hover { color: #fff; }
}

/* The page behind must not scroll under the screen. */
html.offline-locked, html.offline-locked body { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  /* Keep the loader legible as a loader, drop the motion: the paws breathe
     instead of stepping, and nothing translates. */
  .paw-loader-paw {
    animation: pawPulse 1.6s ease-in-out infinite;
    transform: rotate(var(--paw-tilt, 0deg));
  }
  .skel-card .product-img::after { animation: none; opacity: 0.16; }
  .offline-screen { transition: none; }
  .offline-retry:active, .offline-retry:hover { transform: none; }
}

/* ============================================================
   CAROUSEL MOTION — one shared scroll/snap contract (task 88)
   Appended LAST so it wins over the per-breakpoint track rules
   above (media queries add no specificity, so a later #id rule
   beats an earlier one at every width).

   Every carousel on this site is now a REAL scroll container
   instead of an overflow:hidden box driven by a JS drag handler.
   That single change is what buys the momentum: the platform's
   own inertia, rubber-band and fling physics apply, so a hard
   flick travels further than a slow drag. Measured before this
   change, a 600px/s drag and a 12000px/s flick both moved the
   track exactly 327px — identical, because the old
   handler advanced precisely one card on release regardless of
   how fast the finger was going.

   Native scrolling is also compositor-driven, so the animation
   no longer runs as per-frame scrollLeft writes on the main
   thread, and CSS snapping lands slides flush (the reviews
   track used to come to rest 66.5px inside a card).

   scroll-snap-stop is deliberately left at `normal`: `always`
   would clamp every flick to a single slide and throw away the
   momentum this block exists to enable.

   NOT added here: -webkit-overflow-scrolling. It is the default
   since iOS 13, it is deprecated, and this file already documents
   it as the cause of the task-54 dead-tap bug. Do not reintroduce.
   ============================================================ */
#revCarousel,
.det-track {
  overflow-x: auto;
  /* x:auto forces a `visible` y to compute to auto anyway (CSS overflow
     spec), which these tracks were already getting from their old
     overflow-x:hidden. Pinning it to hidden just stops a stray vertical
     scrollbar appearing inside the track. */
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  /* Keep a horizontal fling inside the carousel — without this, running out
     of track hands the gesture to the page and can trigger the browser's
     swipe-back. */
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
#revCarousel::-webkit-scrollbar,
.det-track::-webkit-scrollbar { display: none; }

/* Direct children are the slides in every one of these tracks. */
#revCarousel > *,
.det-track > * {
  scroll-snap-align: start;
}

/* The gallery wrapper used to declare `touch-action: pan-y` so that a
   horizontal drag was left entirely to the JS handler. There is no JS
   handler any more, and pan-y actively BLOCKS the native horizontal
   scroll that replaced it — the gallery would not move at all on touch.
   Allow both axes and let the browser disambiguate, which it does better
   than the axis-locking code this replaces. */
.det-carousel { touch-action: pan-x pan-y pinch-zoom; }



/* ============================================================
   BUNDLES (the /bundles page, tasks 112-113). Cards are
   rendered by renderBundles() in products.js. Two columns on desktop,
   one on phones.
   ============================================================ */
.bundles-page { padding: 60px 5% 100px; }
.bundles-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px;
  max-width: 1180px;
  margin: 0 auto;
}
/* Two-column widths: an odd last card sits centred under the pair. */
@media (min-width: 701px) and (max-width: 1199px) {
  .bundles-grid > .bundle-card:last-child:nth-child(odd) { grid-column: 1 / -1; justify-self: center; width: calc(50% - 11px); }
}
/* Three bundles: one row of three on wide screens, stacked foot so the
   narrower cards keep the price and button readable. */
@media (min-width: 1200px) {
  .bundles-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); max-width: 1320px; }
  .bundles-grid .bundle-foot { flex-direction: column; align-items: stretch; gap: 12px; }
  .bundles-grid .bundle-add { width: 100%; }
}
.bundle-card {
  scroll-margin-top: 90px;
  display: flex;
  flex-direction: column;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 24px 24px 22px;
  min-width: 0;
}
.bundle-media {
  margin: -24px -24px 20px;
  height: 240px;
  overflow: hidden;
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--img-placeholder);
}
.bundle-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bundle-item-role { font-size: 12.5px; font-weight: 700; color: var(--orange); line-height: 1.35; }
.bundle-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.bundle-name { font-family: var(--font-display); font-size: 24px; color: var(--black); line-height: 1.2; }
.bundle-pill {
  flex-shrink: 0;
  background: var(--orange);
  color: #fff;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 0.4px;
  padding: 5px 12px;
  border-radius: 50px;
}
.bundle-blurb { margin: 8px 0 16px; font-size: 14.5px; font-weight: 600; line-height: 1.6; color: var(--gray); }

.bundle-items { list-style: none; margin: 0 0 18px; padding: 0; display: grid; gap: 10px; }
.bundle-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px;
  background: var(--offwhite);
  border-radius: var(--radius-sm);
  min-width: 0;
}
.bundle-thumb {
  flex: 0 0 56px;
  width: 56px; height: 56px;
  border-radius: 10px;
  overflow: hidden;
  background: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 26px;
}
.bundle-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bundle-item-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.bundle-item-name { font-size: 14px; font-weight: 800; color: var(--black); line-height: 1.3; }
.bundle-item-opts { display: flex; flex-wrap: wrap; gap: 6px; }
.bundle-select {
  font: inherit;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--black);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px 8px;
  max-width: 100%;
  cursor: pointer;
}
.bundle-select:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; }
.bundle-item-price { flex-shrink: 0; font-size: 13.5px; font-weight: 800; color: var(--gray); }

.bundle-foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
}
.bundle-prices { display: flex; flex-direction: column; gap: 2px; }
.bundle-sep { font-size: 13px; font-weight: 700; color: var(--gray); }
.bundle-price { display: flex; align-items: baseline; gap: 10px; }
.bundle-price .price-now { font-size: 26px; }
.bundle-save { font-size: 13px; font-weight: 800; color: var(--green); }
.bundle-add { width: auto; padding-left: 26px; padding-right: 26px; }

.summary-bundle span:last-child { color: var(--green); font-weight: 800; }

@media (max-width: 767px) {
  .bundles-page { padding: 32px 4% 70px; }
  .bundles-grid { grid-template-columns: minmax(0, 1fr); gap: 16px; }
  .bundle-card { padding: 20px 16px 18px; }
  .bundle-media { margin: -20px -16px 18px; height: 200px; }
  .bundle-name { font-size: 21px; }
  .bundle-foot { flex-direction: column; align-items: stretch; }
  .bundle-add { width: 100%; }
}

/* ============================================================
   SHOP GRID ON iPAD (task 113). Touchscreen + tablet width is how an iPad
   is told apart from a laptop of the same width (and from phones, which
   are narrower): two big cards per row, and the empty cell beside the
   fifth product becomes a pointer to the Bundles page. Everywhere else
   the promo card stays hidden and the grid is unchanged.
   ============================================================ */
.shop-promo { display: none; }
@media (min-width: 700px) and (max-width: 1366px) and (hover: none) and (pointer: coarse) {
  .shop-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; }
  .shop-grid .product-img { height: clamp(300px, 36vw, 420px); }
  .shop-grid .product-info { padding: 18px 20px 20px; }
  .shop-grid .product-name { font-size: 17px; }
  .shop-grid .btn-black { min-height: 48px; font-size: 15px; }
  .shop-promo {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 0;
    border-radius: var(--radius);
    background: var(--navy);
    color: #fff;
    text-decoration: none;
    box-shadow: var(--shadow-sm);
    -webkit-tap-highlight-color: transparent;
  }
  .shop-promo:active { transform: scale(0.98); }
  /* Takes only the height the row has left over after the text, so the
     promo never grows taller than the product card beside it. */
  .shop-promo-media { display: block; flex: 1 1 0; min-height: 140px; overflow: hidden; background: var(--img-placeholder); }
  .shop-promo-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .shop-promo-body { flex: 0 0 auto; display: flex; flex-direction: column; justify-content: center; gap: 12px; padding: 26px 28px 30px; }
  .shop-promo-tag {
    align-self: flex-start;
    background: var(--orange);
    color: #fff;
    font-size: 11px;
    font-weight: 900;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    padding: 6px 14px;
    border-radius: 50px;
  }
  .shop-promo-head { font-family: var(--font-display); font-size: clamp(28px, 3.6vw, 38px); line-height: 1.12; }
  .shop-promo-copy { font-size: 16px; font-weight: 600; line-height: 1.6; color: rgba(255,255,255,0.8); }
  .shop-promo-cta { font-size: 16px; font-weight: 800; color: var(--orange-light); }
}

/* SHOP HERO — lifestyle photo band behind "Shop Walk Gear". */
.shop-hero { position: relative; width: 100%; height: clamp(280px, calc(18vw + 170px), 480px); overflow: hidden; background: var(--img-placeholder); }
.shop-hero-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.shop-hero-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(26,26,46,0.55), rgba(26,26,46,0.35)); }
.shop-hero-copy {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 0 6%;
}
.shop-hero-copy h1 {
  font-family: var(--font-display);
  font-size: clamp(30px, 5vw, 48px);
  color: #fff;
  line-height: 1.15;
  margin-bottom: 12px;
  text-shadow: 0 2px 12px rgba(0,0,0,0.35);
}
.shop-hero-copy p {
  font-size: 16px;
  color: rgba(255,255,255,0.9);
  font-weight: 600;
  line-height: 1.7;
  max-width: 620px;
  margin: 0 auto;
  text-shadow: 0 1px 8px rgba(0,0,0,0.3);
}

/* Product page "comes in a bundle" box and the cart's complete-the-bundle
   nudge (task 116). */
.detail-bundle {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 16px;
  padding: 14px 16px;
  border: 2px solid var(--orange);
  border-radius: var(--radius-sm);
  background: var(--orange-pale);
  color: var(--black);
  text-decoration: none;
}
.detail-bundle + .detail-bundle { margin-top: 10px; }
.detail-bundle-tag { font-size: 11px; font-weight: 900; letter-spacing: 1px; text-transform: uppercase; color: var(--orange); }
.detail-bundle-tagline { font-size: 16px; font-weight: 900; line-height: 1.3; margin-top: -2px; }
.detail-bundle-head { margin: 16px 0 0; font-size: 13px; font-weight: 800; color: var(--gray, #555); }
.detail-bundle-head + .detail-bundle { margin-top: 8px; }
.detail-bundle-text { font-size: 14px; font-weight: 600; line-height: 1.55; }
.detail-bundle-cta { font-size: 14px; font-weight: 800; color: var(--orange); }
.cart-bundle-nudge {
  margin: 4px 0 12px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--orange-pale);
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.5;
}
.cart-bundle-add {
  margin-top: 8px;
  background: var(--orange);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 8px 14px;
  font: inherit;
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
}
.search-result-thumb--bundle {
  display: flex; align-items: center; justify-content: center;
  background: var(--orange); color: #fff; font-weight: 900; font-size: 20px;
}

/* ============================================================
   FEEL / POLISH PASS (task 116) — motion and press feedback only.
   Nothing here changes layout, content or what anything does.
   Press states use the independent `scale` property, not `transform`, so
   they stack on top of any transform an element already uses to position
   itself (the arrows' translateY(-50%), the wishlist heart's hover grow)
   instead of replacing it. iOS only shows :active at all because of the
   touchstart listener in app.js ("TAP FEEDBACK ON iOS").
   Timing: the press lands fast (0.08s) and the release eases back slower
   (0.25-0.3s, --ease), so a tap feels immediate but never snappy-cheap.
   ============================================================ */

/* ── Press feedback: icon buttons and arrows ── */
.carousel-btn, .det-prev, .det-next {
  transition: opacity 0.2s var(--ease), background 0.2s var(--ease), scale 0.25s var(--ease);
}
.carousel-btn:not(.disabled):active, .det-prev:not(.disabled):active, .det-next:not(.disabled):active {
  scale: 0.9; transition-duration: 0.08s;
}
.wishlist-btn:active { scale: 0.86; transition-duration: 0.08s; }
.qty-btn, .remove-btn {
  transition: background 0.2s var(--ease), color 0.2s var(--ease), scale 0.25s var(--ease);
}
.qty-btn:active, .remove-btn:active { scale: 0.9; transition-duration: 0.08s; }
#chatToggle, #backToTop {
  transition: opacity 0.25s var(--ease), scale 0.25s var(--ease), box-shadow 0.25s var(--ease);
  -webkit-tap-highlight-color: transparent;
}
#chatToggle:active, #backToTop:active { scale: 0.92; transition-duration: 0.08s; }

/* ── Press feedback: options, small buttons ── */
.option-btn:active { scale: 0.95; transition-duration: 0.08s; }
.sticky-atc-btn, .cart-bundle-add, .offer-copy-btn {
  transition: background 0.2s var(--ease), color 0.2s var(--ease), scale 0.25s var(--ease);
}
.sticky-atc-btn:active, .cart-bundle-add:active, .offer-copy-btn:active { scale: 0.96; transition-duration: 0.08s; }

/* ── Press feedback: cards and callouts ── */
.detail-bundle, .bundle-thumb { transition: scale 0.3s var(--ease), background 0.2s var(--ease); -webkit-tap-highlight-color: transparent; }
.blog-card { transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease), scale 0.3s var(--ease); -webkit-tap-highlight-color: transparent; }
.detail-bundle:active, .blog-card:active { scale: 0.985; transition-duration: 0.1s; }
.bundle-thumb:active { scale: 0.94; transition-duration: 0.08s; }

/* ── Press feedback: rows and text links (a dip, not a shrink) ── */
.faq-q { transition: background 0.2s var(--ease); -webkit-tap-highlight-color: transparent; }
.faq-q:active { background: var(--offwhite); transition-duration: 0.06s; }
.pd-acc-head, .back-btn, .quick-answer-link, .logo, .offer-dismiss, footer a {
  transition: opacity 0.25s var(--ease), color 0.2s var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.pd-acc-head:active, .back-btn:active, .quick-answer-link:active, .logo:active, .offer-dismiss:active, footer a:active {
  opacity: 0.55; transition-duration: 0.06s;
}
.social-btn:active { scale: 0.92; transition-duration: 0.08s; }

/* ── FAQ: open to the answer's real height (set inline by toggleFaq), so
   long answers are no longer cut off at 200px on phones and every answer
   opens at the same pace. 1000px is only the fallback for goToFaq(). ── */
.faq-a { transition: max-height 0.4s var(--ease), padding 0.4s var(--ease); }
.faq-item.open .faq-a { max-height: 1000px; }

/* ── Page navigation: a short fade on the new page's content. Opacity
   only, on the page's CHILDREN (the page's own white background stays
   solid, so the mobile navy canvas can't show through), no transform (the
   old slide-up's "drop and snap back" can't happen), and `backwards` fill
   so nothing lingers once it's done. The first page load never fades:
   page-transition is only added by in-site navigation. ── */
@keyframes pageFadeIn { from { opacity: 0; } to { opacity: 1; } }
.page.page-transition > * { animation: pageFadeIn 0.22s cubic-bezier(0.25, 0.1, 0.25, 1) backwards; }

/* ── Chat window: rises in from the button, drops back out on close ── */
@keyframes chatWinIn { from { opacity: 0; transform: translateY(14px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes chatWinOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(10px) scale(0.98); } }
#chatWindow { transform-origin: bottom right; animation: chatWinIn 0.32s var(--ease) backwards; }
#chatWindow.chat-closing { animation: chatWinOut 0.18s var(--ease-exit) forwards; pointer-events: none; }
#chatMessages { overscroll-behavior: contain; }

/* ── Chat window drag-handle: tap to expand to fullscreen ── */
.chat-window-handle {
  background: #1a1a2e;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px 0 6px;
  cursor: pointer;
  flex-shrink: 0;
}
.chat-window-handle-dash {
  width: 36px;
  height: 4px;
  border-radius: 3px;
  background: rgba(255,255,255,0.35);
  transition: background-color 0.15s ease;
}
.chat-window-handle:hover .chat-window-handle-dash,
.chat-window-handle:active .chat-window-handle-dash { background: rgba(255,255,255,0.6); }

#chatWindow.chat-fullscreen {
  /* #chatWindow carries an inline style (width/max-width/max-height/margin/
     border-radius), which beats a class selector on specificity alone —
     !important is the only way to override it from here. */
  position: fixed !important;
  inset: 0 !important;
  width: 100% !important;
  height: 100% !important;
  max-width: none !important;
  max-height: none !important;
  border-radius: 0 !important;
  margin: 0 !important;
  z-index: 10000;
  animation: none;
  transform: none;
}
#chatWindow.chat-fullscreen #chatMessages { flex: 1; height: auto; }
#chatWindow.chat-fullscreen .chat-window-handle-dash { background: rgba(255,255,255,0.5); }

/* ── Policy modal (shipping/returns/privacy): fade + gentle rise ── */
@keyframes policyFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes policyFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes policyRise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
#policyModal { animation: policyFadeIn 0.24s ease backwards; overscroll-behavior: contain; }
#policyModal > div { animation: policyRise 0.36s var(--ease) backwards; }
#policyModal.policy-closing { animation: policyFadeOut 0.18s ease forwards; pointer-events: none; }

/* ── Toast: glides in, fades out without the quick drop ── */
.toast { transition: transform 0.45s var(--ease), opacity 0.3s ease; }

@media (prefers-reduced-motion: reduce) {
  .page.page-transition > *, #chatWindow, #chatWindow.chat-closing,
  #policyModal, #policyModal > div, #policyModal.policy-closing { animation: none; }
  .faq-a { transition: none; }
  [class]:active, #chatToggle:active, #backToTop:active { scale: none; }
}

/* ============================================================
   FIRST-VISIT CHAT GREETING (task 117, redesigned task 118) — a small
   dismissible card near the chat icon, shown once ever per device (armed
   in app.js). Not the chat window itself; tapping the card opens it, the
   × just dismisses. Behavior is unchanged from task 117 — this is the
   visual redesign only: a round orange paw avatar (echoes the chat toggle
   button itself), a small caps name line, the message, and an explicit
   "Chat with us →" call to action instead of plain clickable text.
   ============================================================ */
.chat-greeting {
  display: none;
  flex-direction: column;
  width: 272px;
  max-width: calc(100vw - 48px);
  background: white;
  color: var(--black);
  border-radius: 18px;
  margin-bottom: 14px;
  box-shadow: 0 16px 40px rgba(26,26,46,0.26), 0 4px 12px rgba(26,26,46,0.08);
  position: relative;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}
/* Little tail pointing down at the chat button, like a speech bubble. */
.chat-greeting::after {
  content: '';
  position: absolute;
  bottom: -7px; right: 22px;
  width: 14px; height: 14px;
  background: white;
  transform: rotate(45deg);
  box-shadow: 3px 3px 6px rgba(26,26,46,0.06);
}
.chat-greeting.show {
  display: flex;
  animation: chatGreetingIn 0.4s var(--ease) backwards;
}
.chat-greeting.dismissing { animation: chatGreetingOut 0.2s var(--ease-exit) forwards; }
@keyframes chatGreetingIn { from { opacity: 0; transform: translateY(10px) scale(0.96); } to { opacity: 1; transform: none; } }
@keyframes chatGreetingOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(8px) scale(0.97); } }

.chat-greeting-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 14px 34px 16px 14px;
  cursor: pointer;
  transition: background 0.15s var(--ease);
}
.chat-greeting-row:active { background: var(--offwhite); }
/* task 123: real <img> now — see .logo-circle's comment, same reasoning. */
.chat-greeting-avatar {
  flex-shrink: 0;
  width: 34px; height: 34px;
  border-radius: 50%;
  object-fit: cover;
  box-shadow: 0 3px 8px rgba(232,99,10,0.35);
}
.chat-greeting-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.chat-greeting-name {
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--gray);
}
.chat-greeting-text {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.4;
  color: var(--black);
}
.chat-greeting-cta {
  margin-top: 4px;
  font-size: 12.5px;
  font-weight: 800;
  color: var(--orange);
}
.chat-greeting-close {
  position: absolute;
  top: 8px; right: 8px;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--offwhite);
  border: none;
  color: var(--gray);
  font-size: 11px;
  line-height: 1;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s var(--ease), color 0.15s var(--ease);
}
.chat-greeting-close:active { background: var(--line-soft); color: var(--black); }
@media (prefers-reduced-motion: reduce) {
  .chat-greeting.show, .chat-greeting.dismissing { animation: none; }
}
