/* ============================================================================
   PREMIUM PACKERS & MOVERS — STYLESHEET
   ============================================================================
   One file, no build step. Design tokens live in :root (section 1); change a
   value there and it propagates everywhere.

   CONTENTS
     1.  Design tokens
     2.  Reset and base
     3.  Typography
     4.  Layout: container, section, grid
     5.  Buttons
     6.  Header and navigation
     7.  Hero (and the 3D canvas layer)
     8.  Trust strip
     9.  Scroll journey (the truck storytelling)
    10.  Cards: services, features, comparison
    11.  Testimonials
    12.  FAQ accordion
    13.  Forms and the multi-step modal
    14.  Contact and map
    15.  Footer
    16.  Sticky mobile contact bar
    17.  Breadcrumbs, pagination, misc
    18.  Utilities and scroll reveals
    19.  Reduced motion and print

   MOTION POLICY
     Only `transform` and `opacity` are animated, so nothing triggers layout.
     Every animation is switched off under `prefers-reduced-motion` in §19.

   ACCESSIBILITY
     Body text meets WCAG AA against its background. Focus is always visible
     via a single `:focus-visible` rule in §2 — do not remove it.
   ========================================================================== */


/* ===========================================================================
   0. TYPEFACES
   ---------------------------------------------------------------------------
   Self-hosted, latin subset, ~116 KB total for five files.

   Why not system fonts: they are free and instant, but every site using them
   looks the same, and that sameness is most of what reads as "cheap" on a
   premium brand. A real typeface pairing is the single highest-impact change
   available here.

   Why self-hosted rather than Google Fonts: no third-party connection on the
   critical path, no extra DNS and TLS handshake, nothing to add to the CSP,
   and no requests leaving for another company's server.

   `font-display: swap` means text paints immediately in the fallback and
   re-renders when the font arrives — so a slow font can never leave the page
   blank. The fallback metrics are deliberately close to avoid a big reflow.
   =========================================================================== */
@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-600.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Playfair Display';
  src: url('/assets/fonts/playfair-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Playfair Display';
  src: url('/assets/fonts/playfair-600.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}
/* A real italic, not a synthesised one. Browsers fake italics by slanting
   the upright, which on a high-contrast serif destroys the stroke modelling
   and looks obviously wrong. The genuine italic has different letterforms. */
@font-face {
  font-family: 'Playfair Display';
  src: url('/assets/fonts/playfair-500-italic.woff2') format('woff2');
  font-weight: 500; font-style: italic; font-display: swap;
}


/* ===========================================================================
   1. DESIGN TOKENS
   =========================================================================== */
:root {
  /* =====================================================================
     THE PALETTE IS THE CLIENT'S, NOT A CHOICE MADE HERE
     =====================================================================
     Two colours were supplied and they are the source of truth:

       Wine Red    #7F011F   rgb(127, 1, 31)    hsl(346, 98%, 25%)
       Light Sand  #F5EBD0   rgb(245, 235, 208) hsl(44, 64%, 89%)

     Everything below is either one of those two, a neutral, or a tint
     derived from them. No third hue is introduced.

     An earlier version of this file used #7f011f, measured off a
     photograph of the company's signboard. That was the best available
     evidence at the time; a supplied palette beats a sampled one, so the
     measured value is gone. ===================================== */

  /* --- Dark surfaces: wine-black ----------------------------------------
     The dark surface is the wine red taken almost to black rather than a
     neutral charcoal, so the footer and the dark bands read as the same
     palette as the buttons instead of as a separate grey theme. */
  --ink-900: #1a060d;   /* deepest, the footer                             */
  --ink-800: #260d14;   /* dark section background                         */
  --ink-700: #37161e;   /* raised dark card                                */
  --ink-600: #4a242c;   /* dark card hover, hairline on dark               */
  --ink-500: #5f3139;   /* lightest ink, for hairlines and dividers        */

  /* --- Light surfaces ---------------------------------------------------
     White for cards, Light Sand for the page beneath them. The sand is
     what stops the site reading as a white template, and it is the
     client's own second colour rather than a beige picked to taste.
     Black body text measures 15.2:1 on it — comfortably past AAA. */
  --paper:      #ffffff;
  --paper-sunk: #f5ebd0;  /* Light Sand — section backgrounds              */
  --paper-card: #ffffff;
  --paper-line: #e3d5b4;  /* sand hairline, one step down                  */

  /* --- Text ------------------------------------------------------------- */
  --text-on-dark:        #fbf6ea;
  --text-on-dark-soft:   #dcc9c2;
  --text-on-dark-faint:  #b39a94;
  --text-on-light:       #1a1512;
  --text-on-light-soft:  #4c423b;
  /* Measured, not guessed: 6.28:1 on white and 5.33:1 on the sand. Both
     clear the 4.5:1 that normal-size body text needs. This token is used
     for map notes, footer notes and form hints — all of them real copy, so
     it cannot be treated as decorative. */
  --text-on-light-faint: #6a5e55;

  /* --- Accent: Wine Red --------------------------------------------------
     #7F011F is dark for a brand colour, which is a gift in one direction
     and a trap in the other:

     --accent        the fill. White on it measures 10.9:1, so the buttons
                     are the most readable thing on the page.
     --accent-hover  a deeper step for hover. A LIGHTER wine goes pink and
                     drops white text below 4.5:1, so hover goes down.
     --accent-deep   the text wine for LIGHT backgrounds. Same value as
                     --accent, because at 10.9:1 on white it needs no
                     darkening — the two tokens are kept separate so the
                     distinction survives if the palette ever changes.
     --accent-bright the accent for DARK backgrounds, and it is NOT a red.
                     Every light tint of this wine turns pink, which is off
                     the supplied palette. The brand's own second colour
                     does the job instead: Light Sand on wine-black measures
                     16.8:1 and stays inside the two colours supplied.
     --accent-wash   the palest wine, for tinted chips on white.           */
  --accent:        #7f011f;
  --accent-hover:  #5e0116;
  --accent-bright: #f5ebd0;   /* Light Sand — the on-dark accent           */
  --accent-deep:   #7f011f;
  --accent-wash:   #f9edf0;   /* pale wine tint for light backgrounds      */
  --accent-glow:   rgba(127, 1, 31, 0.30);
  --on-accent:     #ffffff;   /* text that sits ON a wine fill (10.9:1)    */

  /* --- Semantic ---------------------------------------------------------
     The error red deliberately stays its own colour. It sits next to the
     brand red often enough that they are hard to tell apart, which is why
     every error on this site also carries the alert icon and a text
     message rather than relying on colour alone. */
  --ok:     #2e7d32;
  --ok-bg:  #edf7ee;
  --warn:   #8a5a00;
  --warn-bg:#fbf3e3;
  --err:    #b3261e;
  --err-bg: #fdecea;

  /* --- Type -------------------------------------------------------------
     Inter for everything functional: it was drawn for screen UI and stays
     legible at 13px on a phone. Playfair Display only for display-size
     headings, where its high stroke contrast reads as editorial and
     expensive. Playfair is never used below about 20px — at small sizes the
     thin strokes disappear and it just looks blurry. */
  --font: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
  --font-num: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;

  /* Fluid sizes: min at 360px viewport, max at 1280px. */
  --fs-display: clamp(2.35rem, 1.2rem + 5.2vw, 4.75rem);
  --fs-h1:      clamp(2rem,   1.2rem + 3.6vw, 3.4rem);
  --fs-h2:      clamp(1.6rem, 1.1rem + 2.1vw, 2.5rem);
  --fs-h3:      clamp(1.2rem, 1rem + 0.8vw,  1.5rem);
  --fs-lead:    clamp(1.02rem, 0.96rem + 0.5vw, 1.24rem);
  --fs-body:    1rem;
  --fs-sm:      0.9rem;
  --fs-xs:      0.8rem;
  --fs-micro:   0.72rem;

  --lh-tight: 1.08;
  --lh-head:  1.18;
  --lh-body:  1.65;

  --track-tight: -0.022em;
  --track-caps:  0.14em;

  /* --- Space: a 4px-based scale ---------------------------------------- */
  --s-1: 0.25rem;  --s-2: 0.5rem;   --s-3: 0.75rem;  --s-4: 1rem;
  --s-5: 1.5rem;   --s-6: 2rem;     --s-7: 2.75rem;  --s-8: 3.5rem;
  --s-9: 5rem;     --s-10: 7rem;    --s-11: 9rem;

  /* Section rhythm, tighter on phones.

     The lower bound was 3.5rem, which put 56px above and below every one of
     the eleven sections on the homepage — about 1,200px of a 12,000px phone
     page spent on nothing. 2.5rem still reads as deliberate separation and
     gives roughly a screen and a half back. The upper bound is untouched:
     on a desktop the generous rhythm is most of what makes the layout look
     expensive, and there is room for it there. */
  --section-y: clamp(2.5rem, 1.5rem + 6vw, 7.5rem);

  /* --- Shape ------------------------------------------------------------ */
  --r-sm: 6px;  --r-md: 10px;  --r-lg: 16px;  --r-xl: 24px;  --r-pill: 999px;

  /* --- Elevation: layered, low-contrast, never a single heavy blur ------ */
  --sh-1: 0 1px 2px rgba(42, 14, 22, 0.06), 0 1px 3px rgba(42, 14, 22, 0.05);
  --sh-2: 0 2px 4px rgba(42, 14, 22, 0.06), 0 6px 14px rgba(42, 14, 22, 0.07);
  --sh-3: 0 4px 8px rgba(42, 14, 22, 0.07), 0 14px 34px rgba(42, 14, 22, 0.10);
  --sh-4: 0 8px 18px rgba(42, 14, 22, 0.10), 0 30px 64px rgba(42, 14, 22, 0.15);
  --sh-dark: 0 20px 50px rgba(0, 0, 0, 0.5);

  /* --- Motion ----------------------------------------------------------- */
  --dur-fast: 140ms;
  --dur:      240ms;
  --dur-slow: 520ms;
  /* Reveal duration. Kept under ~650ms on purpose: at 900ms a visitor who
     scrolls quickly outruns the fade and sees an apparently empty section. */
  --dur-cine: 620ms;
  --ease:      cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-out:  cubic-bezier(0.19, 1, 0.22, 1);
  --ease-in:   cubic-bezier(0.55, 0, 0.6, 0.2);

  /* --- Layout ----------------------------------------------------------- */
  --wrap:        1180px;
  --wrap-narrow: 760px;
  --header-h:    68px;
}


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

html {
  -webkit-text-size-adjust: 100%;

  /* NO `scroll-behavior: smooth` here. It is incompatible with GSAP
     ScrollTrigger and the failure is silent and confusing.
     ------------------------------------------------------------------
     ScrollTrigger measures a section by jumping the page to scroll 0,
     reading the element's position, then restoring the scroll — all
     synchronously. `scroll-behavior: smooth` turns that instant jump into
     an animation, so the measurement is taken while the page is still at
     the old position. The recorded start/end then come out wrong by exactly
     however far the page was scrolled, and they drift further on every
     refresh. In this project it made the journey truck finish its run while
     the visitor was still reading stage one.

     Anchor links therefore jump instantly. That is also what visitors who
     ask for reduced motion want, so nothing is lost. */

  /* Anchor targets still clear the sticky header. */
  scroll-padding-top: calc(var(--header-h) + var(--s-4));
}

body {
  margin: 0;
  background: var(--paper-sunk);
  color: var(--text-on-light);
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  /* NOTE: there is deliberately NO `overflow-x` on body.
     ------------------------------------------------------------------
     It is tempting to add `overflow-x: hidden` (or `clip`) here as a
     catch-all against sideways scrolling. Both break things badly in this
     project, and each failure is silent:

       `hidden` makes body a scroll container, which stops every
       `position: sticky` descendant from sticking — the journey road
       scrolls away instead of pinning.

       `clip` fixes that, but makes body a clip container, and GSAP
       ScrollTrigger then measures element offsets without adding the
       scroll position. Its start/end come out shifted by however far the
       page happens to be scrolled, so the truck's progress runs far ahead
       of the stages. (Observed: start computed as -219 instead of 1181,
       and drifting further on every refresh.)

     Horizontal overflow is prevented at source instead: see the narrow-phone
     header rules in §6 and the rail gutter in §17b. If a sideways scrollbar
     ever appears, find the element that is too wide — do not reach for
     overflow here. */
}

/* Locked while the quote modal is open. */
body.is-locked { overflow: hidden; }

img, svg, video, canvas { display: block; max-width: 100%; }
img { height: auto; }

/* The `hidden` attribute must actually hide.
   The browser's own rule is `[hidden] { display: none }`, which ANY author
   rule setting `display` silently beats — `.btn { display: inline-flex }`
   being the obvious one here. Several elements in this project start hidden
   and are only revealed once their configuration exists (phone links,
   WhatsApp links, the service-areas section). Without this rule they would
   all show up empty, and visitors would get dead `tel:` links.
   This is the one justified `!important` in the file. */
[hidden] { display: none !important; }

a { color: var(--accent-deep); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { color: var(--accent-deep); }

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }

/* One focus rule for the whole site. Visible on every interactive element,
   on both light and dark backgrounds. */
/* Gold at full strength, which stays visible on both the ivory and the
   near-black surfaces. The pale --accent-bright would vanish on white. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* Skip link: off-screen until focused. */
.skip {
  position: absolute; left: var(--s-4); top: -4rem; z-index: 200;
  background: var(--paper); color: var(--text-on-light);
  padding: var(--s-3) var(--s-4); border-radius: var(--r-md);
  box-shadow: var(--sh-3); font-weight: 500;
  transition: top var(--dur) var(--ease);
}
.skip:focus { top: var(--s-4); }

hr { border: 0; border-top: 1px solid var(--paper-line); margin: var(--s-7) 0; }

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


/* ===========================================================================
   3. TYPOGRAPHY
   =========================================================================== */
h1, h2, h3, h4 {
  margin: 0 0 var(--s-4);
  font-weight: 600;
  line-height: var(--lh-head);
  letter-spacing: var(--track-tight);
  text-wrap: balance;
}

/* Display sizes take the serif. h3 and h4 stay in Inter: they are often
   14–16px, where Playfair's thin strokes break up. */
h1, h2 {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: -0.012em;   /* the serif needs far less negative tracking
                                 than a grotesque; -0.022em closes it up */
  line-height: 1.14;
}
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); font-weight: 600; }
h4 { font-size: var(--fs-body); font-weight: 600; }

