/* =========================================================================
   PETE'S SEATS — SHARED STYLESHEET
   One stylesheet for the whole site. Every page links this file.
   Organized in sections:
     1. Custom properties (brand tokens)
     2. Reset / base
     3. Typography
     4. Layout helpers
     5. Buttons
     6. Header / navigation
     7. Photo "bands" (hero, page-title, dark content, CTA)
     8. Cards / grids / two-column content
     9. Gallery
    9b. Lightbox (JS-created gallery viewer)
    10. Contact form
    11. Sitemap page
    12. Footer
    13. 404 page
    14. Utilities / accessibility
    15. Motion (page transitions, scroll reveals, header lift, carousel)
    16. Mobile refinements (all narrow-viewport overrides, in one place)

   MOTION POLICY (see section 15 for the detail):
   - Every transition and keyframe animation added for the interaction pass
     lives inside `@media (prefers-reduced-motion: no-preference)`, and
     section 15e is a final catch-all that flattens anything missed.
   - Only `opacity` and `transform` are animated. Nothing that triggers
     layout is ever transitioned.
   - Scroll-reveal hidden states are additionally scoped to `.js-motion`, a
     class that only js/main.js can add, so a page with a broken or blocked
     script renders fully visible.
   ========================================================================= */

/* -------------------------------------------------------------------------
   1. CUSTOM PROPERTIES
   ---------------------------------------------------------------------- */
:root {
  /* Brand colour tokens (preserve exactly — see BRIEF) */
  --color-bg: #f7fafc;
  --color-heading: #1a202c;
  --color-body: #2d3748;
  --color-body-muted: #4a5568;
  --color-accent: #fe9001;
  --color-accent-dark: #d97b00;
  --color-white: #ffffff;
  --color-border: #e2e8f0;
  --color-footer-bg: #1e1e1e;

  /* Fonts */
  --font-heading: "Cormorant Garamond", Georgia, serif;
  --font-body: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* Type scale (desktop) — from brand tokens */
  --size-hero-1: clamp(2.75rem, 7vw, 5.625rem);   /* 90px */
  --size-hero-2: clamp(2.25rem, 5vw, 4.0625rem);  /* 65px */
  --size-h2: clamp(2rem, 4vw, 4rem);              /* 64px */
  --size-h3: clamp(1.6rem, 3vw, 2.8125rem);       /* 45px */
  --size-eyebrow: clamp(1.1rem, 2vw, 2rem);       /* 32px */

  /* Layout */
  --content-width: 1290px;
  --content-narrow: 842px;
  --edge-padding: 1.5rem;
  --section-space: clamp(3rem, 8vw, 5rem);

  /* Effects */
  --transition-fast: 150ms ease;
  --transition-base: 250ms ease;
  --focus-ring: 3px solid var(--color-accent);

  /* Motion tokens (section 15). One easing curve for the whole site so the
     reveals, the button sweeps and the page transition all feel related. */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --reveal-distance: 24px;
  --reveal-duration: 700ms;
}

/* -------------------------------------------------------------------------
   2. RESET / BASE
   ---------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-body);
  font-family: var(--font-body);
  font-size: 18px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

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

a {
  color: inherit;
  text-decoration: none;
}

ul,
ol {
  padding: 0;
  margin: 0;
}

button {
  font-family: inherit;
  cursor: pointer;
}

input,
textarea,
select {
  font-family: inherit;
  font-size: 1rem;
}

/* -------------------------------------------------------------------------
   3. TYPOGRAPHY
   ---------------------------------------------------------------------- */
/* WRAPPING CONTRACT FOR DISPLAY HEADINGS
   The live site's phone rendering broke the page title mid-word
   ("RESTAURAN / T & COMMERCI / AL SEATING"). Three things stop that from ever
   happening here:
     1. `hyphens`, `overflow-wrap` and `word-break` are pinned to their
        non-breaking values, so no browser default or inherited rule can start
        splitting a word. A heading only ever wraps at a space.
     2. The mobile type scale in section 16 is sized from the widest word in
        the site's headings ("COMMERCIAL"), measured in both Cormorant Garamond
        and the Georgia fallback, so no word can be wider than its column at
        320px. Rule 1 turns an oversized word into overflow rather than a
        mid-word break, so rule 2 is what actually keeps the page clean.
     3. `text-wrap: balance` (progressive enhancement) evens out the resulting
        short lines instead of leaving one orphaned word. */
h1,
h2,
h3,
h4 {
  font-family: var(--font-heading);
  font-weight: 700;
  line-height: 1.15;
  color: var(--color-heading);
  margin: 0 0 0.5em;
  hyphens: none;
  -webkit-hyphens: none;
  overflow-wrap: normal;
  word-break: normal;
  text-wrap: balance;
}

h1 {
  font-size: var(--size-h2);
}

h2 {
  font-size: var(--size-h2);
}

h3 {
  font-size: var(--size-h3);
}

h4 {
  font-size: 1.5rem;
}

/* Size-only override. The privacy policy's section headings are top-level
   sections of that page, so they have to be <h2> for the document outline to
   run h1 -> h2 with no skipped level; this keeps them at the h3 size they are
   drawn at, without touching the semantics. */
.h3 {
  font-size: var(--size-h3);
}

/* Body copy is allowed to break a word that is wider than its column, because the
   privacy policy quotes bare URLs, and a 320px column cannot hold them. This
   is deliberately scoped away from headings, which must never break. */
p,
li,
dd,
figcaption,
label {
  overflow-wrap: break-word;
}

p {
  margin: 0 0 1.25em;
}

p:last-child {
  margin-bottom: 0;
}

em {
  font-style: italic;
}

strong {
  font-weight: 700;
}

/* Small label used above headings ("eyebrow"). Not a heading element, which
   keeps the document's h1/h2/h3 order clean.
   FIDELITY: on the live site every one of these labels is an IBM Plex Sans
   div at 18px / weight 400 / text-transform:uppercase (see the Kadence block
   CSS for .kt-adv-heading17_20681c-e9 and siblings in reference/index.html).
   They are NOT the 32px serif callout described in the brief token list. */
.eyebrow {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 18px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.4;
  margin: 0 0 1.5rem;
  color: var(--color-heading);
}

.lede {
  font-size: 1.15rem;
  color: var(--color-body-muted);
  max-width: 46rem;
}

.entry-list {
  list-style: none;
  margin: 1.5rem 0;
  display: grid;
  gap: 0.75rem;
}

.entry-list li {
  position: relative;
  padding-left: 1.6rem;
}

.entry-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 8px;
  height: 8px;
  background: var(--color-accent);
}

/* -------------------------------------------------------------------------
   4. LAYOUT HELPERS
   ---------------------------------------------------------------------- */
.container {
  max-width: var(--content-width);
  margin-inline: auto;
  padding-inline: var(--edge-padding);
}

.container--narrow {
  max-width: var(--content-narrow);
}

main {
  display: block;
}

.section {
  padding-block: var(--section-space);
}

/* Content sections that directly follow a full-bleed photo band start flush
   against the photo's bottom edge (their spacing otherwise comes only from
   child text margins). Give them the same breathing room every other section
   transition on the page gets. */
.band + section {
  padding-top: var(--section-space);
}

.text-center {
  text-align: center;
}

.btn-row--center {
  justify-content: center;
}

/* Sits alone in the white strip between the carousel and the next photo
   band; equal margins keep it vertically centred in that strip. */
.btn-row--raised {
  margin-block: clamp(2.25rem, 5vw, 3.25rem);
}

/* 15% larger than the 15px .btn base, per review feedback. */
.btn-row--raised .btn {
  font-size: 17px;
}

/* Centred section header: heading block and its lede sit on the page axis. */
.section-header--center {
  text-align: center;
}

