:root {
  /* Old-site palette (yamamotor-old): extracted from oldyamamotor/*_files/
     style01.css & indexsection.css — #008cd6 is the actual brand blue used
     site-wide for buttons/active nav/brand pills on the legacy Yii site,
     #012956 its darker navy footer/dark-section color. Flat white pages,
     no decorative gradients/textures — the old site didn't have any. */
  --page: #ffffff;
  --surface: #ffffff;
  --surface-2: #f5f5f5;
  --ink: #333333;
  --muted: #777777;
  --line: #dddddd;
  --sea: #008cd6;
  --sea-2: #0074b0;
  --navy: #012956;
  --chart: #999999;
  --action: #008cd6;
  --success: #3c763d;
  --shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
}

* {
  box-sizing: border-box;
}

/* scroll-behavior: smooth was here, removed — confirmed on paragon-next
   (this app's sibling fork) to hijack every programmatic scroll, including
   Next.js's own router "scroll to top" after a client-side navigation,
   landing at a non-zero scrollY instead of 0 when a smooth-scroll
   animation gets interrupted by the destination page still hydrating.
   The back-to-top button passes its own behavior: "smooth" directly to
   scrollTo() in JS, so it's unaffected by removing this. */
html {
  /* Reserves the vertical scrollbar's width on every page, whether or not
     that page's content is actually tall enough to need one. Without this,
     a short page (e.g. company.html) renders with no scrollbar, but
     products.html's long card grid needs one — navigating between them
     makes the scrollbar appear/disappear, shrinking or growing the visible
     body width by ~15-17px and shifting all centered content sideways for a
     moment. This keeps that width constant across every page instead. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  color: var(--ink);
  background: var(--page);
  /* Old-site body font stack: Microsoft JhengHei is the actual Chinese
     face the legacy site declared (style.css/style01.css); Noto Sans TC
     is the reliable cross-platform fallback for machines without it. */
  font-family: "Microsoft JhengHei", "PingFang TC", "Noto Sans TC", Arial, sans-serif;
  letter-spacing: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

img {
  display: block;
  max-width: 100%;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(14px);
}

.nav-shell {
  width: min(1180px, calc(100% - 32px));
  margin: 0 auto;
  min-height: 74px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

.brand img {
  width: auto;
  height: 46px;
  object-fit: contain;
  mix-blend-mode: multiply;
}

.brand small {
  color: var(--muted);
  font-size: 11px;
  line-height: 1.2;
  text-transform: uppercase;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 4px;
}

.nav-links a {
  border-radius: 4px;
  padding: 10px 14px;
  color: #555555;
  font-size: 14px;
  font-weight: 700;
}

.nav-links a:hover,
.nav-links a.active {
  color: var(--sea);
  background: rgba(0, 140, 214, 0.08);
}

.icon-btn,
.cart-link {
  position: relative;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
}

.icon-btn svg,
.cart-link svg {
  width: 20px;
  height: 20px;
}

.cart-count {
  position: absolute;
  top: -5px;
  right: -4px;
  min-width: 21px;
  height: 21px;
  padding: 0 6px;
  border-radius: 4px;
  background: var(--action);
  color: white;
  font-size: 12px;
  font-weight: 800;
  line-height: 21px;
  text-align: center;
}

.mobile-menu-btn {
  display: none;
}

.mobile-menu {
  display: none;
  border-top: 1px solid var(--line);
  background: var(--surface);
}

.mobile-menu a {
  display: block;
  padding: 14px 24px;
  border-bottom: 1px solid var(--line);
  font-weight: 800;
}

.section {
  width: min(1180px, calc(100% - 32px));
  margin: 0 auto;
  padding: 72px 0;
}

.section.tight {
  padding: 40px 0;
}

.eyebrow {
  color: var(--sea);
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.display-title {
  margin: 10px 0 18px;
  max-width: 820px;
  font-size: clamp(34px, 6.4vw, 70px);
  line-height: 0.94;
  letter-spacing: 0;
}

.lead {
  max-width: 680px;
  color: #666666;
  font-size: 18px;
  line-height: 1.75;
  overflow-wrap: break-word;
}

.btn {
  min-height: 46px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  border: 0;
  border-radius: 4px;
  padding: 12px 18px;
  background: var(--ink);
  color: white;
  font-weight: 900;
  cursor: pointer;
  transition: transform 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
}

.btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
}

.btn.primary {
  background: var(--action);
}

/* AddInquiryButton's "Added ✓" state — was a per-click inline style
   (background: var(--success)), moved to a class since CSP's style-src
   blocks inline style attributes. Plain .added selector (not scoped to
   one base-class combo) so it works regardless of which className this
   button is given across the app. */
.added,
.added.ghost {
  background: var(--success) !important;
  border-color: var(--success) !important;
  color: #fff !important;
}

.btn.sea {
  background: var(--sea);
}

.btn.ghost {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink);
}

/* Text treatment matches the homepage hero's "YMM Parts Finder" label
   (color, weight, tracking, case) at the button's own font size — .btn's
   900 weight is already close to .hero-tools-label's 600, so weight isn't
   overridden here, just color/tracking/case. Border color nudged to match
   since the default --line gray reads flat next to gold text. Selector
   matches .btn.ghost's specificity (two classes, not one) — a bare
   .btn-ghost-gold rule was previously losing to .btn.ghost's `color:
   var(--ink)` despite appearing later in the file, since CSS specificity
   is decided by selector weight, not source order. */
.btn.ghost.btn-ghost-gold {
  color: var(--chart);
  border-color: rgba(0, 140, 214, 0.4);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.hero {
  position: relative;
  overflow: hidden;
  min-height: 560px;
  height: min(760px, calc(100vh - 74px));
  height: clamp(560px, calc(100svh - 74px), 760px);
  max-height: 760px;
  display: flex;
  align-items: center;
  color: white;
  background: var(--navy);
}

.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(1, 41, 86, 0.94) 0%, rgba(1, 41, 86, 0.72) 42%, rgba(1, 41, 86, 0.2) 100%),
    /* Re-encoded as hero-ocean.webp (same 1900x890, quality 80 — this
       image already fills the full-bleed hero at a reasonable native
       size, no resize needed) — a Lighthouse audit flagged the original
       JPEG (~289KB) as a legacy format; WebP at the same dimensions is
       ~177KB, same visual quality. Original .jpg left in place (unused
       by this rule now). */
    url("assets/hero-ocean.webp") center / cover;
}

.hero::after,
.chart-lines::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.26;
  background:
    repeating-radial-gradient(ellipse at 82% 26%, transparent 0 28px, rgba(255, 255, 255, 0.22) 29px 31px),
    linear-gradient(118deg, transparent 0 48%, rgba(0, 140, 214, 0.5) 48.2% 48.4%, transparent 48.6% 100%);
}

.hero-inner {
  position: relative;
  z-index: 1;
  width: min(1180px, calc(100% - 32px));
  margin: 0 auto;
  padding: clamp(48px, 8vh, 72px) 0 clamp(56px, 9vh, 96px);
}

.hero-tools {
  max-width: 720px;
  margin-top: 32px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12px;
  padding: 10px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(12px);
}

/* Sits inside the same card as the search input/button (not above it as a
   separate element) — spans both grid columns as its own row, so it reads
   as this search block's own label rather than a page-level heading. */