p { margin: 0 0 var(--s-4); }
p:last-child { margin-bottom: 0; }

/* Opening paragraph of a section. */
.lead {
  font-size: var(--fs-lead);
  color: var(--text-on-light-soft);
  line-height: 1.6;
  max-width: 62ch;
  text-wrap: pretty;
}

/* Small uppercase label above a heading.
   Uses the DARK gold: at 11-12px, uppercase and letterspaced, the surface
   gold (--accent) measures about 2:1 against ivory and is genuinely hard to
   read. Small text is exactly where a gold palette most often fails
   accessibility, so it takes the darkened token. */
.eyebrow {
  display: block;
  margin: 0 0 var(--s-3);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--accent-deep);
}
.on-dark .eyebrow { color: var(--accent-bright); }

/* Section intro block. */
.section-head { max-width: var(--wrap-narrow); margin-bottom: var(--s-7); }

/* Numerals in stats and prices: tabular so columns line up. */
.num { font-variant-numeric: tabular-nums; }


/* ===========================================================================
   4. LAYOUT
   =========================================================================== */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--s-4);
}
@media (min-width: 640px) { .wrap { padding-inline: var(--s-6); } }

.wrap-narrow { max-width: var(--wrap-narrow); margin-inline: auto; }

.section { padding-block: var(--section-y); }
.section.is-tight { padding-block: clamp(2.5rem, 1.5rem + 4vw, 4.5rem); }

/* Light and dark section skins. */
.on-paper { background: var(--paper); color: var(--text-on-light); }
.on-sunk  { background: var(--paper-sunk); color: var(--text-on-light); }
.on-dark  { background: var(--ink-800); color: var(--text-on-dark); }
.on-dark h1, .on-dark h2, .on-dark h3, .on-dark h4 { color: var(--text-on-dark); }
.on-dark .lead { color: var(--text-on-dark-soft); }
/* `:not(.btn)` matters. A button is an <a> too, and without this exclusion
   the rule overrides `--btn-fg` and paints accent-bright text onto the
   accent-filled primary button — amber on amber, 1.27:1. Buttons carry their
   own foreground; this rule is for prose links only. */
.on-dark a:not(.btn) { color: var(--accent-bright); }

/* Auto-fitting grid. Children decide nothing about width. */
.grid { display: grid; gap: var(--s-5); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); }

/* Split layout: content beside a visual. Stacks under 900px. */
.split { display: grid; gap: var(--s-7); align-items: center; }

/* A grid item's `min-width` defaults to `auto`, which means "never shrink
   below my content's minimum width". One wide, un-wrappable child — a table
   with `white-space: nowrap` headers, a long unbroken URL — therefore forces
   the whole column wider than the viewport, and the page gains a horizontal
   scrollbar that no amount of `max-width` on the child will remove.
   `min-width: 0` opts out, so the column tracks the viewport and the child
   with `overflow-x: auto` scrolls inside itself instead. */
.split > * { min-width: 0; }
@media (min-width: 900px) {
  .split { grid-template-columns: 1fr 1fr; gap: var(--s-8); }
  .split.is-wide-left  { grid-template-columns: 1.15fr 0.85fr; }
}


/* ===========================================================================
   5. BUTTONS
   =========================================================================== */