.section-header--center .lede {
  margin-inline: auto;
}

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* -------------------------------------------------------------------------
   5. BUTTONS
   Square corners, uppercase IBM Plex Sans, one accent colour — the brand
   tokens are untouched. What is new is how the states are built: every hover
   treatment is a pseudo-element that scales on the X axis (a directional
   sweep on the solid buttons, a growing rule under the text buttons), so the
   only animated properties are `transform` and `opacity`.

   The state rules themselves are unconditional; only the `transition`
   declarations sit inside a `prefers-reduced-motion: no-preference` block.
   A reduced-motion user therefore still gets every hover, focus and press
   state — they just switch instantly instead of easing.
   ---------------------------------------------------------------------- */
.btn {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 400;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  border-radius: 0;
  padding: 15px 30px;
  border: 2px solid transparent;
}

/* Focus ring: the accent everywhere, except on the solid orange button where
   an orange ring would disappear into the button. That one takes the dark
   heading colour on light backgrounds and white inside a dark photo band. */
.btn:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 3px;
}

.btn--primary:focus-visible {
  outline-color: var(--color-heading);
}

.band .btn--primary:focus-visible {
  outline-color: var(--color-white);
}

/* Primary: solid accent-orange button ("Start a Project").
   `::before` is the sweep — a deeper tone of the same orange that wipes in
   from the left edge. `z-index: -1` inside the button's own stacking context
   paints it over the button background but under the label. `inset: -2px`
   covers the transparent 2px border box as well, so no orange rim is left
   behind once the sweep has run. */
.btn--primary {
  background: var(--color-accent);
  color: var(--color-white);
}

.btn--primary::before,
.btn--outline::before {
  content: "";
  position: absolute;
  inset: -2px;
  z-index: -1;
  transform: scaleX(0);
  transform-origin: left center;
}

.btn--primary::before {
  background: var(--color-accent-dark);
}

.btn--primary:hover::before,
.btn--primary:focus-visible::before,
.btn--outline:hover::before,
.btn--outline:focus-visible::before {
  transform: scaleX(1);
}

/* Secondary: outlined, for use on light sections */
.btn--outline {
  background: transparent;
  border-color: var(--color-heading);
  color: var(--color-heading);
}

.btn--outline::before {
  background: var(--color-heading);
}

.btn--outline:hover,
.btn--outline:focus-visible {
  color: var(--color-white);
}

/* Text link with a trailing arrow ("View Our Work", "Learn More").
   `::after` is the underline: it grows from the left on hover and stops short
   of the arrow so the two do not collide. */
.btn--text {
  padding: 0.4em 0;
  text-transform: uppercase;
  font-size: 15px;
  letter-spacing: 0.03em;
  border: none;
  color: var(--color-heading);
}

.btn--text::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.15em;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
}

.btn--text:has(.btn__arrow)::after {
  right: 1.7em;
}

.btn--text:hover::after,
.btn--text:focus-visible::after {
  transform: scaleX(1);
}

/* Arrow nudge — applies to any button carrying an arrow, not just the text
   links, so a future arrow on a solid button behaves the same way. */
.btn__arrow {
  width: 1em;
  height: 1em;
}

.btn:hover .btn__arrow,
.btn:focus-visible .btn__arrow {
  transform: translateX(5px);
}

.btn--on-dark {
  color: var(--color-white);
}

.btn--on-dark .btn__arrow {
  color: var(--color-white);
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.5rem;
  margin-top: 1.5rem;
}

/* All button motion in one place. Nothing here changes which state applies —
   only how long it takes to get there. */
@media (prefers-reduced-motion: no-preference) {
  .btn {
    transition: background-color 200ms ease, color 200ms ease,
      border-color 200ms ease, transform 120ms ease-out;
  }

  .btn--primary::before,
  .btn--outline::before {
    transition: transform 420ms var(--ease-out-expo);
  }

  .btn--text::after {
    transition: transform 320ms var(--ease-out-expo);
  }

  .btn__arrow {
    transition: transform 260ms var(--ease-out-expo);
  }

  /* Press state: a small, quick give under the finger / cursor. */
  .btn:active {
    transform: translateY(1px) scale(0.98);
  }

  .btn:active .btn__arrow {
    transform: translateX(2px);
  }
}

/* -------------------------------------------------------------------------
   6. HEADER / NAVIGATION
   ---------------------------------------------------------------------- */
.site-header {
  background: var(--color-white);
  position: sticky;
  top: 0;
  z-index: 100;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
}

.site-header__inner {
  max-width: var(--content-width);
  margin-inline: auto;
  padding: 0.75rem var(--edge-padding);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 90px;
}

.site-logo img {
  height: 56px;
  width: auto;
}

/* Primary nav — desktop */
.primary-nav {
  display: flex;
}

.primary-nav__list {
  display: flex;
  align-items: center;
  list-style: none;
  gap: 0.25rem;
}

.primary-nav__list > li {
  position: relative;
}

.primary-nav__list > li > a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 0.75em 1em;
  font-size: 16px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--color-body-muted);
  transition: color var(--transition-fast);
}

/* Underline that slides in from the left on hover / focus, and stays put on
   the current page (where it is drawn in the accent colour instead). */
.primary-nav__list > li > a::after {
  content: "";
  position: absolute;
  left: 1em;
  right: 1em;
  bottom: 0.55em;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
}

@media (prefers-reduced-motion: no-preference) {
  .primary-nav__list > li > a::after {
    transition: transform 300ms var(--ease-out-expo);
  }
}

.primary-nav__list > li > a:hover::after,
.primary-nav__list > li > a:focus-visible::after {
  transform: scaleX(1);
}

.primary-nav__list > li > a:hover,
.primary-nav__list > li > a:focus-visible,
.primary-nav__list > li.is-current > a {
  color: var(--color-accent);
}

.primary-nav__list > li > a[aria-current="page"] {
  color: var(--color-heading);
}

.primary-nav__list > li > a[aria-current="page"]::after {
  background: var(--color-accent);
  transform: scaleX(1);
}

.nav-caret {
  width: 0.6em;
  height: 0.6em;
}

/* Services dropdown (desktop): CSS-only, opens on hover and on keyboard focus */
.has-dropdown {
  position: relative;
}

.dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 300px;
  background: var(--color-white);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.12);
  list-style: none;
  padding: 0.5rem 0;
  margin: 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(6px);
  transition: opacity var(--transition-fast), transform var(--transition-fast), visibility var(--transition-fast);
}

.has-dropdown:hover > .dropdown,
.has-dropdown:focus-within > .dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.dropdown a {
  display: block;
  padding: 0.85em 1.25em;
  font-size: 15px;
  color: var(--color-heading);
  white-space: nowrap;
}

.dropdown a:hover,
.dropdown a:focus-visible {
  background: var(--color-bg);
  color: var(--color-accent);
}

/* Mobile menu toggle button */
.menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: none;
  border: 1px solid var(--color-border);
  color: var(--color-body-muted);
}

.menu-toggle:focus-visible {
  outline: var(--focus-ring);
}

.menu-toggle svg {
  width: 22px;
  height: 22px;
}

.menu-toggle .icon-close {
  display: none;
}

.menu-toggle[aria-expanded="true"] .icon-open {
  display: none;
}

.menu-toggle[aria-expanded="true"] .icon-close {
  display: block;
}

