/*
Theme Name: Port Lincoln Tiles & Pavers
Theme URI: https://portlincolntilesandpavers.com
Author: WizzIT Computer & Web Services
Author URI: https://wizzit.au
Description: A fully custom, hand-built theme for Port Lincoln Tiles & Pavers — no page builder, no bloat. Features: timezone-aware showroom open/closed status (with public holiday support) gating every "Call Now" button site-wide; a mobile-friendly project gallery with a modal detail view, draggable photo thumbnails, and a swipeable card carousel; an auto-scrolling, draggable supplier/brand logo marquee; a feature-rich mobile menu with live store status and click-to-call; individually-editable per-day business hours with automatic display grouping; visibility-triggered muted autoplay video; a sticky header with a scroll-responsive logo; full LocalBusiness JSON-LD schema, Open Graph tags and sitemap integration for SEO; a flexible renovations page with configurable content blocks; a live map and hours on the Contact page; toggleable site-wide comment disabling; and SVG upload support in the media library.
Version: 1.41.0
Requires at least: 6.4
Requires PHP: 8.0
Text Domain: pltp
*/

/* ==========================================================================
   1. Custom properties / palette
   ========================================================================== */
:root {
  --color-navy: #18323f;
  --color-teal: #18838e;
  --color-cream: #faf7f2;
  --color-tan: #f1ece2;
  --color-navy-rgb: 24, 50, 63;
  --color-teal-rgb: 24, 131, 142;
  /* Default/fallback only — the real, admin-configurable value is
     applied via an inline <style> tag in wp_head (see
     pltp_output_accent_color_css() in inc/template-tags.php), which
     overrides this at the same :root specificity by printing later in
     the page. This value only matters if that tag is ever missing for
     some reason. */
  --accent: #18838e;
  --font-serif: 'Source Serif 4', Georgia, serif;
  --font-sans: 'Work Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --container: 1280px;
  --radius: 2px;
}

/* ==========================================================================
   2. Reset / base
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* Keeps anchor-linked sections (nav links, "Explore Our Services", etc.)
     from landing partly hidden under the sticky header — offsets the
     scroll target by roughly the header's own height. Also used by the
     section-by-section scroll script in main.js so both stay in sync. */
  scroll-padding-top: 96px;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}
/* Gallery mobile carousel's scroll-snap strip (style.css "15. Responsive")
   uses its own scroll-behavior:smooth for the auto-advance JS's
   scrollTo() calls — same reduced-motion treatment as the html rule
   above, kept as a separate rule since it's a different element. */
@media (prefers-reduced-motion: reduce) {
  .gallery__grid { scroll-behavior: auto; }
}

/* NOTE: full-section snapping used to be tried here with pure CSS
   (scroll-snap-type: y mandatory + scroll-snap-align: start on each
   section), then with a JS-driven pagination/easing controller. Both
   were removed — see CLAUDE.md's scroll-behaviour history — and the
   only scroll behaviour left is the plain `scroll-behavior: smooth`
   above, for anchor links. Don't reintroduce wheel-hijacking here. */

body {
  margin: 0;
  background: var(--color-cream);
  color: var(--color-navy);
  font-family: var(--font-sans);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }

a {
  color: var(--accent);
  text-decoration: none;
}
a:hover { color: var(--color-navy); }

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

/* Visually hidden except when focused — lets keyboard/screen-reader users
   jump past the header/nav straight to <main id="main"> (see header.php).
   Off-screen rather than display:none so it still receives focus. */