.btn {
  --btn-bg: var(--accent);
  /* White on the brand red, measured at 7.33:1. This is the one place the
     palette change actually flipped: on the old amber fill the text had to
     be near-black, and white would have failed. */
  --btn-fg: var(--on-accent);
  --btn-bd: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 48px;                 /* comfortable touch target */
  padding: 0 var(--s-5);
  border: 1px solid var(--btn-bd);
  border-radius: var(--r-md);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-decoration: none;
  white-space: nowrap;
  transition: transform var(--dur-fast) var(--ease),
              background-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
/* Hover goes DARKER, not lighter. --accent-bright is the on-dark tint; using
   it as a fill here would put white text on a pale coral at 2.8:1. */
.btn:hover { background: var(--accent-hover); color: var(--on-accent); }
.btn:active { transform: translateY(1px); }

.btn--primary { box-shadow: 0 2px 10px var(--accent-glow); }
.btn--primary:hover { box-shadow: 0 4px 18px var(--accent-glow); }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--text-on-light);
  --btn-bd: var(--paper-line);
}
.btn--ghost:hover { --btn-bg: var(--paper-sunk); color: var(--text-on-light); background: var(--paper-sunk); border-color: #ddc9a6; }

.on-dark .btn--ghost, .btn--ghost-dark {
  /* `--btn-bg: transparent` is load-bearing. Without it this rule sets only
     the foreground and border, and the button keeps the base `.btn` accent
     FILL — which put near-white text on an amber background in the header at
     1.84:1. `.btn--ghost` above declares it; this one used to inherit the
     fill and quietly fail contrast on every page. */
  --btn-bg: transparent;
  --btn-fg: var(--text-on-dark);
  --btn-bd: rgba(255, 255, 255, 0.22);
}
.on-dark .btn--ghost:hover, .btn--ghost-dark:hover {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-on-dark);
  border-color: rgba(255, 255, 255, 0.38);
}

/* WhatsApp green, used only on genuine WhatsApp actions.
   Darkened from the bright #1faa54 brand green, which measured 3.03:1
   against the white label on it and failed AA for 14px text. This is the
   same hue taken down until white clears 4.5:1 (it measures 5.0:1), which
   is what most accessible implementations of a WhatsApp button do. */
.btn--wa { --btn-bg: #17803f; }
.btn--wa:hover { background: #125f2f; }

.btn--lg { min-height: 56px; padding-inline: var(--s-6); font-size: var(--fs-body); }
.btn--sm { min-height: 40px; padding-inline: var(--s-4); font-size: var(--fs-xs); }
.btn--block { width: 100%; }

.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; }
.btn[disabled]:active { transform: none; }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--s-3); }

/* On a 320px phone the narrow grid columns leave a button about 126px of
   room, while `white-space: nowrap` above demands ~172px for its label —
   which no amount of flex-wrap can solve, because it is one unbreakable
   item. The result was a horizontal scrollbar on the whole page. Below
   360px the label is allowed to run onto a second line instead; it is less
   tidy than a single line and far tidier than a sideways-scrolling page. */
@media (max-width: 360px) {
  .btn { white-space: normal; }
  .btn-row > .btn { flex: 1 1 100%; }
}

/* A text link that behaves like a secondary CTA. */
.link-cta {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-size: var(--fs-sm); font-weight: 600; text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid currentColor;
}

/* --- Touch targets ---------------------------------------------------------
   Measured on a 390px viewport: the brand link came out 38px tall, the
   "How pricing works" style links 27px, and the footer's legal links 24px.
   Buttons and form fields were already comfortable (48px minimum) and the
   consent tick-box is fine because its whole 309x43 label toggles it, so
   this is only about the handful that were not.

   Width-based rather than `pointer: coarse`, to match how the rest of this
   stylesheet makes decisions, and because a touch laptop reporting a fine
   pointer would otherwise miss out. The extra padding costs nothing on a
   phone, where there is vertical room and none of these sit in a tight row. */
@media (max-width: 759px) {
  .brand { min-height: 44px; }
  .link-cta { padding-block: var(--s-2); }
  .footer__legal nav a { display: inline-block; padding-block: var(--s-2); }
  .crumbs a { display: inline-block; padding-block: var(--s-1); }
}
.link-cta .ic { transition: transform var(--dur) var(--ease); }
.link-cta:hover .ic { transform: translateX(3px); }

/* Inline icons, sized from the text. */
.ic { width: 1.15em; height: 1.15em; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }


/* ===========================================================================
   6. HEADER AND NAVIGATION
   =========================================================================== */
/* The header is LIGHT, and that is a consequence of the logo rather than a
   style preference. The company's mark is black line art on a transparent
   field — a thin black ellipse, a black ring, and a black truck. On the dark
   glass bar this used to be, every one of those strokes disappears and all
   that survives is the white lettering and the white inner field: the logo
   reads as a pale smear. The alternatives were to sit it on a white plate,
   which looks like a sticker stuck on the page, or to invert the artwork,
   which is altering a logo we were asked not to alter. A white bar is the
   one option that shows the mark as drawn. */
.header {
  position: sticky; top: 0; z-index: 100;
  background: rgba(255, 255, 255, 0.86);
  /* The one place glass is used. Degrades to the solid colour above. */
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  backdrop-filter: saturate(160%) blur(14px);
  border-bottom: 1px solid var(--paper-line);
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
/* Firms up once the page scrolls. Toggled by main.js. */
.header.is-stuck {
  background: rgba(255, 255, 255, 0.97);
  border-bottom-color: var(--paper-line);
  box-shadow: var(--sh-1);
}

.header__bar {
  display: flex; align-items: center; gap: var(--s-4);
  min-height: var(--header-h);
}

.brand {
  display: inline-flex; align-items: center; gap: var(--s-3);
  color: var(--text-on-light); text-decoration: none; font-weight: 600;
  letter-spacing: -0.015em; margin-right: auto;
}
/* The lockup is a wide oval (1442 x 1028), so it is sized by HEIGHT and the
   width follows. Fixing both would squash it. */
.brand__mark { height: 38px; width: auto; flex: none; }

/* In the footer the surface is near-black, so the same problem the white
   header solves comes back. Here a plate IS the right answer: a logo on a
   light chip at the foot of a dark page reads as a deliberate badge, which
   is not true of the same trick in the top bar. */
.brand--footer .brand__mark {
  height: 48px;
  padding: 5px 9px;
  background: var(--paper);
  border-radius: var(--r-md);
}
.brand__name { font-size: 1.02rem; line-height: 1.1; }
.brand__sub {
  display: block; font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--text-on-light-faint);
}

.nav { display: none; }
@media (min-width: 1000px) {
  .nav { display: flex; align-items: center; gap: var(--s-5); }
  .nav a {
    position: relative; color: var(--text-on-light-soft); text-decoration: none;
    font-size: var(--fs-sm); font-weight: 500; padding-block: var(--s-2);
    transition: color var(--dur) var(--ease);
  }
  .nav a:hover, .nav a[aria-current="page"] { color: var(--text-on-light); }
  /* Underline grows from the centre on hover. */
  .nav a::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
    background: var(--accent); border-radius: 2px;
    transform: scaleX(0); transition: transform var(--dur) var(--ease);
  }
  .nav a:hover::after, .nav a[aria-current="page"]::after { transform: scaleX(1); }
}

.header__actions { display: flex; align-items: center; gap: var(--s-3); flex: none; }
/* The call button carries the full number, which is wide. Below 560px there
   is not room for it next to the brand and the menu toggle, and squeezing it
   in collapses the brand name to one letter per line. Nothing is lost by
   hiding it: the same number is in the sticky bar at the foot of the screen
   on exactly those widths.

   This selector follows the class the markup actually uses. It said
   `.btn--ghost-dark` until the header turned white, at which point it
   stopped matching anything and the button reappeared at 320px. */
.header__actions .btn--ghost { display: none; }
@media (min-width: 560px) { .header__actions .btn--ghost { display: inline-flex; } }

/* Very narrow phones (360px and below is still common in India).
   At that width the brand, the quote button and the menu toggle together
   are wider than the viewport, which pushes the page sideways. The quote
   button is the one to drop: it is the first item in the mobile drawer and
   it is also in the sticky call/WhatsApp bar, so nothing is lost. */
@media (max-width: 430px) {
  .header__actions .btn--primary { display: none; }
  .brand__sub { display: none; }
  .brand__name { font-size: 0.95rem; }
}

/* Lets the brand shrink instead of forcing the row wider than the screen. */
.brand { min-width: 0; }
.brand__name { min-width: 0; overflow-wrap: anywhere; }

/* Hamburger. */
.nav-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: none;
  background: transparent; border: 1px solid var(--paper-line);
  border-radius: var(--r-md); color: var(--text-on-light);
}
@media (min-width: 1000px) { .nav-toggle { display: none; } }

/* Mobile drawer. Height animates via max-height so nothing jumps.
   It opens INSIDE the header, so it inherits the white bar. */