@media (max-width: 900px) {
  .primary-nav {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(320px, 85vw);
    background: var(--color-heading);
    transform: translateX(100%);
    transition: transform var(--transition-base);
    z-index: 200;
    overflow-y: auto;
  }

  .primary-nav.is-open {
    transform: translateX(0);
  }

  .primary-nav__list {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    padding: 5.5rem 0 2rem;
    gap: 0;
  }

  .primary-nav__list > li > a {
    width: 100%;
    padding: 1em 1.5rem;
    color: rgba(255, 255, 255, 0.85);
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  }

  /* The drawer rows already have their own divider lines; the desktop
     underline would read as a second, competing rule. */
  .primary-nav__list > li > a::after {
    display: none;
  }

  .primary-nav__list > li > a:hover,
  .primary-nav__list > li > a:focus-visible {
    color: var(--color-accent);
  }

  /* On mobile the Services submenu is always visible (no second toggle),
     to keep the shared JS limited to a single open/close action. */
  .dropdown {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    min-width: 0;
    background: rgba(255, 255, 255, 0.05);
    box-shadow: none;
    padding: 0;
  }

  /* `white-space: nowrap` is right for the desktop dropdown, which sizes
     itself to its longest label. In the drawer the width is fixed, so
     "Restaurant & Commercial Seating" has to be allowed to wrap rather than
     run into the panel edge. */
  .dropdown a {
    color: rgba(255, 255, 255, 0.7);
    padding: 0.85em 1.5rem 0.85em 2.25rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    white-space: normal;
  }

  .dropdown a:hover,
  .dropdown a:focus-visible {
    background: rgba(255, 255, 255, 0.08);
    color: var(--color-accent);
  }

  .menu-toggle {
    display: flex;
  }

  .nav-scrim {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition-base), visibility var(--transition-base);
    z-index: 150;
  }

  .nav-scrim.is-open {
    opacity: 1;
    visibility: visible;
  }
}

@media (min-width: 901px) {
  .nav-scrim {
    display: none;
  }
}

/* -------------------------------------------------------------------------
   7. PHOTO "BANDS" — hero / page-title / dark content / closing CTA
   Each band sets its own background photo + overlay strength via inline
   custom properties: style="--band-image:url(...); --band-overlay:0.45"
   ---------------------------------------------------------------------- */
.band {
  position: relative;
  background-color: var(--color-heading);
  background-image: var(--band-image, none);
  background-size: cover;
  background-position: var(--band-position, center);
  background-repeat: no-repeat;
  color: var(--color-white);
  overflow: hidden;
}

.band::before {
  content: "";
  position: absolute;
  inset: 0;
  background: #000;
  opacity: var(--band-overlay, 0.45);
  z-index: 1;
}

/* `width: 100%` matters on the two bands that are flex containers (hero and
   page title): without it the inner is a shrink-to-fit flex item, so a short
   title like "About" makes the whole content column only as wide as the word.
   Nothing moves visually (the content inside is centred either way), but the
   column now has a predictable width to wrap against. */
.band__inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--content-width);
  margin-inline: auto;
  padding: var(--section-space) var(--edge-padding);
}

.band .eyebrow,
.band h1,
.band h2,
.band h3,
.band p {
  color: var(--color-white);
}

/* Hero — homepage only.
   FIDELITY: the live hero column is `text-align:center` with
   `align-items:center` (Kadence .kadence-column17_ba2c86-ad), so the eyebrow,
   both headline lines, the subtext and the button row are all centred. */
.band--hero {
  min-height: 92vh;
  display: flex;
  align-items: center;
  /* Slideshow photos vary in brightness (slide 3 is near-white); a flat 0.45
     scrim is not enough for legible white text on the bright frames. */
  --band-overlay: 0.52;
}

.band--hero::before {
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.75) 0%,
    rgba(0, 0, 0, 0.92) 45%,
    rgba(0, 0, 0, 0.92) 70%,
    rgba(0, 0, 0, 0.8) 100%
  );
}

.band--hero .band__inner {
  padding-top: clamp(6rem, 14vw, 9rem);
  padding-bottom: clamp(3rem, 8vw, 6rem);
  text-align: center;
}

.band--hero .lede {
  margin-inline: auto;
}

.band--hero .btn-row {
  justify-content: center;
}

/* --- Hero cross-fade slideshow (the three professional photos) ---
   Sits behind the .band::before dark overlay. Slide 1 ships with a real src
   so it renders (and stays put) with JavaScript disabled; js/main.js fills in
   the other two after first paint and rotates the `is-active` class. */
.hero-slideshow {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}

.hero-slideshow__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transition: opacity 1200ms ease-in-out;
  will-change: opacity;
}

/* Only the active slide is visible. A slide tagged `is-leaving` keeps the base
   opacity:0 and therefore transitions out over the 1200ms cross-fade. */
.hero-slideshow__slide.is-active {
  opacity: 1;
}

/* Subtle Ken Burns drift on whichever slide is showing. The `is-leaving`
   selector repeats the identical animation shorthand so the outgoing slide
   keeps drifting through the cross-fade instead of snapping back. */
@media (prefers-reduced-motion: no-preference) {
  .hero-slideshow[data-ready] .hero-slideshow__slide.is-active,
  .hero-slideshow[data-ready] .hero-slideshow__slide.is-leaving {
    animation: hero-drift 7400ms ease-out both;
  }
}

@keyframes hero-drift {
  from {
    transform: scale(1.02);
  }
  to {
    transform: scale(1.08);
  }
}

.band--hero h1 {
  font-size: var(--size-hero-1);
  text-transform: uppercase;
  line-height: 1;
  margin-bottom: 0.15em;
}

/* `.line-2` marks the second line of a two-line display heading. On the live
   site those lines are separate elements ("Built with Craftsmanship" then
   "and Precision") or are split by a <br>; here they are one heading with a
   span, so the span is what has to break the line. The markup deliberately
   carries no whitespace across the span boundary, which means that without
   this rule the two halves render as one run-on word ("Craftsmanshipand
   Precision", "SomethingBeautiful"). The hero adds its own size and style on
   top of this; every other band only needs the break. */
.band h1 .line-2,
.band h2 .line-2 {
  display: block;
}

.band--hero h1 .line-2 {
  font-size: var(--size-hero-2);
  text-transform: none;
  font-style: italic;
  line-height: 1.05;
}

.band--hero .lede {
  color: rgba(255, 255, 255, 0.9);
  font-size: 1.2rem;
  max-width: 40rem;
}

/* Page-title band — interior pages ("About", "Contact", "Services"...)
   FIDELITY: every interior title column on the live site is `text-align:center`
   with `align-items:center` (about / services / projects / contact / privacy /
   sitemap / both service detail pages all use the same pattern). */
.band--title {
  min-height: 40vh;
  display: flex;
  align-items: center;
}

.band--title .band__inner {
  padding-block: clamp(4rem, 10vw, 6rem);
  text-align: center;
}

.band--title .lede {
  margin-inline: auto;
}

.band--title .btn-row {
  justify-content: center;
}

.band--title h1 {
  font-size: var(--size-hero-2);
  text-transform: uppercase;
  margin: 0;
}

/* Full-width dark content band (mid-page statements) */
.band--content .band__inner {
  max-width: var(--content-narrow);
  text-align: left;
}

/* Split band: copy on the left, a CTA vertically centred on the right.
   Collapses to a stacked layout with the button under the text on mobile. */
.band__inner--split {
  display: flex;
  align-items: center;
  gap: clamp(2rem, 5vw, 4rem);
}

.band--content .band__inner--split {
  max-width: var(--content-width);
}

.band__inner--split .band__copy {
  max-width: var(--content-narrow);
}

.band__inner--split .band__action {
  flex-shrink: 0;
}

@media (max-width: 900px) {
  .band__inner--split {
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
  }
}

.band--content h2 {
  font-size: var(--size-h2);
}

/* Centered variant (homepage marine intro): statement sits centred over the
   photo, slightly smaller, with a soft blur shadow lifting it off the image
   since this band runs a much lighter overlay (0.2). */
.band--content-center .band__inner {
  text-align: center;
}

.band--content-center h2 {
  font-size: calc(var(--size-h2) - 2pt);
  /* Two layers: a tight offset shadow for the drop-shadow read, plus a wide
     gaussian halo that lifts the text off the bright parts of the photo. */
  text-shadow:
    0 3px 6px rgba(0, 0, 0, 0.55),
    0 6px 24px rgba(0, 0, 0, 0.65);
}

/* Closing "Let's Build Something Beautiful" CTA band */
.band--cta {
  text-align: center;
}