.hero-tools-label {
  grid-column: 1 / -1;
  padding: 4px 6px 0;
  color: var(--chart);
  font-size: 28px;
  line-height: 1;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.hero-tools input {
  min-width: 0;
  border: 0;
  border-radius: 12px;
  padding: 14px 16px;
  background: rgba(255, 255, 255, 0.92);
  color: var(--ink);
  font-size: 16px;
}

/* /products page heading — same color/weight/tracking/case treatment as
   .hero-tools-label above, kept at this page's own text-5xl size (the
   heading itself, not a small label like the hero's) rather than
   .hero-tools-label's 28px. */
.products-title-gold {
  color: var(--chart);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.hero-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 34px;
}

.stat-pill {
  min-width: 142px;
  padding: 14px 16px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.12);
}

.stat-pill strong {
  display: block;
  font-size: 28px;
  line-height: 1;
}

.stat-pill span {
  display: block;
  margin-top: 7px;
  color: rgba(255, 255, 255, 0.72);
  font-size: 12px;
  text-transform: uppercase;
}

/* Lead-in/trailing copy around the stat number (e.g. "Export to" / "100+"
   / "countries") — normal case and a bit larger than .stat-pill span's
   all-caps micro-label style, since these read as part of one sentence
   with the number rather than a standalone label underneath it. */
.stat-pill span.stat-copy {
  margin-top: 0;
  color: rgba(255, 255, 255, 0.85);
  font-size: 14px;
  text-transform: none;
}

.stat-pill span.stat-copy + strong {
  margin-top: 2px;
}

.stat-pill strong + span.stat-copy {
  margin-top: 2px;
}

/* Featured-categories band — replaces the old .category-grid tile wall's
   small 68px icon-style thumbnails with a larger, photo-led card (292x148
   photo tile, ~1.97:1) while staying on the site's plain white ground —
   background and card surface both stay white/var(--surface), matching
   .category-card's own existing border+shadow treatment, not a colored
   band. Its own .featured-band/-tile classes rather than a .category-card
   reskin because that class is shared with the homepage's "Latest news"
   cards (.home-news-card, see that class's own comment on the reuse). */
.featured-band-heading {
  display: grid;
  gap: 8px;
  margin-bottom: 34px;
}

.featured-band-heading h2 {
  margin: 0;
  color: var(--ink);
}

.featured-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 18px;
}

.featured-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--surface);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
  padding: 12px;
  text-decoration: none;
  transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}

.featured-tile:hover {
  transform: translateY(-4px);
  box-shadow: 0 20px 40px rgba(1, 41, 86, 0.14);
  border-color: rgba(0, 140, 214, 0.35);
}

.featured-tile:hover .featured-tile-photo img {
  transform: scale(1.05);
}

.featured-tile:hover .featured-tile-arrow {
  transform: translateX(3px);
  opacity: 1;
}

/* Photo tile is the card's visual lead now, not a small 68px icon-style
   thumbnail in the corner — full-bleed at the top of the card. 292x148
   (~1.97:1) stays under the source images' native resolution once
   uploaded through the matching 2:1 admin crop (featured-category-
   form.tsx), so it's never upscaled past source and stays sharp. */
.featured-tile-photo {
  width: 100%;
  height: 148px;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
  flex-shrink: 0;
}

.featured-tile-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.3s ease;
}

.featured-tile-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex-grow: 1;
  padding: 0 6px;
}

.featured-tile-body h3 {
  margin: 0;
  font-size: 17px;
  font-weight: 800;
  letter-spacing: 0.01em;
  color: var(--ink);
}

.featured-tile-body p {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
  line-height: 1.55;
  overflow-wrap: break-word;
}

.featured-tile-arrow {
  position: absolute;
  right: 18px;
  bottom: 14px;
  color: var(--navy);
  opacity: 0.45;
  transition: transform 0.2s ease, opacity 0.2s ease;
}

/* Trailing card, linking to /products instead of a single category — the
   site's own --sea blue instead of the glass treatment, same "distinct
   see-everything-else CTA" role .category-card-cta played before. */
.featured-tile-cta {
  background: var(--sea);
  border-color: var(--sea);
  justify-content: space-between;
}

.featured-tile-cta:hover {
  background: var(--sea-2);
}

.featured-tile-cta-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.2);
  color: #fff;
}

.featured-tile-cta .featured-tile-body p {
  color: rgba(255, 255, 255, 0.85);
  font-size: 13px;
  margin-bottom: 2px;
}

.featured-tile-cta .featured-tile-body h3 {
  color: #fff;
  text-transform: uppercase;
}

.category-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
  margin-top: 34px;
}

.category-card {
  min-height: 190px;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 18px;
  background: var(--surface);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
}

.category-card::after {
  content: "";
  position: absolute;
  right: -34px;
  bottom: -34px;
  width: 120px;
  height: 120px;
  border: 1px solid rgba(0, 140, 214, 0.2);
  border-radius: 4px;
}

.category-card img {
  width: 78px;
  height: 78px;
  object-fit: cover;
  border-radius: 8px;
}

.category-card h3 {
  margin: 18px 0 8px;
  font-size: 22px;
}

.category-card p {
  margin: 0;
  color: var(--muted);
  line-height: 1.55;
  /* A long unbroken run of characters (no spaces to wrap at — e.g. a URL
     pasted into an excerpt, or CJK text with no natural break opportunity
     the browser recognizes) would otherwise overflow the card's fixed
     width instead of wrapping. overflow-wrap only forces a break once a
     word genuinely can't fit on its own line — normal space-separated
     text still wraps at word boundaries exactly as before. */
  overflow-wrap: break-word;
}

/* Homepage "Latest news" cards. Used to reuse .category-card's border/
   padding/background/decorative-corner-square via a shared class (that
   class was the Featured Categories tiles' own styling, before that
   section was redesigned into .featured-tile) — now standalone, since
   .category-card has no other user left (confirmed by grep) and its
   `justify-content: space-between`/decorative ::after square never fit
   this card's own rhythm anyway. Deliberately NOT named .news-card —
   that class already exists (further down this file) for the /news
   listing page's own, structurally unrelated grid-based cards
   (thumbnail + text columns); reusing the name here collided with it
   and broke this section's layout during development. */
.home-news-card {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  min-height: 190px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 18px;
  background: var(--surface);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
  transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}

.home-news-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 20px 40px rgba(1, 41, 86, 0.14);
  border-color: rgba(0, 140, 214, 0.35);
}

.home-news-card h3 {
  margin: 18px 0 0;
  font-size: 22px;
}

.home-news-card p {
  margin: 1.5em 0 0;
  color: var(--muted);
  line-height: 1.55;
  overflow-wrap: break-word;
}

/* Trailing card in the Featured Categories grid, linking to /products
   instead of a single category — dark + gold to read as a distinct "see
   everything else" CTA rather than one more category among equals. */
.category-card-cta {
  justify-content: flex-end;
  border-color: var(--navy);
  background: var(--navy);
}

.category-card-cta::after {
  border-color: rgba(0, 140, 214, 0.25);
}

.category-card-cta p {
  margin: 0 0 6px;
  color: rgba(255, 255, 255, 0.7);
  font-size: 22px;
  line-height: 1.2;
}

/* Same style treatment as .hero-tools-label (color, weight, tracking,
   case), just not its 28px size — this card's own h3 sizing (22px,
   matching sibling category cards) stays as-is. */