.screen-reader-text {
  position: absolute;
  left: -9999px;
  top: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.skip-link:focus {
  position: fixed;
  left: 16px;
  top: 16px;
  width: auto;
  height: auto;
  overflow: visible;
  z-index: 9999;
  padding: 12px 20px;
  background: var(--color-navy);
  color: #fff;
  border-radius: var(--radius);
  font-family: var(--font-sans);
  font-weight: 600;
}

h1, h2, h3 { font-family: var(--font-serif); font-weight: 500; margin: 0; }

.container {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 32px;
}

.btn {
  display: inline-block;
  border-radius: var(--radius);
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 15px 28px;
  border: none;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--color-navy); color: #fff; transform: translateY(-2px); box-shadow: 0 8px 18px rgba(var(--color-navy-rgb), 0.2); }
.btn-outline { border: 1.5px solid var(--color-cream); color: var(--color-cream); padding: 13.5px 26.5px; }
.btn-outline:hover { background: rgba(250,247,242,0.1); color: var(--color-cream); transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) {
  .btn { transition: background-color 0.2s ease, color 0.2s ease; }
  .btn:hover { transform: none; }
}

.eyebrow {
  font-size: 13px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 600;
  margin-bottom: 18px;
}

.section-heading { font-size: 36px; line-height: 1.2; margin-bottom: 0; }
.section-intro { max-width: 560px; margin: 0 0 56px; }

/* ==========================================================================
   2b. Scroll reveal (subtle entrance animation)
   ========================================================================== */
/* Applied via main.js's IntersectionObserver-based reveal IIFE: elements
   start faded/lowered, then settle into place the first (and only)
   time they cross into the viewport — see the "Subtle motion" note in
   CLAUDE.md for why this stays deliberately restrained (one-time,
   short distance, no bounce) rather than a scroll-tied effect. Cards
   within the same grid are given a small staggered transition-delay by
   the JS so a row settles in left-to-right rather than all at once. */
.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.7s ease, transform 0.7s ease;
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* ==========================================================================
   3. Announcement bar
   ========================================================================== */
.announcement-bar {
  background: var(--color-navy);
  color: var(--color-cream);
  font-size: 14px;
  letter-spacing: 0.02em;
  text-align: center;
  padding: 10px 20px;
}
/* Hidden on mobile only — see the shared 900px block in "15. Responsive"
   for the actual display:none rule. Desktop keeps it. */

/* ==========================================================================
   3b. Topbar (address / phone / social / Visit Showroom) — a slim,
   non-sticky row so this info doesn't have to live in the sticky header
   itself, which kept wrapping the nav onto its own line once enough
   nav items were added.
   ========================================================================== */
.topbar { background: var(--color-tan); border-bottom: 1px solid rgba(var(--color-navy-rgb), 0.08); }
.topbar__inner {
  padding: 8px 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 32px;
  flex-wrap: wrap;
}
.topbar__info { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; font-size: 13px; }
.topbar__item { color: var(--color-navy); opacity: 0.8; }
a.topbar__item:hover { opacity: 1; }
.topbar__actions { display: flex; align-items: center; gap: 16px; }
.topbar__social { display: flex; align-items: center; gap: 10px; }
.topbar__social a { display: inline-flex; color: var(--color-navy); opacity: 0.7; }
.topbar__social a:hover { opacity: 1; }
.topbar__social svg { width: 16px; height: 16px; }
.topbar .btn { padding: 7px 18px; font-size: 13px; }

/* Store status indicator — evaluated server-side in Adelaide time, see
   pltp_get_store_status() in inc/template-tags.php. Red/yellow/green
   here regardless of the site's navy/teal palette, since that's the
   universally understood convention for a status light. */
.store-status-wrap { position: relative; display: inline-flex; }
.store-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: 20px;
  white-space: nowrap;
  border: none;
  cursor: default;
  font-family: inherit;
}
.store-status__dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.store-status--open { background: rgba(34, 163, 94, 0.12); color: #1a7a44; }
.store-status--open .store-status__dot {
  background: #22a35e;
  animation: pltp-status-pulse 2s ease-out infinite;
}
/* Opening Soon shares Closing Soon's yellow — both mean "the status is
   about to change," just in opposite directions (about to open vs about
   to close), so the same colour reads correctly for either. */
.store-status--closing-soon, .store-status--opening-soon { background: rgba(217, 158, 30, 0.15); color: #8a6110; }
.store-status--closing-soon .store-status__dot, .store-status--opening-soon .store-status__dot { background: #d99e1e; }
.store-status--closed { background: rgba(199, 62, 62, 0.1); color: #a3312f; }
.store-status--closed .store-status__dot { background: #c73e3e; }

@keyframes pltp-status-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(34, 163, 94, 0.55); }
  70%  { box-shadow: 0 0 0 6px rgba(34, 163, 94, 0); }
  100% { box-shadow: 0 0 0 0 rgba(34, 163, 94, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .store-status--open .store-status__dot { animation: none; }
}

/* Hover popup: compact opening-hours breakdown. The outer .store-status__popup
   box starts flush at top:100% (no real gap) with the visual offset done
   via padding-top instead, so the mouse never crosses genuinely
   unhovered space between the badge and the popup card — a real gap
   here would make the popup vanish mid-transit. */
.store-status__popup {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 30;
  padding-top: 10px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
}
.store-status-wrap:hover .store-status__popup,
.store-status-wrap:focus-within .store-status__popup {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
.store-status__popup-card {
  min-width: 300px;
  background: var(--color-cream);
  border-radius: var(--radius);
  box-shadow: 0 12px 28px rgba(var(--color-navy-rgb), 0.18);
  padding: 16px;
}
.store-status__popup-title {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-navy);
  opacity: 0.6;
  margin-bottom: 10px;
}
.store-status__popup-row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  font-size: 13px;
  padding: 6px 0;
  color: var(--color-navy);
  white-space: nowrap;
}
.store-status__popup-row + .store-status__popup-row {
  border-top: 1px solid rgba(var(--color-navy-rgb), 0.08);
}

/* Disabled call links/buttons while the store is closed — rendered as
   plain, non-interactive text/span (not a real <a href> or button) so
   there's nothing to click, just dimmed to read as unavailable. */
.topbar__item--disabled { opacity: 0.5; cursor: not-allowed; }
.btn.is-disabled {
  opacity: 0.55;
  cursor: not-allowed;
  pointer-events: none;
}
.btn.is-disabled:hover { background: var(--accent); transform: none; box-shadow: none; }

/* ==========================================================================
   4. Header
   ========================================================================== */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: rgba(250, 247, 242, 0.96);
  backdrop-filter: blur(6px);
  border-bottom: 1px solid rgba(24, 50, 63, 0.13);
}

.site-header .container {
  padding-top: 18px;
  padding-bottom: 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}

/* Larger at the top of the page, shrinking once .is-scrolled is added
   (main.js — a plain scroll-position class toggle, not scroll-hijacking
   JS) below. transform (not padding/margin) for vertical centering at
   either size, so this never affects the header's own box height by
   itself — .site-header's height still legitimately changes between the
   two logo sizes here (that's the point), but the .topbar directly below
   it (style.css, non-sticky by design) just reflows with it naturally,
   nothing is depending on the header staying a fixed height. */
.site-logo img, .site-logo svg {
  height: 60px;
  width: auto;
  transform: translateY(-3px);
  transition: height 0.25s ease;
}
.site-header.is-scrolled .site-logo img,
.site-header.is-scrolled .site-logo svg {
  height: 34px;
}
@media (prefers-reduced-motion: reduce) {
  .site-logo img, .site-logo svg { transition: none; }
}

.main-nav {
  display: flex;
  gap: 28px;
  flex-wrap: wrap;
  font-size: 15px;
  font-weight: 500;
}
.main-nav a { color: var(--color-navy); }
.main-nav ul { list-style: none; display: flex; gap: 28px; flex-wrap: wrap; margin: 0; padding: 0; }

.header-actions { display: flex; align-items: center; gap: 16px; }

.nav-toggle {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
}
.nav-toggle span {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--color-navy);
  margin: 5px 0;
}

/* ==========================================================================
   5. Hero
   ========================================================================== */
/* min-height (not a rigid height) so the section grows if its content
   ever needs more room than the vh/px floor gives it — with a fixed
   height + overflow:hidden, a tall .hero__panel (long ACF-edited heading/
   subheading, or just a viewport that isn't tall enough for the vh
   value to clear the panel's real content height) got its top sliced off
   by the clip, since align-items:flex-end anchors the panel to the
   bottom and lets it grow upward past the section's own top edge. Bug
   was invisible on a tall monitor (vh comfortably exceeds panel height)
   and reproducible on a shorter viewport — confirmed via getBoundingClientRect
   at 950x700 and 1440x900 before this fix (panel top rendered above
   hero top by ~50-110px, clipping the eyebrow/heading in both cases).
   max() combines the vh and px floors that used to be two separate
   height/min-height declarations. */
.hero {
  position: relative;
  min-height: max(560px, 88vh);
  display: flex;
  align-items: flex-end;
  overflow: hidden;
}
/* Compact variant (Gallery page) — a full 88vh hero is excessive for a
   page whose real content is the project grid below it. */
.hero--compact {
  min-height: max(380px, 45vh);
}
.hero__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}
.hero__slide {
  opacity: 0;
  z-index: 0;
  transition: opacity 2.5s ease-in-out;
}
.hero__slide.is-active {
  opacity: 1;
  z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
  .hero__slide {
    transition: none;
  }
}
/* Background video variant (used on the Renovations page hero): fills
   the section edge to edge in place of .hero__slides, sized by JS in
   main.js (like object-fit:cover, which a plain iframe can't do with
   CSS alone). No overlay wash — the existing .hero__panel already gives
   the heading/subheading their own solid background, matching the
   image variant's treatment (which also has no overlay). */
.hero__video-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  background: var(--color-navy);
}
.hero__video-bg iframe {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  transform: translate(-50%, -50%);
  border: 0;
  pointer-events: none;
}
.hero__inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 32px 56px;
}
.hero__panel {
  max-width: 560px;
  background: var(--color-navy);
  padding: 44px 40px;
}
.hero__eyebrow { color: var(--accent); }
.hero__heading {
  font-size: clamp(30px, 4vw, 44px);
  line-height: 1.1;
  color: var(--color-cream);
  margin: 0 0 22px;
}
.hero__subheading {
  font-size: 17px;
  line-height: 1.65;
  color: var(--color-cream);
  opacity: 0.9;
  margin: 0 0 30px;
}
/* Stacked, not side-by-side — flex-direction:column's default
   align-items:stretch makes both buttons the same width (the panel's
   full content width) regardless of label length, without needing to
   size one off the other. */
.hero__actions { display: flex; flex-direction: column; gap: 16px; }
.hero__actions .btn { text-align: center; }
/* Contact page only: a "Call Now" button that's redundant on desktop
   (the phone number is already in the topbar) but useful on mobile,
   where the topbar is hidden entirely — see the 900px breakpoint below. */
.hero__actions--mobile-only { display: none; }

/* ==========================================================================
   5b. Feature alert bar (3D walkthrough / online catalogue)
   ========================================================================== */