.band--cta .band__inner {
  max-width: 54rem;
}

.band--cta h2 {
  font-size: var(--size-hero-2);
  text-transform: uppercase;
  margin-bottom: 0.2em;
}

.band--cta .lede {
  margin-inline: auto;
  margin-bottom: 1.5rem;
}

.band--cta .btn-row {
  justify-content: center;
  margin-top: 0.5rem;
}

/* -------------------------------------------------------------------------
   8. CARDS / GRIDS / TWO-COLUMN CONTENT
   ---------------------------------------------------------------------- */
.intro {
  padding-block: var(--section-space);
}

.intro__inner {
  max-width: var(--content-narrow);
  margin-inline: auto;
  text-align: center;
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2.5rem;
  padding-block: var(--section-space);
}

@media (max-width: 900px) {
  .card-grid {
    grid-template-columns: 1fr;
  }
}

.card {
  display: flex;
  flex-direction: column;
}

.card__media {
  aspect-ratio: 3 / 4;
  overflow: hidden;
  margin-bottom: 1.5rem;
  background: var(--color-border);
}

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.card h3 {
  font-size: 1.9rem;
  margin-bottom: 0.4rem;
}

.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  padding-block: var(--section-space);
}

@media (max-width: 900px) {
  .two-col {
    grid-template-columns: 1fr;
  }
}

.two-col--reverse .two-col__media {
  order: 2;
}

@media (max-width: 900px) {
  .two-col--reverse .two-col__media {
    order: 0;
  }
}

/* Clips the scale-settle in section 15 so the photo never bleeds past its
   column while it is easing back down to 1. */
.two-col__media {
  overflow: hidden;
}

.two-col__media img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
}

.two-col__media--tall img {
  aspect-ratio: 3 / 4;
}

.two-col__media--square img {
  aspect-ratio: 4 / 3;
}

.industries-list {
  list-style: none;
  display: grid;
  gap: 0.6rem;
  margin: 1.5rem 0;
  font-size: 1.1rem;
}

.industries-list li {
  position: relative;
  padding-left: 1.5rem;
  margin-left: 1rem;
}

.industries-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 8px;
  height: 8px;
  background: var(--color-accent);
}

/* -------------------------------------------------------------------------
   9. GALLERY
   ---------------------------------------------------------------------- */
/* A 12-track grid so ordinary tiles (span 3 = 4 per row) and the wider
   "feature" tiles (span 4 = 3 per row) can share one grid and still finish
   their rows cleanly. The gap maths works out identically to repeat(4, 1fr). */
.gallery {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1.25rem;
  padding-block: var(--section-space);
  list-style: none;
}

.gallery__item {
  grid-column: span 3;
}

.gallery__item img {
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
}

@media (max-width: 1024px) {
  .gallery__item {
    grid-column: span 4;
  }
}

@media (max-width: 700px) {
  .gallery {
    gap: 0.75rem;
  }

  .gallery__item {
    grid-column: span 6;
  }
}

/* Feature tiles: the three professional photos at the head of the homepage
   gallery. Landscape crop, three across, so they read as a full-width band
   above the rest of the grid. Declared after the base rules on purpose:
   equal specificity, later source order wins at every breakpoint. */
.gallery__item--feature {
  grid-column: span 4;
}

.gallery__item--feature img {
  aspect-ratio: 3 / 2;
}

@media (max-width: 700px) {
  .gallery__item--feature {
    grid-column: span 12;
  }
}

/* Each tile is a link to the full-size file: the lightbox intercepts the click
   when JavaScript runs, and it is a plain image link when it does not. */
.gallery__link {
  display: block;
  position: relative;
  overflow: hidden;
  background: var(--color-border);
  cursor: zoom-in;
}

.gallery__link:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 3px;
}

/* NOTE: `.gallery--masonry` (Projects page) previously re-declared the exact
   column counts of the base grid, so it now carries no rules of its own and
   simply follows `.gallery`. The class is left on the markup as a hook. */

/* -------------------------------------------------------------------------
   9b. LIGHTBOX
   Dependency-free replacement for the live site's glightbox / Magnific
   gallery viewer. All of this markup is created by js/main.js on first use,
   so nothing here renders unless JavaScript is available.
   ---------------------------------------------------------------------- */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(1rem, 4vw, 3rem);
  background: rgba(0, 0, 0, 0.92);
  opacity: 0;
  visibility: hidden;
  transition: opacity 200ms ease, visibility 200ms ease;
}

.lightbox.is-open {
  opacity: 1;
  visibility: visible;
}

/* The author-level `display: flex` above would otherwise beat the UA rule for
   the `hidden` attribute, so restate it here. */
.lightbox[hidden],
.lightbox__btn[hidden] {
  display: none;
}