.category-card-cta h3 {
  margin: 0;
  color: var(--chart);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.route-band {
  position: relative;
  overflow: hidden;
  background: var(--surface);
}

.route-band .section {
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  gap: 44px;
  align-items: center;
}

.route-band .packaging-section {
  position: relative;
  z-index: 1;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(32px, 5vw, 80px);
  width: min(1360px, calc(100% - 32px));
}

.packaging-intro {
  display: grid;
  gap: 14px;
  min-width: 0;
}

.packaging-intro h2 {
  margin: 0;
  color: var(--ink);
  font-size: clamp(26px, 3vw, 40px);
  font-weight: 900;
  line-height: 1.08;
}

.packaging-copy {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  color: var(--muted);
  font-size: 17px;
  line-height: 1.5;
}

.packaging-copy span {
  position: relative;
}

.packaging-copy span:not(:last-child)::after {
  content: "";
  position: absolute;
  top: 0.38em;
  right: -10px;
  width: 1px;
  height: 0.9em;
  background: var(--line);
}

.packaging-points {
  display: grid;
  gap: 14px;
  margin-top: 26px;
  max-width: 560px;
}

.packaging-points div {
  border-top: 1px solid var(--line);
  padding-top: 14px;
}

.packaging-points strong,
.packaging-points span {
  display: block;
}

.packaging-points strong {
  color: var(--ink);
  font-size: 15px;
}

.packaging-points span {
  margin-top: 4px;
  color: var(--muted);
  line-height: 1.6;
}

.packaging-carousel {
  display: grid;
  gap: 12px;
  justify-self: stretch;
  width: 100%;
  min-width: 0;
}

.packaging-carousel-stage {
  position: relative;
  display: grid;
  place-items: center;
  box-sizing: border-box;
  padding: 14px 36px;
  border: 1px solid rgba(1, 41, 86, 0.08);
  border-radius: 8px;
  background: #fff;
  overflow: hidden;
  filter: drop-shadow(0 28px 34px rgba(1, 41, 86, 0.14));
  /* Fixed, not per-slide (box vs bag used to each set their own height) —
     crossfade (below) stacks every slide on top of each other during the
     transition, so a height that changed with the active slide would
     mismatch the outgoing/incoming images mid-fade. Both slides used the
     same clamp() anyway, so this loses nothing. */
  height: clamp(360px, 34vw, 520px);
}

.packaging-carousel-media {
  position: relative;
  display: grid;
  width: 100%;
  height: 100%;
  min-height: 0;
  padding-bottom: 12px;
  box-sizing: border-box;
}

.packaging-carousel-media-frame {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 0;
}

/* Crossfade transition, matching omax-old's homepage banner
   (.header-banner.home > img / img.active in that repo's public/styles.css):
   every slide is stacked in the same spot via position:absolute, and
   .active is the only one visible — opacity + a slight scale-down-to-1
   settle is what actually reads as a crossfade rather than a hard cut. */
.packaging-carousel-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  opacity: 0;
  transform: scale(1.035);
  transition: opacity 0.72s ease, transform 1.2s ease;
}

.packaging-carousel-image.active {
  opacity: 1;
  transform: scale(1);
}

.packaging-carousel-button {
  position: absolute;
  top: 50%;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border: 1px solid rgba(1, 41, 86, 0.14);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.55);
  color: var(--navy);
  box-shadow: 0 12px 28px rgba(1, 41, 86, 0.14);
  transform: translateY(-50%);
  transition: background 0.2s ease;
}

.packaging-carousel-button:hover {
  background: rgba(255, 255, 255, 0.85);
  color: var(--sea);
}

.packaging-carousel-button-prev {
  left: 14px;
}

.packaging-carousel-button-next {
  right: 14px;
}

.packaging-carousel-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  color: var(--muted);
  font-size: 14px;
  font-weight: 600;
}

.packaging-carousel-dots {
  display: flex;
  gap: 8px;
}

.packaging-carousel-dots button {
  width: 9px;
  height: 9px;
  border: 0;
  border-radius: 999px;
  background: rgba(1, 41, 86, 0.2);
}

.packaging-carousel-dots button.active {
  width: 24px;
  background: var(--sea);
}

.route-list {
  display: grid;
  gap: 12px;
}

.route-item {
  display: grid;
  grid-template-columns: 80px 1fr;
  gap: 16px;
  align-items: start;
  border-top: 1px solid var(--line);
  padding-top: 16px;
}

.route-item strong {
  color: var(--sea);
  font-size: 13px;
}

.route-item h3 {
  margin: 0 0 4px;
}

.route-item p {
  margin: 0;
  color: var(--muted);
  line-height: 1.6;
}

.footer {
  background: var(--navy);
  color: white;
}

.footer-inner {
  width: min(1180px, calc(100% - 32px));
  margin: 0 auto;
  padding: 38px 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

.footer img {
  width: 128px;
  filter: brightness(0) invert(1);
  opacity: 0.72;
}

/* The logo <img> is now wrapped in a <Link href="/"> (site-footer.tsx) —
   an inline-level <a> defaults to a text underline and gets its own
   focus-ring box that reads as slightly awkward around just an image.
   display: inline-flex collapses the link's box down to exactly the
   image's own size (no extra baseline gap under it), and
   text-decoration: none matches how .footer's other real text links
   (inside .footer-inner's nav, if any) already look with no underline. */
.footer-inner > a {
  display: inline-flex;
  text-decoration: none;
}

.footer p {
  margin: 0;
  color: rgba(255, 255, 255, 0.72);
  font-size: 13px;
}

/* Every non-homepage page's title band (Contact/News/Company/Products/
   Products detail/Inquiry cart/Inquiry history/Catalog/not-found — see
   each page.tsx's own <section className="page-title">) now carries the
   same wave photo the homepage's own hero banner uses (HOME_BANNERS in
   site-header.tsx), instead of a flat --surface fill. One shared rule
   here covers all nine call sites at once — no per-page JSX change
   needed. position: relative + the ::before overlay below follow the
   same "photo background, dark gradient overlay, white text" pattern
   paragon-next's hero section already established (see that project's
   public/styles.css .hero comment) — the eyebrow/h1/lead text inside
   .page-title .section is switched to white/light below since the
   default --ink/--muted/#666 text colors (fine against the old flat
   --surface fill) don't have enough contrast against a photo. */
.page-title {
  position: relative;
  overflow: hidden;
  background: var(--navy) url("/assets/banner-inner.webp") center / cover no-repeat;
  border-bottom: 1px solid var(--line);
}

/* Darkens the photo so white text stays readable across its full width —
   the source photo is bright, high-key water, not uniformly dark enough
   on its own. A flat, fairly strong tint (not a directional gradient
   like the homepage hero's own overlay) since page-title's photo has no
   single focal subject to preserve visibility of, unlike the hero's
   boat/logo composition. */
.page-title::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(1, 41, 86, 0.62);
}

.page-title .section {
  position: relative;
  z-index: 1;
  padding: 44px 0;
}

.page-title .eyebrow {
  color: #ffffff;
}

/* .page-title h1, .page-title .products-title-gold — the latter needed
   explicitly, not left to rely on source order beating .products-title-gold's
   own rule above (365): same specificity (two classes each), so which one
   wins would otherwise depend on which happens to be declared later in
   this file, a fragile thing to depend on. .products-title-gold's own
   var(--chart) gray (#999999, tuned for the flat --surface fill this
   section used to have) has nowhere near enough contrast against the
   photo/dark-overlay background below it. */