.feature-alert { background: var(--accent); color: #fff; }
.feature-alert__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  flex-wrap: wrap;
  padding: 18px 32px;
  text-align: center;
}
.feature-alert__badge {
  background: var(--color-cream);
  color: var(--color-navy);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: var(--radius);
}
.feature-alert__text { font-size: 14px; font-weight: 600; }
.feature-alert__links { display: flex; gap: 22px; flex-wrap: wrap; }
.feature-alert__links a { color: #fff; font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }
.feature-alert__links a:hover { color: var(--color-navy); }


/* ==========================================================================
   7. Intro editorial
   ========================================================================== */
.intro {
  position: relative;
  padding: 96px 0;
  display: flex;
  align-items: center;
  overflow: hidden;
}
.intro .container {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 64px;
  align-items: center;
  flex-wrap: wrap;
  width: 100%;
}
.intro.intro--image-right .container { flex-direction: row-reverse; }
/* Optional section background (independent of the Photo/Video column
   below) — colour and opacity are admin-adjustable per instance, so the
   overlay's actual colour/opacity come from inline styles in
   front-page.php rather than being fixed here like the Renovations
   content-block sections' wash. */
.intro__bg-image { position: absolute; inset: 0; z-index: 0; }
.intro__bg-image img { width: 100%; height: 100%; object-fit: cover; }
.intro__bg-overlay { position: absolute; inset: 0; z-index: 0; }
/* Text Colour (intro_text_color, only shown in wp-admin when the
   background image is on) — an admin-chosen overlay colour/opacity
   can't be assumed light or dark, so this is an explicit choice rather
   than automatic. "Dark" needs no override — .eyebrow/.section-heading/
   body text are already navy by default (see "2. Reset / base").
   .intro__link isn't listed here — it's a .btn-primary now (self-colored
   white regardless of section) rather than plain text needing a flip. */
.intro--text-light .intro__content .eyebrow,
.intro--text-light .intro__content .section-heading,
.intro--text-light .intro__content .intro__body { color: #fff; }
.intro--text-light .intro__content .intro__body { opacity: 0.9; }
.intro__media, .intro__content { flex: 1 1 420px; min-width: 320px; }
.intro__media {
  position: relative;
  aspect-ratio: 5 / 4;
  border-radius: var(--radius);
  overflow: hidden;
  background-color: var(--color-tan);
}
.intro__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Video variant: a normal, user-controlled player (sound + controls,
   not a silent background loop, unlike the Renovations hero's video) —
   so it keeps the video's native 16:9 shape rather than the photo's
   cropped 5:4 box, avoiding cutting off part of the frame. */
/* Subtle frame (Ben's request) — sits right at the same rounded corner
   as .intro__media's own overflow:hidden clip, since border-box sizing
   (global reset) keeps the border inside the box and .intro__media-video
   below fills inset:0 up to its inner edge, rather than overlapping or
   sitting outside it. */
.intro__media--video {
  aspect-ratio: 16 / 9;
  border: 2px solid rgba(var(--color-navy-rgb), 0.15);
}
.intro__media-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.intro__body { font-size: 16px; line-height: 1.75; opacity: 0.82; margin: 0 0 16px; }
/* Was a plain text link; now a .btn.btn-primary (see markup) — margin-top
   gives it the breathing room a button needs that a text link didn't. */
.intro__link { margin-top: 8px; }

/* ==========================================================================
   7a. Suppliers / brand logo strip
   ========================================================================== */
.suppliers {
  padding: 24px 0;
  border-top: 1px solid rgba(var(--color-navy-rgb), 0.08);
  border-bottom: 1px solid rgba(var(--color-navy-rgb), 0.08);
}
.suppliers__eyebrow { margin-bottom: 16px; }
.suppliers__row {
  display: grid;
  /* minmax(0, 1fr), not bare 1fr — bare `fr` tracks have an implicit
     `auto` (content-based) minimum, so a track won't shrink below its
     logo image's natural width. At 8 columns that floor gets hit well
     before desktop widths (confirmed real horizontal overflow around
     783-900px, i.e. right where this becomes an 8-column grid) —
     minmax(0, 1fr) removes it so columns divide evenly at any width;
     no visible change at widths wide enough that the floor was never
     hit, only fixes the ones where it was. */
  grid-template-columns: repeat(8, minmax(0, 1fr));
  align-items: center;
  /* justify-items: stretch (the default — no declaration needed) rather
     than center. `center` only centers each item WITHIN its track
     without constraining the item's own width to that track — a logo
     image up to 140px wide (see .suppliers__logo img below) can then
     render wider than an evenly-divided ~93-140px column and visually
     overlap the neighbouring logo (confirmed real report: two specific
     logos overlapping on a real device). Stretching .suppliers__logo to
     its track's actual width fixes that; it still LOOKS centered because
     .suppliers__logo itself is `display:flex; justify-content:center`,
     centering the image inside whatever width it's stretched to. */
  gap: 40px;
}
.suppliers__logo {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 48px;
}
.suppliers__logo img {
  max-height: 100%;
  max-width: 140px;
  width: auto;
  height: auto;
  /* min-width:0 — .suppliers__logo is a flex container, and a flex
     item's default min-width is `auto` (won't shrink below its own
     intrinsic/natural size), not 0. At the narrowest columns this grid
     can produce (e.g. an 8-column desktop grid right at the ~900px
     breakpoint, before there's much room to spread across) the image's
     natural width can exceed that already-correctly-sized column,
     re-overflowing by a few px despite the grid-track and justify-items
     fixes elsewhere in this file — same root-cause family, just one
     level deeper (flex item this time, not grid track/item). */
  min-width: 0;
  object-fit: contain;
  opacity: 0.75;
  transition: opacity 0.25s ease;
}
.suppliers__logo:hover img,
.suppliers__logo:focus-visible img {
  opacity: 1;
}
.suppliers__logo-placeholder {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: rgba(var(--color-navy-rgb), 0.55);
  border: 1px dashed rgba(var(--color-navy-rgb), 0.3);
  border-radius: var(--radius);
  padding: 10px 16px;
  white-space: nowrap;
}

/* Desktop-only continuously-scrolling marquee (main.js builds/tears this
   down; the mobile paginated-grid system further down in this file, and
   in main.js, is completely separate and untouched by any of this — the
   two are gated by mutually-exclusive breakpoints). display:block here
   overrides the desktop grid above so .suppliers__marquee (the JS-built
   wrapper, when present) isn't squeezed into a single 1-of-8 grid track. */
@media (min-width: 901px) {
  .suppliers__row { display: block; }
  .suppliers__marquee {
    overflow: hidden;
    cursor: grab;
    /* pan-y (not the pan-x used for genuine horizontal-scroll strips
       elsewhere in this file) — this element isn't natively scrolled at
       all, position comes entirely from main.js setting `transform`
       directly, so a vertical touch swipe should still behave like
       normal page scroll; only *horizontal* pointer movement is ours to
       capture, via the pointerdown/pointermove handlers in main.js, not
       through the browser's own touch-scroll gesture recognition. */
    touch-action: pan-y;
    /* Prevents a click-drag from also selecting the logos' alt text /
       any placeholder text as if it were a text-selection drag. */
    user-select: none;
  }
  .suppliers__marquee.is-dragging { cursor: grabbing; }
  .suppliers__marquee-inner {
    display: flex;
    width: max-content;
    /* transform is set directly by main.js's requestAnimationFrame loop
       (both for the auto-scroll step and for dragging) — no CSS
       @keyframes animation here, on purpose: dragging needs to take over
       the same `transform` mid-motion and hand it back afterwards from
       wherever the visitor actually left it, which a declarative CSS
       animation has no clean way to do. */
  }
  .suppliers__track {
    display: flex;
    align-items: center;
    flex-shrink: 0;
    gap: 40px;
    /* Trailing gap so the space between the end of one track and the
       start of the next (as they sit side by side) matches the gap
       between logos within a track — otherwise the seam between the two
       looped copies would visibly read as tighter or looser than the
       rest of the strip. */
    padding-right: 40px;
  }
  /* Stops the browser's own native "drag this image" ghost-cursor from
     kicking in and fighting our custom pointer-drag interaction — every
     logo is an <img> (or an <a> wrapping one), both draggable by default. */
  .suppliers__marquee img, .suppliers__marquee a {
    -webkit-user-drag: none;
    user-drag: none;
  }
}
@media (min-width: 901px) and (prefers-reduced-motion: reduce) {
  /* main.js's sync() never builds the marquee when prefers-reduced-motion
     matches — .suppliers__row stays the original flat list of
     .suppliers__logo it always was, so just restore the plain desktop
     grid display for it instead of the display:block override above. */
  .suppliers__row { display: grid; }
}

/* ==========================================================================
   8. Services
   ========================================================================== */
.services { background: var(--color-tan); padding: 96px 0; }
.services__intro { font-size: 16px; line-height: 1.6; opacity: 0.82; margin: 12px 0 0; max-width: 640px; }
.services__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px;
}
@media (max-width: 900px) {
  .services__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .services__grid { grid-template-columns: 1fr; }
}
.service-card { background: var(--color-cream); border-radius: var(--radius); overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; }
.service-card:hover { transform: translateY(-4px); box-shadow: 0 14px 28px rgba(var(--color-navy-rgb), 0.1); }
@media (prefers-reduced-motion: reduce) {
  .service-card:hover { transform: none; }
}
.service-card__image {
  position: relative;
  aspect-ratio: 4 / 3;
  background-color: rgba(var(--color-navy-rgb), 0.06);
  overflow: hidden;
}
.service-card__image img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.service-card__body { padding: 24px; }
.service-card__title { font-size: 21px; font-weight: 600; margin: 0 0 8px; }
.service-card__desc { font-size: 15px; opacity: 0.78; line-height: 1.6; margin: 0; }

/* ==========================================================================
   8b. Contact page — form (left) + Visit Us / map stacked (right)
   ========================================================================== */
.contact-main { padding: 96px 0; }
.contact-main .container { display: flex; gap: 64px; flex-wrap: wrap; align-items: flex-start; }
.contact-main__form, .contact-main__visit { flex: 1 1 420px; min-width: 320px; }
.contact-form__embed { margin-top: 24px; }
.contact-main__visit { display: flex; flex-direction: column; gap: 32px; }
.contact-main__map--spaced { margin-top: 32px; }
.contact-main__hours-note { font-size: 13px; opacity: 0.65; margin: 20px 0 0; }

/* ==========================================================================
   9. Showroom / visit
   ========================================================================== */
.showroom { padding: 96px 0; }
.showroom .container { display: flex; gap: 64px; flex-wrap: wrap; }
.showroom__content, .showroom__media { flex: 1 1 420px; min-width: 320px; }
.showroom__body { font-size: 16px; line-height: 1.75; opacity: 0.82; margin: 0 0 32px; max-width: 460px; }
.showroom__facts { display: flex; flex-direction: column; gap: 18px; margin-bottom: 32px; }
.showroom__fact { display: flex; gap: 14px; }
.showroom__fact-label { font-weight: 600; min-width: 90px; }
.showroom__fact-value { opacity: 0.82; }
.showroom__media {
  border-radius: var(--radius);
  overflow: hidden;
  min-height: 320px;
  background-size: cover;
  background-position: center;
  background-color: var(--color-tan);
}
.showroom__media iframe { width: 100%; height: 100%; min-height: 320px; border: 0; display: block; }

/* ==========================================================================
   10. Gallery
   ========================================================================== */
.gallery { background: var(--color-tan); padding: 96px 0; }
.gallery__header {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 24px;
  flex-wrap: wrap;
  margin-bottom: 48px;
}
.gallery__header .section-intro { margin-bottom: 0; }
.gallery__intro { font-size: 16px; line-height: 1.6; opacity: 0.82; margin: 12px 0 0; }
/* Wraps .gallery__grid — a positioning context for .gallery__nav (the
   mobile-only prev/next arrows, style.css "15. Responsive") to sit over.
   No effect on desktop layout: on its own this is just a plain block
   around the grid. */
.gallery__carousel { position: relative; }
.gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
}
/* Prev/next carousel arrows — hidden by default (desktop shows the full
   static grid, nothing to page through); shown only under 900px, see
   "15. Responsive". Same visual pattern as .project-modal__nav. */
.gallery__nav {
  display: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(var(--color-navy-rgb), 0.55);
  color: var(--color-cream);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  align-items: center;
  justify-content: center;
}
.gallery__nav:hover { background: var(--color-navy); }
.gallery__nav--prev { left: 10px; }
.gallery__nav--next { right: 10px; }
.gallery__item {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius);
  overflow: hidden;
  position: relative;
  background-color: rgba(var(--color-navy-rgb), 0.06);
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.gallery__item img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.4s ease;
}
.gallery__item:hover img,
.gallery__item:focus-visible img {
  transform: scale(1.04);
}
/* Stacked photos crossfade in place (see main.js): only the .is-active
   one is visible at a time. Needs a selector at least as specific as
   ".gallery__item img" above so the opacity transition isn't dropped in
   favour of that rule's transform-only transition. */