.lightbox__figure {
  margin: 0;
  max-width: min(1100px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.lightbox__image {
  max-width: 100%;
  max-height: calc(100vh - 11rem);
  width: auto;
  height: auto;
  object-fit: contain;
  background: rgba(255, 255, 255, 0.04);
}

.lightbox__caption {
  margin: 0;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.95rem;
  text-align: center;
  max-width: 46rem;
}

.lightbox__counter {
  display: block;
  margin-top: 0.35rem;
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.lightbox__btn {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  padding: 0;
  background: rgba(0, 0, 0, 0.45);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 0;
  color: var(--color-white);
  transition: background-color var(--transition-fast), border-color var(--transition-fast);
}

.lightbox__btn:hover {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.lightbox__btn:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

.lightbox__btn svg {
  width: 22px;
  height: 22px;
}

.lightbox__close {
  top: clamp(0.75rem, 2vw, 1.5rem);
  right: clamp(0.75rem, 2vw, 1.5rem);
}

.lightbox__prev {
  left: clamp(0.5rem, 2vw, 1.5rem);
  top: 50%;
  transform: translateY(-50%);
}

.lightbox__next {
  right: clamp(0.5rem, 2vw, 1.5rem);
  top: 50%;
  transform: translateY(-50%);
}

@media (max-width: 700px) {
  .lightbox__prev,
  .lightbox__next {
    top: auto;
    bottom: clamp(0.75rem, 3vw, 1.5rem);
    transform: none;
  }

  .lightbox__image {
    max-height: calc(100vh - 14rem);
  }
}

/* Scroll lock while the dialog is open. */
body.lightbox-is-open {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .lightbox {
    transition: none;
  }

  .hero-slideshow__slide {
    transition: none;
  }
}

/* -------------------------------------------------------------------------
   10. CONTACT FORM
   ---------------------------------------------------------------------- */
.contact-grid {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: clamp(2rem, 6vw, 5rem);
  padding-block: var(--section-space);
}

@media (max-width: 900px) {
  .contact-grid {
    grid-template-columns: 1fr;
  }
}

.contact-form {
  display: grid;
  gap: 1.25rem;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem;
}

@media (max-width: 560px) {
  .form-row {
    grid-template-columns: 1fr;
  }
}

.form-field {
  display: grid;
  gap: 0.5rem;
}

.form-field label {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--color-heading);
}

.form-field .required {
  color: var(--color-accent);
}

.form-field input,
.form-field textarea {
  border: 1px solid var(--color-border);
  background: var(--color-white);
  padding: 0.85em 1em;
  color: var(--color-body);
  border-radius: 0;
  width: 100%;
}

.form-field input:hover,
.form-field textarea:hover {
  border-color: var(--color-body-muted);
}

/* Same ring the buttons use, so focus reads as one system across the site.
   `:focus` as well as `:focus-visible` — a text field should always show
   where the caret is, however it was reached. */
.form-field input:focus,
.form-field textarea:focus,
.form-field input:focus-visible,
.form-field textarea:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 2px;
  border-color: var(--color-accent);
}

@media (prefers-reduced-motion: no-preference) {
  .form-field input,
  .form-field textarea {
    transition: border-color 200ms ease;
  }
}

.form-note {
  font-size: 0.9rem;
  color: var(--color-body-muted);
}

/* Spam honeypot field: removed from the visual page and the accessibility
   tree, but still present in the POST so contact.php can check it. Not
   `display:none` — some bots skip fields they can tell are hidden. */
.form-field--hp {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Inline submit confirmation, inserted by js/main.js (block 10). Empty until
   the first submit, so it costs no space on load. */
.form-notice {
  margin: 0;
  padding: 0.85em 1em;
  border-left: 4px solid var(--color-accent);
  background: var(--color-white);
  color: var(--color-heading);
  font-weight: 600;
  opacity: 0;
}

.form-notice:empty {
  display: none;
}

.form-notice.is-shown {
  opacity: 1;
}

.form-notice--error {
  border-left-color: #c53030;
}

@media (prefers-reduced-motion: no-preference) {
  .form-notice {
    transform: translateY(6px);
    transition: opacity 300ms var(--ease-out-expo), transform 300ms var(--ease-out-expo);
  }

  .form-notice.is-shown {
    transform: translateY(0);
  }
}

.contact-details h3 {
  font-size: 1.6rem;
  margin-bottom: 0.3rem;
}

.contact-details h3:not(:first-child) {
  margin-top: 1.75rem;
}

.contact-details a {
  color: var(--color-body);
  transition: color var(--transition-fast);
}

.contact-details a:hover,
.contact-details a:focus-visible {
  color: var(--color-accent);
}

/* -------------------------------------------------------------------------
   11. SITEMAP PAGE
   ---------------------------------------------------------------------- */
.sitemap-list {
  list-style: none;
  padding-block: var(--section-space);
  display: grid;
  gap: 0.75rem;
  font-size: 1.2rem;
  max-width: var(--content-narrow);
}

.sitemap-list a {
  display: inline-block;
  padding: 0.35em 0;
  color: var(--color-heading);
  border-bottom: 1px solid transparent;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.sitemap-list a:hover,
.sitemap-list a:focus-visible {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.sitemap-list .sub-list {
  list-style: none;
  margin: 0.5rem 0 0.5rem 1.5rem;
  display: grid;
  gap: 0.5rem;
  font-size: 1.05rem;
}

/* -------------------------------------------------------------------------
   12. FOOTER
   ---------------------------------------------------------------------- */
.site-footer {
  background: var(--color-footer-bg);
  color: var(--color-white);
}

.site-footer a {
  color: var(--color-white);
  transition: color var(--transition-fast);
}

.site-footer a:hover,
.site-footer a:focus-visible {
  color: var(--color-accent);
}

.footer-main {
  padding: 3rem var(--edge-padding);
  max-width: var(--content-width);
  margin-inline: auto;
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: 3rem;
}

/* Center the Menu column in its track so it doesn't hug the brand blurb. */
.footer-main > div:nth-child(2) {
  justify-self: center;
}

@media (max-width: 800px) {
  .footer-main {
    grid-template-columns: 1fr;
    gap: 2rem;
  }

  .footer-main > div:nth-child(2) {
    justify-self: start;
  }
}

.footer-brand img {
  width: 120px;
  margin-bottom: 1rem;
}

.footer-brand p {
  color: rgba(255, 255, 255, 0.75);
  font-size: 0.95rem;
}

.footer-heading {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 1rem;
  color: var(--color-white);
}

.footer-menu {
  list-style: none;
  display: grid;
  gap: 0.6rem;
}

.footer-contact {
  list-style: none;
  display: grid;
  gap: 0.75rem;
  font-size: 0.95rem;
}

.footer-contact li {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
}

.footer-contact svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  margin-top: 0.15em;
}

.footer-bottom {
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.footer-bottom__inner {
  max-width: var(--content-width);
  margin-inline: auto;
  padding: 1.25rem var(--edge-padding);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.75);
}

.footer-bottom__inner a {
  color: rgba(255, 255, 255, 0.75);
}

.footer-social {
  display: flex;
  gap: 0.75rem;
}

.footer-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 50%;
}

.footer-social svg {
  width: 16px;
  height: 16px;
}

/* -------------------------------------------------------------------------
   13. 404 PAGE
   ---------------------------------------------------------------------- */
.error-page {
  min-height: 60vh;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--section-space) var(--edge-padding);
}

.error-page__code {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: clamp(4rem, 12vw, 7rem);
  color: var(--color-accent);
  line-height: 1;
  margin-bottom: 0.25rem;
}

/* -------------------------------------------------------------------------
   14. UTILITIES / ACCESSIBILITY
   ---------------------------------------------------------------------- */
.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--color-accent);
  color: var(--color-white);
  padding: 0.75em 1.25em;
  z-index: 1000;
  font-size: 0.9rem;
}

.skip-link:focus {
  left: 0;
}

:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

/* -------------------------------------------------------------------------
   15. MOTION
   Page transitions, scroll reveals, the header lift and the homepage
   carousel. Read the MOTION POLICY at the top of this file first.

   15a. Page entrance / exit + View Transitions
   15b. Scroll reveals (headline words, content blocks, photos)
   15c. Header lift on scroll
   15d. Homepage featured carousel
   15e. Print
   15f. Reduced-motion catch-all
   ---------------------------------------------------------------------- */

/* --- 15a. PAGE ENTRANCE / EXIT -------------------------------------------
   The entrance is a plain CSS keyframe animation and is deliberately NOT
   gated on the `js-motion` class:
     - a keyframe animation always runs to completion by itself, so there is
       no code path that can leave a page stranded half-faded;
     - it starts with the first paint instead of waiting on the deferred
       script, so there is no flash of un-animated content;
     - `animation-fill-mode: backwards` means nothing is left applied once it
       has finished, which keeps `body.is-leaving` below free to take over and
       keeps `main` from holding on to a stale transform.
   ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .site-header,
  .site-footer {
    animation: page-enter 380ms var(--ease-out-expo) backwards;
  }

  main {
    animation: page-enter-rise 420ms var(--ease-out-expo) backwards;
  }

  .site-footer {
    animation-delay: 60ms;
  }

  /* Fade-out immediately before an internal navigation. js/main.js adds this
     class, waits 190ms and then navigates, and removes it again on `pageshow`
     so a back/forward-cache restore can never come back invisible. */
  body.is-leaving {
    opacity: 0;
    transition: opacity 190ms ease-in;
  }
}

@keyframes page-enter {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes page-enter-rise {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Cross-document View Transitions, as a progressive enhancement. Where they
   are supported the browser owns the whole navigation and js/main.js skips
   its own fade-out entirely (one transition, never two). Everywhere else the
   at-rule is simply ignored and the JS fallback above runs.

   `@view-transition` has to sit at the top level of the stylesheet, so the
   reduced-motion case is handled by flattening the generated pseudo-elements
   rather than by wrapping the rule itself. */
@view-transition {
  navigation: auto;
}

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) {
    animation: vt-leave 180ms ease-in both;
  }

  ::view-transition-new(root) {
    animation: vt-arrive 360ms var(--ease-out-expo) both;
  }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(root),
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 1ms !important;
    animation-delay: 0s !important;
  }
}