.page-title h1,
.page-title .products-title-gold {
  color: #ffffff;
}

.page-title .lead {
  color: rgba(255, 255, 255, 0.88);
}

/* products/[slug]'s breadcrumb ("Home / Products / <name>") is the one
   .page-title call site with neither .eyebrow nor .lead — it's .sku
   instead (styles.css's shared small-muted-text class, see its own
   comment), styled as plain dark text + var(--sea) links elsewhere on
   the site. Both need the same white-on-photo treatment as everything
   else in this band. */
.page-title .sku {
  color: rgba(255, 255, 255, 0.88);
}

/* Global `a { text-decoration: none }` (this file, line 55) means the
   two breadcrumb links ("Home", "Products") would otherwise be
   indistinguishable from the plain white text around them here — an
   underline on hover only (not always-on, to stay closer to that same
   site-wide "no underline" convention) restores the distinction without
   a permanent visual departure from how every other link on the site
   looks. */
.page-title .sku a:hover {
  text-decoration: underline;
}

.products-layout {
  display: grid;
  grid-template-columns: 302px 1fr;
  gap: 24px;
  align-items: start;
}

/* scrollbar-gutter: stable (architecture.md §0-C) reserves a dedicated
   track for the scrollbar alongside the padding box, rather than the
   scrollbar overlapping/hugging the content directly — content sits
   inset from the true edge with visible breathing room before the
   scrollbar, instead of the two touching. Also prevents the classic
   "content shifts sideways the moment a scrollbar appears" layout jump,
   since the gutter is reserved whether or not scrolling is actually
   needed right now. A prior version of this fix moved overflow to a
   separate inner wrapper instead (so the scrollbar would sit flush at
   the rounded corner) — reverted; that wasn't the effect wanted. */
.filter-sidebar {
  position: sticky;
  top: 94px;
  max-height: calc(100vh - 118px);
  overflow: auto;
  scrollbar-gutter: stable;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: var(--shadow);
}

.filter-section {
  border-bottom: 1px solid var(--line);
}

.filter-section:last-child {
  border-bottom: 0;
}

.filter-section-head {
  padding: 16px;
}

.filter-details {
  padding: 0 16px 16px;
}

.filter-sidebar-actions {
  position: sticky;
  bottom: 0;
  display: grid;
  border-top: 1px solid var(--line);
  padding: 12px;
  background: var(--surface);
}

.filter-sidebar-actions .btn {
  width: 100%;
}

.filter-section-head h3 {
  margin: 0;
  color: var(--ink);
  font-size: 13px;
  font-weight: 900;
  text-transform: uppercase;
}

/* Was `.filter-details summary` (native <details>'s own toggle element) —
   now a real <button> since <details>'s [open] state can't be animated by
   CSS (architecture.md §0-C). Same visual treatment, `width: 100%` and
   `text-align: left`/`border: 0`/`background` reset added since a <button>
   doesn't get <summary>'s block-level, borderless, inherited-background
   defaults for free the way that element does. */
.filter-summary {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  margin: 0 0 10px;
  padding: 16px 16px 10px;
  margin-left: -16px;
  margin-right: -16px;
  border: 0;
  background: var(--surface);
  color: var(--ink);
  font-size: 13px;
  font-weight: 900;
  text-align: left;
  text-transform: uppercase;
  cursor: pointer;
}

.filter-summary[aria-expanded="false"] {
  margin-bottom: 0;
}

.filter-summary-label {
  min-width: 0;
}

.filter-summary-chevron {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  transition: transform 0.2s ease;
}

.filter-summary[aria-expanded="true"] .filter-summary-chevron {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .filter-summary-chevron {
    transition: none;
  }
}

.filter-summary-selected {
  text-transform: none;
  font-weight: 600;
  color: var(--muted);
}

/* Collapsible section content — two nested layers (see filter-panel.tsx's
   own comment on why one element can't do both jobs): this outer layer
   owns the animated height via grid-template-rows (0fr collapsed, 1fr
   open — the standard variable-height CSS transition, no JS height
   measurement needed and no fixed max-height guess to keep in sync with
   content that can grow, e.g. a long option list). `sectionOpen` in
   filter-panel.tsx drives the class swap.

   prefers-reduced-motion below removes the transition (jumps instantly)
   rather than removing the collapse behavior itself — the open/closed
   *state* isn't motion, only the interpolation between them is. */
.filter-collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.25s ease;
}

.filter-collapse-open {
  grid-template-rows: 1fr;
}

.filter-collapse-inner {
  overflow: hidden;
  min-height: 0;
}

@media (prefers-reduced-motion: reduce) {
  .filter-collapse {
    transition: none;
  }
}

.filter-search {
  width: 100%;
  min-height: 40px;
  margin-bottom: 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 8px 10px;
  background: #fff;
}

/* Was previously an absolutely-positioned button overlaid on top of the
   input (position: absolute; top/right: 0 with its own fixed min-height) —
   fragile because the input's real rendered height wasn't guaranteed to
   match the button's fixed height on every viewport, so the two drifted
   apart and read as two separate controls instead of one search field.
   Now the row itself is the bordered/rounded control; the input and button
   are plain flex children inside it, sharing its height by construction,
   which can't drift regardless of viewport or font metrics. */
.sku-search-row {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  overflow: hidden;
}