.nav-drawer {
  display: grid; overflow: hidden;
  max-height: 0;
  background: var(--paper);
  transition: max-height var(--dur-slow) var(--ease);
  border-top: 1px solid transparent;
}
.nav-drawer.is-open { max-height: 70vh; overflow-y: auto; border-top-color: var(--paper-line); }
.nav-drawer__inner { padding: var(--s-4) 0 var(--s-6); }
/* `:not(.btn)` is load-bearing. `.nav-drawer a` is more specific than
   `.btn`, so without it this rule repaints the "Request a callback" button
   inside the drawer: dark body text on the solid red fill, at 1.33:1. The
   same trap caught `.on-dark a` earlier in this stylesheet. */
.nav-drawer a:not(.btn) {
  display: block; padding: var(--s-3) 0; color: var(--text-on-light-soft);
  text-decoration: none; font-weight: 500;
  border-bottom: 1px solid var(--paper-line);
}
.nav-drawer a:not(.btn):hover { color: var(--text-on-light); }
.nav-drawer .btn { margin-top: var(--s-4); }
@media (min-width: 1000px) { .nav-drawer { display: none; } }


/* ===========================================================================
   7. HERO
   =========================================================================== */
/* THE HERO IS LIGHT, and that follows from the palette rather than taste.
   Wine Red is a dark colour. On the near-black hero this used to be, the
   wine CTA was a dark shape on a dark field: the white label on it still
   measured 10.9:1, so it was readable, but the BUTTON did not read as a
   button until you looked for it, and the first call to action on the site
   is the last thing that should need looking for.

   On Light Sand the same button is the darkest thing on the screen. The
   supplied palette is a dark accent and a pale ground; using it the other
   way round fights it. */
.hero {
  position: relative;
  isolation: isolate;
  background: var(--paper-sunk);
  color: var(--text-on-light);
  overflow: hidden;
  border-bottom: 1px solid var(--paper-line);
}

/* Depth behind the content: a single soft warm wash, no banding. */
.hero::before {
  content: "";
  position: absolute; inset: 0; z-index: -2;
  background:
    radial-gradient(120% 90% at 78% 8%, #fffaf0 0%, #f8f0da 46%, #f0e3c2 100%);
}
/* A very faint grid, for structure rather than decoration. */
.hero::after {
  content: "";
  position: absolute; inset: 0; z-index: -2; opacity: 0.5;
  background-image:
    linear-gradient(to right, rgba(127, 1, 31, 0.045) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(127, 1, 31, 0.045) 1px, transparent 1px);
  background-size: 76px 76px;
  mask-image: radial-gradient(70% 60% at 50% 40%, #000 0%, transparent 78%);
  -webkit-mask-image: radial-gradient(70% 60% at 50% 40%, #000 0%, transparent 78%);
}

.hero__inner {
  position: relative; z-index: 2;
  display: grid; gap: var(--s-6);
  padding-block: clamp(2.5rem, 1.5rem + 6vw, 5.5rem) clamp(2rem, 1rem + 4vw, 3.5rem);
}
@media (min-width: 980px) {
  .hero__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: center;
    gap: var(--s-7);
    min-height: min(84vh, 760px);
  }
}

.hero__copy { max-width: 36rem; }

.hero h1 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--fs-display);
  line-height: 1.06;
  letter-spacing: -0.014em;
  /* The tagline sits immediately beneath, so the gap here is small and the
     larger breathing space comes after the pair. */
  margin-bottom: var(--s-3);
}
/* A secondary clause in the headline, set in italic wine. One typographic
   gesture, not three — the restraint is what makes it read as expensive. */
.hero h1 .soft {
  font-style: italic;
  color: var(--accent-deep);
}

/* The tagline, directly under the business name. It borrows the headline's
   display face and the same italic wine as `.soft`, so the name and the line
   beneath it read as one typographic unit rather than two unrelated pieces
   of text. Sized well below the display scale: it supports the name, it does
   not compete with it.

   Wine on Light Sand measures 9.17:1 — this is a colour that can carry real
   text, which the pale coral it replaced could not. */
.hero__tagline {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.25rem, 1rem + 1.1vw, 1.85rem);
  line-height: 1.25;
  color: var(--accent-deep);
  margin: 0 0 var(--s-5);
  max-width: 34rem;
  /* Playfair's italic descenders clip against a tight line-height at the
     largest clamp value without this. */
  padding-bottom: 0.08em;
}

.hero__lead {
  font-size: var(--fs-lead);
  color: var(--text-on-light-soft);
  max-width: 34rem;
  margin-bottom: var(--s-6);
}

.hero .btn-row { margin-bottom: var(--s-6); }

/* Small commitments under the CTAs. Wording is service commitment only. */
.hero__points { display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-5); padding: 0; margin: 0; list-style: none; }
.hero__points li {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-size: var(--fs-sm); color: var(--text-on-light-soft);
}
.hero__points .ic { color: var(--accent-deep); }

/* The same row of commitments, for a LIGHT section. `.hero__points` sets
   its colours for the dark hero; dropping that class onto a white panel put
   #dcc9c2 text on white at 1.64:1, which is invisible rather than subtle. */
.point-list {
  display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-5);
  padding: 0; margin: 0; list-style: none;
}
.point-list li {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-size: var(--fs-sm); color: var(--text-on-light-soft);
}
.point-list .ic { color: var(--accent-deep); }

/* --- The hero photograph ---------------------------------------------------
   This cell used to hold a WebGL canvas with a generic 3D truck in it. The
   truck belonged to no one; it cost 813 KB of Three.js and three loaders to
   render, and a visitor deciding whether to trust a mover learned nothing
   from it. It is now a photograph of the company's own shopfront, which is
   both a fraction of the weight and the thing a customer actually wants to
   see: that there is a real office, with a real sign, at a real address.

   The aspect ratio is declared so the space is reserved before the image
   arrives and nothing shifts underneath it (CLS). */
.hero__stage {
  position: relative;
  /* `margin: 0` is load-bearing. This is a <figure>, and the browser default
     for one is `margin: 1em 40px`. With it left in, the stage sat 40px to
     the right of its grid cell and the hero's `overflow: hidden` quietly
     clipped the far edge of the photograph — visible only as a slightly
     cropped image, with no horizontal scrollbar to give it away. */
  margin: 0;
  aspect-ratio: 16 / 11;
  width: 100%;
  border-radius: var(--r-xl);
  overflow: hidden;
  background: var(--ink-700);
  box-shadow: var(--sh-dark);
}
@media (min-width: 980px) {
  .hero__stage { aspect-ratio: 4 / 3; }
}

.hero__photo {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}

/* A short caption strip over the foot of the photograph, saying what is in
   it. The gradient keeps the text legible whatever the photo does behind it. */
.hero__caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  margin: 0;
  padding: var(--s-7) var(--s-4) var(--s-3);
  font-size: var(--fs-xs);
  line-height: 1.4;
  color: #fff;
  background: linear-gradient(to top, rgba(10, 7, 5, 0.86) 0%, rgba(10, 7, 5, 0.6) 45%, transparent 100%);
}

/* Scroll cue. */
.hero__cue {
  display: none;
  position: absolute; left: 50%; bottom: var(--s-5); z-index: 3;
  transform: translateX(-50%);
  font-size: var(--fs-micro); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-on-light-faint);
}
@media (min-width: 980px) { .hero__cue { display: block; } }
.hero__cue .ic { margin-inline: auto; animation: cue 2.4s var(--ease) infinite; }
@keyframes cue {
  0%, 100% { transform: translateY(0); opacity: 0.5; }
  50%      { transform: translateY(5px); opacity: 1; }
}


/* ===========================================================================
   8. TRUST STRIP
   =========================================================================== */
/* White, directly under the sand hero. The strip used to be a dark band,
   which worked when the hero above it was dark too; between a sand hero and
   white services it would now be a stripe of near-black for no reason. */
.trust {
  background: var(--paper);
  border-bottom: 1px solid var(--paper-line);
}
.trust__grid {
  display: grid; gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  padding-block: var(--s-6);
}
.trust__item { display: flex; gap: var(--s-3); align-items: flex-start; }
.trust__item .ic { width: 1.5rem; height: 1.5rem; color: var(--accent-deep); margin-top: 2px; }
.trust__item h3 { font-size: var(--fs-sm); margin: 0 0 2px; color: var(--text-on-light); }
.trust__item p { font-size: var(--fs-xs); color: var(--text-on-light-soft); margin: 0; line-height: 1.5; }

/* Section 9 held the scroll-journey: a pinned road panel, a travelling
   truck, six milestone pins and the six stage cards beside them. It was
   the "How it works" section, and it went on 17 Oct 2026 at the client's
   request. ~300 lines of CSS, one JavaScript module, and the GSAP +
   ScrollTrigger pair that loaded on every page of the site to drive it. */

/* ===========================================================================
   10. CARDS
   =========================================================================== */