@keyframes vt-leave {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

@keyframes vt-arrive {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* --- 15b. SCROLL REVEALS -------------------------------------------------
   js/main.js adds `js-motion` to <html>, tags elements with
   `data-reveal="words"` (h1/h2, split into per-word spans at runtime) or
   `data-reveal="block"` (everything else), and adds `is-revealed` the first
   time each one scrolls into view. Every hidden state below therefore needs
   BOTH a class the script has to add and a no-preference motion query:

     - JavaScript off, blocked, or throwing before it gets here  -> visible
     - IntersectionObserver missing (script bails out)           -> visible
     - prefers-reduced-motion: reduce                            -> visible

   The per-element delay arrives as an inline `--reveal-delay` custom
   property; words override the value they inherit from their heading with
   their own, which is how the stagger is built.
   ---------------------------------------------------------------------- */
.reveal-word {
  display: inline-block;
}

@media (prefers-reduced-motion: no-preference) {
  .js-motion [data-reveal],
  .js-motion [data-reveal="words"] .reveal-word {
    transition:
      opacity var(--reveal-duration) var(--ease-out-expo) var(--reveal-delay, 0ms),
      transform var(--reveal-duration) var(--ease-out-expo) var(--reveal-delay, 0ms);
  }

  .js-motion [data-reveal="block"]:not(.is-revealed),
  .js-motion [data-reveal="words"]:not(.is-revealed) .reveal-word {
    opacity: 0;
    transform: translateY(var(--reveal-distance));
  }

  /* The hero is the one place the reveal doubles as an on-load entrance, so
     it runs a little tighter: the whole eyebrow -> headline -> lede -> CTA
     choreography finishes inside about a second. */
  .js-motion .band--hero [data-reveal] {
    --reveal-duration: 620ms;
    --reveal-distance: 20px;
  }

  /* Photos settle out of a very slight over-scale as their tile arrives, and
     lift again on hover. Every wrapper involved clips, so nothing around them
     moves. One shared transition declaration keeps the reveal and the hover
     from fighting over `transform`. */
  .gallery__link img,
  .card__media img,
  .two-col__media img {
    transition: transform 700ms var(--ease-out-expo);
  }

  .js-motion [data-reveal-media]:not(.is-revealed) img {
    transform: scale(1.045);
  }

  .gallery__link:hover img,
  .card:hover .card__media img {
    transform: scale(1.035);
  }
}

/* --- 15c. HEADER LIFT ON SCROLL ------------------------------------------
   A soft shadow fades in under the sticky header once the page has moved.
   Preferred path is a CSS scroll-driven animation, which needs no JavaScript
   at all. Where `animation-timeline` is missing, js/main.js adds
   `js-header-fallback` to <html> and drives the identical gradient from an
   IntersectionObserver sentinel. Neither path uses a scroll listener, and
   both animate `opacity` only.
   ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .site-header::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      top: 100%;
      height: 20px;
      pointer-events: none;
      background: linear-gradient(
        to bottom,
        rgba(26, 32, 44, 0.13),
        rgba(26, 32, 44, 0)
      );
      opacity: 0;
      animation: header-lift linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 90px;
    }
  }

  .js-header-fallback .site-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 20px;
    pointer-events: none;
    background: linear-gradient(
      to bottom,
      rgba(26, 32, 44, 0.13),
      rgba(26, 32, 44, 0)
    );
    opacity: 0;
    transition: opacity 260ms ease;
  }

  .js-header-fallback.is-scrolled .site-header::after {
    opacity: 1;
  }
}

@keyframes header-lift {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* --- 15d. HOMEPAGE FEATURED CAROUSEL -------------------------------------
   The live site runs the homepage "Featured Seating Installations" gallery as
   a Splide carousel; this is the dependency-free equivalent. It is a
   scroll-snap row, so dragging, swiping, trackpad scrolling and the browser's
   own keyboard scrolling all come for free.

   The `gallery--carousel` class is in the homepage markup rather than added
   by script, so with JavaScript off it is still a working scrollable row —
   js/main.js only layers the arrow buttons and their end states on top. Other
   pages keep the plain grid.
   ---------------------------------------------------------------------- */
.gallery--carousel {
  --carousel-gap: 1.25rem;
  --carousel-visible: 2.5; /* ~20% larger tiles than the original 3-up row */
  display: flex;
  flex-wrap: nowrap;
  gap: var(--carousel-gap);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  /* Breathing room so a focused tile's ring is not clipped by the scroll
     container; the negative margin cancels it so slide 1 still lines up with
     the text above. */
  padding-inline: 4px;
  margin-inline: -4px;
}

.gallery--carousel .gallery__item {
  grid-column: auto;
  flex: 0 0 calc(
    (100% - (var(--carousel-visible) - 1) * var(--carousel-gap)) /
      var(--carousel-visible)
  );
  scroll-snap-align: start;
}

/* One crop for the whole row: mixed portrait and landscape tiles would give
   the flex row a ragged edge. */
.gallery--carousel .gallery__item img {
  aspect-ratio: 3 / 2;
}

@media (max-width: 1024px) {
  .gallery--carousel {
    --carousel-visible: 1.65;
  }
}

@media (max-width: 700px) {
  .gallery--carousel {
    --carousel-gap: 0.75rem;
    --carousel-visible: 1.05;
  }
}

/* --- Marquee mode (js/main.js block 9) ------------------------------------
   When motion is allowed the featured carousel becomes a slow, continuously
   looping marquee on a virtual cylinder. Three elements, three jobs, and
   deliberately no overlap between them, because mixing them is what made the
   first attempt jitter:

     .gallery__item   (li)  position only. The loop writes translate3d() here
                            and nothing else ever transforms it, so the hit
                            area the pointer tests against is always exactly
                            where the loop put it.
     .carousel-tile   (div) the cylinder. rotateY + translateZ + opacity, all
                            written by the loop. Created by script.
     .gallery__link   (a)   the hover/focus grow, driven by the .is-lifted
                            class below. It sits INSIDE the li, so growing it
                            cannot move the li out from under the cursor.

   Perspective is per tile rather than on the stage. A single shared vanishing
   point pulls receding tiles toward the centre of the row, which is what used
   to bunch the edges up and tear gaps open mid-row; giving each tile its own
   perspective origin means depth only ever shrinks a tile about its own
   centre, and the loop keeps the spacing even in script.

   Without JS, or with reduced motion, none of this applies and the row stays
   the plain scroll-snap strip declared above. */
.carousel-stage {
  position: relative;
}

/* The clip is a separate element from the stage for one reason: the edge
   fades below have to start OUTSIDE the clipped area, and a pseudo-element of
   the clipping box itself would be clipped along with the photos.
   `overflow: clip` also means this never becomes a scroll container, so the
   browser cannot quietly scroll a focused off-stage tile into view behind the
   loop's back. `hidden` is the fallback for browsers without `clip`. */
.carousel-stage__clip {
  overflow: hidden;
  overflow: clip;
}

.carousel-stage::before,
.carousel-stage::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: clamp(3rem, 8vw, 7rem);
  z-index: 3;
  pointer-events: none;
}

/* The fades start 4px OUTSIDE the clip and hold solid page colour for 14px
   past that, so a tile leaving the row is fully covered before the feather
   begins and no sliver of photo can ever show past the edge. */
.carousel-stage::before {
  left: -4px;
  background: linear-gradient(90deg, var(--color-bg) 0, var(--color-bg) 14px, rgba(247, 250, 252, 0) 100%);
}

.carousel-stage::after {
  right: -4px;
  background: linear-gradient(270deg, var(--color-bg) 0, var(--color-bg) 14px, rgba(247, 250, 252, 0) 100%);
}

/* The row is no longer a scroller: it is a plain overflowing flex line whose
   tiles the loop places individually. Its layout positions (offsetLeft) are
   the coordinates the loop measures, which is why nothing here may change
   once the loop is running. */
.gallery--carousel.is-marquee {
  overflow: visible;
  scroll-snap-type: none;
  padding-inline: 0;
  margin-inline: 0;
}

.gallery--carousel.is-marquee .gallery__item {
  /* Per-tile perspective: see the note above. Must match PERSPECTIVE in
     js/main.js block 9. */
  perspective: 1200px;
  will-change: transform;
}

.gallery--carousel.is-marquee .carousel-tile {
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}

.gallery--carousel.is-marquee .gallery__item img,
.gallery--carousel.is-marquee .gallery__link {
  border-radius: 10px;
}

.gallery--carousel.is-marquee .gallery__link {
  display: block;
  overflow: hidden;
  transform-origin: 50% 50%;
}

/* Arrow controls, created by js/main.js and inserted after the row. When they
   are present the row gives up its bottom section padding to the control bar
   so the rhythm of the page is unchanged.

   Marquee mode hides them rather than deleting them (a loop with no ends has
   nothing to page through), so that turning reduced motion on mid-session can
   hand the row and its arrows straight back to block 7. */
.carousel-nav.is-hidden {
  display: none;
}

.gallery--carousel.has-nav {
  padding-bottom: 1.5rem;
}

.carousel-nav {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
  padding-bottom: var(--section-space);
}

.carousel-nav__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: 0;
  color: var(--color-heading);
}