.sku-search-row .filter-search {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

.sku-search-btn {
  flex-shrink: 0;
  width: 44px;
  min-height: 0;
  padding: 0;
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Overrides .btn:hover's lift transform + box-shadow (not just background):
   this button lives flush inside .sku-search-row, which clips overflow to
   keep the row's own rounded corners clean, so a translateY lift would get
   visibly cut off by that clip instead of "lifting" — a background swap is
   the hover feedback that actually reads correctly in this context. */
.sku-search-btn:hover {
  background: var(--sea-2);
  transform: none;
  box-shadow: none;
}

.sku-search-btn svg {
  width: 18px;
  height: 18px;
}

.option-list {
  display: grid;
  gap: 6px;
}

.filter-option,
.check-option {
  min-height: 34px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  border-radius: 8px;
  padding: 7px 9px;
  color: #555555;
  font-size: 13px;
}

.filter-option {
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.filter-option.active,
.check-option:has(input:checked) {
  background: rgba(0, 140, 214, 0.1);
  color: var(--sea);
  font-weight: 900;
}

.filter-option:disabled,
.check-option.disabled {
  opacity: 0.42;
}

.check-option input {
  width: 16px;
  height: 16px;
  accent-color: var(--sea);
}

.count {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Collapsed rows stay mounted (see filter-panel.tsx's OptionList) so
   expanding "Show more" never remounts/reflows the rows already visible
   above it — display:none just removes them from layout and paint. */
.filter-option-hidden {
  display: none;
}

.filter-show-more {
  min-height: 34px;
  width: 100%;
  border: 0;
  background: transparent;
  border-radius: 8px;
  padding: 7px 9px;
  text-align: left;
  cursor: pointer;
  color: var(--sea);
  font-weight: 700;
  font-size: 13px;
}

.filter-show-more:hover {
  background: rgba(0, 140, 214, 0.08);
}

.mobile-filter-bar {
  display: none;
  gap: 10px;
  align-items: center;
  margin-bottom: 16px;
}

.active-chips {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding-bottom: 2px;
}

.chip {
  flex: 0 0 auto;
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 1px solid rgba(0, 140, 214, 0.16);
  border-radius: 4px;
  padding: 6px 10px;
  background: #fff;
  color: var(--sea);
  font-size: 12px;
  font-weight: 900;
}

.chip button {
  border: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.results-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 16px;
}

.results-top select {
  min-height: 42px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 8px 12px;
  background: var(--surface);
}

.results-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}

.view-toggle {
  min-height: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 8px 12px;
  background: var(--surface);
  color: var(--ink);
  font-weight: 900;
  cursor: pointer;
}

.view-toggle svg {
  width: 18px;
  height: 18px;
}

/* auto-fill/minmax instead of a hardcoded per-breakpoint column count
   (was repeat(3, ...) here, repeat(2, ...) under the 1040px tier) — the
   fixed-2-column tier let cards balloon to 368-488px wide with sidebar
   hidden (721-1040px), a lot of dead space around .product-media's fixed
   1:1 square since real parts photos don't fill that frame, and produced
   a jarring snap right at the sidebar-reappear boundary (488px cards at
   1040px viewport, 231px cards one pixel later at 1041px — column count
   more than halving for a 1px width change, since both the sidebar
   visibility and this column count were pinned to the same 1040px
   breakpoint independently). 250px keeps 3 columns at the capped desktop
   container width (854px, same as before — 274px cards, unchanged from
   the old fixed rule) while letting column count respond continuously
   to whatever width is actually available as the sidebar comes and
   goes, instead of two independent breakpoints fighting each other. */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}

.product-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
}

.product-card {
  display: flex;
  flex-direction: column;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
  /* Same lift-on-hover as .featured-tile — .product-card is an <article>
     with several of its own interactive children (Add Inquiry button, a
     "Details" link — see products-client.tsx) on the grid/list pages, so
     unlike .featured-tile this class is never itself a single <Link> on
     those pages (it is on the related-products variant in
     products/[slug]/page.tsx). The lift only signals "this is the card
     you're looking at," not "click anywhere on it," so it stays safe to
     apply to the whole card regardless of which variant is rendering. */
  transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}

.product-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 20px 40px rgba(1, 41, 86, 0.14);
  border-color: rgba(0, 140, 214, 0.35);
}

.product-media {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #eaf6ff, #fff);
  overflow: hidden;
}

.product-media img {
  width: 82%;
  height: 82%;
  object-fit: contain;
  mix-blend-mode: multiply;
  /* Scoped to the image itself, not a card-wide zoom — .product-media is
     its own <Link> on the grid/list pages, so this reads as "this photo
     enlarges," not an ambiguous whole-card gesture the way scaling
     .product-card itself would when the card holds unrelated buttons. */
  transition: transform 0.3s ease;
}

.product-card:hover .product-media img {
  transform: scale(1.05);
}

/* No-image placeholder inside a product card/detail image slot must fill the
   1:1 .product-media box exactly, not the fixed min-height: 260px that
   .photo-box uses elsewhere (company.html, news-detail.html) — those other
   usages are unrelated standalone placeholders, not a sibling of a real
   product photo, so .photo-box's shared base rule stays untouched. */
.product-media .photo-box {
  width: 100%;
  height: 100%;
  min-height: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

.product-media .product-image-fallback {
  width: 100%;
  height: 100%;
}

.product-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  padding: 16px;
}

.product-body h3 {
  margin: 0;
  font-size: 20px;
}

.sku {
  color: var(--muted);
  font-family: "Roboto Mono", Consolas, monospace;
  font-size: 12px;
}

/* Product cards swap the title/part-number colors relative to every other
   .sku usage on the site (breadcrumbs, company.html's stat labels, inquiry
   numbers, etc. — .sku is a generic small-muted-text class shared well
   beyond actual SKUs, confirmed by grep before touching it) — scoped to
   .product-card specifically rather than changing the shared base rules,
   so this swap applies only to the product list's cards and the related-
   products mini-cards on the detail page (both use .product-card), not
   every other place .sku/h3 appear across the site. h3 has no color rule
   of its own elsewhere (inherits --ink via body), so this is the only
   place a title color is set at all — not overriding a competing rule. */
.product-card h3 {
  color: var(--muted);
}

.product-card .sku {
  color: var(--ink);
}

/* Title + Part No. share one small block with its own tight internal gap,
   independent of whatever spacing rule positions the block as a whole —
   Grid view is a flex column (margin-bottom below carries the ~5px
   breathing room before the next item, e.g. brand badges), List view
   places it via CSS Grid (see .product-list .product-title-block below) —
   either way the 2px gap between title and SKU stays exactly 2px, it's
   never entangled with row-gap/margins meant for surrounding content. */
.product-title-block {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 5px;
}

/* .sku is shared site-wide (News, Company, inquiry pages, etc. — see its
   base rule above), so the slightly-larger size requested for product
   cards specifically is scoped to just the two card-list wrappers, not
   applied to the base .sku rule everyone else also uses. */
.product-grid .sku,
.product-list .sku {
  font-size: 14px;
}

.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 12px 0;
}

.stroke-preview {
  display: grid;
  gap: 6px;
  margin: 12px 0;
}

.stroke-row {
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  align-items: start;
  gap: 8px;
}

.stroke-row .tag-row {
  margin: 0;
}

.stroke-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 25px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.08);
  color: var(--muted);
  font-size: 10px;
  font-weight: 900;
}

.badge {
  display: inline-flex;
  align-items: center;
  min-height: 25px;
  border-radius: 4px;
  padding: 4px 8px;
  background: rgba(0, 140, 214, 0.1);
  color: var(--sea);
  font-size: 11px;
  font-weight: 900;
  line-height: 1.2;
}

.badge.hp {
  background: #fcf8e3;
  color: #8a6d3b;
}

.badge.tag {
  background: rgba(31, 122, 107, 0.1);
  color: var(--success);
}

.card-actions {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
  margin-top: auto;
}

.product-list .product-card {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr);
  align-items: stretch;
}

.product-list .product-media {
  aspect-ratio: auto;
  min-height: 132px;
}

.product-list .product-media img {
  width: 78%;
  height: 78%;
}

.product-list .product-body {
  display: grid;
  grid-template-columns: minmax(160px, 1.05fr) minmax(190px, 1fr) minmax(132px, 132px);
  grid-template-rows: auto auto;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  min-width: 0;
}

.product-list .product-title-block {
  grid-column: 1;
  grid-row: 1 / span 2;
  margin: 0;
}

.product-list .product-body h3 {
  font-size: 18px;
}

.product-list .brand-row,
.product-list .stroke-preview {
  margin: 0;
}

.product-list .brand-row {
  grid-column: 2;
  grid-row: 1;
}

.product-list .stroke-preview {
  grid-column: 2;
  grid-row: 2;
}

.product-list .tags-row {
  display: none;
}

.product-list .card-actions {
  grid-column: 3;
  grid-row: 1 / span 2;
  align-self: center;
  margin-top: 0;
  min-width: 0;
}

.product-list .card-actions .btn {
  width: 100%;
  min-width: 0;
  padding-inline: 12px;
  white-space: nowrap;
}

.drawer-overlay {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: none;
  background: rgba(1, 41, 86, 0.46);
}

.drawer-overlay.open {
  display: block;
}

.filter-drawer {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 90;
  width: min(340px, 88vw);
  display: flex;
  flex-direction: column;
  transform: translateX(-104%);
  transition: transform 0.24s cubic-bezier(0.22, 1, 0.36, 1);
  background: var(--surface);
  box-shadow: 24px 0 50px rgba(1, 41, 86, 0.24);
}