.card {
  display: flex; flex-direction: column;
  background: var(--paper-card);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: var(--sh-1);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
/* Lift only on devices that actually have a pointer. */
@media (hover: hover) {
  .card:hover { transform: translateY(-3px); box-shadow: var(--sh-3); border-color: #d4dde9; }
}

.card__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; margin-bottom: var(--s-4);
  border-radius: var(--r-md);
  background: var(--accent-wash); color: var(--accent-deep);
}
.card__icon .ic { width: 1.4rem; height: 1.4rem; }

.card h3 { margin-bottom: var(--s-2); }
.card p { color: var(--text-on-light-soft); font-size: var(--fs-sm); margin-bottom: var(--s-4); }
.card .link-cta { margin-top: auto; align-self: flex-start; color: var(--accent-deep); }

/* Whole-card link: the anchor covers the card without nesting interactives. */
.card--linked { position: relative; }
.card--linked .card__hit { position: absolute; inset: 0; border-radius: inherit; }

/* Feature row: icon beside text, no card chrome. */
.feature { display: grid; grid-template-columns: auto 1fr; gap: var(--s-4); }
.feature .ic { width: 1.6rem; height: 1.6rem; color: var(--accent-deep); }
.feature h3 { font-size: var(--fs-body); margin-bottom: var(--s-1); }
.feature p { font-size: var(--fs-sm); color: var(--text-on-light-soft); margin: 0; }

/* Numbered process steps. */
.steps { counter-reset: step; display: grid; gap: var(--s-5); }
.step { counter-increment: step; display: grid; grid-template-columns: auto 1fr; gap: var(--s-4); }
.step::before {
  content: counter(step, decimal-leading-zero);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm); font-weight: 600; color: var(--accent-deep);
  padding-top: 2px;
}

/* Checklist. */
.ticks { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s-3); }
.ticks li { display: grid; grid-template-columns: auto 1fr; gap: var(--s-3); font-size: var(--fs-sm); }
.ticks .ic { color: var(--ok); margin-top: 3px; }

/* ===========================================================================
   11. TESTIMONIALS
   =========================================================================== */
.quote {
  display: flex; flex-direction: column; gap: var(--s-4);
  background: var(--paper-card);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-lg);
  padding: var(--s-5);
}
.quote blockquote { margin: 0; font-size: var(--fs-sm); line-height: 1.65; color: var(--text-on-light); }
.quote__who { display: flex; align-items: center; gap: var(--s-3); margin-top: auto; }
.quote__avatar {
  display: grid; place-items: center;
  width: 38px; height: 38px; flex: none; border-radius: var(--r-pill);
  background: var(--accent-wash); color: var(--accent-deep);
  font-size: var(--fs-xs); font-weight: 600;
}
/* Both are spans inside one wrapper, so they must be made block-level or
   they run together on a single line. */
.quote__name { display: block; font-size: var(--fs-sm); font-weight: 600; line-height: 1.35; }
.quote__move { display: block; font-size: var(--fs-xs); color: var(--text-on-light-faint); line-height: 1.35; }

/* --- Star ratings ----------------------------------------------------------
   One star shape, filled or not. The row carries `role="img"` and an
   aria-label in reviews.js, so the stars themselves are decorative and a
   screen reader hears "Rated 5 out of 5" rather than five separate images. */
.stars { display: inline-flex; gap: 2px; }
.stars__star {
  width: 1em; height: 1em;
  stroke: var(--accent);
  stroke-width: 1.4;
  fill: none;
}
.stars__star.is-on { fill: var(--accent); }

/* --- A review card ---------------------------------------------------------
   The same `.quote` card as a first-party testimonial, plus a star row and a
   link back to the source. */
.quote--review { gap: var(--s-3); }
.quote--review .stars { font-size: 1rem; margin-bottom: var(--s-1); }

.quote__source {
  display: inline-flex; align-items: center; gap: var(--s-2);
  margin-top: var(--s-2);
  font-size: var(--fs-xs); font-weight: 600;
  color: var(--accent-deep);
  text-decoration: none;
}
.quote__source:hover { text-decoration: underline; }
.quote__source .ic { width: 1em; height: 1em; }

/* --- The overall rating, above the cards -----------------------------------
   This shows the profile's REAL figure across every review, and says so. It
   is never computed from the cards below, which are a selection of the good
   ones — see the note in js/reviews.js. */
.rating-summary {
  display: grid; justify-items: center; gap: var(--s-3);
  text-align: center;
  margin-bottom: var(--s-7);
}
.rating-summary__head {
  display: flex; align-items: center; flex-wrap: wrap;
  justify-content: center; gap: var(--s-2) var(--s-3);
}
.rating-summary__score {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  line-height: 1;
  color: var(--text-on-light);
}
.rating-summary__head .stars { font-size: 1.25rem; }
.rating-summary__count { font-size: var(--fs-sm); color: var(--text-on-light-soft); }
.rating-summary__note {
  max-width: 44ch;
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--text-on-light-faint);
}

.demo-banner {
  display: flex; align-items: flex-start; gap: var(--s-3);
  background: var(--warn-bg); border: 1px solid #f0dcb6;
  border-radius: var(--r-md); padding: var(--s-4);
  font-size: var(--fs-sm); color: var(--warn);
  margin-bottom: var(--s-5);
}
.demo-banner .ic { flex: none; margin-top: 2px; }


/* ===========================================================================
   11b. OUR CUSTOMERS, OUR WORK
   ---------------------------------------------------------------------------
   The business's own testimonials and job photographs. Kept visually
   distinct from the Google cards above: those are somebody else's platform,
   these are the company's own material, and a visitor is entitled to see
   which is which at a glance.
   =========================================================================== */

.quote--own { gap: var(--s-3); border-left: 3px solid var(--accent); }

/* The source badge. Not decoration, and not optional — it is the thing that
   stops a testimonial the office collected being mistaken for a Google
   review. js/own-content.js always renders it. */
.quote__source-tag {
  align-self: flex-start;
  margin-top: var(--s-2);
  padding: 2px var(--s-2);
  border-radius: var(--r-sm);
  background: var(--accent-wash);
  color: var(--accent-deep);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* Job photographs. `auto-fill` rather than a fixed column count, so one
   photo does not stretch across the whole page and twelve do not shrink to
   postage stamps. */
.workshots {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
}
.workshot {
  margin: 0;
  background: var(--paper-card);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--sh-1);
}
.workshot__img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--paper-sunk);
}
.workshot__cap {
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-sm);
  color: var(--text-on-light-soft);
}


/* ===========================================================================
   11e. PHOTO GALLERY
   ---------------------------------------------------------------------------
   CSS COLUMNS, not a grid, and that is the whole design decision.

   The photographs are a mix of portrait (963x1280) and landscape (1280x963).
   A grid of equal tiles would have to crop them to fit, and cropping a
   photograph of packed goods throws away the part that shows the work — the
   taped edge, the label, the corner protection. Columns let every picture
   keep its own shape at its own height.

   The cost is that reading order runs down each column rather than across
   the page. For a set of photographs with no narrative order, that is a fair
   trade; for anything sequential it would not be.
   =========================================================================== */
.gallery { columns: 1; column-gap: var(--s-4); }
@media (min-width: 560px) { .gallery { columns: 2; } }
@media (min-width: 900px) { .gallery { columns: 3; } }

.shot {
  /* Without this a tile can be split across two columns, which puts half a
     photograph at the foot of one column and half at the head of the next. */
  break-inside: avoid;
  margin: 0 0 var(--s-4);
  background: var(--paper-card);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--sh-1);
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
@media (hover: hover) {
  .shot:hover { box-shadow: var(--sh-3); transform: translateY(-2px); }
}

.shot__link { display: block; }
/* The focus ring belongs on the tile, not on the inline link box. */
.shot__link:focus-visible { outline: none; }
.shot:focus-within { box-shadow: 0 0 0 3px var(--accent); }

.shot img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-sunk);
}
.shot figcaption {
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-sm);
  color: var(--text-on-light-soft);
}


/* --- Lightbox --------------------------------------------------------------
   Progressive enhancement only. Each thumbnail is already a plain link to
   the full-size photograph, so with JavaScript off, or before this module
   loads, clicking one simply opens the image. js/lightbox.js intercepts. */
.lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: grid;
  grid-template-rows: 1fr auto;
  place-items: center;
  gap: var(--s-4);
  padding: var(--s-6) var(--s-4) var(--s-5);
  background: rgba(26, 6, 13, 0.94);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.lightbox[hidden] { display: none; }

.lightbox__img {
  max-width: 100%;
  max-height: 100%;
  width: auto; height: auto;
  border-radius: var(--r-md);
  box-shadow: var(--sh-dark);
}
.lightbox__cap {
  margin: 0;
  max-width: 60ch;
  text-align: center;
  font-size: var(--fs-sm);
  color: var(--text-on-dark-soft);
}
.lightbox__count {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--text-on-dark-faint);
}