.carousel-nav__btn svg {
  width: 20px;
  height: 20px;
}

.carousel-nav__btn:hover {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-white);
}

.carousel-nav__btn:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 3px;
}

/* Kept focusable and in the tab order at the ends of the row — only dimmed
   and made inert — so focus is never yanked out from under the user. */
.carousel-nav__btn[aria-disabled="true"] {
  opacity: 0.35;
  cursor: default;
}

.carousel-nav__btn[aria-disabled="true"]:hover {
  background: transparent;
  border-color: var(--color-border);
  color: var(--color-heading);
}

@media (prefers-reduced-motion: no-preference) {
  .gallery--carousel {
    scroll-behavior: smooth;
  }

  .carousel-nav__btn {
    transition: background-color 200ms ease, border-color 200ms ease,
      color 200ms ease, opacity 200ms ease, transform 120ms ease-out;
  }

  .carousel-nav__btn:active {
    transform: translateY(1px) scale(0.98);
  }

  /* Hover / focus grow for marquee mode. Script adds `is-lifted` to the <a>
     inside the hovered or focused tile, never to the tile itself: the li keeps
     its own untouched geometry, so the pointer target cannot move as a result
     of the visual feedback and there is nothing for a hover state to chase.
     Growth is also monotonic (the tile only ever covers MORE of the cursor's
     surroundings), which is the other half of why this cannot oscillate. */
  .gallery--carousel.is-marquee .gallery__link {
    transition: transform 260ms var(--ease-out-expo);
  }

  /* The scroll reveal (section 15b) puts a 700ms transition on `transform` for
     every [data-reveal] element, the li included. The loop rewrites that
     transform every frame, so leaving the transition in place would drag the
     whole row 700ms behind itself. Opacity is still the reveal's to animate;
     the loop takes over the reveal's 24px rise in script instead. */
  .gallery--carousel.is-marquee .gallery__item {
    transition-property: opacity;
  }

  .gallery--carousel.is-marquee .gallery__link.is-lifted {
    transform: scale(1.1);
  }

  /* The site-wide photo hover (section 15b) would compound with the grow
     above, and the tile clips, so inside the marquee the photo holds still and
     the whole tile does the moving. */
  .gallery--carousel.is-marquee .gallery__link:hover img {
    transform: none;
  }
}

/* --- 15e. PRINT ----------------------------------------------------------
   Reveals are driven by an IntersectionObserver, and the entrance is a
   keyframe animation. Both of those depend on the page actually being
   rendered, which is not guaranteed: a page loaded into a background tab (or
   printed before it has been scrolled) can still be sitting at its hidden
   starting state. Printing must never come out blank, so force every motion
   state to its finished value on paper.
   ---------------------------------------------------------------------- */
@media print {
  .site-header,
  main,
  .site-footer,
  .js-motion [data-reveal],
  .js-motion [data-reveal] .reveal-word,
  .js-motion [data-reveal-media] img,
  body.is-leaving {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
  }
}

/* --- 15f. REDUCED-MOTION CATCH-ALL ---------------------------------------
   Belt and braces over the per-block gating above. If a visitor has asked
   their operating system for reduced motion, nothing on this site moves —
   including anything added to this stylesheet later that forgets to gate
   itself. States still apply, they just apply instantly.
   ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}

/* =========================================================================
   16. MOBILE REFINEMENTS

   Every narrow-viewport override lives here, last in the file, so the cascade
   is obvious: the sections above describe the design, this section describes
   what changes on a phone. Nothing here touches colour, font family, copy or
   the motion system.

   Two breakpoints:
     - `max-width: 900px`  tablets and the drawer nav (already the nav
                           breakpoint, reused so the header only has one story)
     - `max-width: 600px`  phones

   16a. Fluid type scale for phones
   16b. Compact header
   16c. Hero above the fold
   16d. Page-title band
   16e. Section rhythm, cards, two-column rows
   16f. Galleries (scroll budget)
   16g. Touch targets
   16h. Navigation drawer
   16i. Lightbox on small screens
   ---------------------------------------------------------------------- */

/* --- 16a. FLUID TYPE SCALE FOR PHONES ------------------------------------
   The desktop scales in section 1 are `clamp()`s whose *floor* is reached at
   about 600px and then holds flat all the way down to 320px, which is what
   left the display sizes too big on a phone. These replacements are straight
   linear ramps (`calc(rem + vw)`) with no floor, tuned so that:

     - each one meets the desktop clamp's floor exactly at 600px, so there is
       no jump at the breakpoint;
     - at 320px the widest word in any heading on the site still fits its
       column with room to spare.

   The 320px budget is 280px of content (320 minus 2 x 20px edge padding).
   Measured widths of the worst-case words, per 100px of font-size:

       COMMERCIAL     Cormorant 665   Georgia (fallback) 771
       RESTAURANT     Cormorant 651   Georgia 752
       UPHOLSTERY     Cormorant 630   Georgia 754
       Craftsmanship  Cormorant 578   Georgia 764

   At the 28px the title band gets at 320px, COMMERCIAL is 186px in Cormorant
   and 216px in Georgia: 66% and 77% of the column. The acid test,
   "RESTAURANT & COMMERCIAL SEATING", wraps to three whole words.
   ---------------------------------------------------------------------- */
@media (max-width: 600px) {
  :root {
    /* 35px at 320 -> 44px at 600 (the desktop clamp floor) */
    --size-hero-1: calc(1.545rem + 3.214vw);
    /* 28px at 320 -> 36px at 600 */
    --size-hero-2: calc(1.179rem + 2.857vw);
    /* 26px at 320 -> 32px at 600 */
    --size-h2: calc(1.196rem + 2.143vw);
    /* 22px at 320 -> 25.6px at 600 */
    --size-h3: calc(1.118rem + 1.286vw);
    /* 36px at 320 -> 48px at 600 */
    --section-space: calc(1.393rem + 4.286vw);
    /* A little more of the screen for the content itself */
    --edge-padding: 1.25rem;
  }

  .eyebrow {
    font-size: 16px;
    margin-bottom: 1rem;
  }

  .lede {
    font-size: 1.05rem;
  }

  .card h3 {
    font-size: 1.5rem;
  }

  .contact-details h3 {
    font-size: 1.35rem;
  }
}

/* Hard line breaks that are set in the markup for the desktop composition
   ("Custom Commercial Seating in / Vancouver and the Lower Mainland"). On a
   phone the heading has to find its own breaks, so these are switched off.
   The markup carries a real space after each one, which is collapsed away
   while the <br> is displayed and takes over when it is not. */
@media (max-width: 700px) {
  .opt-break {
    display: none;
  }
}

/* --- 16b. COMPACT HEADER -------------------------------------------------
   The sticky header is the first thing competing with the hero for the fold,
   so it gives back 26px of the 90px it takes on desktop. The logo is sized by
   height with `width: auto`, so the wordmark and its tagline keep their exact
   proportions; at 40px the tagline is still legible and never truncated.
   ---------------------------------------------------------------------- */
@media (max-width: 900px) {
  .site-header__inner {
    min-height: 72px;
    padding-block: 0.65rem;
  }

  .site-logo img {
    height: 46px;
  }
}