.filter-drawer.open {
  transform: translateX(0);
}

.drawer-head {
  min-height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid var(--line);
  padding: 12px 16px;
}

.drawer-body {
  overflow: auto;
}

.drawer-actions {
  position: sticky;
  bottom: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  border-top: 1px solid var(--line);
  padding: 12px;
  background: var(--surface);
}

.detail-layout {
  display: grid;
  grid-template-columns: minmax(280px, 0.92fr) 1.08fr;
  gap: 34px;
  align-items: start;
}

.detail-media {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  padding: 18px;
  box-shadow: var(--shadow);
}

.detail-media img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: contain;
}

/* fallbackImage() renders a .photo-box <div>, not an <img>, when a product
   has no image — the aspect-ratio: 1 rule above only ever applied to the img
   element, so the no-image case never picked up any ratio and fell back to
   .photo-box's shared 260px min-height (same class also used standalone in
   company.html/news-detail.html, which stays untouched). */
.detail-media .photo-box {
  width: 100%;
  aspect-ratio: 1;
  min-height: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

.detail-media .product-image-fallback {
  width: 100%;
  aspect-ratio: 1;
}

.gallery-main {
  position: relative;
  overflow: hidden;
  cursor: zoom-in;
}

.gallery-main img {
  display: block;
}

/* Cursor-tracked magnifier — mouse-only input (see product-gallery.tsx's
   comment on why this is gated on actual input capability, not a
   screen-width breakpoint). While .zooming, the <img> is hidden and the
   container's own background-image (set inline via JS from
   mousemove) takes over at 220% size, positioned by cursor coordinates —
   the classic "background-position tracks the cursor" zoom technique,
   avoiding a second full-resolution <img> element or a canvas. .zooming
   only ever gets added by the mouseenter handler in product-gallery.tsx,
   which touch input never fires, so no separate touch-side override is
   needed here — this block only matters on devices where it can trigger. */
@media (hover: hover) and (pointer: fine) {
  .gallery-main.zooming img {
    opacity: 0;
  }

  .gallery-main.zooming {
    background-repeat: no-repeat;
    background-size: 220%;
  }
}

/* Fullscreen tap-to-view for touch devices (also reachable by clicking
   the main image on desktop, as a bonus, not a requirement). Pinch-zoom
   and pan are the browser's own native gesture handling on the <img> —
   deliberately not reimplemented in JS: this site's viewport meta never
   sets user-scalable=no or a maximum-scale, so native pinch-zoom already
   works on any fullscreen image without extra code. */
.gallery-lightbox {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(16, 25, 32, 0.92);
  padding: 24px;
}

.gallery-lightbox img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  touch-action: pinch-zoom;
}

.gallery-lightbox-close {
  position: absolute;
  top: 18px;
  right: 18px;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  cursor: pointer;
}

.gallery-lightbox-close:hover {
  background: rgba(255, 255, 255, 0.22);
}

.gallery-thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 8px;
  margin-top: 12px;
}

.gallery-thumbs button {
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  padding: 4px;
  aspect-ratio: 1;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.gallery-thumbs button.active,
.gallery-thumbs button:hover {
  border-color: var(--sea);
  box-shadow: 0 0 0 2px rgba(27, 113, 184, 0.16);
}

.gallery-thumbs img {
  width: 100%;
  height: 100%;
  aspect-ratio: 1;
  object-fit: contain;
}

.detail-panel {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  padding: 24px;
}

.spec-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin: 18px 0;
}

/* Product detail page's upper identity panel (Category/Brand/Tags) — one
   cell per row, not the 2-column layout .spec-grid uses everywhere else
   (including the Specifications tab below it), which still wants 2
   columns unchanged. */
.spec-grid-single {
  grid-template-columns: 1fr;
}

.spec-cell {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 12px;
  background: #fff;
}

.spec-cell span {
  display: block;
  color: var(--muted);
  font-size: 12px;
}

.spec-cell strong {
  display: block;
  margin-top: 5px;
}

.detail-description {
  border-radius: 8px;
  background: var(--surface-2);
  padding: 14px 16px;
}

/* News post rich-text content styling (.article-body) moved to
   news-content.css, shared between the public /news/[slug] page and the
   back-office post editor so both render identically — see that file's
   header comment for why it's split out instead of living here. */

.hp-block {
  margin-top: 18px;
  border-radius: 8px;
  background: var(--surface-2);
  padding: 14px;
}

.related-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
}

.detail-tabs {
  margin-top: 34px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  overflow: hidden;
}

.detail-tabs-list {
  display: flex;
  overflow-x: auto;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}

.detail-tab-btn {
  flex: none;
  padding: 14px 20px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.detail-tab-btn:hover {
  color: var(--ink);
}

.detail-tab-btn.active {
  color: var(--sea);
  border-bottom-color: var(--sea);
}

.detail-tab-panel {
  padding: 24px;
}

.toast {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: min(360px, calc(100vw - 36px));
  transform: translateY(16px);
  opacity: 0;
  pointer-events: none;
  border-radius: 8px;
  padding: 14px 16px;
  background: var(--ink);
  color: white;
  box-shadow: var(--shadow);
  transition: opacity 0.18s ease, transform 0.18s ease;
}

.toast.show {
  transform: translateY(0);
  opacity: 1;
}

/* Only a toast carrying a real action becomes clickable — a plain
   confirmation toast has nothing to interact with, and staying
   pointer-events: none for that case means it can never accidentally eat
   a click meant for whatever's underneath it during the fade transition. */
.toast.show.has-action {
  pointer-events: auto;
}

.toast-action {
  flex: 0 0 auto;
  color: white;
  font-weight: 800;
  text-decoration: underline;
  white-space: nowrap;
}

.toast-action:hover {
  opacity: 0.8;
}

/* Sits above .toast's bottom-right corner (18px + its own footprint) so an
   active "added to inquiry" toast and this button never overlap — both can
   be on screen at once since adding an item while browsing a long filtered
   list is the exact moment a visitor is also likely to be scrolled deep
   enough for this button to be visible. z-index stays below .cookie-banner
   (95) and .toast (100) so both remain fully interactive if they're ever
   showing at the same time as this button. */
.back-to-top-btn {
  position: fixed;
  right: 18px;
  bottom: 84px;
  z-index: 60;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  border: none;
  background: var(--sea);
  color: white;
  box-shadow: var(--shadow);
  cursor: pointer;
  transition: opacity 0.18s ease, transform 0.18s ease;
}

.back-to-top-btn:hover {
  transform: translateY(-2px);
}

.empty-state {
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 34px;
  background: rgba(255, 255, 255, 0.58);
  text-align: center;
}

.content-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
  gap: 24px;
  align-items: start;
}

.content-grid > *,
.content-grid aside {
  min-width: 0;
  max-width: 100%;
}

.panel {
  min-width: 0;
  max-width: 100%;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
}

.panel.pad {
  padding: 22px;
}

.info-list {
  display: grid;
  gap: 12px;
}

.info-row {
  display: grid;
  grid-template-columns: minmax(86px, 112px) minmax(0, 1fr);
  gap: 14px;
  border-top: 1px solid var(--line);
  padding-top: 12px;
}

.info-row:first-child {
  border-top: 0;
  padding-top: 0;
}

.info-row span {
  color: var(--muted);
  font-size: 12px;
  font-weight: 900;
  text-transform: uppercase;
}