.gallery__item img.gallery__item-slide {
  opacity: 0;
  transition: opacity 1s ease, transform 0.4s ease;
}
.gallery__item img.gallery__item-slide.is-active {
  opacity: 1;
}
.gallery__tag {
  position: absolute;
  left: 14px;
  bottom: 14px;
  display: inline-flex;
  align-items: center;
  height: 30px;
  box-sizing: border-box;
  background: var(--color-navy);
  color: var(--color-cream);
  font-size: 12px;
  font-weight: 600;
  padding: 0 12px;
  border-radius: var(--radius);
  z-index: 2;
}
/* A real, always-visible button (not a full-card darkening overlay that
   only appeared on :hover/:focus-visible, which was also invisible by
   default on touch devices with no hover). Sits bottom-right so it
   doesn't collide with .gallery__tag, which stays bottom-left. It's a
   <span>, not a nested <button>, because the whole card (.gallery__item)
   already is one and buttons can't nest — styled to read as a button
   regardless. */
.gallery__item-cta {
  position: absolute;
  right: 14px;
  bottom: 14px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  height: 30px;
  box-sizing: border-box;
  color: var(--color-navy);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  background: var(--color-cream);
  padding: 0 16px;
  border-radius: var(--radius);
  transition: background-color 0.2s ease, color 0.2s ease;
}
.gallery__item:hover .gallery__item-cta,
.gallery__item:focus-visible .gallery__item-cta {
  background: var(--accent);
  color: #fff;
}
/* Detailed card variant (Gallery page only): title + short excerpt sit
   permanently over a bottom gradient scrim, instead of only appearing
   after opening the project modal — the tag badge moves up top so it
   doesn't collide with them. */
.gallery__item--detailed .gallery__tag { bottom: auto; top: 14px; }
.gallery__item-info {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 2;
  padding: 28px 14px 14px;
  background: linear-gradient(to top, rgba(var(--color-navy-rgb), 0.9), rgba(var(--color-navy-rgb), 0));
  text-align: left;
}
.gallery__item-title { color: var(--color-cream); font-size: 16px; font-weight: 600; margin: 0 0 4px; }
.gallery__item-excerpt {
  color: var(--color-cream);
  opacity: 0.85;
  font-size: 13px;
  line-height: 1.5;
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* "View All Projects" — the homepage teaser's trailing card (only
   rendered when pltp_render_project_gallery() is called with
   $show_view_all_card = true, i.e. the homepage only, never /gallery/
   itself). Reuses .gallery__item's own sizing/aspect-ratio/radius so it
   sits in the grid exactly like a real project card and becomes the
   mobile carousel's last swipeable "slide" for free — no JS special-casing
   needed, it's just one more .gallery__item to the carousel script. */
.gallery__item--view-all {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: var(--color-navy);
  color: var(--color-cream);
  text-decoration: none;
  transition: background-color 0.2s ease;
}
.gallery__item--view-all:hover,
.gallery__item--view-all:focus-visible {
  background: var(--accent);
}
.gallery__item-view-all-text { font-size: 16px; font-weight: 600; letter-spacing: 0.02em; }
.gallery__item-view-all-arrow {
  font-size: 22px;
  line-height: 1;
  transition: transform 0.2s ease;
}
.gallery__item--view-all:hover .gallery__item-view-all-arrow,
.gallery__item--view-all:focus-visible .gallery__item-view-all-arrow {
  transform: translateX(4px);
}

/* ==========================================================================
   11a. Project Modal
   ========================================================================== */
.project-modal {
  border: none;
  border-radius: var(--radius);
  padding: 0;
  width: min(760px, calc(100vw - 32px));
  max-height: min(88vh, 900px);
  background: var(--color-cream);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
}
.project-modal::backdrop {
  background: rgba(var(--color-navy-rgb), 0.72);
}
.project-modal[open] {
  display: flex;
}
.project-modal__inner {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-height: inherit;
  overflow-y: auto;
}
.project-modal__close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 5;
  width: 36px;
  height: 36px;
  line-height: 1;
  border: none;
  border-radius: 50%;
  background: rgba(var(--color-navy-rgb), 0.55);
  color: var(--color-cream);
  font-size: 22px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.project-modal__close:hover {
  background: var(--color-navy);
}
.project-modal__media {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  background: rgba(var(--color-navy-rgb), 0.06);
  flex-shrink: 0;
}
.project-modal__slides {
  position: absolute;
  inset: 0;
}
.project-modal__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.project-modal__slide.is-active {
  opacity: 1;
  z-index: 1;
}
.project-modal__zoom {
  display: block;
  width: 100%;
  height: 100%;
  border: none;
  padding: 0;
  margin: 0;
  background: none;
  cursor: zoom-in;
}
.project-modal__slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.project-modal__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(var(--color-navy-rgb), 0.55);
  color: var(--color-cream);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.project-modal__nav:hover { background: var(--color-navy); }