@media (max-width: 600px) {
  .site-header__inner {
    min-height: 64px;
    padding-block: 0.55rem;
    gap: 0.75rem;
  }

  .site-logo img {
    height: 40px;
  }
}

/* --- 16c. HERO ABOVE THE FOLD --------------------------------------------
   `100svh` is the *small* viewport height, the one that is left when the
   browser's URL bar is fully expanded, so sizing against it means the CTA is
   inside the first screen at the worst moment, not just after the chrome has
   collapsed. The plain `vh` line above it is the fallback for browsers
   without the newer units.

   Subtracting the header height is exact rather than approximate: the header
   is `position: sticky`, so it occupies its own space in the flow and
   hero + header adds up to precisely one screen.

   The desktop hero's large top padding (which pushes the wording into the
   lower third of a tall photo) is dropped: on a phone the band is already
   centring its content, and that padding was the single biggest reason the
   button fell past the fold.
   ---------------------------------------------------------------------- */
@media (max-width: 900px) {
  .band--hero {
    min-height: calc(100vh - 72px);
    min-height: calc(100svh - 72px);
  }

  .band--hero .band__inner {
    padding-top: 2.5rem;
    padding-bottom: 2.5rem;
  }

  .band--hero .lede {
    font-size: 1.1rem;
    max-width: 34rem;
  }
}

@media (max-width: 600px) {
  .band--hero {
    min-height: calc(100vh - 64px);
    min-height: calc(100svh - 64px);
  }

  .band--hero .band__inner {
    padding-top: 2rem;
    padding-bottom: 2rem;
  }

  .band--hero .lede {
    font-size: 1.05rem;
  }

  .btn-row {
    gap: 1rem;
    margin-top: 1.25rem;
  }
}

/* --- 16d. PAGE-TITLE BAND ------------------------------------------------
   40vh of a 568px screen is 227px of photo before the visitor reaches a word
   of the page. Trimmed, and the inner padding comes down with it, so interior
   pages start delivering content roughly a third of a screen earlier.
   ---------------------------------------------------------------------- */
@media (max-width: 900px) {
  .band--title {
    min-height: 34vh;
  }

  .band--title .band__inner {
    padding-block: clamp(2.25rem, 7vw, 3.5rem);
  }
}

@media (max-width: 600px) {
  .band--title {
    min-height: 30vh;
  }
}

/* --- 16e. SECTION RHYTHM, CARDS, TWO-COLUMN ROWS -------------------------
   The three service cards go two-up on a tablet instead of jumping straight
   from three columns to one, and every stacked photo takes a shallower crop
   so a column of them is not a column of near-square-metre images.
   ---------------------------------------------------------------------- */
@media (min-width: 601px) and (max-width: 900px) {
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 2rem;
  }
}

@media (max-width: 600px) {
  .card-grid {
    gap: 2rem;
  }

  .card__media {
    aspect-ratio: 4 / 5;
    margin-bottom: 1.1rem;
  }

  .two-col__media img,
  .two-col__media--tall img {
    aspect-ratio: 4 / 5;
  }

  .two-col {
    gap: 2rem;
  }

  .industries-list,
  .entry-list {
    font-size: 1rem;
  }

  /* The stacked contact column needs a little air from the form above it. */
  .contact-grid {
    gap: 2.5rem;
  }
}

/* --- 16f. GALLERIES ------------------------------------------------------
   The Projects page is 55 photos. At the 2:3 portrait crop the base grid
   uses, two-up on a 320px screen is 28 rows of 200px-tall tiles, about
   6,000px of gallery on its own, which is exactly the "scrolling forever"
   the client described. A square crop on phones cuts that by a third and
   makes the grid scannable, and `object-fit: cover` was already in place so
   nothing is distorted.

   Specificity keeps the two exceptions intact without any extra work: the
   three professional photos (`.gallery__item--feature img`) and the homepage
   carousel (`.gallery--carousel .gallery__item img`) are both two-class
   selectors and continue to win with their own 3:2 crop.
   ---------------------------------------------------------------------- */
@media (max-width: 700px) {
  .gallery__item img {
    aspect-ratio: 1 / 1;
  }
}

/* --- 16g. TOUCH TARGETS --------------------------------------------------
   Everything tappable clears 44px on the axis it was short on. The links that
   grow are all in the footer, the sitemap and the contact details, where they
   are set as running text on desktop; giving them a minimum box on small
   screens costs nothing anywhere else.
   ---------------------------------------------------------------------- */
@media (max-width: 900px) {
  .btn--text {
    min-height: 44px;
  }

  /* The logo is 40px tall on a phone; the link around it keeps a 44px box. */
  .site-logo {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  .footer-menu a,
  .footer-contact a,
  .contact-details p > a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* "Reach us directly at info@petesseats.ca" runs inside a sentence, so it
     grows by padding rather than by becoming a block. */
  .form-note a {
    display: inline-block;
    padding-block: 0.7rem;
  }

  .footer-contact li {
    align-items: center;
  }

  .footer-contact svg {
    margin-top: 0;
  }

  .footer-bottom__inner a {
    display: inline-block;
    padding-block: 0.7rem;
  }

  .footer-social a {
    /* Undo the generic .footer-bottom__inner a tap-target rule above: it
       switches these to inline-block with block padding, which knocks the
       icon out of the centre of the circle. The 44px box is the tap target. */
    display: inline-flex;
    padding-block: 0;
    width: 44px;
    height: 44px;
  }

  .footer-social svg {
    width: 18px;
    height: 18px;
  }

  .sitemap-list a {
    padding-block: 0.55em;
  }

  .sitemap-list {
    gap: 0.25rem;
  }
}

/* --- 16h. NAVIGATION DRAWER ----------------------------------------------
   Three corrections the drawer needs before it works on a phone.

   1. Stacking. The drawer markup sits inside `.site-header`, and the header's
      own `z-index: 100` opens a stacking context the drawer cannot escape:
      whatever `z-index` the drawer carries, the whole header still paints at
      100. The scrim is a sibling of the header at 150, so it was landing on
      top of the open drawer and swallowing every tap. Lifting the header
      above the scrim for as long as the drawer is open (js/main.js already
      puts `nav-is-open` on <body>) restores the intended order.
   2. The close button. Inside the header the drawer covers the toggle, so the
      toggle is raised over it and recoloured for the dark panel it now sits
      on. Without this the X is unreachable at 320px, where the drawer is
      wider than the gap the button sits in.
   3. Scroll. The page behind a modal drawer should stay put, the same way it
      does behind the lightbox.
   ---------------------------------------------------------------------- */
@media (max-width: 900px) {
  body.nav-is-open .site-header {
    z-index: 200;
  }

  body.nav-is-open {
    overflow: hidden;
  }

  .menu-toggle {
    position: relative;
    z-index: 210;
  }

  .menu-toggle[aria-expanded="true"] {
    color: var(--color-white);
    border-color: rgba(255, 255, 255, 0.4);
  }
}

/* --- 16i. LIGHTBOX ON SMALL SCREENS --------------------------------------
   The controls already move to the bottom corners below 700px so they never
   sit on top of the photo. What was missing is the room for them: the figure
   now reserves the height of the control bar, and the image is measured
   against `dvh` so a phone's collapsing URL bar cannot crop it.
   ---------------------------------------------------------------------- */
@media (max-width: 700px) {
  .lightbox {
    padding-bottom: 5rem;
  }

  .lightbox__image {
    max-height: calc(100vh - 16rem);
    max-height: calc(100dvh - 16rem);
  }
}

/* -------------------------------------------------------------------------
   17. TOP OVERSCROLL BOUNCE (companion to js/main.js block 8)
   The JS applies an inline translateY while the visitor pulls past the top;
   this class animates the snap back with a slight overshoot so it lands with
   the elastic feel. Duration must stay in sync with RELEASE_MS in the JS.
   ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  main.is-bounce-settling {
    transition: transform 260ms cubic-bezier(0.3, 1.3, 0.45, 1);
  }
}