.info-row strong {
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
}

.field {
  display: grid;
  gap: 7px;
}

.field.full {
  grid-column: 1 / -1;
}

.field label {
  color: #222222;
  font-size: 13px;
  font-weight: 900;
}

.field input,
.field textarea,
.field select {
  width: 100%;
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px 12px;
  background: #fff;
  color: var(--ink);
}

.field textarea {
  min-height: 132px;
  resize: vertical;
}

.map-box,
.photo-box {
  max-width: 100%;
  min-height: 260px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--sea);
  font-weight: 900;
  text-align: center;
}

/* News detail page's og:image preview box (news/[slug]/page.tsx) — was two
   React inline style attributes (fixed 1200:630 aspect-ratio + 600px cap on
   the wrapper, cover-fit on the <img>), moved to a class since CSP's
   style-src blocks inline style attributes. Both values are fixed
   constants, not per-post computed ones, so a plain class (not a
   generated <style nonce> block) is enough — see the image-cropper.tsx
   fix for the same pattern. Scoped under its own class rather than
   folded into .photo-box above so the shared placeholder styling used on
   Company/Contact/other News list cards stays untouched. */
.news-detail-og-photo {
  width: 100%;
  aspect-ratio: 1200 / 630;
  max-width: 600px;
}

.news-detail-og-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* No-photo placeholder for product imagery specifically (list cards,
   detail page, related products, cart line items) — the site logo fills
   the frame on a plain white background, replacing the grid-pattern +
   name-initials .photo-box used for non-product placeholders elsewhere
   (map embeds, News). Per-context sizing rules below mirror whatever
   that slot's real <img> would have used, same pattern .photo-box itself
   already follows (see .product-media/.detail-media overrides above). */
.product-image-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: #fff;
}

.product-image-fallback img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 12%;
}

.map-box {
  position: relative;
  display: block;
  aspect-ratio: 4 / 3;
  min-height: 0;
  padding: 0;
  background: #fff;
  color: inherit;
}

.map-box iframe {
  display: block;
  width: 100%;
  max-width: 100%;
  height: 100%;
  min-height: 0;
  border: 0;
}

.cart-list {
  display: grid;
  gap: 12px;
}

.cart-item {
  display: grid;
  grid-template-columns: 86px minmax(0, 1fr) 120px;
  gap: 14px;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 12px;
  background: #fff;
}

.cart-item img {
  width: 86px;
  height: 86px;
  object-fit: contain;
  border-radius: 8px;
  background: var(--surface-2);
}

.cart-item .product-image-fallback {
  width: 86px;
  height: 86px;
  border-radius: 8px;
}

.qty-control {
  display: flex;
  align-items: center;
  gap: 8px;
}

.qty-control button {
  width: 34px;
  height: 34px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--surface);
  font-weight: 900;
  cursor: pointer;
}

.news-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 20px;
}

.segmented {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.segmented button,
.pagination a {
  min-height: 38px;
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 8px 12px;
  background: var(--surface);
  color: var(--ink);
  font-weight: 900;
  cursor: pointer;
}

.segmented button.active,
.pagination a.active {
  border-color: var(--sea);
  background: rgba(0, 140, 214, 0.1);
  color: var(--sea);
}

.news-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

.news-card {
  display: grid;
  grid-template-columns: 140px minmax(0, 1fr);
  gap: 16px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 14px;
  background: var(--surface);
  /* Same lift treatment as .product-card/.featured-tile: this class
     renders as either a plain <article> with its own "Read More" link
     (news-client.tsx) or a single <Link> (news/[slug].tsx's Related/
     Pinned sidebar, which has no thumbnail at all) — the lift reads as
     "this is the card in focus" either way, safe on both. */
  transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}

.news-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 20px 40px rgba(1, 41, 86, 0.14);
  border-color: rgba(0, 140, 214, 0.35);
}

/* Clips the thumbnail's own hover zoom to its own 140x120 box — clipping
   at .news-card's outer edge instead (which has 14px padding) would let a
   scaled image visibly push into that padding before being cut off,
   unlike .product-media's own dedicated clip box. */
.news-card-thumb {
  width: 140px;
  height: 120px;
  border-radius: 8px;
  overflow: hidden;
}

.news-card:hover .news-card-thumb img {
  transform: scale(1.06);
}

/* No cover image uploaded: skip the thumbnail column entirely (no <img>
   is rendered) rather than showing a placeholder box — a placeholder
   needed a fixed height to look intentional next to a real 140x120
   thumbnail, and no single fixed height/ratio matched both this card's
   proportions and the article page's own now-unused cover-image slot, so
   it always read as an odd blank tile rather than "just no image here."
   Text content fills the full card width instead. */
.news-card-no-image {
  grid-template-columns: 1fr;
}

.news-card-thumb img {
  width: 100%;
  height: 100%;
  /* cover, not contain: News cover images are admin-uploaded photos of
     varying aspect ratios (unlike product photos, which are consistently
     near-square) — contain preserved the full image but let a wide/
     landscape upload shrink to a thin sliver inside this near-square
     140x120 box to keep its ratio, which read as "the image is cut off"
     even though nothing was actually cropped. cover fills the box and
     crops the overflow instead, so every upload reads as a normal-sized
     thumbnail regardless of its original ratio. */
  object-fit: cover;
  background: var(--surface-2);
  transition: transform 0.3s ease;
}

.pagination {
  display: flex;
  gap: 8px;
  margin-top: 22px;
}

@media (max-width: 1180px) {
  .content-grid {
    grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
    gap: 20px;
  }

  .product-list .product-card {
    grid-template-columns: 132px minmax(0, 1fr);
  }

  .product-list .product-media {
    min-height: 126px;
  }

  .product-list .product-body {
    grid-template-columns: minmax(150px, 1fr) minmax(180px, 1.1fr);
    grid-template-rows: auto auto auto;
  }

  .product-list .product-title-block {
    grid-column: 1;
    grid-row: 1 / span 2;
  }

  .product-list .brand-row {
    grid-column: 2;
    grid-row: 1;
  }

  .product-list .stroke-preview {
    grid-column: 2;
    grid-row: 2;
  }

  .product-list .card-actions {
    grid-column: 1 / -1;
    grid-row: 3;
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-self: stretch;
  }
}

@media (max-width: 1040px) {
  .category-grid,
  .featured-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .products-layout,
  .content-grid,
  .route-band .section {
    grid-template-columns: 1fr;
  }

  .packaging-carousel-stage {
    padding-inline: 32px;
    height: clamp(340px, 58vw, 460px);
  }

  .packaging-carousel {
    width: min(100%, 680px);
  }

  .filter-sidebar {
    display: none;
  }

  /* Sticky under .site-header (74px, itself sticky) rather than scrolling
     away with the page — a catalog with a long product list otherwise
     means re-opening filters requires scrolling all the way back up
     first. Own background/padding/z-index since it's no longer just an
     inline block in the page flow, it's a strip pinned above whatever
     scrolls beneath it within .section's own width — not full-bleed to
     the viewport edge, since .section has no fixed side padding to
     offset with a negative margin (it's width-clamped + auto-centered,
     not padded). */
  .mobile-filter-bar {
    display: flex;
    position: sticky;
    top: 74px;
    z-index: 30;
    padding: 10px 0;
    background: var(--page);
    border-bottom: 1px solid var(--line);
  }

  /* .results-top's own FilterChips copy is redundant once
     .mobile-filter-bar's copy is switched on above — without this, both
     render at this breakpoint and the active-filter chips show up twice
     on the page (once under the Filters(N) button, once under the
     product count). */
  .results-top-chips {
    display: none;
  }

  /* No List-specific overrides at this breakpoint — the 1180px tier's
     132px-image / 2-column-info horizontal row (above) is a sensible fit
     all the way down to where the 720px tier's stacked mobile split
     (below) takes over. A stray flex-column override used to sit here
     that stretched the image to ~full card width, i.e. a List row lost
     its own distinct shape and read as an ordinary Grid card — the gap
     the user flagged: "List looks like Grid somewhere between tablet and
     phone width." It went unnoticed because List mode was hard-locked
     to Grid under 720px (before §0-C-105) so nobody exercised the (720,
     1040] range in List mode to begin with. */
}