.project-modal__nav--prev { left: 14px; }
.project-modal__nav--next { right: 14px; }
.project-modal__counter {
  position: absolute;
  right: 14px;
  bottom: 14px;
  z-index: 3;
  background: rgba(var(--color-navy-rgb), 0.55);
  color: var(--color-cream);
  font-size: 12px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: var(--radius);
}
/* Positioning context for the scroll arrows (.project-modal__thumbs-nav
   below) so they can sit over the strip's own left/right padding without
   taking up a grid/flex slot of their own. */
.project-modal__thumbs-wrap {
  position: relative;
  padding: 12px 32px 0;
}
/* Single row, never wraps — was a wrapping auto-fill grid; a photo set
   that didn't fit one row just grew a second (then third...) row of
   thumbnails, which is exactly what this was changed away from. Flex
   row + overflow-x is the same horizontal-carousel pattern used for
   .gallery__grid's mobile strip, including its two known gotchas: only
   declaring overflow-x forces overflow-y to compute as 'auto' too (CSS
   2.1 §11.1.1), so overflow-y:hidden is required explicitly rather than
   left implicit, and touch-action:pan-x keeps a vertical swipe over the
   strip from being captured here instead of bubbling to the page/modal
   scroll — see style.css "15. Responsive" .gallery__grid for the fuller
   writeup of both. Native scrollbar hidden since main.js's arrows (and
   real touch/trackpad scrolling) are how this strip actually gets
   navigated, same reasoning as that same .gallery__grid rule. */
.project-modal__thumbs {
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
  overflow-x: auto;
  overflow-y: hidden;
  touch-action: pan-x;
  scroll-behavior: smooth;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.project-modal__thumbs::-webkit-scrollbar { display: none; }
.project-modal__thumb {
  flex: 0 0 64px;
  width: 64px;
  aspect-ratio: 1 / 1;
  border-radius: calc(var(--radius) / 2);
  overflow: hidden;
  border: 2px solid transparent;
  padding: 0;
  margin: 0;
  cursor: pointer;
  background: rgba(var(--color-navy-rgb), 0.06);
  opacity: 0.6;
  transition: opacity 0.2s ease, border-color 0.2s ease;
}
.project-modal__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.project-modal__thumb:hover {
  opacity: 0.85;
}
.project-modal__thumb.is-active {
  opacity: 1;
  border-color: var(--color-teal, var(--accent));
}
/* Hidden by default — main.js only adds .is-visible to whichever end
   still has more thumbnails to scroll to (never shown at all if the
   whole set already fits on one row without scrolling). Small enough to
   sit inside .project-modal__thumbs-wrap's own 32px side padding without
   overlapping the thumbnails themselves. */
.project-modal__thumbs-nav {
  display: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 26px;
  height: 26px;
  border: none;
  border-radius: 50%;
  background: rgba(var(--color-navy-rgb), 0.55);
  color: var(--color-cream);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  align-items: center;
  justify-content: center;
}
.project-modal__thumbs-nav.is-visible { display: flex; }
.project-modal__thumbs-nav:hover { background: var(--color-navy); }
.project-modal__thumbs-nav--prev { left: 3px; }
.project-modal__thumbs-nav--next { right: 3px; }
.project-modal__body {
  padding: 20px 32px 32px;
}
/* Row holding the labelled Close button plus, when there's more than one
   project to cycle through, Previous/Next *project* buttons flanking it
   on either side (inc/template-tags.php only renders those two when
   count($gallery_posts) > 1 — see $g_count there). */
.project-modal__actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 20px;
}
/* Labelled close button, in addition to the top-corner "×" icon
   (.project-modal__close) — same data-project-modal-close hook, so one
   click handler in main.js covers both (see querySelectorAll there).
   Sits at the end of the body content rather than floating, so it's
   always reachable without hunting for the small icon button, which is
   the whole point of adding it. */
.project-modal__close-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 28px;
  border: 1.5px solid var(--color-navy);
  border-radius: var(--radius);
  background: none;
  color: var(--color-navy);
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease;
}
.project-modal__close-btn:hover {
  background: var(--color-navy);
  color: var(--color-cream);
}
/* Prev/Next *project* buttons either side of Close — switches to a
   sibling project's own modal (main.js's [data-project-modal-goto]
   handler), not to be confused with .project-modal__nav above, which
   pages through photos within the same project. Same circular-icon
   visual language as that component, just sat inline in the body
   instead of floating over the media. */
.project-modal__project-nav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(var(--color-navy-rgb), 0.08);
  color: var(--color-navy);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.2s ease;
}
.project-modal__project-nav:hover {
  background: rgba(var(--color-navy-rgb), 0.16);
}

/* ==========================================================================
   11b. Project Photo Lightbox
   ========================================================================== */
.project-lightbox {
  border: none;
  padding: 0;
  background: transparent;
  max-width: 96vw;
  max-height: 96vh;
}
.project-lightbox::backdrop {
  background: rgba(0, 0, 0, 0.88);
}
.project-lightbox[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}
.project-lightbox__image {
  display: block;
  max-width: 96vw;
  max-height: 92vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius);
}
.project-lightbox__close {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 10;
  width: 44px;
  height: 44px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.project-lightbox__close:hover {
  background: rgba(255, 255, 255, 0.3);
}
.project-modal__heading-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 12px;
}
.project-modal__tag {
  display: inline-block;
  background: var(--color-teal, var(--accent));
  color: var(--color-cream);
  font-size: 12px;
  font-weight: 600;
  padding: 5px 12px;
  border-radius: var(--radius);
  flex-shrink: 0;
}
.project-modal__title {
  margin: 0;
}
.project-modal__description {
  margin: 0;
  line-height: 1.6;
  color: rgba(var(--color-navy-rgb), 0.85);
}
@media (max-width: 640px) {
  .project-modal__body { padding: 22px 20px 26px; }
}

/* ==========================================================================
   11. Testimonials
   ========================================================================== */
.testimonials { padding: 96px 0; }
.testimonials__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 32px;
}
.testimonial { border-top: 2px solid var(--accent); padding-top: 24px; transition: transform 0.3s ease; }
.testimonial:hover { transform: translateY(-3px); }
@media (prefers-reduced-motion: reduce) {
  .testimonial:hover { transform: none; }
}
.testimonial__quote { font-family: var(--font-serif); font-size: 19px; line-height: 1.55; font-weight: 400; margin: 0 0 20px; color: var(--color-navy); }
.testimonial__name { font-size: 14px; font-weight: 600; }
.testimonial__project { font-size: 13px; opacity: 0.65; }

/* ==========================================================================
   11c. Flexible content block (Renovations page — Intro / 3D Plans /
   Choosing Products sections). Two independent things, each switchable
   per-section in wp-admin:
   1. An optional column image next to the text (.content-block--has-media,
      staggered left/right via .content-block--media-right).
   2. The section's own background — solid colour (.content-block--cream/
      --tan/--navy) or a full-bleed image behind everything
      (.content-block--bg-image + .content-block__bg-image/__bg-overlay),
      independent of whether there's also a column image.
   Deliberately tighter than the homepage's full-viewport .intro sections
   — a plain 72px padding rather than min-height:100vh — since the same
   treatment repeated several times down one page reads as excess empty
   space.
   ========================================================================== */
.content-block { position: relative; padding: 72px 0; overflow: hidden; }
.content-block--cream { background: var(--color-cream); }
.content-block--tan { background: var(--color-tan); }
.content-block--navy { background: var(--color-navy); color: var(--color-cream); }
.content-block--bg-image { background: var(--color-tan); }