.lightbox__btn {
  position: absolute;
  display: grid; place-items: center;
  width: 48px; height: 48px;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--r-pill);
  color: var(--text-on-dark);
  cursor: pointer;
}
.lightbox__btn:hover { background: rgba(255, 255, 255, 0.22); }
.lightbox__btn .ic { width: 1.4rem; height: 1.4rem; }
.lightbox__btn--close { top: var(--s-4); right: var(--s-4); }
/* `scaleX(-1)` turns the single arrow glyph around for "previous", so the
   sprite carries one arrow rather than two mirror images of it. */
.lightbox__btn--prev  { left: var(--s-3);  top: 50%; transform: translateY(-50%) scaleX(-1); }
.lightbox__btn--next  { right: var(--s-3); top: 50%; transform: translateY(-50%); }

/* On a phone the side buttons would sit on top of the photograph, so they
   move to the bottom corners where the caption already is. The flip has to
   be restated here, because `transform` replaces rather than merges. */
@media (max-width: 600px) {
  .lightbox__btn--prev { left: var(--s-4); top: auto; bottom: var(--s-4); transform: scaleX(-1); }
  .lightbox__btn--next { right: var(--s-4); top: auto; bottom: var(--s-4); transform: none; }
  .lightbox__cap { padding-inline: 56px; }
}


/* ===========================================================================
   11c. REVIEW POPUP
   ---------------------------------------------------------------------------
   One genuine review, bottom LEFT. The side matters: the floating WhatsApp
   button is bottom right and the sticky call bar is along the bottom on
   phones. A card over either of those costs the business the enquiry it was
   supposed to encourage.
   =========================================================================== */
.review-pop {
  position: fixed;
  left: var(--s-4);
  bottom: var(--s-4);
  z-index: 90;
  width: min(330px, calc(100vw - 2 * var(--s-4)));
  padding: var(--s-4) var(--s-5) var(--s-4) var(--s-4);
  background: var(--paper-card);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);

  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease-out);
}
.review-pop.is-in { opacity: 1; transform: none; }

/* On phones the sticky contact bar owns the bottom of the screen, so the
   card is lifted clear of it rather than overlapping. */
@media (max-width: 720px) {
  .review-pop { bottom: calc(var(--s-4) + 64px); }
}

.review-pop__close {
  position: absolute; top: 6px; right: 6px;
  display: grid; place-items: center;
  width: 32px; height: 32px;
  background: transparent; border: 0; border-radius: var(--r-sm);
  color: var(--text-on-light-faint);
  cursor: pointer;
}
.review-pop__close:hover { background: var(--paper-sunk); color: var(--text-on-light); }
.review-pop__close .ic { width: 1rem; height: 1rem; }

.stars--sm { font-size: 0.85rem; }

.review-pop__text {
  margin: var(--s-2) 0 var(--s-3);
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--text-on-light);
}
.review-pop__who { display: flex; align-items: baseline; gap: var(--s-2); margin: 0; flex-wrap: wrap; }
.review-pop__name { font-size: var(--fs-xs); font-weight: 600; }
.review-pop__src { font-size: var(--fs-micro); color: var(--text-on-light-faint); }
.review-pop__link {
  display: inline-flex; align-items: center; gap: var(--s-2);
  margin-top: var(--s-3);
  font-size: var(--fs-xs); font-weight: 600;
  color: var(--accent-deep); text-decoration: none;
}
.review-pop__link:hover { text-decoration: underline; }
.review-pop__link .ic { width: 1em; height: 1em; }


/* ===========================================================================
   11d. INLINE CALLBACK CARD
   ---------------------------------------------------------------------------
   The same form as the dialog, mounted in the page flow on the homepage.
   Built by js/callback-form.js.
   =========================================================================== */
.callback-card {
  background: var(--paper-card);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-2);
  padding: var(--s-5);
}
@media (min-width: 560px) { .callback-card { padding: var(--s-6); } }

.callback-card .field:last-of-type { margin-bottom: var(--s-5); }
.callback-card .form-note { margin-top: var(--s-3); }


/* The shopfront board on the contact page, above the map. */
.board-shot {
  /* A <figure> carries `margin: 1em 40px` by default; that 40px pushed the
     hero photo out of its own grid cell before it was caught. */
  margin: 0 0 var(--s-6);
  max-width: 32rem;
  background: var(--paper-card);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--sh-1);
}
.board-shot img { display: block; width: 100%; height: auto; }
.board-shot figcaption {
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-sm);
  color: var(--text-on-light-soft);
}


/* ===========================================================================
   12. FAQ ACCORDION
   ---------------------------------------------------------------------------
   Built on <details>, so every answer is in the DOM and crawlable, and it
   still works with JavaScript disabled.
   =========================================================================== */
.faq { display: grid; gap: var(--s-3); max-width: var(--wrap-narrow); }
.faq details {
  background: var(--paper-card);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-md);
  overflow: hidden;
}
.faq summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  font-weight: 600; font-size: var(--fs-sm);
  cursor: pointer; list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: ""; flex: none;
  width: 10px; height: 10px;
  border-right: 2px solid var(--text-on-light-faint);
  border-bottom: 2px solid var(--text-on-light-faint);
  transform: rotate(45deg) translateY(-2px);
  transition: transform var(--dur) var(--ease);
}
.faq details[open] summary::after { transform: rotate(-135deg) translateY(-2px); }
.faq summary:hover { background: var(--paper-sunk); }
.faq__body { padding: 0 var(--s-5) var(--s-5); }
.faq__body p { font-size: var(--fs-sm); color: var(--text-on-light-soft); }
.faq__body p:first-child { margin-top: 0; }


/* ===========================================================================
   13. FORMS AND THE MULTI-STEP MODAL
   =========================================================================== */
.field { display: grid; gap: var(--s-2); }
.field > label, .field__label {
  font-size: var(--fs-sm); font-weight: 600; color: var(--text-on-light);
}
.field__hint { font-size: var(--fs-xs); color: var(--text-on-light-faint); }

.input, .select, .textarea {
  width: 100%;
  min-height: 48px;
  padding: var(--s-3) var(--s-4);
  background: var(--paper);
  border: 1px solid var(--paper-line);
  border-radius: var(--r-md);
  font-size: var(--fs-body);   /* 16px: stops iOS zooming on focus */
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.textarea { min-height: 104px; resize: vertical; padding-top: var(--s-3); }
.input:hover, .select:hover, .textarea:hover { border-color: #c4d0e0; }
.input:focus, .select:focus, .textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(127, 1, 31, 0.22);
}
.input::placeholder, .textarea::placeholder { color: var(--text-on-light-faint); }

.select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-on-light-soft) 50%),
                    linear-gradient(135deg, var(--text-on-light-soft) 50%, transparent 50%);
  background-position: calc(100% - 19px) 21px, calc(100% - 14px) 21px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: var(--s-8);
}

/* Phone field with a fixed +91 prefix. */
.phone-field { display: flex; }
.phone-field__cc {
  display: inline-flex; align-items: center;
  padding: 0 var(--s-3);
  border: 1px solid var(--paper-line); border-right: 0;
  border-radius: var(--r-md) 0 0 var(--r-md);
  background: var(--paper-sunk);
  font-size: var(--fs-sm); font-weight: 600; color: var(--text-on-light-soft);
}
.phone-field .input { border-radius: 0 var(--r-md) var(--r-md) 0; }

/* Checkbox and radio rows. */
.check { display: grid; grid-template-columns: auto 1fr; gap: var(--s-3); align-items: start; cursor: pointer; }
.check input { width: 20px; height: 20px; margin: 2px 0 0; accent-color: var(--accent); flex: none; }
.check span { font-size: var(--fs-sm); line-height: 1.5; }