@media (max-width: 720px) {
  /* Given its own breakpoint, split off from the shared 1040px one above
     (.products-layout/.content-grid/.route-band .section) — that breakpoint
     exists because .filter-sidebar genuinely needs the width a real tablet
     doesn't reliably have, but .detail-layout's two columns (image + spec
     panel) fit comfortably at 768px: left column is grid-clamped to
     minmax(280px, 0.92fr), so it no longer needs a manual .detail-media
     max-width once it's inside the grid — that cap only mattered for the
     single-column case below, where nothing else constrains the image's
     width. Confirmed via screenshot: 768px/820px were needlessly forced
     into a single stacked column with an oversized image, even though the
     two-column layout has room to spare at those widths.  */
  .detail-layout {
    grid-template-columns: 1fr;
  }

  .detail-media {
    max-width: 480px;
    margin: 0 auto;
  }

  .nav-links {
    display: none;
  }

  .mobile-menu-btn {
    display: inline-flex;
  }

  .mobile-menu.open {
    display: block;
  }

  .brand small {
    display: none;
  }

  .hero {
    height: auto;
    max-height: none;
    min-height: auto;
  }

  .hero::before {
    background:
      linear-gradient(180deg, rgba(1, 41, 86, 0.94) 0%, rgba(1, 41, 86, 0.78) 62%, rgba(1, 41, 86, 0.46) 100%),
      /* See the desktop .hero::before rule above for why this is .webp now. */
      url("assets/hero-ocean.webp") center / cover;
  }

  .hero-tools {
    grid-template-columns: 1fr;
  }

  .hero-tools {
    width: calc(100vw - 28px);
    max-width: calc(100vw - 28px);
  }

  .section,
  .hero-inner {
    width: min(calc(100% - 28px), 1180px);
    padding: 48px 0;
  }

  .category-grid,
  .featured-grid,
  .product-grid,
  .related-grid,
  .spec-grid,
  .form-grid,
  .news-grid {
    grid-template-columns: 1fr;
  }

  .news-toolbar,
  .cart-item,
  .news-card {
    grid-template-columns: 1fr;
  }

  .results-controls {
    width: 100%;
  }

  .results-controls select {
    flex: 1;
    min-width: 0;
  }

  /* List mode's own mobile layout (architecture.md §0-C-105) — top band
     split image-left (~30%) / info-right (~70%), bottom band full-width
     for the HP/CC spec rows + actions (spanning under the image, not
     confined to the 70% info column). ProductCard's markup nests
     .stroke-preview/.tags-row/.card-actions inside .product-body (a
     sibling of .product-media, not a sibling of each other at the
     .product-card level) — grid-column/grid-row on a nested item only
     positions it within its own nearest grid-container ANCESTOR, so
     without unwrapping .product-body first, those rows could only ever
     span .product-body's own 70% box, never the full card width under
     the image. display: contents on .product-body removes its own box
     from the layout and promotes its children to be direct items of
     .product-card's grid instead, which is what makes a genuine
     full-width bottom band possible here without touching ProductCard's
     JSX (kept as a pure CSS reshape, same as how desktop List mode
     already reshapes this same markup with no viewMode branching in the
     component itself). */
  .product-list .product-card {
    display: grid;
    grid-template-columns: 30% minmax(0, 1fr);
    grid-template-rows: auto auto auto auto;
    align-items: stretch;
  }

  .product-list .product-media {
    grid-column: 1;
    grid-row: 1 / span 2;
    aspect-ratio: 1;
    min-height: 0;
    align-self: start;
  }

  .product-list .product-media img {
    width: 82%;
    height: 82%;
  }

  .product-list .product-body {
    display: contents;
  }

  .product-list .product-title-block {
    grid-column: 2;
    grid-row: 1;
    align-self: end;
    margin: 0;
    padding: 10px 12px 2px;
  }

  .product-list .product-body h3 {
    font-size: 15px;
    line-height: 1.25;
  }

  .product-list .brand-row {
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    margin: 0;
    padding: 0 12px 10px;
  }

  /* Spec rows (2S/4S/CC) + actions drop to their own full-width band
     below the image/info split, rather than squeezing into the ~70%
     info column where three HP/CC rows would wrap illegibly at this
     width — same "vertical space is now the scarce resource, not
     horizontal" tradeoff the List-mode field audit (chat discussion
     preceding §0-C-105) called out for the tags row. */
  .product-list .stroke-preview {
    grid-column: 1 / -1;
    grid-row: 3;
    margin: 0;
    padding: 10px 12px 0;
    border-top: 1px solid var(--line);
  }

  .product-list .tags-row {
    display: none;
  }

  .product-list .card-actions {
    grid-column: 1 / -1;
    grid-row: 4;
    display: grid;
    grid-template-columns: 1fr 1fr;
    margin-top: 0;
    padding: 10px 12px 12px;
  }

  .news-toolbar {
    align-items: stretch;
    flex-direction: column;
  }

  .news-card-thumb {
    width: 100%;
    height: auto;
    aspect-ratio: 140 / 120;
  }

  .info-row {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .results-top,
  .footer-inner {
    align-items: flex-start;
    flex-direction: column;
  }

  .display-title {
    font-size: 38px;
  }

  .lead {
    font-size: 16px;
  }

  .packaging-copy {
    display: grid;
    gap: 6px;
    font-size: 16px;
  }

  .packaging-copy span:not(:last-child)::after {
    display: none;
  }

  .packaging-carousel-stage {
    padding: 10px 34px;
    height: clamp(280px, 78vw, 380px);
  }

  .packaging-carousel-button {
    width: 34px;
    height: 34px;
  }

  .packaging-carousel-button-prev {
    left: 6px;
  }

  .packaging-carousel-button-next {
    right: 6px;
  }

  .card-actions {
    grid-template-columns: 1fr;
  }

  .btn,
  .icon-btn,
  .cart-link {
    min-height: 46px;
  }
}

.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 95;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 24px;
  background: var(--navy);
  color: rgba(255, 255, 255, 0.92);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.24);
}

.cookie-banner p {
  margin: 0;
  font-size: 13px;
  line-height: 1.6;
  max-width: 640px;
}

.cookie-banner a {
  color: white;
  text-decoration: underline;
}

.cookie-banner-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.cookie-banner .btn.ghost {
  border-color: rgba(255, 255, 255, 0.4);
  color: white;
}

@media (max-width: 640px) {
  .cookie-banner {
    flex-direction: column;
    align-items: stretch;
  }

  .cookie-banner-actions {
    justify-content: flex-end;
  }
}