/* Background image: fills the section, with a light wash so the eyebrow/
   heading/body (and column image, if also present) stay legible over any
   photo, without needing to flip to light text. */
.content-block__bg-image { position: absolute; inset: 0; z-index: 0; }
.content-block__bg-image img { width: 100%; height: 100%; object-fit: cover; }
.content-block__bg-overlay { position: absolute; inset: 0; z-index: 0; background: rgba(250, 247, 242, 0.85); }
.content-block .container { position: relative; z-index: 1; width: 100%; }

/* Column image: image in one column, text in the other (like the
   homepage's "Who We Are" editorial block). */
.content-block--has-media .container {
  display: flex;
  gap: 64px;
  align-items: center;
  flex-wrap: wrap;
}
.content-block--has-media.content-block--media-right .container { flex-direction: row-reverse; }
.content-block--has-media .content-block__inner { flex: 1 1 420px; min-width: 320px; }

/* No column image: a single centred editorial column. */
.content-block:not(.content-block--has-media) .container { display: flex; justify-content: center; }
.content-block:not(.content-block--has-media) .content-block__inner { max-width: 640px; }

.content-block__media {
  flex: 1 1 420px;
  min-width: 320px;
  position: relative;
  aspect-ratio: 5 / 4;
  border-radius: var(--radius);
  overflow: hidden;
  background-color: var(--color-tan);
}
.content-block__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.content-block__body { font-size: 16px; line-height: 1.75; opacity: 0.85; margin: 20px 0 0; }

/* ==========================================================================
   11d. Renovation "How It Works" steps — same two-independent-options
   pattern as .content-block above (column image + separate background),
   applied to the section's top heading area; the step grid below is
   unaffected either way.
   ========================================================================== */
.reno-steps { position: relative; padding: 72px 0; overflow: hidden; }
.reno-steps--cream { background: var(--color-cream); }
.reno-steps--tan { background: var(--color-tan); }
.reno-steps--navy { background: var(--color-navy); color: var(--color-cream); }
.reno-steps--bg-image { background: var(--color-tan); }
.reno-steps .container { position: relative; z-index: 1; }
.reno-steps__top {
  display: flex;
  gap: 64px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 56px;
}
.reno-steps--media-right .reno-steps__top { flex-direction: row-reverse; }
.reno-steps__top .content-block__inner { flex: 1 1 420px; min-width: 320px; }
.reno-steps__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 28px;
}
@media (max-width: 900px) {
  .reno-steps__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .reno-steps__grid { grid-template-columns: 1fr; }
}
.reno-step { background: var(--color-cream); border-radius: var(--radius); padding: 28px 24px; }
.reno-step__number { font-family: var(--font-serif); font-size: 28px; color: var(--accent); margin-bottom: 12px; }
.reno-step__heading { font-size: 18px; font-weight: 600; margin: 0 0 8px; color: var(--color-navy); }
.reno-step__body { font-size: 14px; line-height: 1.6; opacity: 0.78; margin: 0; color: var(--color-navy); }

/* ==========================================================================
   12. Guide banner
   ========================================================================== */
.guide-banner { background: var(--color-navy); color: var(--color-cream); }
.guide-banner .container {
  padding-top: 72px;
  padding-bottom: 72px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 32px;
  flex-wrap: wrap;
}
.guide-banner__content { max-width: 560px; }
.guide-banner__heading { font-size: 30px; line-height: 1.25; margin: 0 0 12px; }
.guide-banner__body { font-size: 16px; opacity: 0.8; margin: 0; max-width: 460px; }

/* ==========================================================================
   12a. HandyPay banner (Renovations page) — logo shown at its natural
   aspect ratio via object-fit:contain, not cropped like a photo, since
   it's a wordmark rather than a decorative image.
   ========================================================================== */
.handypay-banner { padding: 72px 0; background: var(--color-cream); }
.handypay-banner .container {
  display: flex;
  align-items: center;
  gap: 48px;
  flex-wrap: wrap;
}
.handypay-banner__logo { flex: 0 1 260px; min-width: 180px; }
.handypay-banner__logo img { width: 100%; height: auto; display: block; object-fit: contain; }
.handypay-banner__content { flex: 1 1 420px; min-width: 320px; }
.handypay-banner__content .btn { margin-top: 24px; }

/* ==========================================================================
   12b. Renovation Guide Form Modal
   ========================================================================== */
.guide-form-modal {
  border: none;
  border-radius: var(--radius);
  padding: 0;
  width: min(600px, calc(100vw - 32px));
  max-height: min(88vh, 800px);
  background: var(--color-cream);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
}
.guide-form-modal::backdrop {
  background: rgba(var(--color-navy-rgb), 0.72);
}
.guide-form-modal[open] {
  display: flex;
}
.guide-form-modal__inner {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-height: inherit;
  overflow-y: auto;
  padding: 32px;
}
.guide-form-modal__close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 5;
  width: 36px;
  height: 36px;
  line-height: 1;
  border: none;
  border-radius: 50%;
  background: rgba(var(--color-navy-rgb), 0.1);
  color: var(--color-navy);
  font-size: 22px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.guide-form-modal__close:hover {
  background: rgba(var(--color-navy-rgb), 0.2);
}
.guide-form-modal__title {
  margin: 0 0 20px;
  padding-right: 36px;
}
.guide-form-modal__form {
  font-size: 16px;
}

/* ==========================================================================
   13. Footer
   ========================================================================== */
.site-footer { background: var(--color-tan); border-top: 1px solid rgba(24, 50, 63, 0.13); }
.site-footer__back-to-top {
  max-width: var(--container);
  margin: 0 auto;
  padding: 20px 32px 0;
  display: flex;
  justify-content: center;
}
.back-to-top {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border-radius: 999px;
  border: 1px solid rgba(24, 50, 63, 0.15);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-navy);
  opacity: 0.7;
  transition: opacity 0.2s ease, transform 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.back-to-top:hover, .back-to-top:focus-visible {
  opacity: 1;
  color: var(--accent);
  border-color: var(--accent);
  transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
  .back-to-top { transition: none; }
  .back-to-top:hover, .back-to-top:focus-visible { transform: none; }
}
.site-footer__top {
  max-width: var(--container);
  margin: 0 auto;
  padding: 64px 32px 40px;
  display: flex;
  justify-content: space-between;
  gap: 48px;
  flex-wrap: wrap;
}
.site-footer__col { flex: 1 1 200px; }
.site-footer__col--brand { flex: 1 1 260px; min-width: 240px; }
.site-footer__logo img, .site-footer__logo svg { height: 36px; width: auto; margin-bottom: 14px; }
.site-footer__desc { font-size: 14px; opacity: 0.75; line-height: 1.65; max-width: 280px; }
.site-footer__label { font-size: 13px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 16px; opacity: 0.7; }
.site-footer__links { display: flex; flex-direction: column; gap: 10px; font-size: 14px; }
/* wp_nav_menu() always wraps each item in <li>, even with an empty
   items_wrap (no <ul>) — reset it so a real assigned menu still reads
   as plain stacked links, matching the theme's own no-menu-assigned
   fallback markup (plain <a> tags, no <li>). */
.site-footer__links li { list-style: none; margin: 0; padding: 0; }
.site-footer__address { font-size: 14px; opacity: 0.82; line-height: 1.8; }
.site-footer__social { display: flex; gap: 14px; margin-top: 16px; }
.site-footer__social a { display: inline-flex; align-items: center; justify-content: center; }
.site-footer__social svg { display: block; }
.site-footer__bottom {
  max-width: var(--container);
  margin: 0 auto;
  padding: 20px 32px;
  border-top: 1px solid rgba(24, 50, 63, 0.1);
  font-size: 13px;
  opacity: 0.6;
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px 24px;
}
.site-footer__credit a { color: inherit; text-decoration: underline; text-decoration-color: rgba(24, 50, 63, 0.35); }
.site-footer__credit a:hover { text-decoration-color: currentColor; }

/* ==========================================================================
   14. Generic page template (non-homepage content)
   ========================================================================== */