/* Multi-select service tiles. */
.choices { display: grid; gap: var(--s-2); grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); }
.choice { position: relative; }
.choice input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.choice label {
  display: flex; align-items: center; gap: var(--s-3);
  min-height: 48px; padding: var(--s-3) var(--s-4);
  border: 1px solid var(--paper-line); border-radius: var(--r-md);
  background: var(--paper); font-size: var(--fs-sm); cursor: pointer;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.choice label::before {
  content: ""; flex: none; width: 18px; height: 18px;
  border: 1.5px solid #c4d0e0; border-radius: 4px;
  transition: all var(--dur) var(--ease);
}
.choice input:checked + label { border-color: var(--accent); background: var(--accent-wash); }
.choice input:checked + label::before {
  background: var(--accent); border-color: var(--accent);
  /* A tick, drawn with a clip so no image is needed. */
  box-shadow: inset 0 0 0 2px var(--accent-wash), inset 0 0 0 9px var(--accent);
}
.choice input:focus-visible + label { outline: 3px solid var(--accent); outline-offset: 2px; }

/* Validation. */
.field.has-error .input,
.field.has-error .select,
.field.has-error .textarea { border-color: var(--err); box-shadow: 0 0 0 3px rgba(192, 39, 45, 0.12); }
.field__error {
  display: none;
  font-size: var(--fs-xs); font-weight: 500; color: var(--err);
}
.field.has-error .field__error { display: flex; align-items: center; gap: var(--s-2); }

.form-note { font-size: var(--fs-xs); color: var(--text-on-light-faint); line-height: 1.55; }

/* --- Modal --- */
.modal {
  position: fixed; inset: 0; z-index: 300;
  display: grid; place-items: end center;
  padding: 0;
  border: 0;
  background: transparent;
}
.modal[hidden] { display: none; }
@media (min-width: 720px) { .modal { place-items: center; padding: var(--s-5); } }

.modal__backdrop {
  position: fixed; inset: 0;
  background: rgba(20, 6, 11, 0.62);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}

.modal__panel {
  position: relative;
  display: flex; flex-direction: column;
  width: 100%; max-width: 640px;
  max-height: 92vh;
  background: var(--paper);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  box-shadow: var(--sh-4);
  overflow: hidden;
}
@media (min-width: 720px) { .modal__panel { border-radius: var(--r-xl); max-height: 88vh; } }

/* The callback dialog is four fields. 640px made it look like a form with
   something missing. */
.modal__panel--compact { max-width: 480px; }

.modal__head {
  display: grid; gap: var(--s-3);
  padding: var(--s-5) var(--s-5) var(--s-4);
  border-bottom: 1px solid var(--paper-line);
}
.modal__title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-4); }
.modal__head h2 { font-size: var(--fs-h3); margin: 0; }
.modal__head p { font-size: var(--fs-xs); color: var(--text-on-light-faint); margin: 0; }

.modal__close {
  display: grid; place-items: center;
  width: 40px; height: 40px; flex: none;
  background: var(--paper-sunk); border: 1px solid var(--paper-line);
  border-radius: var(--r-md); color: var(--text-on-light-soft);
}
.modal__close:hover { background: var(--paper-sunk); color: var(--text-on-light); }

/* The step meter and the review table that used to live here went with the
   five-step estimate form. The callback form has one step and four fields,
   so there is no progress to report and nothing to review before sending. */

.modal__body { padding: var(--s-5); overflow-y: auto; -webkit-overflow-scrolling: touch; }
.modal__foot {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-top: 1px solid var(--paper-line);
  background: var(--paper-sunk);
}

/* One full-width button with the response-time line beneath it. */
.modal__foot--stack { display: grid; gap: var(--s-2); }
.modal__foot--stack .form-note { margin: 0; }

.fstep { display: grid; gap: var(--s-5); }
.fstep[hidden] { display: none; }
.fstep__pair { display: grid; gap: var(--s-4); }
@media (min-width: 560px) { .fstep__pair { grid-template-columns: 1fr 1fr; } }

/* Submission states. */
.form-state { display: grid; gap: var(--s-4); place-items: center; text-align: center; padding-block: var(--s-6); }
.form-state__icon { display: grid; place-items: center; width: 56px; height: 56px; border-radius: var(--r-pill); }
.form-state--ok .form-state__icon { background: var(--ok-bg); color: var(--ok); }
.form-state--err .form-state__icon { background: var(--err-bg); color: var(--err); }
.form-state .ic { width: 1.8rem; height: 1.8rem; }

/* Spinner inside the submit button. */
.spinner {
  width: 1.05em; height: 1.05em; flex: none;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Honeypot: a real field, visually removed. Bots fill it, humans never see
   it. Not `display:none`, which some bots skip. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }


/* ===========================================================================
   14. CONTACT AND MAP
   =========================================================================== */
.contact-grid { display: grid; gap: var(--s-6); }
@media (min-width: 900px) { .contact-grid { grid-template-columns: 0.9fr 1.1fr; gap: var(--s-8); } }

.contact-list { display: grid; gap: var(--s-5); list-style: none; padding: 0; margin: 0; }
.contact-list li { display: grid; grid-template-columns: auto 1fr; gap: var(--s-4); }
.contact-list .ic { width: 1.4rem; height: 1.4rem; color: var(--accent-deep); margin-top: 3px; }
.contact-list dt, .contact-list .label {
  font-size: var(--fs-micro); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-on-light-faint); margin-bottom: 2px;
}
.contact-list a { font-weight: 600; text-decoration: none; }

/* Map frame. Height reserved so the iframe cannot shift the layout.
   4:3 on a phone is a tall letterbox that pushes everything below it off the
   screen, so the ratio widens out as there is room to spare. */
.map {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--paper-sunk);
  border: 1px solid var(--paper-line);
}
@media (min-width: 560px) { .map { aspect-ratio: 4 / 3; } }
@media (min-width: 900px) { .map { aspect-ratio: 21 / 9; } }

.map iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Shown when no coordinates are configured yet. */
.map__empty { display: grid; place-items: center; height: 100%; padding: var(--s-5); text-align: center; font-size: var(--fs-sm); color: var(--text-on-light-faint); }

/* The honesty note under an address-derived pin. */
.map__note {
  max-width: var(--wrap-narrow);
  color: var(--text-on-light-faint);
}


/* ===========================================================================
   15. FOOTER
   =========================================================================== */
.footer { background: var(--ink-900); color: var(--text-on-dark-soft); }
.footer__top { display: grid; gap: var(--s-7); padding-block: var(--s-8); }

/* On phones the four blocks used to stack into a single column: eight
   service links, then six company links, then the contact details, one
   under the other. That is most of a screen of footer before the legal
   line. The two link lists sit side by side instead — they are short labels
   and they read fine in half a phone width — while the blurb at the top and
   the contact details at the bottom keep the full width, because an address
   broken across 170px does not. */
@media (max-width: 759px) {
  .footer__top { grid-template-columns: 1fr 1fr; gap: var(--s-6) var(--s-5); }
  .footer__about,
  .footer__top > div:last-child { grid-column: 1 / -1; }
}

@media (min-width: 760px) { .footer__top { grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: var(--s-6); } }

.footer h3 {
  font-size: var(--fs-micro); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-on-dark-faint);
  margin-bottom: var(--s-4);
}
.footer ul { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s-3); }
.footer a { color: var(--text-on-dark-soft); text-decoration: none; font-size: var(--fs-sm); }
.footer a:hover { color: var(--text-on-dark); text-decoration: underline; }
.footer__about p { font-size: var(--fs-sm); color: var(--text-on-dark-faint); max-width: 34ch; }

.footer__legal {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3) var(--s-5);
  padding-block: var(--s-5);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: var(--fs-xs); color: var(--text-on-dark-faint);
}
.footer__legal nav { display: flex; flex-wrap: wrap; gap: var(--s-4); margin-left: auto; }