.page-hero { background: var(--color-navy); color: var(--color-cream); padding: 72px 0; }
.page-hero h1 { color: var(--color-cream); font-size: clamp(28px, 4vw, 40px); }
.page-content { padding: 72px 0; max-width: 800px; }
.page-content p { font-size: 16px; line-height: 1.75; opacity: 0.85; }

/* ==========================================================================
   15. Responsive — mobile mode kicks in at 900px, not before. This is
   the single shared threshold for everything in this block (nav
   collapse, topbar, hero, intro stacking, suppliers carousel, gallery
   carousel, full-screen modal, announcement bar) — was 782px, moved to
   900px at Ben's request. Kept as one value rather than splitting the
   nav specifically from the rest: a gap between two different
   thresholds would mean e.g. the hamburger nav showing while the
   topbar/socials are still visible too, which looks broken rather than
   like a deliberate design.
   ========================================================================== */
@media (max-width: 900px) {
  .announcement-bar { display: none; }
  .main-nav {
    display: none;
    /* Higher than the (default 0) logo/header-actions so the expanded
       menu always wraps onto its own row below them, regardless of its
       place in the DOM — without this it renders between the logo and
       the Visit Showroom button/toggle, which is what it did before. */
    order: 3;
  }
  .nav-toggle { display: block; }
  .site-header.is-open .main-nav {
    display: block;
    width: 100%;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid rgba(var(--color-navy-rgb), 0.12);
  }
  .site-header.is-open .main-nav ul {
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 0;
  }
  .site-header.is-open .main-nav li { border-bottom: 1px solid rgba(var(--color-navy-rgb), 0.08); }
  .site-header.is-open .main-nav li:last-child { border-bottom: none; }
  /* :not(.main-nav__call) — this menu-link sizing would otherwise also
     hit the Call Now button (it's technically a .main-nav a too, just
     nested inside .main-nav__social), stomping its plain .btn.btn-primary
     padding/font-size down to these much smaller link values. */
  .site-header.is-open .main-nav a:not(.main-nav__call) {
    display: block;
    padding: 13px 2px;
    font-size: 16px;
  }
  .header-actions { gap: 10px; }
  .topbar { display: none; }
  .hero__actions--mobile-only { display: flex; }
  .hero { height: auto; min-height: 0; padding-top: 64px; }
  .hero__panel { padding: 32px 24px; max-width: none; }
  .intro .container, .intro.intro--image-right .container { flex-direction: column; }
  /* .intro__media/.intro__content's desktop rule is `flex: 1 1 420px;
     min-width: 320px;` — flex-direction:column here stacks them, but
     align-items:center (not stretch) means that 320px min-width floor
     still applies to each stacked child's own width. Combined with
     .container's 64px of horizontal padding (32px each side), any
     viewport narrower than ~384px was forced to overflow horizontally —
     the "columns stretch off the edge" bug. min-width:0 removes the
     floor; width:100% makes the intent (full column width) explicit
     regardless of align-items. */
  .intro__media, .intro__content {
    flex: 1 1 auto;
    min-width: 0;
    width: 100%;
  }
  /* Text first, media beneath — DOM order is media-then-content (matters
     for desktop's left/right .intro--image-right variant), but on
     mobile everything just stacks top-to-bottom via flex-direction:
     column, so `order` alone re-sequences the visual stack without
     touching markup or desktop layout at all. */
  .intro__content { order: -1; }
  .showroom .container { flex-direction: column; }
  .guide-banner .container { text-align: left; }

  /* Suppliers carousel — no-JS/pre-hydration fallback: a plain 3-column
     grid of every logo (no horizontal overflow, no pagination). The JS
     below (assets/js/main.js) progressively enhances this into paginated
     crossfading pages; if JS never runs this is what stays, and it's
     already correct on its own. */
  .suppliers__row {
    display: grid;
    /* minmax(0, 1fr), not bare 1fr — a bare `fr` track has an implicit
       `auto` minimum, so it won't shrink below its content's natural
       size (these logo images can be wider than an equal 3-way split),
       breaking equal column distribution. minmax(0, 1fr) removes that
       floor so columns actually divide evenly; the images' own
       max-width/object-fit:contain (see ".suppliers__logo img") then
       scale down to fit whatever width that leaves them. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: center;
    /* justify-items: stretch (default) not center — see the matching
       comment on the desktop .suppliers__row rule above for why. */
    gap: 20px 16px;
  }
  .suppliers__row.is-paginated { display: block; }
  /* width:100% explicit — same "Flex shrink-to-fit collapse" class of bug
     documented elsewhere in this file (.intro .container,
     .project-modal__inner): a block box whose only children are all
     position:absolute (removed from normal flow) doesn't reliably
     auto-fill its parent's width in every engine — confirmed collapsing
     to 0px width without this. */
  .suppliers__pages { position: relative; width: 100%; min-height: 184px; touch-action: pan-y; }
  .suppliers__page {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr)); /* minmax(0,...) — see .suppliers__row above */
    grid-template-rows: repeat(3, 48px);
    align-items: center;
    /* justify-items: stretch (default) not center — see the matching
       comment on the desktop .suppliers__row rule above for why (this is
       the exact grid a real device report showed two logos overlapping
       in). */
    row-gap: 20px;
    column-gap: 16px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.6s ease;
  }
  .suppliers__page.is-active { opacity: 1; pointer-events: auto; }
  @media (prefers-reduced-motion: reduce) {
    .suppliers__page { transition: none; }
  }

  /* Gallery mobile carousel — .gallery__grid becomes a native horizontal
     scroll-snap strip, one full-width project card per "slide". Real
     touch scrolling handles the swipe for free; assets/js/main.js only
     adds auto-advance on top (see "Gallery mobile carousel" IIFE). */
  .gallery__grid {
    display: flex;
    overflow-x: auto;
    /* CSS forces overflow-y to compute as 'auto' too whenever
       overflow-x isn't 'visible' (you can't mix 'visible' with a
       non-visible axis — CSS2.1 §11.1.1) — confirmed via
       getComputedStyle even though only overflow-x is set here.
       That makes this a *vertically* scrollable box as a side effect,
       any time its content is even briefly taller than its box (the
       .reveal entrance transition mid-flight, a late-loading image
       nudging layout, sub-pixel rounding) — and on a touchscreen a
       vertical swipe can get grabbed by that instead of bubbling to
       the page. Explicit overflow-y:hidden closes that off outright,
       and touch-action:pan-x (same pattern as .suppliers__pages'
       pan-y below) tells the browser this element only ever handles
       horizontal pan gestures, so vertical swipes are never captured
       here to begin with — belt and suspenders. */
    overflow-y: hidden;
    touch-action: pan-x;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scroll-behavior: smooth;
    gap: 0;
    /* Swiping is already handled by real touch input + the JS
       auto-advance (see main.js), so the native scrollbar is just
       visual noise here — worse, on browsers that reserve layout
       space for it (rather than overlay it), the bar's own height
       shrinks .gallery__grid's clientHeight below its content's
       actual height, which then triggers a *second*, vertical
       scrollbar on top of the intended horizontal one. Hiding it
       removes both. */
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .gallery__grid::-webkit-scrollbar { display: none; }
  .gallery__item {
    flex: 0 0 100%;
    max-width: 100%;
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }
  /* .gallery__item-cta is always-visible now (style.css "10. Gallery"),
     no mobile-specific override needed for it any more. */
  .gallery__nav { display: flex; }

  /* Full-screen project modal — .project-modal is a centered card at
     every other width (width: min(760px, calc(100vw - 32px))); here it
     becomes the whole screen instead. */
  .project-modal {
    width: 100vw;
    max-width: 100vw;
    height: 100vh;
    max-height: 100vh;
    border-radius: 0;
    box-shadow: none;
  }
  .project-modal__inner {
    /* width:100% already existed (the documented flex shrink-to-fit
       collapse fix) — height:100% is the same fix applied to the
       vertical axis now that the dialog itself is a fixed 100vh box
       rather than a max-height:min(88vh,900px) shrink-to-content one. */
    width: 100%;
    height: 100%;
    max-height: 100vh;
  }
  .project-modal__media {
    /* Was a fixed aspect-ratio:4/3, flex-shrink:0 — on a tall phone that
       left an awkward fixed-height photo strip with empty space below.
       flex-grow lets it fill the available height instead; min-height
       keeps it the visual focus even with a long description below. */
    aspect-ratio: auto;
    flex: 1 1 auto;
    min-height: 45vh;
  }
  /* Smaller thumbnails, no gap between them — the desktop sizing (64px
     flex-basis, 8px gap, 32px side padding) eats a lot of the limited
     vertical space on a full-screen mobile modal that's already
     competing with the photo + description for room. Still the same
     single-row flex/overflow-x strip as desktop (style.css "11a.") —
     only the sizing changes here, not the layout mode. */
  .project-modal__thumbs-wrap { padding: 10px 16px 0; }
  .project-modal__thumbs { gap: 0; }
  .project-modal__thumb { flex-basis: 44px; width: 44px; border-radius: 0; }
  .project-modal__thumbs-nav--prev { left: 1px; }
  .project-modal__thumbs-nav--next { right: 1px; }

  /* Mobile nav social icons (Facebook/Instagram) — hidden by default,
     only shown once the mobile menu is actually open. Placed inside
     .main-nav in header.php so it inherits that element's existing
     show/hide plumbing; only its own internal layout needs styling
     here, matching .topbar__social's visual convention. Call Now
     (.main-nav__call, same row — header.php) is pushed to the far
     right via margin-left:auto so the icons stay left-aligned exactly
     as before. */
  .site-header.is-open .main-nav__social {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 16px;
    padding: 16px 2px 4px;
    margin-top: 4px;
    border-top: 1px solid rgba(var(--color-navy-rgb), 0.08);
  }
  /* flex-wrap:wrap above (not nowrap) — this row now has to fit up to 7
     social icons plus the status badge plus Call Now, and with several
     networks filled in that's genuinely more content than a phone-width
     row can hold on one line. Confirmed real horizontal overflow without
     this (Call Now pushed ~170px past the viewport edge, fully
     inaccessible) once more than 2-3 icons are set. When everything
     *does* fit on one line (the common case — most clients only fill in
     a couple of networks), this changes nothing: the row still behaves
     exactly as a plain nowrap flex row would, including the
     margin-left:auto centering trick on .main-nav__status/.main-nav__call
     below. It only kicks in once there's genuinely too much content for
     one line, wrapping the overflow naturally onto a second line instead
     of running it off-screen. */
  /* :not(.main-nav__call) — the dimmed/opacity icon treatment below is
     meant for the social icons only; the Call Now button needs its full,
     undimmed .btn-primary styling to still read as a real CTA. */
  .main-nav__social a:not(.main-nav__call) { display: inline-flex; color: var(--color-navy); opacity: 0.7; }
  .main-nav__social a:not(.main-nav__call):hover, .main-nav__social a:not(.main-nav__call):focus-visible { opacity: 1; }
  .main-nav__social svg { width: 20px; height: 20px; }
  /* Deliberately smaller than the plain .btn.btn-primary base (Ben's
     request) — this sits inline in a tight menu row next to two small
     social icons, not as a standalone hero/topbar CTA, so the full
     15px/28px sizing read as oversized here. Two-line layout (label +
     phone number, header.php) stacked and centred instead of the base
     rule's single-line inline-block. */
  .main-nav__call {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    flex-shrink: 0;
    margin-left: auto;
    padding: 8px 16px;
  }
  /* white-space:nowrap on both lines — without it, a squeezed flex row
     (icons + status badge + this button all fighting for the same ~300px)
     wraps "Call Now" itself mid-word onto three lines instead of just
     shrinking something else, which is exactly what happened before the
     status badge got a shorter mobile-only label (see
     $header_status_labels_compact, above) to free up room in the first
     place. Belt and suspenders: even a longer phone number now stays on
     one line rather than wrapping. */
  /* line-height:1 on both lines (Ben's request) — the browser's default
     ~1.4-1.5 "normal" line-height adds noticeable extra height above and
     below the text itself on top of the .main-nav__call gap:2px already
     separating the two lines, making this two-line button taller than it
     needs to be in an already-tight mobile menu row. */
  .main-nav__call-label { font-size: 13px; font-weight: 600; line-height: 1; white-space: nowrap; }
  /* Lighter weight + reduced opacity distinguishes it as secondary
     information under the label, same "sub-text" convention as
     .hero__subheading etc. elsewhere on the site. */
  .main-nav__call-phone { font-size: 11px; font-weight: 500; line-height: 1; opacity: 0.85; white-space: nowrap; }
  /* Store-status badge, reusing the exact same .store-status component
     as the desktop topbar (header.php) — sits between the social icons
     and Call Now. Both this and .main-nav__call above carry
     margin-left:auto, which in a flex row splits the row's remaining
     free space evenly *before* each auto-margined item — i.e. the gap
     after the icons gets divided into two equal halves, one before this
     badge and one before Call Now, landing the badge roughly halfway
     between them without any manual width math. */
  .main-nav__status { margin-left: auto; }
  /* The popup's default left:0/min-width:300px (style.css "4. Header")
     assumes plenty of room to its right, true in the topbar but not
     here — this badge sits mid-row on a ~350–390px-wide phone screen,
     so a 300px-wide popup anchored at its left edge would run off the
     right side of the viewport. Centring it under the badge instead,
     with a viewport-relative max-width as a hard backstop, keeps it
     fully on-screen at any phone width. */
  .main-nav__status .store-status__popup {
    left: 50%;
    /* translateX(-50%) for centring, combined with the base rule's own
       translateY(-4px) closed-state offset (style.css "4. Header") —
       overwriting transform outright here would silently drop that
       entrance animation instead of just adding the horizontal centring
       to it. */
    transform: translateX(-50%) translateY(-4px);
  }
  .main-nav__status:hover .store-status__popup,
  .main-nav__status:focus-within .store-status__popup {
    transform: translateX(-50%) translateY(0);
  }
  .main-nav__status .store-status__popup-card {
    min-width: 0;
    width: max-content;
    max-width: calc(100vw - 40px);
  }
  /* Two classes (.main-nav__social .main-nav__call) needed to outrank
     the unscoped, always-on `.main-nav a { color: var(--color-navy) }`
     base rule (style.css "4. Header") — that selector's specificity
     (class+type) actually beats a single `.main-nav__call` class alone,
     which is why the button's text was rendering navy instead of the
     white every other .btn-primary gets, until this was added. */
  .main-nav__social .main-nav__call,
  .main-nav__social .main-nav__call:hover,
  .main-nav__social .main-nav__call:focus-visible {
    color: #fff;
  }
}

/* Suppliers carousel — three tiers: 2 columns (8/page) below 360px, 3
   columns (9/page, the default set inside the main 900px block above)
   from 360–479px, 4 columns (12/page) from 480px up to the main 900px
   mobile breakpoint, then the plain desktop 8-column grid takes over
   unchanged. Fewer/wider columns at the narrowest widths is deliberate,
   not just "more pagination" — narrow columns paired with a wide logo
   image are exactly the overlap bug fixed once already (see Known
   gotchas in CLAUDE.md); staying at 3 columns all the way down to 0
   would risk the same thing recurring on the smallest real phones. */
@media (min-width: 480px) and (max-width: 900px) {
  .suppliers__row:not(.is-paginated) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .suppliers__page { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 359px) {
  .suppliers__row:not(.is-paginated) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .suppliers__page {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(4, 48px);
  }
  /* Recomputed for 4 rows instead of 3: 4×48px + 3×20px row-gap = 252px
     (was 184px for 3 rows) — .suppliers__pages must be tall enough to
     contain the tallest possible page or the absolute-positioned
     crossfade pages would visually clip/overlap the section below. */
  .suppliers__pages { min-height: 252px; }
}

/* Hidden outside the mobile open state at every width — desktop already
   shows socials in .topbar, this is mobile-menu-only. */
.main-nav__social { display: none; }