.socials { display: flex; gap: var(--s-3); margin-top: var(--s-4); }
.socials a {
  display: grid; place-items: center;
  width: 40px; height: 40px;
  border: 1px solid rgba(255, 255, 255, 0.14); border-radius: var(--r-md);
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.socials a:hover { background: rgba(255, 255, 255, 0.07); border-color: rgba(255, 255, 255, 0.3); }


/* ===========================================================================
   16. STICKY MOBILE CONTACT BAR
   =========================================================================== */
.contact-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--paper-line);
  border-top: 1px solid var(--paper-line);
  box-shadow: 0 -4px 20px rgba(42, 14, 22, 0.12);
  /* Clears the iOS home indicator. */
  padding-bottom: env(safe-area-inset-bottom);
}
.contact-bar a {
  display: flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: 54px; background: var(--paper);
  font-size: var(--fs-sm); font-weight: 600; text-decoration: none;
  color: var(--text-on-light);
}
.contact-bar a:first-child { color: var(--accent-deep); }
.contact-bar a:last-child { color: #15803d; }
@media (min-width: 860px) { .contact-bar { display: none; } }
/* Keeps the bar from covering the end of the page. */
@media (max-width: 859px) { body.has-contact-bar { padding-bottom: 54px; } }

/* Floating WhatsApp button, desktop only (mobile uses the bar above). */
.wa-float {
  position: fixed; right: var(--s-5); bottom: var(--s-5); z-index: 90;
  display: none;
  align-items: center; gap: var(--s-3);
  min-height: 52px; padding: 0 var(--s-5);
  /* Same darkened green as `.btn--wa` — the bright brand green put this
     label at 3.03:1 against its own background. */
  background: #17803f; color: #fff;
  border-radius: var(--r-pill);
  box-shadow: 0 6px 20px rgba(23, 128, 63, 0.34);
  font-size: var(--fs-sm); font-weight: 600; text-decoration: none;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
@media (min-width: 860px) { .wa-float { display: inline-flex; } }
.wa-float:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(23, 128, 63, 0.42); color: #fff; }


/* The side scroll-truck rail (section 17b) was removed on 17 Oct 2026,
   together with the journey section it belonged to. It was decorative, it
   restated the scrollbar, and below 1280px it had to push every heading,
   paragraph and card 24px out of true to make room for itself. */

/* ===========================================================================
   17. BREADCRUMBS AND MISC
   =========================================================================== */
.crumbs { padding-block: var(--s-4); font-size: var(--fs-xs); color: var(--text-on-light-faint); }
.crumbs ol { display: flex; flex-wrap: wrap; gap: var(--s-2); list-style: none; padding: 0; margin: 0; }
.crumbs li:not(:last-child)::after { content: "/"; margin-left: var(--s-2); color: #c9b896; }
.crumbs a { color: var(--text-on-light-soft); text-decoration: none; }
.crumbs a:hover { color: var(--accent-deep); text-decoration: underline; }
.crumbs [aria-current="page"] { color: var(--text-on-light); font-weight: 500; }

/* Page banner on inner pages. */
.page-head { background: var(--ink-800); color: var(--text-on-dark); padding-block: clamp(2.5rem, 1.5rem + 4vw, 4.5rem); }
.page-head h1 { margin-bottom: var(--s-4); }
.page-head .lead { color: var(--text-on-dark-soft); }
.page-head .crumbs { color: var(--text-on-dark-faint); padding-top: 0; }
.page-head .crumbs a { color: var(--text-on-dark-soft); }
.page-head .crumbs [aria-current="page"] { color: var(--text-on-dark); }

/* Final call-to-action band.

   SOLID Wine Red, and it is the only full-width block of brand colour on
   the site. With a light hero and light sections throughout, the page needs
   one place where the colour carries the whole width, and the last ask
   before the footer is the right place for it. Everywhere else the wine is
   a button, a rule or a heading.

   The primary button inside it cannot also be wine — it would vanish. It
   inverts to Light Sand with wine text, which measures 9.17:1. */
.cta-band {
  position: relative;
  isolation: isolate;
  background: var(--accent);
  color: #fff;
  overflow: hidden;
}
/* One soft highlight from the top, so the block is not a flat slab. */
.cta-band::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: radial-gradient(70% 120% at 50% 0%, rgba(255, 255, 255, 0.10), transparent 70%);
}
.cta-band h2 { color: #fff; }
.cta-band .lead, .cta-band p { color: rgba(255, 255, 255, 0.86); }

.cta-band .btn--primary {
  --btn-bg: var(--paper-sunk);
  --btn-fg: var(--accent-deep);
  box-shadow: none;
}
.cta-band .btn--primary:hover { background: #fff; color: var(--accent-deep); }

.cta-band .btn--ghost { --btn-fg: #fff; --btn-bd: rgba(255,255,255,0.45); }
.cta-band .btn--ghost:hover { background: rgba(255,255,255,0.12); color: #fff; border-color: rgba(255,255,255,0.7); }

/* 404. */
.notfound { display: grid; place-items: center; min-height: 68vh; text-align: center; }
.notfound__code { font-size: clamp(4rem, 2rem + 12vw, 8rem); font-weight: 600; line-height: 1; letter-spacing: -0.04em; color: var(--accent-deep); }


/* ===========================================================================
   18. UTILITIES AND SCROLL REVEALS
   =========================================================================== */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.text-centre { text-align: center; }
.text-sm { font-size: var(--fs-sm); }
.mt-0 { margin-top: 0; }
.mb-0 { margin-bottom: 0; }
.mt-2 { margin-top: var(--s-2); }
.mt-4 { margin-top: var(--s-4); }
.mt-5 { margin-top: var(--s-5); }
.mt-6 { margin-top: var(--s-6); }
.mt-7 { margin-top: var(--s-7); }
.mt-8 { margin-top: var(--s-8); }
.mb-3 { margin-bottom: var(--s-3); }
.mb-4 { margin-bottom: var(--s-4); }
.gap-6 { gap: var(--s-6); }
.flow > * + * { margin-top: var(--s-4); }

/* Centred button row, used in the CTA band and the form states. */
.btn-row--centre { justify-content: center; }

/* ---------------------------------------------------------------------------
   Data table — the intercity route list on the city pages.

   The wrapper scrolls rather than the page: a table is the one element that
   genuinely cannot reflow below about 320px, and letting it push the document
   wider would reintroduce the horizontal overflow this build works to avoid.
   `overflow-x: auto` on a wrapper div is safe here because, unlike on <body>,
   it creates no clip container that `position: sticky` or ScrollTrigger has
   to measure through.
   --------------------------------------------------------------------------- */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--paper-line);
  border-radius: var(--r-md);
  background: var(--paper);
}

.table {
  width: 100%;
  min-width: 18rem;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.table th,
.table td {
  padding: var(--s-3) var(--s-4);
  text-align: left;
  border-bottom: 1px solid var(--paper-line);
}

.table thead th {
  font-family: var(--font);
  font-weight: 600;
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-500);
  background: var(--paper-sunk);
  white-space: nowrap;
}

/* The figure column reads better right-aligned against the border. */
.table td + td { text-align: right; white-space: nowrap; color: var(--ink-600); }
.table tbody tr:last-child th,
.table tbody tr:last-child td { border-bottom: 0; }
/* NOT `color-mix(... , transparent)`. Mixing with `transparent` in srgb mixes
   its CHANNELS too, and `transparent` is rgba(0,0,0,0) — so the result is
   pulled towards black, not towards "less of this colour". It rendered as
   mid-grey rgb(115,115,115) and dropped the row text to 2.65:1. The stripe is
   just the sunk paper tone; the table sits on white, so that is all it
   needs. */
.table tbody tr:nth-child(even) { background: var(--paper-sunk); }

/* A horizontal list of links, e.g. the suggestions on the 404 page. */
.link-row {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: var(--s-3) var(--s-5);
  list-style: none; padding: 0; margin: 0;
  font-size: var(--fs-sm);
}
.link-row a { color: var(--accent-deep); text-decoration: none; font-weight: 500; }
.link-row a:hover { text-decoration: underline; }

/* The off-screen SVG sprite holder. */
.sprite { position: absolute; }

/* Icon size overrides, so no element needs an inline width. */
.ic--lg { width: 1.4rem; height: 1.4rem; }
.ic--cue { width: 1rem; height: 1rem; margin-top: 4px; }

/* A hint sitting directly under a legend, pulled up against it. */
.hint-tight { margin-top: calc(var(--s-4) * -1); }

/* "(optional)" inside a field label: same size, normal weight. */
.label-optional { font-weight: 400; }

/* Small faint note in the footer, e.g. the postal address. */
.footer__note { font-size: var(--fs-xs); color: var(--text-on-dark-faint); }
/* The footer address is an <address> element, which every browser defaults to
   italic. It is the right element -- it tells a parser and a screen reader
   what that block of text is -- but the italic is a browser default, not a
   decision anyone made here, and nothing else on this site is set in it. */
address.footer__note { font-style: normal; }

/* --- Reveal on scroll ------------------------------------------------------
   Elements start hidden ONLY when JavaScript is available (the `js` class is
   set by a tiny inline script in <head>). Without JS everything is visible,
   so content is never lost to a failed script. */
.js .reveal { opacity: 0; transform: translateY(18px); }
.js .reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity var(--dur-cine) var(--ease-out), transform var(--dur-cine) var(--ease-out);
}
/* Stagger children of a revealed group. */
.js .reveal-group > * { opacity: 0; transform: translateY(18px); }
.js .reveal-group.is-in > * {
  opacity: 1; transform: none;
  transition: opacity var(--dur-cine) var(--ease-out), transform var(--dur-cine) var(--ease-out);
}
.js .reveal-group.is-in > *:nth-child(2) { transition-delay: 70ms; }
.js .reveal-group.is-in > *:nth-child(3) { transition-delay: 140ms; }
.js .reveal-group.is-in > *:nth-child(4) { transition-delay: 210ms; }
.js .reveal-group.is-in > *:nth-child(5) { transition-delay: 280ms; }
.js .reveal-group.is-in > *:nth-child(6) { transition-delay: 350ms; }
.js .reveal-group.is-in > *:nth-child(7) { transition-delay: 420ms; }
.js .reveal-group.is-in > *:nth-child(8) { transition-delay: 490ms; }

/* ===========================================================================
   19. REDUCED MOTION AND PRINT
   =========================================================================== */
/* Honour the operating-system setting. Content is never hidden by this —
   reveals resolve to their final state rather than being skipped. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .js .reveal, .js .reveal-group > * { opacity: 1 !important; transform: none !important; }
  .hero__cue .ic { animation: none; }
  @media (hover: hover) { .card:hover { transform: none; } }
}

@media print {
  .header, .nav-drawer, .contact-bar, .wa-float, .modal,
  .hero__stage, .hero__cue, .review-pop { display: none !important; }
  body { background: #fff; color: #000; }
  .on-dark, .hero, .footer, .cta-band { background: #fff !important; color: #000 !important; }
  .on-dark h1, .on-dark h2, .on-dark h3, .footer a { color: #000 !important; }
  a { text-decoration: underline; }
  /* Print the destination of a link, since the reader cannot click it. */
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; }
  .section { padding-block: 1.2rem; }
}
