/* ============================================================
   Seawolf Cleaning Services — site.css
   Design system implementation of STYLE-GUIDE.md.
   Becomes style.css of the WordPress theme — see README-WORDPRESS.md.
   ============================================================ */

/* ---- Fonts (self-hosted; copy /assets/fonts/ into the theme and
   wp_enqueue_style this file — never a Google Fonts CDN link) ---- */
@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/golos-text-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url('../fonts/golos-text-900.woff2') format('woff2');
}
/* Fonts are .woff2 subsets of the original .ttf files (kept locally in
   _unused-assets/site/assets/fonts/, with the retired Public Sans and Bitter), cut down
   to the characters the site uses. Re-run the subset step if copy gains new symbols. */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/poppins-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/poppins-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/poppins-600.woff2') format('woff2');
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/poppins-700.woff2') format('woff2');
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
img, svg { display: block; max-width: 100%; height: auto; }
ul, ol { list-style: none; }
a { color: inherit; text-decoration: none; }
button, input, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; }

/* ---- Tokens ---- */
:root {
  /* "White" sections lean cool toward the brand blue instead of a warm cream — a subtle
     tint derived from --navy via color-mix(), not a new hardcoded colour, so it still
     only ever authors the three approved hex values. */
  --bg:         color-mix(in srgb, #2587f7 3%, white);
  --bg-2:       color-mix(in srgb, #2587f7 6%, white);
  --surface:    #FFFFFF;
  --ink:        #0B1F2E;
  --ink-2:      #48545F;
  --line:       color-mix(in srgb, #2587f7 15%, white);
  --navy:       #2587f7;
  --navy-deep:  #163663;
  --navy-ink:   #FFFFFF; /* text on navy */
  --brass:      #88eb37;   /* the one accent */
  --brass-deep: color-mix(in srgb, #88eb37 78%, black);
  --sea-glass:  #88eb37;

  --font-display: 'Golos Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  --font-body:    'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --container: 1600px;
  --gutter:    clamp(20px, 5vw, 64px);
  /* .site-header's rendered height: 76px with the desktop bar (the CTA button is its
     tallest item), 67px in hamburger mode (redefined in that media block). .hero-full
     pulls itself up by exactly this much to sit behind the glass header; a mismatch
     either pokes the hero photo up over the ribbon's bottom edge (a visible band
     between ribbon and header) or leaves a strip of the header with no photo behind it.
     If the header's contents or padding change, re-measure and update both values. */
  --header-h:  76px;
  --rhythm:    clamp(64px, 8vw, 120px);
  --radius:    5px;

  --shadow-card: 0 1px 2px rgba(14,30,43,0.06);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* Intentionally no `@media (prefers-color-scheme: dark)` override: the site stays on
   the light palette regardless of the visitor's OS setting, mostly-white with navy/blue
   used only in deliberate accent bands (hero, CTA strips). There is no theme toggle in
   the UI, so auto-switching here previously meant any visitor with OS dark mode got a
   near-black site with no way to opt out. The dead :root[data-theme] blocks below are
   kept in case a manual toggle is added later, but nothing currently sets that attribute. */
:root[data-theme="dark"] {
  --bg:         #081521;
  --bg-2:       #0D1F2E;
  --surface:    #112B3E;
  --ink:        #EEF1F4;
  --ink-2:      #B9C4CC;
  --line:       #1E4362;
  --navy:       #2587f7;
  --navy-deep:  #163663;
  --brass:      #88eb37;
  --brass-deep: color-mix(in srgb, #88eb37 78%, black);
  --sea-glass:  #88eb37;
  --shadow-card: 0 1px 2px rgba(0,0,0,0.4);
}
:root[data-theme="light"] {
  --bg:         color-mix(in srgb, #2587f7 3%, white);
  --bg-2:       color-mix(in srgb, #2587f7 6%, white);
  --surface:    #FFFFFF;
  --ink:        #0B1F2E;
  --ink-2:      #48545F;
  --line:       color-mix(in srgb, #2587f7 15%, white);
  --navy:       #2587f7;
  --navy-deep:  #163663;
  --brass:      #88eb37;
  --brass-deep: color-mix(in srgb, #88eb37 78%, black);
  --sea-glass:  #88eb37;
  --shadow-card: 0 1px 2px rgba(14,30,43,0.06);
}

/* ---- Base ---- */
html {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--ink);
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  scroll-behavior: smooth;
  overflow-x: hidden;
  /* .site-header is sticky (--header-h: 76px desktop / 67px hamburger) with no scroll-padding to
     compensate — any scrollIntoView (anchor links, :target, browser find-in-page,
     DevTools "scroll into view") tucks the target's top edge under the header
     instead of stopping below it. This is a global inset, not per-element scroll-margin. */
  scroll-padding-top: 90px;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
body { line-height: 1.65; }
:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }

.container { max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
/* Left-anchored narrow content column (headline/intro/prose blocks capped narrower than
   the site's normal 1600px container, for readability): margin-inline:0 alone only lines
   up with the full-width container's own content edge on viewports <=1600px, where that
   wider container's own auto-centering resolves to 0 anyway. On anything wider (a large
   desktop monitor), the full-width container still centers itself with a real margin, so
   a flat 0 here pulls the narrow column further left than the header/breadcrumb above it
   — this recreates that same centering offset instead of discarding it. */
.container--left { margin-inline: 0; margin-left: max(0px, calc((100vw - var(--container)) / 2)); }
/* Section layouts. Every section starts on the same left edge as the logo and breadcrumb
   (the full .container); a narrow section caps its CHILDREN's width, it never shrinks and
   centres the container itself. Pages used to do the latter with an inline
   max-width:clamp(880px, 60vw, 1200px), which put every narrow section on its own
   centred left edge, so on a wide screen the breadcrumb, intro, lists and FAQ each
   started at a different x and the page read as loose blocks rather than one grid. */
/* Page intro: breadcrumb-page H1, lede, ticks and CTAs (also carries whole policy pages). */
.container--intro > * { max-width: 880px; }
/* Blog article body: a reading measure, on the same edge as the article's H1 above it. */
.container--prose > * { max-width: 700px; }
/* Heading + body (FAQ blocks, a short explainer): stacked on small screens; on wide ones
   the heading sits in its own column beside the body, so the section uses the page width
   instead of leaving a narrow strip with empty space either side. */
.container--split > * { max-width: 880px; }
@media (min-width: 1000px) {
  .container--split {
    display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    column-gap: clamp(48px, 6vw, 112px); align-items: start;
  }
  .container--split > * { grid-column: 2; max-width: 820px; margin-top: 0; }
  .container--split > * + * { margin-top: 16px; }
  /* span 20: the heading's column runs the full height of however many body children
     there are, which is also what lets it stay in view (sticky) down a long FAQ. */
  .container--split > :first-child {
    grid-column: 1; grid-row: 1 / span 20; max-width: 22ch;
    position: sticky; top: calc(var(--header-h) + 32px);
  }
}
/* Running-text lists (policy pages, articles). The global ul reset strips markers, which
   left policy lists reading as unbroken run-on lines. Classed lists (checklists, nav,
   footers) keep their own styling. */
.container--intro ul:not([class]), .container--prose ul:not([class]) { list-style: disc; padding-left: 20px; }
.container--intro ul:not([class]) li + li, .container--prose ul:not([class]) li + li { margin-top: 6px; }
.container--intro ul:not([class]) li::marker, .container--prose ul:not([class]) li::marker { color: var(--brass); }
.band { padding-block: var(--rhythm); border-top: 1px solid var(--line); }
.band:first-of-type { border-top: none; }
.band--bg2 { background: var(--bg-2); }
.band--navy { background: var(--navy); color: #fff; border-top-color: rgba(255,255,255,0.12); }
.band--navy .ink-2, .band--navy p { color: rgba(255,255,255,0.78); }
.band--navy-deep { background: var(--navy-deep); color: #fff; }
.band--navy-deep .ink-2, .band--navy-deep p { color: rgba(255,255,255,0.78); }
/* p.lede (element+class, same specificity as .band--navy p above) is defined further
   down and would otherwise win on source order alone, putting dark ink text back on a
   navy background — these need the extra class to actually out-specificity it. */
.band--navy .lede, .band--navy-deep .lede { color: rgba(255,255,255,0.9); }

/* ---- Type ---- */
h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 800; line-height: 1.08; text-wrap: balance; color: var(--ink); }
.band--navy h1, .band--navy h2, .band--navy h3,
.band--navy-deep h1, .band--navy-deep h2, .band--navy-deep h3 { color: #fff; }

h1 { font-size: clamp(2.6rem, 1rem + 5vw, 4.6rem); font-weight: 900; letter-spacing: -0.01em; }
h2 { font-size: clamp(1.9rem, 1rem + 2.6vw, 2.75rem); font-weight: 800; }
h3 { font-size: 1.375rem; font-weight: 700; }
p { color: var(--ink-2); max-width: 65ch; }
p.lede { font-size: 1.125rem; line-height: 1.6; color: var(--ink); max-width: 58ch; }

.eyebrow {
  display: inline-flex; align-items: center; gap: 8px;
  font: 700 0.75rem/1 var(--font-body); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--brass);
}
.eyebrow::before { content: ''; width: 18px; height: 1.5px; background: var(--brass); }

.tabular { font-variant-numeric: tabular-nums; }

/* ---- Buttons ---- */
/* Same interaction system as Custodia's public-site CTA button (app/styles/public-site.css
   .btn / .btn.primary): pill shape, lift + growing shadow on hover, press-down on active,
   a diagonal shine sweep across the primary variant on hover, focus-visible ring. Colour
   swapped to the brass accent; every shade here is derived from the three approved tokens
   via color-mix(), never a new literal hex. */
.btn {
  position: relative; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 13px 26px; border-radius: 999px; font: 700 0.9375rem var(--font-body);
  border: 1px solid transparent; max-width: 100%;
  box-shadow: 0 8px 20px rgba(11,31,46,0.1);
  transition: transform 180ms var(--ease-out), box-shadow 180ms var(--ease-out), background 180ms var(--ease-out), border-color 180ms var(--ease-out), color 180ms var(--ease-out);
  white-space: nowrap; cursor: pointer;
}
/* white-space:nowrap above is right for the site's normal short CTA labels (keeps them
   from breaking on an awkward word), but a longer label ("Ask us, we'll give you a
   straight answer") has no room to stay on one line once its column has shrunk to an
   actual phone's width — nowrap then just runs the text off the edge of the pill instead
   of wrapping it. Letting it wrap below 480px, where this only ever bites, keeps every
   other button's single-line look intact. */
@media (max-width: 480px) {
  .btn { white-space: normal; text-align: center; }
}
.btn svg { transition: transform 200ms ease; flex-shrink: 0; }
/* Lift kept minimal (was -2px) and the shadow growth toned down to match — still gives
   hover feedback without the pronounced "jump". */
.btn:hover { box-shadow: 0 10px 20px rgba(11,31,46,0.12); }
.btn:active { transform: translateY(0) scale(0.98); transition-duration: 80ms; }
.btn:focus-visible { outline: 3px solid color-mix(in srgb, var(--brass) 45%, transparent); outline-offset: 2px; }
.btn-primary {
  background: linear-gradient(180deg, color-mix(in srgb, var(--brass) 70%, white) 0%, var(--brass) 55%, var(--brass-deep) 100%);
  border-color: color-mix(in srgb, var(--brass) 80%, black);
  /* Light-green fill needs dark text for contrast, but full black reads harsh against a
     brand colour — navy-deep is dark enough and ties back to the palette instead of
     being a generic near-black. */
  color: var(--navy-deep);
  box-shadow: 0 6px 14px color-mix(in srgb, var(--brass) 22%, transparent), inset 0 1px 0 rgba(255,255,255,0.45);
}
.btn-primary::after {
  content: ''; position: absolute; top: 0; left: -70%; width: 45%; height: 100%;
  background: linear-gradient(105deg, transparent, rgba(255,255,255,0.5), transparent);
  transform: skewX(-20deg); transition: left 500ms ease; pointer-events: none;
}
.btn-primary:hover {
  background: linear-gradient(180deg, color-mix(in srgb, var(--brass) 45%, white) 0%, color-mix(in srgb, var(--brass) 80%, white) 55%, var(--brass) 100%);
  box-shadow: 0 9px 18px color-mix(in srgb, var(--brass) 27%, transparent), inset 0 1px 0 rgba(255,255,255,0.5);
}
.btn-primary:hover::after { left: 140%; }
.btn-primary:hover svg { transform: translateX(3px); }
.btn-secondary { background: transparent; color: var(--ink); border: 1.5px solid var(--ink); box-shadow: none; }
.band--navy .btn-secondary, .band--navy-deep .btn-secondary {
  color: rgba(255,255,255,0.92); text-shadow: 0 1px 2px rgba(0,0,0,0.2); border-color: rgba(255,255,255,0.55);
}
.btn-secondary:hover { border-color: var(--brass); color: var(--brass); }
.btn-text { padding: 0; color: var(--brass); font-weight: 700; border-bottom: 1.5px solid transparent; border-radius: 0; }
.btn-text:hover { border-bottom-color: var(--brass); }

/* ---- Plaque card ---- */
.plaque {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-card); padding: 28px; transition: transform 200ms var(--ease-out), border-color 200ms var(--ease-out);
}
.plaque:hover { transform: translateY(-2px); border-color: var(--brass); }
.plaque .icon { width: 40px; height: 40px; color: var(--brass); margin-bottom: 16px; }

/* Solid navy tiles sitting on a light section (not glass-on-dark): the section itself
   stays white/cream per the "mostly white, occasional blue" rule, and the blue lives in
   the cards, not the background. Numbers dropped (this is 7 unordered services, not a
   real sequence — STYLE-GUIDE.md's own anti-pattern rule) in favour of a proper tinted
   icon badge, which is what was actually missing before: real Lucide-drawn icons instead
   of hand-sketched ones, given real visual weight instead of floating bare on the card. */
.service-card {
  display: flex; flex-direction: column; height: 100%;
  background: var(--navy-deep);
  border: 1px solid var(--navy-deep); border-radius: 20px;
  box-shadow: var(--shadow-card);
  transition: background 220ms var(--ease-out), border-color 220ms var(--ease-out), transform 220ms var(--ease-out), box-shadow 220ms var(--ease-out);
}
.icon-badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; border-radius: 14px; margin-bottom: 18px;
  background: color-mix(in srgb, var(--brass) 16%, transparent);
  transition: background 220ms var(--ease-out), transform 220ms var(--ease-out);
}
.icon-badge .icon { width: 26px; height: 26px; color: var(--brass); margin: 0; }
.service-card h3 { margin-top: 0; color: #fff; }
.service-card p { color: rgba(255,255,255,0.8); }
.service-card .go {
  margin-top: auto; padding-top: 20px; color: #fff; font-weight: 700;
  opacity: 0; transform: translateX(-4px); transition: opacity 200ms, transform 200ms;
}
.service-card:hover {
  background: var(--navy); border-color: var(--navy); transform: translateY(-4px);
  box-shadow: 0 20px 40px -16px rgba(3,10,18,0.4);
}
.service-card:hover .icon-badge { background: color-mix(in srgb, var(--brass) 30%, transparent); transform: scale(1.08) rotate(-4deg); }
.service-card:hover .go { opacity: 1; transform: none; }
.service-card:hover h3 { color: var(--brass); }

/* CTA tile folded into the services grid as its own 8th card (fills what would
   otherwise be an orphaned gap when 7 services sit in a 4-column grid), solid
   brass so it reads as the "do this next" action rather than an eighth service. */
.service-card--cta { background: var(--brass); border-color: var(--brass); justify-content: center; }
.service-card--cta:hover { background: var(--brass-deep); border-color: var(--brass-deep); transform: translateY(-4px); }
.service-card--cta .icon-badge { background: rgba(23,16,8,0.14); }
.service-card--cta .icon-badge .icon { color: #171008; }
.service-card--cta:hover .icon-badge { background: rgba(23,16,8,0.22); }
.service-card--cta h3, .service-card--cta p { color: #171008; }
.service-card--cta:hover h3 { color: #171008; }
.service-card--cta .go { color: #171008; opacity: 1; transform: none; }

/* ---- Badge (stamped plaque) ---- */
.badge {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line); border-radius: 4px; padding: 6px 12px;
  font: 600 0.8125rem var(--font-body); color: var(--ink-2);
}
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--sea-glass); }

/* ---- Chart-line divider (signature move — used sparingly) ---- */
.chart-rule { height: 2px; background: var(--brass); width: 64px; margin-block: 32px; }

/* ---- Photo frame (brass corner-bracket, the one photo treatment) ---- */
.photo-frame { position: relative; }
.photo-frame::before, .photo-frame::after {
  content: ''; position: absolute; width: 30px; height: 30px; border: 3px solid var(--brass); pointer-events: none;
}
/* For the bracket's arc to actually trace the image's own 16px-radius corner rather than
   just sit near it, it needs to be CONCENTRIC with that corner, not the same radius —
   the bracket's box is offset 8px outside the image's edge (top/left:-8px below), so its
   own arc's centre point only lands on the image's arc centre if its radius is the
   image's radius PLUS that 8px offset (16+8=24), not a bare copy of 16px. Using 16px
   here (matching the image's radius but ignoring the offset) put the two arcs at
   different centres entirely, which is what read as the bracket not following the
   image's corner at all. */
.photo-frame::before { top: -8px; left: -8px; border-right: none; border-bottom: none; border-top-left-radius: 24px; }
.photo-frame::after  { bottom: -8px; right: -8px; border-left: none; border-top: none; border-bottom-right-radius: 24px; }
.photo-frame img, .photo-placeholder {
  width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border-radius: 16px;
}
.photo-frame.ratio-4-3 img, .photo-frame.ratio-4-3 .photo-placeholder { aspect-ratio: 4 / 3; }
.photo-frame.ratio-1-1 img, .photo-frame.ratio-1-1 .photo-placeholder { aspect-ratio: 1 / 1; }
/* Fills the height of a stretched grid sibling instead of a fixed aspect ratio — for a
   grid-2 row where the text column's height varies with content (e.g. the logbook
   section), so the photo matches however tall that content actually is rather than being
   a fixed shape the text column has to accommodate. Needs the grid itself to actually
   stretch (CSS Grid's default align-items, so just not overridden to start/center). */
.photo-frame.ratio-fill { height: 100%; }
.photo-frame.ratio-fill img, .photo-frame.ratio-fill .photo-placeholder { height: 100%; aspect-ratio: auto; }
.photo-placeholder {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; text-align: center; padding: 28px;
  /* --surface sits too close to --bg-2 in dark mode (both near-black navy), which read
     as a flat black hole rather than a diagonal stripe — --line is calibrated as a
     divider tone so it stays visibly distinct from --bg-2 in both themes. */
  background: repeating-linear-gradient(135deg, var(--bg-2), var(--bg-2) 10px, var(--line) 10px, var(--line) 20px);
  border: 1px dashed var(--line); color: var(--ink-2); font: 500 0.75rem/1.6 var(--font-body);
}
.photo-placeholder::before {
  content: ''; width: 30px; height: 30px; flex-shrink: 0;
  background-color: var(--brass); opacity: 0.6;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 8h3l1.6-2.4A1 1 0 0 1 8.4 5h7.2a1 1 0 0 1 .8.4L17.8 8H20a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3Ccircle cx='12' cy='13.5' r='3.5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 8h3l1.6-2.4A1 1 0 0 1 8.4 5h7.2a1 1 0 0 1 .8.4L17.8 8H20a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3Ccircle cx='12' cy='13.5' r='3.5'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* ---- Compass mark (used once per page) ---- */
.compass-mark { width: 56px; height: 56px; color: var(--brass); opacity: 0.9; }

/* ---- Grid helpers ---- */
.grid { display: grid; gap: 24px; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) { .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }
/* Grid items default to min-width:auto, so a single wide-intrinsic-content child (a
   <select> sized by its longest <option>, a nowrap button label, an unbroken long word)
   can force its whole track — and with it the page — wider than the viewport, silently
   clipped by html's overflow-x:hidden below instead of wrapping. Confirmed live on
   contact.html at 320-375px (the service <select>'s longest option dragged the entire
   two-column form+copy grid, including the H1, off the right edge) and on the homepage
   areas section (the "Ask us..." button did the same). min-width:0 lets a track shrink to
   the space actually available and pushes the overflow-handling down to the child itself. */
.grid > * { min-width: 0; }

/* ---- Scroll reveal ---- */
.reveal { opacity: 0; transform: translateY(8px); transition: opacity 500ms var(--ease-out), transform 500ms var(--ease-out); }
.reveal.in { opacity: 1; transform: none; }

/* ---- Ribbon (scrolling marquee CTA bar) ---- */
.ribbon {
  background: var(--navy-deep); color: #fff; overflow: hidden; position: relative;
  border-bottom: 1px solid rgba(255,255,255,0.1);
  max-height: 100px; transition: max-height 200ms var(--ease-out), opacity 150ms var(--ease-out);
  /* At fractional browser zoom (DevTools "Fit to window" is exactly this — reproduced
     at 820px wide, not reproducible at exact integer 1:1 rendering), sub-pixel layout
     rounding can leave a hairline gap between this box and .site-header right below it.
     html's own background is the light --bg token, so even a sub-device-pixel gap there
     reads as a stark bright seam between two dark-navy bars. A few extra px of this same
     solid color, inside the box itself (not an overlapping negative margin, which would
     fight with .site-header's own sticky top:0 offset), closes it regardless of the
     exact rounding at any given zoom level. */
  padding-bottom: 4px;
}
/* Hidden while the mobile menu is open — an animated marquee competing for attention
   above a full-screen menu (and no longer visually part of the same surface once the
   header below it goes solid to merge with the panel, see .site-header:has(...) above)
   read as clutter rather than a second, disconnected header. */
body:has(.primary-nav.nav-open) .ribbon { max-height: 0; opacity: 0; border-bottom-color: transparent; }
.ribbon-track {
  display: flex; width: max-content; animation: ribbonScroll 140s linear infinite;
  padding-block: 8px; gap: 14px;
}
@media (prefers-reduced-motion: reduce) { .ribbon-track { animation: none; } }
.ribbon-item {
  display: flex; align-items: center; gap: 14px;
  font: 700 0.75rem var(--font-body); text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap;
}
.ribbon-item .dot-sep { width: 7px; height: 7px; border-radius: 50%; background: var(--brass); flex-shrink: 0; }
.ribbon-item a { color: #fff; }
.ribbon-item a:hover { color: var(--brass); }
@keyframes ribbonScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ---- Header / nav (glass) ---- */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--navy) 80%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid rgba(255,255,255,0.14);
  overflow: visible;
  transition: background 200ms var(--ease-out), border-color 200ms var(--ease-out);
}
/* With the mobile menu open, the translucent glass header sat as a visibly different
   layer between the ribbon above and the solid-navy dropdown panel below — two solid
   navy-deep blocks with a lighter, blurred strip sandwiched between them, reading as
   three stacked containers instead of one. Solid-filling the header (matching the panel)
   and dropping its own border while open makes the whole ribbon+header+panel stack read
   as a single continuous surface. */
.site-header:has(.primary-nav.nav-open) {
  background: var(--navy-deep); border-bottom-color: transparent;
}
.site-header .container { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding-block: 12px; }
/* flex-shrink:0 so the logo is never the thing that gives when the header is tight —
   the nav is what should collapse to the hamburger before the logo gets compressed. */
.brand { display: flex; align-items: center; gap: 10px; font: 800 1.125rem var(--font-display); color: #fff; flex-shrink: 0; }
.brand .compass-mark { width: 30px; height: 30px; color: var(--brass); }
/* Slim header bar, but the logo is allowed to overflow it (negative margin) so it reads
   bigger than the bar itself dictates, without dragging the whole header taller. */
.brand-logo { height: 58px; width: auto; margin-block: -9px; }
@media (max-width: 640px) { .brand-logo { height: 44px; margin-block: -4px; } }

/* Nav links and dropdown triggers as glass pills */
.primary-nav { display: flex; align-items: center; gap: 6px; }
.primary-nav a {
  font: 600 0.9375rem var(--font-body); color: rgba(255,255,255,0.9);
  padding: 9px 16px; border-radius: 999px; transition: background 160ms var(--ease-out), color 160ms var(--ease-out);
}
/* A top-level pill whose label breaks onto two lines (or whose chevron drops under the
   label) reads as a broken header, not a tight one. Never wrap; the breakpoints below
   switch layout before the row runs out of room instead. Top-level only: the dropdown
   sub-links sit in narrow grid columns in the hamburger panel and must still wrap. */
.primary-nav > a, .primary-nav .has-dropdown > a { white-space: nowrap; }
.primary-nav a:hover { color: #fff; background: rgba(255,255,255,0.14); }
.primary-nav .has-dropdown { position: relative; padding-block: 20px; margin-block: -20px; }
/* Duplicate of .nav-cta's CTA button, living inside .primary-nav so it can be relocated
   into the opened mobile panel (see the hamburger max-width block below) once the header
   bar itself is too narrow to hold the original. Hidden unconditionally here so it never
   renders as a second "Get a Free Quote" inline in the desktop nav — the mobile block
   is the only place that turns it back on, and only while .nav-open is present. */
.nav-mobile-cta { display: none; }
/* Chevron on dropdown trigger links (Services, Areas We Cover, Login): rotates open on
   hover/focus, purely CSS, no state to track. */
.chevron { display: inline-block; width: 14px; height: 14px; flex-shrink: 0; vertical-align: -2px; transition: transform 220ms var(--ease-out); margin-left: 2px; }
.has-dropdown:hover .chevron, .has-dropdown:focus-within .chevron { transform: rotate(180deg); }
.dropdown {
  position: absolute; top: 100%; left: 0; padding-top: 12px; min-width: 360px; display: none;
}
.has-dropdown:hover .dropdown, .has-dropdown:focus-within .dropdown { display: block; }
/* Deliberately dark navy always, not theme-toggled, it's a flyout of the navy header, so
   it should read as part of the header regardless of the page's own light/dark mode.
   Same navy as the ribbon bar above the header, for visual consistency. */
.dropdown-inner {
  background: var(--navy-deep); border: 1px solid rgba(255,255,255,0.1);
  border-radius: 14px; box-shadow: 0 20px 40px -16px rgba(3,10,18,0.5); padding: 14px;
  display: grid; grid-template-columns: repeat(2, minmax(170px, 1fr)); gap: 2px 6px;
  align-items: start;
}
.dropdown a {
  display: block; padding: 10px 14px; border-radius: 999px; color: rgba(255,255,255,0.88);
  font-weight: 600; font-size: 0.9375rem; transition: background 150ms, color 150ms;
}
.dropdown a:hover { background: rgba(255,255,255,0.1); color: var(--brass); }
.nav-cta { display: flex; align-items: center; gap: 12px; }
.site-header .btn {
  border-radius: 999px; padding-inline: 22px;
  box-shadow: 0 4px 16px -4px rgba(0,0,0,0.4);
}
.site-header .btn-primary:hover { box-shadow: 0 6px 22px -4px rgba(0,0,0,0.5); }
.nav-phone {
  display: none; align-items: center; gap: 8px; padding: 9px 18px; border-radius: 999px;
  border: 1.5px solid rgba(255,255,255,0.32); background: rgba(255,255,255,0.05);
  transition: border-color 160ms, background 160ms, transform 160ms;
  white-space: nowrap;
}
.nav-phone:hover { background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.6); transform: translateY(-1px); }
.nav-phone svg:not(.chevron) { width: 15px; height: 15px; flex-shrink: 0; }
/* Matches the primary-nav/hamburger breakpoint below (1360px) so the header doesn't pass
   through a mismatched state where the phone chip is back but the rest of the nav isn't. */
@media (min-width: 1360px) { .nav-phone { display: inline-flex; color: #fff; font-weight: 700; } }
/* Compact desktop header. At full spacing the row (logo, 6 links with 3 dropdowns, Login,
   phone, CTA) needs ~1510px of viewport, so between the hamburger breakpoint and that the
   pills used to wrap mid-label. Here the spacing tightens and Login becomes an icon-only
   chip (font-size:0 hides the label visually but keeps it as the link's accessible name,
   so no header markup change is needed across the 58 pages that inline it). */
@media (min-width: 1360px) and (max-width: 1579.98px) {
  .site-header .container { gap: 16px; }
  .brand-logo { height: 52px; margin-block: -6px; }
  .primary-nav { gap: 2px; }
  .primary-nav a { padding-inline: 11px; }
  .nav-cta { gap: 8px; }
  .nav-phone { padding-inline: 14px; }
  .nav-phone[href*="/login"] { font-size: 0; gap: 0; padding: 0; width: 46px; height: 46px; justify-content: center; }
  .nav-phone[href*="/login"] svg { width: 18px; height: 18px; }
  .site-header .btn { padding-inline: 18px; }
}
/* A proper glass chip button (border, translucent fill, rounded corners) rather than a
   bare icon sitting directly in the header — plus the three spans morph into an X on open
   (keyed off aria-expanded, which site.js already sets, so no extra JS is needed). */
.nav-toggle {
  display: none; position: relative; width: 42px; height: 42px; flex-shrink: 0;
  background: rgba(255,255,255,0.14); border: 1.5px solid rgba(255,255,255,0.32);
  border-radius: 12px; cursor: pointer;
  transition: background 160ms var(--ease-out), border-color 160ms var(--ease-out);
}
.nav-toggle:hover { background: rgba(255,255,255,0.22); border-color: rgba(255,255,255,0.5); }
.nav-toggle span {
  position: absolute; left: 50%; top: 50%; width: 18px; height: 2px;
  background: #fff; border-radius: 99px; transform: translate(-50%, -50%);
  transition: transform 260ms var(--ease-out), opacity 200ms ease;
}
.nav-toggle span:nth-child(1) { margin-top: -6px; }
.nav-toggle span:nth-child(3) { margin-top: 6px; }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { margin-top: 0; transform: translate(-50%, -50%) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { margin-top: 0; transform: translate(-50%, -50%) rotate(-45deg); }
@media (prefers-reduced-motion: reduce) { .nav-toggle span { transition: opacity 200ms ease; } }
@media (max-width: 1359.98px) {
  /* Logo + 42px toggle only: 42 + 2x12px padding + 1px border. See --header-h in :root. */
  :root { --header-h: 67px; }
  /* The full nav — logo, 6 links (3 with dropdowns), login chip, phone chip and CTA
     button — needs ~1360px even in its compact form (block above). The old 1200px
     breakpoint predates the Sectors dropdown and Login chip, so 1200-1530px (most
     laptops) stayed in "desktop" mode without room, and the pills wrapped mid-label.
     If a link is ever added to the bar, re-measure and move this breakpoint, the
     compact block's range and site.js's matching matchMedia together. */
  .primary-nav { display: none; }
  .nav-toggle { display: inline-flex; align-items: center; justify-content: center; }
  /* Below this breakpoint, logo + CTA button + hamburger can't all fit one unwrapping row
     on real phone widths — the button's nowrap text and the toggle's fixed size both
     refuse to shrink, so the row overflowed and html's overflow-x:hidden silently clipped
     the hamburger off-screen entirely, making the menu unreachable. Bar shows logo +
     hamburger only now; the CTA moves into the opened panel instead (.nav-mobile-cta
     below), so it's still one tap away rather than gone. */
  .nav-cta > .btn-primary { display: none; }
  /* Hidden entirely until JS toggles .nav-open on click (site.js). Previously this class
     was toggled but had no visible-state rule at all, so the hamburger button did
     nothing. Submenus stay reachable the same way as desktop (:focus-within, i.e. tab or
     tap into a dropdown link), just laid out inline/static instead of as a flyout, since
     there's no hover on touch. */
  .primary-nav.nav-open {
    display: flex; flex-direction: column; align-items: stretch; gap: 4px;
    position: absolute; top: 100%; left: 0; right: 0;
    background: var(--navy-deep); padding: 10px clamp(20px, 5vw, 64px) 20px;
    border-bottom: 1px solid rgba(255,255,255,0.14);
    border-radius: 0 0 20px 20px;
    box-shadow: 0 20px 40px -16px rgba(3,10,18,0.5);
    /* calc(100svh - 100%) is meaningless against an auto-height element (100% resolves
       against its own indeterminate height); a fixed viewport-relative cap that leaves
       clear headroom for the header above it is what was actually intended. */
    max-height: calc(100svh - 90px); overflow-y: auto;
  }
  /* Dims the page behind the open panel instead of leaving it visible right up against
     the panel's own bottom edge (previously looked like a floating card cut off mid-page,
     worse still whenever the cookie banner was also showing beneath it). Anchored the same
     way as the panel itself (absolute against the sticky header, top:100%) rather than
     position:fixed, and rendered via ::before — inserted before .container in the box
     tree — so it paints underneath .primary-nav.nav-open, not on top of it. */
  .site-header::before {
    content: ''; position: absolute; top: 100%; left: 0; right: 0; height: 100vh;
    background: rgba(3,10,18,0.55);
    opacity: 0; pointer-events: none; transition: opacity 260ms var(--ease-out);
  }
  .site-header:has(.primary-nav.nav-open)::before { opacity: 1; }
  /* Each top-level row (About/Blog/Contact, and the Services/Areas triggers) reads as its
     own rounded, tappable pill rather than flush-left text separated only by a hairline —
     matches the pill-button language used everywhere else on the site instead of a plain
     "list with dividers" pattern. A hairline still exists but only as a 1px gap of true
     transparent space between rows (via the parent's own gap), not a drawn rule. */
  .primary-nav.nav-open > a:not(.nav-mobile-cta), .primary-nav.nav-open .has-dropdown > a {
    border-radius: 12px; transition: background 160ms var(--ease-out);
  }
  .primary-nav.nav-open > a:not(.nav-mobile-cta):hover,
  .primary-nav.nav-open > a:not(.nav-mobile-cta):focus-visible,
  .primary-nav.nav-open .has-dropdown:not(.mobile-open) > a:hover,
  .primary-nav.nav-open .has-dropdown:not(.mobile-open) > a:focus-visible {
    background: rgba(255,255,255,0.08);
  }
  /* The active background now lives on .has-dropdown itself, not just its trigger link,
     so the trigger row and its expanded sublist read as one rounded card rather than a
     pill sitting above a separate, differently-treated list (which also used to hang a
     vertical accent line off its left edge — dropped that too, the shared card background
     is enough to show it's all one contained group). */
  .primary-nav.nav-open .has-dropdown {
    border-radius: 12px; overflow: hidden; transition: background 160ms var(--ease-out);
  }
  .primary-nav.nav-open .has-dropdown.mobile-open {
    background: rgba(255,255,255,0.1);
  }
  .primary-nav.nav-open > a, .primary-nav.nav-open .has-dropdown > a { padding: 14px 14px; }
  .primary-nav.nav-open .has-dropdown > a {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
  }
  .primary-nav.nav-open .has-dropdown > a .chevron {
    width: 26px; height: 26px; padding: 6px; box-sizing: border-box; border-radius: 50%;
    background: rgba(255,255,255,0.1); flex-shrink: 0;
  }
  /* margin-top and padding-top were both set to 24px — space before AND after the
     divider line stacking to ~53px of near-invisible dead air (the divider itself is
     barely-there at 14% opacity) before the button. Trimmed to a single, tighter gap. */
  .primary-nav.nav-open .nav-mobile-cta {
    display: inline-flex; width: 100%; justify-content: center;
    padding: 13px 26px; margin-top: 12px; border-top: 1px solid rgba(255,255,255,0.14); padding-top: 16px;
  }
  /* .primary-nav a:hover outranks .btn-primary:hover on specificity (element+class+pseudo
     beats class+pseudo), which would otherwise paint a translucent white overlay on hover
     instead of the button's own brass gradient. Restated explicitly here rather than left
     to lose the fight silently. */
  .primary-nav.nav-open .nav-mobile-cta:hover {
    color: var(--navy-deep);
    background: linear-gradient(180deg, color-mix(in srgb, var(--brass) 45%, white) 0%, color-mix(in srgb, var(--brass) 80%, white) 55%, var(--brass) 100%);
  }
  .primary-nav.nav-open .has-dropdown { padding-block: 0; margin-block: 0; }
  /* Services (7 links) and Areas We Cover (11 links) used to render permanently expanded
     inline, so opening the menu at all meant scrolling past 18 sub-links before reaching
     About/Blog/Contact. They're now closed by default and toggle open per-section (JS in
     site.js flips .mobile-open on the .has-dropdown wrapper instead of navigating, only
     while the menu is in this hamburger/accordion mode). The 0fr/1fr grid-template-rows
     trick animates height smoothly without measuring content in JS. */
  .primary-nav.nav-open .dropdown {
    position: static; padding-top: 0; min-width: 0;
    display: grid; grid-template-rows: 0fr;
    transition: grid-template-rows 320ms var(--ease-out);
  }
  .primary-nav.nav-open .has-dropdown.mobile-open .dropdown { grid-template-rows: 1fr; }
  .primary-nav.nav-open .has-dropdown.mobile-open > a .chevron { transform: rotate(180deg); }
  /* padding-block and border-left are fixed lengths, so the 0fr row above can only
     collapse to those values, never truly to 0 — grid-template-rows:0fr still leaves
     min-height:0's item tall enough to fit its own border-box, which was showing as a
     ~16px sliver (2px + 14px padding) under Services/Areas We Cover even fully closed.
     Confirmed via computed styles: grid-template-rows resolved to "16px", not "0px".
     Applying that padding/border only once .mobile-open is present removes the floor. */
  .primary-nav.nav-open .dropdown-inner {
    background: none; border: none; box-shadow: none; overflow: hidden; min-height: 0;
    padding: 0 8px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 2px 12px;
  }
  .primary-nav.nav-open .has-dropdown.mobile-open .dropdown-inner {
    padding-top: 2px; padding-bottom: 10px;
  }
  /* Sub-links deliberately lighter (smaller, lower weight, dimmer) than the bold
     top-level items, so Services/Areas We Cover/About/Blog/Contact stay the visually
     dominant scan path and the expanded sub-items read as detail, not equal-weight nav. */
  .primary-nav.nav-open .dropdown a {
    padding: 9px 10px; font-size: 0.875rem; font-weight: 500; color: rgba(255,255,255,0.72);
    border-radius: 10px; transition: background 160ms var(--ease-out), color 160ms var(--ease-out);
  }
  .primary-nav.nav-open .dropdown a:hover, .primary-nav.nav-open .dropdown a:focus-visible {
    color: #fff; background: rgba(255,255,255,0.08);
  }
}

/* ---- Footer ---- */
.site-footer { background: var(--navy-deep); color: rgba(255,255,255,0.8); }
/* The global `p { color: var(--ink-2) }` rule (a dark grey meant for light backgrounds)
   targets every <p> directly, which wins over an ancestor's inherited colour regardless
   of specificity — so footer paragraphs were rendering in near-invisible dark grey on
   navy without this explicit override. */
.site-footer p { color: rgba(255,255,255,0.72); }
.site-footer .container { padding-block: var(--rhythm); }
.footer-grid { display: grid; grid-template-columns: 1.3fr 0.85fr 0.85fr 0.85fr 0.85fr; gap: 32px; }
@media (max-width: 1080px) { .footer-grid { grid-template-columns: 1fr 1fr 1fr; } }
@media (max-width: 700px) { .footer-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 480px) { .footer-grid { grid-template-columns: 1fr; } }
.site-footer h4 { color: #fff; font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.06em; font-family: var(--font-body); font-weight: 700; margin-bottom: 14px; }
.site-footer ul { display: flex; flex-direction: column; gap: 10px; }
.site-footer a:hover { color: var(--brass); }
.footer-bottom { border-top: 1px solid rgba(255,255,255,0.12); margin-top: 48px; padding-top: 24px; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; font-size: 0.8125rem; color: rgba(255,255,255,0.55); }
.footer-social { display: flex; align-items: center; gap: 16px; }
.footer-social a { color: rgba(255,255,255,0.75); display: inline-flex; }
.footer-social a:hover { color: var(--brass); }
.footer-social svg { width: 19px; height: 19px; }

/* ---- Wolf watermark (decorative background mark) ---- */
.wolf-watermark {
  /* Square source image, so width and height track together. Capped well under a typical
     CTA band's rendered height (~510px) so a centred watermark always has clear padding
     above and below instead of colliding with the section's own overflow:hidden edge. */
  position: absolute; pointer-events: none; opacity: 0.05; z-index: 0;
  width: clamp(240px, 26vw, 380px); height: auto;
}

/* ---- Full-viewport hero: at least one screen tall at any size (min-height, not a fixed
   height — a fixed height plus overflow:hidden could clip the wave, the text, or both
   whenever content is taller than a short viewport, which is worse than occasionally
   needing a small scroll). Wave sits at the very bottom of the actual content, so it is
   always reachable, on very short viewports that means a touch of scroll, never hidden. */
.hero-full {
  min-height: 92svh;
  display: flex;
  /* Plain center overflows equally in both directions once content (H1 + lede + two
     buttons + trust ticks + badge) is taller than the space 92svh minus the top/bottom
     padding leaves for it — which happens on any shorter phone viewport (confirmed at
     360x740: h1 top was already 69px, under the header, at scrollY 0, no scrolling
     needed to reproduce it). Tried `safe center` first — doesn't help here: safe's
     fallback only triggers when overflow would make content inaccessible via scrolling,
     and this box clips with overflow:hidden rather than scrolling, so it never engages.
     The mobile override below switches to flex-start instead, which has no such
     condition — content just anchors at padding-top and any leftover space goes below
     it, never above. */
  align-items: center;
  position: relative;
  /* z-index:0 (not the default auto) so this establishes its own stacking context —
     without it, .hero-slideshow's z-index:-1 doesn't necessarily nest locally beneath
     just this section's own background; it can resolve against a stacking context
     further up the tree instead, which is what was actually happening (slideshow
     rendering, but painted behind something else entirely, invisible). */
  z-index: 0;
  overflow: hidden;
  /* Pulled up behind .site-header (sticky, translucent + backdrop-blur) so the slideshow
     shows through it at the very top of the page, rather than the hero starting flush
     below the header's own box. padding-top adds back exactly what margin-top removes,
     so the headline/buttons land in the same visual spot as before — only the background
     actually extends further up. --header-h is the header's own height in each layout
     (see its note in :root); hardcoded 71px/68px here used to drift from the real
     67px/76px and show as a band of photo between the ribbon and the header. */
  margin-top: calc(-1 * var(--header-h));
  padding-block: calc(var(--header-h) + clamp(16px, 2vw, 28px)) clamp(110px, 13vw, 190px);
}
@media (max-width: 640px) {
  /* clamp(16px, 2vw, 28px) left only ~16-28px of clearance below the sticky header at
     rest, so the H1 started scrolling under it after almost no scroll at all — read as
     the heading being broken/hidden rather than normal sticky-header overlap. A bigger
     buffer here doesn't stop it disappearing on a long scroll (nothing can, short of an
     unblurred opaque header that never overlaps content, a bigger design change), but it
     stops it vanishing on the first small nudge. */
  /* flex-start (not center): on a short viewport the content stack (H1 + lede + buttons
     + ticks + badge) is routinely taller than 92svh minus this padding, and centering
     overflows that excess equally above AND below — pushing the H1 up past the padding
     entirely and under the header even with zero scroll. Anchoring to the top instead
     means any shortfall shows up only as reduced bottom whitespace, never as content
     disappearing off the top. Confirmed against 360x740, 375x667 and 320x568, all of
     which reproduced the bug under centering. */
  .hero-full { padding-top: calc(var(--header-h) + clamp(40px, 9vw, 64px)); align-items: flex-start; }
}
/* Crossfading background slideshow, reusing real photos already on the site (markup in
   index.html) instead of one static shot. Pure CSS: one shared @keyframes, each slide
   staggered onto its own slice of the same cycle via a negative animation-delay — the
   standard technique for an N-slide crossfade with a single keyframe definition. Sits at
   z-index:-1, one layer behind the scrim below (z-index:0), so it occupies exactly the
   layer the old flat background image used to. --navy-deep fallback on .hero-photo covers
   the instant before the first slide has painted (and CSS/image-load failure generally).
   The dark scrim on top (a gradient, painted via ::before so text stays readable over the
   left side) is a deliberate permanent choice, not theme-dependent — this used to switch
   to a light cream scrim whenever the visitor's OS was in light mode, which is why the
   hero and this dark-scrim look weren't always the same thing; it's now always the dark
   version regardless of OS setting. */
.hero-photo { background: var(--navy-deep); }
.hero-slideshow { position: absolute; inset: 0; z-index: -1; }
.hero-slide {
  position: absolute; inset: 0;
  background-size: cover; background-position: center 30%;
  opacity: 0;
  animation: heroSlideFade 36s ease-in-out infinite;
}
/* Positive delays, not negative — a positive delay holds a slide at its 0% keyframe
   (opacity:0) until its turn arrives, which is what staggers them into sequential order.
   (Negative delay does the opposite: it fast-forwards a slide as if already partway
   through at page load, which put later slides on screen too early.) */
.hero-slide:nth-child(1) { animation-delay: 0s; }
.hero-slide:nth-child(2) { animation-delay: 6s; }
.hero-slide:nth-child(3) { animation-delay: 12s; }
.hero-slide:nth-child(4) { animation-delay: 18s; }
.hero-slide:nth-child(5) { animation-delay: 24s; }
.hero-slide:nth-child(6) { animation-delay: 30s; }
/* Each slide's own 36s timeline: fade in (2s), hold, then fade out (2s) DURING the same
   2s window the next slide is fading in (its delay is exactly one 6s slot later, so its
   own 0%-to-5.56% fade-in window lands in real time exactly on top of this slide's
   16.67%-to-22.22% fade-out window) — a true cross-dissolve where both images overlap on
   screen throughout the handoff, rather than this slide finishing its fade-out before the
   next one starts fading in, which left a moment where neither was meaningfully visible
   and the navy background behind them showed through. */
@keyframes heroSlideFade {
  0%     { opacity: 0; }
  5.56%  { opacity: 1; }
  16.67% { opacity: 1; }
  22.22% { opacity: 0; }
  100%   { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-slide { animation: none; opacity: 0; }
  .hero-slide:first-child { opacity: 1; }
}
.hero-photo::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(90deg, rgba(8,21,33,0.96) 0%, rgba(8,21,33,0.92) 38%, rgba(8,21,33,0.5) 60%, rgba(8,21,33,0.05) 82%);
}
@media (max-width: 800px) {
  /* Stack: on narrow screens the gradient goes top-to-bottom instead of side-to-side,
     since there's no room for text-left/photo-right. */
  .hero-photo::before {
    background: linear-gradient(180deg, rgba(8,21,33,0.96) 0%, rgba(8,21,33,0.92) 45%, rgba(8,21,33,0.5) 65%, rgba(8,21,33,0.1) 100%);
  }
}
/* Hero copy runs smaller than the site's default h1/lede scale on purpose, so the whole
   hero, headline, copy, buttons, trust strip and the wave, realistically fits in one
   screen on ordinary laptop/desktop viewports, not just after a scroll. */
.hero-full h1 { font-size: clamp(2.1rem, 1rem + 3.4vw, 3.4rem); }
.hero-full .lede { font-size: 1rem; line-height: 1.55; }
.hero-full .trust-strip { font-size: 0.75rem; }
/* Hardcoded, not var(--ink)-based: light text, permanently, to match the dark scrim above. */
.hero-full h1, .hero-full .lede { color: #EEF1F4; }
.hero-full .trust-strip { color: #B9C4CC; }
/* The hero isn't wrapped in .band--navy/.band--navy-deep (it's its own photo+scrim
   component), so the secondary "Call" button doesn't automatically pick up the
   light-text-on-dark-background treatment those classes give it elsewhere — needs its
   own override here or it renders with dark ink text/border, illegible on this scrim. */
.hero-full .btn-secondary { color: rgba(255,255,255,0.92); text-shadow: 0 1px 2px rgba(0,0,0,0.25); border-color: rgba(255,255,255,0.55); }
.hero-full .btn-secondary:hover { border-color: var(--brass); color: var(--brass); text-shadow: none; }
/* On short viewports (laptops in a small window, embedded preview panes), shrink further
   so the whole hero still fits in one screen without clipping anything. This rule is
   keyed on height, the mobile rule above on width — a short-but-narrow phone (360x740,
   375x667, 320x568 all reproduced it) matches both, and this one's padding-block came
   *after* it in the cascade, silently resetting padding-top back to the ~16-28px buffer
   the width-based fix above was specifically raising. Same fix, same reasoning, applied
   here too — plus flex-start again, since a short-but-wide viewport (a tablet in
   landscape, say) would hit this rule without ever matching the max-width:640px one. */
@media (max-height: 760px) {
  .hero-full { padding-block: calc(var(--header-h) + clamp(40px, 9vw, 64px)) clamp(90px, 10vw, 140px); align-items: flex-start; }
  .hero-full h1 { font-size: clamp(1.75rem, 1rem + 2.6vw, 2.6rem); }
  .hero-full .lede { font-size: 0.9375rem; }
  .hero-full .wave-divider svg { height: clamp(80px, 10vw, 140px); }
}

/* ---- Wave divider: layered (3 depths, like a real shoreline), genuinely animated (SMIL,
   see index.html), fully opaque distinct blues at every depth (no transparency-over-light-
   background, which is what was reading as grey), zero-gap hand-off into the next (navy)
   section since the front layer is painted the exact navy of that section's background. ---- */
.wave-divider { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; line-height: 0; pointer-events: none; }
.wave-divider svg { width: 100%; height: clamp(100px, 13vw, 190px); display: block; }
/* Layer 1 (front, fastest, 17s): fully opaque, this is the reference layer and also the
   one that must stay solid for the seamless hand-off into the section below.
   Layer 2 (mid, slower, 21s): ~15% more transparent than layer 1 (within the 10-20% ask).
   Layer 3 (back, slowest, 26s): ~30% more transparent than layer 1 (within the 20-40% ask). */
.wave-divider .layer-back { fill: color-mix(in srgb, var(--navy) 55%, white); opacity: 0.7; }
.wave-divider .layer-mid { fill: color-mix(in srgb, var(--navy) 80%, white); opacity: 0.85; }
.wave-divider .layer-front { fill: var(--navy); opacity: 1; }
/* The overlap promised above never actually landed as code, and couldn't have worked from
   this side anyway: .hero-full has overflow:hidden, which clips off any negative bottom
   offset on its own child before it could reach past the section boundary. Pulling the
   next section up over the seam instead lives outside .hero-full entirely, so nothing
   clips it — and since .band--navy's background is the exact same --navy as the front
   wave layer, a few px of overlap is invisible rather than a visible double-edge. This is
   what was producing the hairline/gap under the wave at various viewport widths: the
   wave's height is a vw-based clamp(), so at most widths it lands on a fractional pixel
   that browsers round differently for layout vs. rasterizing the SVG, leaving a 1-device-
   pixel sliver of whatever sits behind the wave (the hero photo/scrim) showing through. */
/* -3px, then -6px, still wasn't enough at every width/zoom/DPR combination — the exact
   gap size tracks whatever fractional browser zoom is in play (confirmed: DevTools
   "Fit to window" device emulation applies a non-integer zoom, which is exactly what
   reproduces this — a plain 1:1-pixel viewport of the identical CSS width did not).
   Since the two colors either side of the seam are identical (see above), there's no
   visual cost to a generous overlap; there is a real cost to an insufficient one. */
/* The overlap alone never closed it, at any size: .hero-full is positioned with
   z-index:0, and a positioned box paints AFTER a normal-flow sibling, so the hero was
   drawn on top of these 12px, and the hero's own bottom edge (the photo sliver under a
   fractionally-sized wave) still showed as a 1-device-pixel line over the band.
   Reproduced headless at every zoom from 110% to 200%. position+z-index:1 puts the band
   above the hero, so its solid --navy covers that edge; the wave's front layer is the
   same --navy, so the covered 12px of wave is invisible. */
.hero-full + .band { margin-top: -12px; position: relative; z-index: 1; }

/* ---- Stat counters ---- */
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; }
@media (max-width: 800px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
.stat-item { text-align: center; min-width: 0; }
.stat-num { font: 900 clamp(4rem, 8vw, 5.5rem) var(--font-display); color: #fff; text-shadow: 0 2px 8px rgba(0,0,0,0.16); }
/* clamp()'s 4rem floor is sized for the 800px 2-column handoff (8vw == 4rem right at
   800px) and never actually shrinks further below it — every phone width was rendering
   "2023"/"100%" at a fixed 64px, wider than a half-width column has room for. Confirmed
   live: the counted-up row measured a fixed ~384px wide from 320px through 375px
   viewports (min-width:0 above stops it forcing the page wider; this stops it forcing
   the number's own box wider than that). */
@media (max-width: 560px) {
  .stat-num { font-size: clamp(2.25rem, 13vw, 3.5rem); }
}
.stat-label { font: 700 0.8125rem var(--font-body); text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.85); margin-top: 6px; text-shadow: 0 1px 3px rgba(0,0,0,0.14); }

/* ---- Brand mark (real logo, not a drawn substitute) ---- */
.brand-badge { width: 40px; height: 40px; }

/* ---- "Powered by" plate: white chip so the Custodia logo (dark wordmark, transparent
   background) stays legible over the hero photo in both light and dark theme. ---- */
.powered-by-plate {
  display: inline-flex; align-items: center; gap: 10px;
  background: rgba(255,255,255,0.94);
  border-radius: 10px;
  padding: 8px 14px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.14);
  text-decoration: none;
  transition: box-shadow 0.15s, transform 0.15s;
}
.powered-by-plate:hover { box-shadow: 0 4px 14px rgba(0,0,0,0.2); transform: translateY(-1px); }
.powered-by-label {
  font: 700 0.6875rem var(--font-body);
  text-transform: uppercase; letter-spacing: 0.05em;
  color: #6B7280;
}
.powered-by-logo { height: 32px; width: auto; display: block; }

/* ---- Trust strip (inline manifest-style markers, replaces badge pills in hero) ---- */
/* column-gap (not margin) is what actually provides horizontal spacing — it's the one
   gap property that keeps working correctly regardless of how flex-wrap breaks the row,
   including when only one item ends up alone on a line. */
.trust-strip { display: flex; flex-wrap: wrap; align-items: center; row-gap: 12px; column-gap: 20px; font: 700 0.8125rem var(--font-body); text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2); }
.trust-strip span { display: flex; align-items: center; gap: 8px; padding-right: 20px; border-right: 1.5px solid var(--line); }
.trust-strip span:last-child { border-right: none; padding-right: 0; }
@media (max-width: 480px) {
  /* Below ~480px the three items wrap one-per-line often enough that the vertical
     divider reads as a stray mark rather than a separator between neighbours, so drop
     it here — column-gap above still keeps items apart when two do land on one line,
     which margin-right alone (the previous approach) didn't: it left a real bug where
     adjacent items rendered with zero gap between them, ticks touching the prior label. */
  .trust-strip span { border-right: none; padding-right: 0; }
}
.trust-strip .tick {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 20px; height: 20px; border-radius: 50%; background: var(--brass);
}
.trust-strip .tick svg { width: 11px; height: 11px; }

/* ---- Manifest list (services / areas) ---- */
.manifest { border-top: 1px solid var(--line); }
.manifest-row {
  display: grid; grid-template-columns: 64px 1fr auto; gap: 20px; align-items: start;
  padding: 26px 0; border-bottom: 1px solid var(--line);
  text-decoration: none; color: inherit;
}
/* One row per item across a 1600px container left 4-27 short rows with most of the
   width empty. Columns on wide screens: 2, or 3 for long lists (the 27-area index).
   Each row draws its own top rule as a 1px shadow, landing on the same pixel row as the
   bottom border of the row above, so the first row in every column gets a top line
   without doubling the rest, and the rules break at the column gap. */
@media (min-width: 1000px) {
  .manifest { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: clamp(32px, 4vw, 64px); border-top: none; }
  .manifest-row { box-shadow: 0 -1px 0 var(--line); }
}
@media (min-width: 1280px) {
  .manifest:has(> :nth-child(9)) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.manifest-row .num { font: 800 1.5rem var(--font-display); color: var(--brass); }
.manifest-row h3 { margin-bottom: 6px; }
.manifest-row .go { align-self: center; color: var(--navy); font-weight: 700; opacity: 0; transform: translateX(-4px); transition: opacity 200ms, transform 200ms; }
.manifest-row:hover .go { opacity: 1; transform: none; }
.manifest-row:hover h3 { color: var(--brass-deep); }
@media (max-width: 640px) {
  .manifest-row { grid-template-columns: 40px 1fr; }
  .manifest-row .go { display: none; }
}

/* ---- Voyage scroll timeline (how it works) ---- */
/* Six-stage process, plate cards in a single horizontal row. On desktop this section
   pins to the viewport while the visitor scrolls, translating .voyage-track sideways
   (see initVoyageScroll() in site.js) — .voyage-pin-wrapper's height is set in JS to
   exactly the extra scroll distance the horizontal travel needs, so the page resumes
   normal scrolling the instant the last card arrives, no scroll-jacking/preventDefault.
   Mobile and reduced-motion get a plain native horizontally-swipeable row instead —
   see the max-width:900px block. */
.voyage-scroll-section { background: var(--bg-2); border-top: 1px solid var(--line); }
.voyage-pin-wrapper { position: relative; }
.voyage-pin-inner {
  position: sticky; top: 0; min-height: 100vh;
  display: flex; flex-direction: column; justify-content: center;
  padding-block: var(--rhythm);
}
/* container-type establishes .voyage-viewport as the reference for the cqw unit below —
   deliberately not a plain % on --voyage-edge-pad, because a % custom property re-resolves
   independently everywhere it's var()'d in, against THAT usage's own containing block.
   Used in .voyage-track's padding-inline it correctly meant .voyage-viewport's width, but
   the exact same variable reused in .voyage-line's left/right (a child of .voyage-track)
   resolved against .voyage-track's own width:max-content instead — thousands of pixels
   too wide — collapsing the line to zero width. cqw always means "this queried
   container's width" regardless of which element/property references it, so both usages
   agree. */
/* padding-block gives the dots' pulse ring (a few px larger than the dot itself) genuine
   box space to expand into before it reaches this element's own clip boundary. Splitting
   into overflow-x:hidden/overflow-y:visible looks like the more surgical fix but doesn't
   work — per the CSS Overflow spec, pairing a non-visible overflow-x with visible
   overflow-y forces the visible one to compute as auto instead, which still clips. */
.voyage-viewport { width: 100%; padding-block: 24px; overflow: hidden; container-type: inline-size; }
.voyage-track {
  --voyage-stop-w: 320px;
  --voyage-edge-pad: calc((100cqw - var(--voyage-stop-w)) / 2);
  position: relative; display: flex; gap: 32px;
  width: max-content; padding-inline: var(--voyage-edge-pad);
  will-change: transform;
}
.voyage-stop { flex: 0 0 var(--voyage-stop-w); display: flex; flex-direction: column; align-items: center; }

/* The line spans exactly from the first dot's centre to the last dot's centre — inset by
   half a stop's width on each side — rather than edge-to-edge of the track, which is what
   previously left a bare line trailing past the final dot with nothing at the end of it. */
.voyage-line {
  position: absolute; top: 10px;
  left: calc(var(--voyage-edge-pad) + var(--voyage-stop-w) / 2);
  right: calc(var(--voyage-edge-pad) + var(--voyage-stop-w) / 2);
  height: 2px; background: var(--brass); opacity: 0.35;
  transform-origin: left center; transform: scaleX(0);
  /* No transition here on purpose — its scaleX is set every scroll frame in
     updateVoyageScroll() (site.js), directly from the same progress value that drives
     the track's translateX, so the line's drawn length and the cards' horizontal
     position never fall out of sync with each other or lag behind the scroll input. */
}

/* Dot is a 20px box, border-box like everything else on this site (site.css's global
   `*{box-sizing:border-box}` reset) — the 3px border sits inside that 20px, not added
   outside it, leaving a 14px visible brass circle. Its vertical centre (10px) is what
   .voyage-line's own top:10px is matched against above; horizontally it's centred by
   .voyage-stop's flex column, the same centre the line's left/right calc lands on. */
.voyage-dot {
  width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0;
  background: var(--brass); border: 3px solid var(--bg-2);
  position: relative; z-index: 2; margin-bottom: 28px; opacity: 1; scale: 1;
  transition: scale 200ms var(--ease-out), opacity 320ms var(--ease-out);
}
/* Stem connecting the dot down to its plate, filling exactly the 28px margin-bottom
   gap above so it visually reads as one continuous line from the route down to the card. */
.voyage-dot::before {
  content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  width: 2px; height: 28px; background: var(--brass); opacity: 0.3;
}
.voyage-dot::after {
  content: ''; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid var(--brass);
  opacity: 0; pointer-events: none;
}
.voyage-track.in .voyage-dot::after { animation: voyagePulse 2600ms var(--ease-out) infinite var(--pd, 0ms); }
@keyframes voyagePulse {
  0% { transform: scale(0.7); opacity: 0.55; }
  100% { transform: scale(2); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .voyage-dot::after { animation: none !important; } }

.voyage-plate {
  width: 100%; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-card); padding: 24px; opacity: 1; translate: 0 0;
  /* transform is reserved for the hover lift below; the reveal-on-scroll offset uses the
     standalone translate property instead so the two never fight over the same property. */
  transition: transform 200ms var(--ease-out), border-color 200ms var(--ease-out), opacity 380ms var(--ease-out) 60ms, translate 380ms var(--ease-out) 60ms;
}
.voyage-plate:hover { transform: translateY(-2px); border-color: var(--brass); }
.voyage-num {
  font: 800 1.5rem var(--font-display); color: var(--brass); font-variant-numeric: tabular-nums;
}

/* Progressive reveal (desktop/pinned mode only — see updateVoyageScroll() in site.js):
   only stop 1 is visible the moment the section is reached; each later stop (marked
   .voyage-stop--hideable in the markup — NOT :not(:first-child), since .voyage-line is
   itself .voyage-track's actual first child, which quietly made :first-child match
   nothing and hid every stop including the first) pops in with a quick, eased
   opacity+motion transition once the line (scaleX'd every scroll frame, above) reaches
   it, toggled by .is-revealed. Deliberately reversible both ways rather than "stays
   revealed forever" — scrolling back up retracts the line and re-hides later stops in
   step with it, which is what keeps the line's drawn length and each card's presence
   always describing the same, consistent state instead of drifting apart. */
@media (min-width: 901px) {
  .voyage-stop--hideable .voyage-dot { opacity: 0; scale: 0.3; }
  .voyage-stop--hideable .voyage-plate { opacity: 0; translate: 0 18px; }
  .voyage-stop--hideable.is-revealed .voyage-dot { opacity: 1; scale: 1; }
  .voyage-stop--hideable.is-revealed .voyage-plate { opacity: 1; translate: 0 0; }
  .voyage-track .voyage-dot:hover { scale: 1.4; }
}

@media (max-width: 900px) {
  /* No pin, no JS transform (initVoyageScroll bails out under this breakpoint) — a plain
     native horizontally-scrollable row instead, since scroll-jacking a vertical gesture
     on a touchscreen is exactly the wrong move on mobile. */
  .voyage-pin-wrapper { height: auto !important; }
  .voyage-pin-inner { position: static; min-height: 0; overflow: visible; }
  .voyage-viewport {
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity; padding-bottom: 14px;
  }
  .voyage-track { transform: none !important; padding-inline: 20px; }
  .voyage-stop { --voyage-stop-w: 78vw; scroll-snap-align: start; }
  .voyage-line { display: none; }
}

/* ---- Logbook (why us) ---- */
/* Line grows downward, then each entry (with its dot) lights up one by one as it's
   scrolled into view, staggered via the --sd custom property set per entry.
   .logbook's own left padding used to be part of this calculation, but padding on the
   containing block does nothing for an absolutely positioned child's `left` origin (only
   normal-flow children like .logbook-entry respect it) — so the line's left:0 and the
   dot's left:-3px (measured from two different effective baselines once you factor that
   in) were 6px apart instead of sharing a centre. Padding removed; both the line and the
   dot now measure from the same left:0 baseline, sized so a 2px line centres exactly
   under a 12px dot. */
.logbook { position: relative; }
.logbook::before {
  content: ''; position: absolute; left: 5px; top: 6px; width: 2px; height: 0;
  background: var(--brass);
  transition: height 900ms var(--ease-out);
}
/* --logbook-line-h is set precisely by site.js from the last entry's actual measured dot
   position — a fallback of calc(100% - 12px) covers the instant before that JS runs (or
   if it's unavailable), but that heuristic is exactly what let the line overshoot past
   the last dot and run on through that entry's own body paragraph: "100% of the
   container" includes the trailing paragraph text below the last dot, which has nothing
   to do with where the line should actually stop. */
.logbook.in::before { height: var(--logbook-line-h, calc(100% - 12px)); }
.logbook-entry {
  padding: 0 0 0 28px; margin-bottom: 30px; position: relative;
  opacity: 0; transform: translateY(16px);
  transition: opacity 500ms var(--ease-out) var(--sd, 0ms), transform 500ms var(--ease-out) var(--sd, 0ms);
}
.logbook-entry.in { opacity: 1; transform: none; }
/* Dot centred on the heading's own line box (h3 line-height 1.08 at 1.375rem ≈ 23.75px
   tall) rather than a guessed fixed offset. A soft ring behind the solid dot gives it a
   bit more presence on the line, like a lit waypoint rather than a bare mark. */
.logbook-entry::before {
  content: ''; position: absolute; left: 0; top: calc((1.375rem * 1.08 - 12px) / 2);
  width: 12px; height: 12px; border-radius: 50%; background: var(--brass);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--brass) 16%, transparent);
  transform: scale(0); transition: transform 400ms var(--ease-out) var(--sd, 0ms);
}
.logbook-entry.in::before { transform: scale(1); }
.logbook-entry:last-child { margin-bottom: 0; }

/* ---- Google reviews carousel (live, see assets/js/google-reviews.js) ---- */
/* JS-driven (translateX on .review-track), not native overflow-scroll — needed so the
   same track can both auto-advance and be nudged by the prev/next buttons, and so the
   index can wrap seamlessly for the infinite loop (see buildCarousel in the JS file). */
.reviews-carousel { position: relative; padding: 0 8px; }
.reviews-viewport { overflow: hidden; }
.review-track {
  display: flex; gap: 20px;
  transition: transform 550ms var(--ease-out);
}
.review-track.no-transition { transition: none; }
.review-card {
  flex: 0 0 320px; text-align: left; position: relative;
  background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
  box-shadow: var(--shadow-card); padding: 28px 24px 24px;
  transition: box-shadow 200ms, transform 200ms;
}
.review-card:hover { box-shadow: 0 10px 30px rgba(14,30,43,0.12); transform: translateY(-3px); }
.review-card::before {
  content: '\201C'; position: absolute; top: 10px; right: 20px;
  font: 800 3.5rem/1 Georgia, serif; color: var(--line);
}
.review-stars { color: var(--brass-deep); font-size: 1.0625rem; letter-spacing: 2px; position: relative; z-index: 1; }
.review-text {
  margin-top: 12px; font-size: 0.9375rem; color: var(--ink-2); line-height: 1.6;
  position: relative; z-index: 1;
  /* Cards vary a lot in review length; clamping keeps every card the same height so the
     row reads as one aligned set rather than a ragged, mismatched grid. */
  display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;
}
.review-author { display: flex; align-items: center; gap: 10px; margin-top: 18px; font-size: 0.875rem; }
.review-author img {
  border-radius: 50%; width: 38px; height: 38px; object-fit: cover;
  border: 2px solid var(--bg-2);
}
.review-author-fallback {
  width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0;
  background: var(--navy-deep); color: #fff; font-weight: 800; font-size: 0.875rem;
  display: flex; align-items: center; justify-content: center;
}
.review-time { color: var(--ink-2); font-size: 0.8125rem; }
.review-footer {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;
  margin-top: 24px; font-size: 0.875rem; color: var(--ink-2);
}
.review-footer a { color: var(--brass-deep); font-weight: 700; }

.reviews-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-card);
  color: var(--navy-deep); cursor: pointer; transition: background 150ms, color 150ms, transform 150ms;
}
.reviews-nav:hover { background: var(--navy-deep); color: #fff; transform: translateY(-50%) scale(1.06); }
.reviews-nav:active { transform: translateY(-50%) scale(0.96); }
.reviews-nav--prev { left: -20px; }
.reviews-nav--next { right: -20px; }
@media (max-width: 720px) {
  .reviews-nav--prev { left: -6px; }
  .reviews-nav--next { right: -6px; }
  .reviews-nav { width: 36px; height: 36px; }
  .review-card { flex-basis: 260px; }
}

/* ---- Areas map (real Leaflet + OpenStreetMap tiles, see assets/js/areas-map.js) ---- */
#areas-map {
  width: 100%; height: 420px; border-radius: 20px; border: 1px solid var(--line);
  overflow: hidden; z-index: 0; /* keep Leaflet's internal z-indexing below the sticky header */
}
/* Leaflet's own tile container/panes are square by default; the radius above is clipped by
   overflow:hidden on #areas-map, but Leaflet re-applies its own layout on the inner .leaflet-
   container, so it needs the same radius or corner tiles show square behind the mask. */
.leaflet-container { border-radius: 20px; }
/* The map frames all 27 areas (Southampton to Chichester), so the Portsmouth-city pins
   sit a few px apart; 12px keeps them distinguishable. Keep iconSize in areas-map.js
   in step. On phones the zone is ~3x wider than tall, so a shorter box wastes less. */
@media (max-width: 640px) { #areas-map { height: 300px; } }
.area-pin span {
  display: block; width: 12px; height: 12px; border-radius: 50%;
  background: var(--navy-deep); border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.4);
}
/* The one marker on the map that isn't a coverage-area dot — Seawolf's own registered
   office. Bigger, carries the favicon, and gets a soft beacon pulse (same nautical
   "lighthouse" motif as the voyage-timeline dots elsewhere on the homepage) so it reads
   as the map's home port rather than another area pin. */
.office-pin {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%; position: relative;
  background: #fff; border: 3px solid var(--brass); box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}
.office-pin img { width: 20px; height: 20px; border-radius: 50%; display: block; }
.office-pin::after {
  content: ''; position: absolute; inset: -6px; border-radius: 50%; border: 2px solid var(--brass);
  opacity: 0; pointer-events: none;
  animation: officePinPulse 2600ms var(--ease-out) infinite;
}
@keyframes officePinPulse {
  0%   { transform: scale(0.8); opacity: 0.6; }
  100% { transform: scale(1.7); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .office-pin::after { animation: none; } }
.leaflet-popup-content-wrapper { border-radius: 10px; font-family: var(--font-body); }
.leaflet-popup-content { margin: 12px 14px; font-size: 0.9375rem; }
.leaflet-popup-content a { color: var(--brass-deep); font-weight: 700; }
.leaflet-popup-content a:hover { text-decoration: underline; }

/* ---- Areas list (pill buttons beside the areas map, see index.html) ---- */
/* Compact enough for all 27 areas; 38px tall, still a comfortable tap target. */
.areas-list { display: flex; flex-wrap: wrap; gap: 8px; }
.area-link {
  display: inline-flex; align-items: center; padding: 9px 16px;
  border-radius: 999px; background: var(--surface); border: 1.5px solid var(--line);
  color: var(--ink); font-weight: 700; font-size: 0.875rem; text-decoration: none;
  transition: background 160ms var(--ease-out), border-color 160ms var(--ease-out), color 160ms var(--ease-out), transform 160ms var(--ease-out);
}
.area-link:hover { background: var(--navy); border-color: var(--navy); color: #fff; transform: translateY(-2px); }

/* ---- Areas CTA (below the pill list, see index.html) ---- */
.areas-cta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px;
  padding: 18px 22px; border-radius: var(--radius); background: var(--bg-2); border: 1px solid var(--line);
}
.areas-cta p { font-weight: 700; color: var(--ink); }

/* ---- FAQ accordion ---- */
.faq-item { border-bottom: 1px solid var(--line); }
.faq-q { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 20px 0; font: 700 1.0625rem var(--font-body); text-align: left; color: var(--ink); }
.faq-q .plus { flex-shrink: 0; width: 22px; height: 22px; border: 1.5px solid var(--brass); border-radius: 50%; position: relative; }
.faq-q .plus::before, .faq-q .plus::after { content: ''; position: absolute; background: var(--brass); }
.faq-q .plus::before { top: 50%; left: 5px; right: 5px; height: 1.5px; transform: translateY(-50%); }
.faq-q .plus::after { left: 50%; top: 5px; bottom: 5px; width: 1.5px; transform: translateX(-50%); transition: opacity 150ms; }
.faq-item.open .plus::after { opacity: 0; }
.faq-a { max-height: 0; overflow: hidden; transition: max-height 300ms var(--ease-out); }
.faq-item.open .faq-a { max-height: 400px; }
.faq-a p { padding-bottom: 20px; }

/* ---- Breadcrumb ---- */
.breadcrumb { display: flex; align-items: center; gap: 8px; font: 600 0.8125rem var(--font-body); color: var(--ink-2); flex-wrap: wrap; }
.breadcrumb a:hover { color: var(--brass); }
.breadcrumb .sep { opacity: 0.5; }

/* ---- Form ---- */
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font: 600 0.8125rem var(--font-body); color: var(--ink-2); }
.field input, .field select, .field textarea {
  border: 1.5px solid var(--line); border-radius: 4px; padding: 12px 14px; background: var(--surface); color: var(--ink);
}
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--brass); outline: none; }
/* Honeypot: kept in the DOM (not display:none, some bots skip those) but off-screen and
   out of the tab order / AT tree, so it never reaches a real visitor. */
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.form-notice {
  border: 1.5px solid var(--line); border-radius: var(--radius); padding: 14px 18px;
  font: 600 0.9375rem var(--font-body); margin-top: 24px;
}
.form-notice--success { border-color: var(--sea-glass); background: color-mix(in srgb, var(--sea-glass) 10%, var(--surface)); color: var(--ink); }
.form-notice--error { border-color: #C0392B; background: color-mix(in srgb, #C0392B 8%, var(--surface)); color: var(--ink); }

/* ---- Utility ---- */
.stack { display: flex; flex-direction: column; }
.row { display: flex; align-items: center; }
.gap-8 { gap: 8px; } .gap-12 { gap: 12px; } .gap-16 { gap: 16px; } .gap-24 { gap: 24px; }
.mt-8{margin-top:8px;} .mt-16{margin-top:16px;} .mt-24{margin-top:24px;} .mt-32{margin-top:32px;} .mt-48{margin-top:48px;}
.text-center { text-align: center; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }

/* ---- Cookie consent ---- */
/* Built and injected by /assets/js/cookie-consent.js — the markup lives in JS,
   not here, so every page (generated or hand-written) gets it for free from
   the single <div id="cookie-consent-root"> in the shared footer partial. */
.cc-banner {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 4000;
  background: var(--navy-deep); border-top: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 -8px 24px rgba(11,31,46,0.22);
  transform: translateY(100%); transition: transform 320ms var(--ease-out);
}
.cc-banner.cc-visible { transform: translateY(0); }
.cc-banner-inner {
  max-width: var(--container); margin-inline: auto; padding: 20px var(--gutter);
  display: flex; flex-wrap: wrap; align-items: center; gap: 16px 24px;
}
.cc-banner-text { margin: 0; flex: 1 1 360px; font-size: 0.875rem; line-height: 1.6; color: rgba(255,255,255,0.85); }
.cc-banner-text a { color: var(--brass); font-weight: 700; border-bottom: 1.5px solid transparent; }
.cc-banner-text a:hover { border-bottom-color: var(--brass); }
.cc-banner-actions { display: flex; flex-wrap: wrap; gap: 10px; flex: 0 0 auto; }
.cc-banner .btn-secondary { color: rgba(255,255,255,0.92); border-color: rgba(255,255,255,0.55); }
.cc-banner .btn-secondary:hover { border-color: var(--brass); color: var(--brass); }

.cc-modal-overlay {
  position: fixed; inset: 0; z-index: 4100; background: rgba(11,31,46,0.55);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  opacity: 0; pointer-events: none; transition: opacity 220ms var(--ease-out);
}
.cc-modal-overlay.cc-visible { opacity: 1; pointer-events: auto; }
.cc-modal-dialog {
  position: relative; width: 100%; max-width: 520px; max-height: calc(100vh - 40px); overflow-y: auto;
  background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 24px 60px rgba(11,31,46,0.35); padding: 32px;
  transform: translateY(12px); transition: transform 220ms var(--ease-out);
}
.cc-modal-overlay.cc-visible .cc-modal-dialog { transform: translateY(0); }
.cc-modal-dialog h2 { font-size: 1.375rem; margin: 0 0 8px; }
.cc-modal-intro { font-size: 0.9375rem; margin: 0 0 24px; }
.cc-modal-close {
  position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; color: var(--ink-2); font-size: 0.9375rem;
  border: 1px solid var(--line); background: var(--surface);
}
.cc-modal-close:hover { border-color: var(--brass); color: var(--brass); }
.cc-modal-list { display: flex; flex-direction: column; gap: 18px; margin-bottom: 28px; }
.cc-modal-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding-bottom: 18px; border-bottom: 1px solid var(--line);
}
.cc-modal-row:last-child { border-bottom: none; padding-bottom: 0; }
.cc-modal-row-label { display: block; font-weight: 700; font-size: 0.9375rem; margin-bottom: 4px; cursor: pointer; }
.cc-modal-row-desc { margin: 0; font-size: 0.8125rem; color: var(--ink-2); max-width: 34ch; }
.cc-modal-actions { display: flex; flex-wrap: wrap; gap: 12px; }
.cc-modal-actions .btn { flex: 1 1 160px; justify-content: center; }

.cc-toggle { position: relative; flex-shrink: 0; display: inline-block; width: 44px; height: 26px; cursor: pointer; margin-top: 2px; }
.cc-toggle-input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.cc-toggle-track {
  position: absolute; inset: 0; background: var(--line); border-radius: 999px; transition: background 180ms var(--ease-out);
}
.cc-toggle-track::after {
  content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(11,31,46,0.3); transition: transform 180ms var(--ease-out);
}
.cc-toggle-input:checked + .cc-toggle-track { background: var(--brass); }
.cc-toggle-input:checked + .cc-toggle-track::after { transform: translateX(18px); }
.cc-toggle-input:disabled + .cc-toggle-track { opacity: 0.6; cursor: not-allowed; }
.cc-toggle-input:focus-visible + .cc-toggle-track { outline: 2px solid var(--brass); outline-offset: 2px; }

@media (max-width: 640px) {
  /* Full-height-stacked (Manage / Reject / Accept, each its own full-width row) ate
     roughly half of a typical phone viewport. Manage + Reject share a row here, Accept
     all keeps its own row below — same DOM/tab order, two rows shorter overall. */
  .cc-banner-inner { padding: 14px 20px; gap: 12px 16px; }
  .cc-banner-text { font-size: 0.8125rem; }
  .cc-banner-actions { width: 100%; }
  .cc-banner-actions .btn { flex: 1 1 calc(50% - 5px); padding-block: 11px; }
  .cc-banner-actions .btn:last-child { flex: 1 1 100%; }
  .cc-modal-dialog { padding: 24px 20px; }
}

/* ---- Quote chat -> WhatsApp (assets/js/quote-chat.js) ---- */
.qc-launcher {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  display: inline-flex; align-items: center; gap: 8px; padding: 13px 20px; border-radius: 999px;
  background: var(--brass); color: var(--navy-deep); font: 700 0.9375rem var(--font-body);
  box-shadow: 0 10px 24px -6px rgba(11,31,46,0.45); cursor: pointer; border: none;
}
.qc-launcher svg { width: 20px; height: 20px; }
.qc-launcher.is-open span { display: none; }
.qc-panel {
  position: fixed; right: 20px; bottom: 84px; z-index: 60;
  width: min(380px, calc(100vw - 32px)); max-height: min(600px, calc(100vh - 120px));
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line); border-radius: 18px;
  box-shadow: 0 24px 48px -16px rgba(11,31,46,0.45);
}
.qc-panel[hidden] { display: none; }
.qc-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 14px 16px; background: var(--navy-deep); color: #fff; }
.qc-head strong { display: block; font-size: 1rem; }
.qc-head span { font-size: 0.8125rem; opacity: 0.75; }
.qc-close { color: #fff; font-size: 1.6rem; line-height: 1; width: 44px; height: 44px; border-radius: 50%; background: none; border: none; cursor: pointer; }
.qc-close:hover { background: rgba(255,255,255,0.12); }
.qc-log { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px; min-height: 160px; }
.qc-msg { max-width: 85%; padding: 10px 14px; border-radius: 16px; font-size: 0.9375rem; line-height: 1.45; }
.qc-bot { align-self: flex-start; background: var(--bg-2); color: var(--ink); border-bottom-left-radius: 4px; }
.qc-me { align-self: flex-end; background: var(--navy); color: #fff; border-bottom-right-radius: 4px; }
.qc-summary { white-space: pre-wrap; font: 500 0.8125rem/1.5 var(--font-body); background: var(--bg); border: 1px dashed var(--line); border-radius: 12px; padding: 12px; color: var(--ink); }
.qc-controls { padding: 12px 16px 16px; border-top: 1px solid var(--line); }
.qc-options { display: flex; flex-wrap: wrap; gap: 8px; }
.qc-option { padding: 9px 14px; border-radius: 999px; border: 1.5px solid var(--line); background: var(--surface); color: var(--ink); font: 600 0.875rem var(--font-body); cursor: pointer; min-height: 40px; }
.qc-option:hover, .qc-option:focus-visible { border-color: var(--navy); color: var(--navy); }
.qc-form { display: flex; gap: 8px; }
.qc-form input { flex: 1; min-width: 0; padding: 11px 14px; border: 1.5px solid var(--line); border-radius: 999px; font: 500 16px var(--font-body); color: var(--ink); background: var(--surface); }
.qc-form input:focus { outline: 2px solid var(--navy); outline-offset: 1px; }
.qc-send { width: 100%; justify-content: center; }
.qc-note { margin-top: 10px; font-size: 0.8125rem; text-align: center; max-width: none; }
.qc-note a, .qc-restart { color: var(--navy); font-weight: 700; background: none; border: none; cursor: pointer; font-size: inherit; padding: 0; }
@media (max-width: 480px) {
  .qc-launcher { right: 16px; bottom: 16px; }
  .qc-panel { right: 16px; bottom: 76px; }
}
/* Mobile menu open: the chat button would sit on top of the full-screen menu. */
body:has(.primary-nav.nav-open) .qc-launcher, body:has(.primary-nav.nav-open) .qc-panel { display: none; }

/* ---- Micro-interactions: small, purposeful feedback on hover and tap ----
   Everything here is motion, so it only exists for visitors who haven't asked their
   OS for reduced motion. Hover lifts are kept to hover-capable pointers so they don't
   stick on touch screens after a tap. */
@media (prefers-reduced-motion: no-preference) {
  .area-link, .qc-option, .qc-launcher, .manifest-row, .faq-q .plus, .nav-phone, .plaque {
    transition: transform 180ms var(--ease-out), box-shadow 180ms var(--ease-out), background 160ms var(--ease-out), border-color 160ms var(--ease-out), color 160ms var(--ease-out);
  }
  .area-link:active, .qc-option:active, .qc-launcher:active, .nav-phone:active { transform: scale(0.96); transition-duration: 80ms; }
  .manifest-row:active { transform: scale(0.99); }
  .faq-q:hover .plus { transform: rotate(90deg); }
  .faq-item.open .faq-q .plus { transform: rotate(180deg); }
  @media (hover: hover) {
    .qc-launcher:hover { transform: translateY(-2px); box-shadow: 0 14px 28px -8px rgba(11,31,46,0.5); }
    .qc-option:hover { transform: translateY(-1px); }
    .manifest-row:hover .num { transform: translateX(3px); }
    .manifest-row .num { display: inline-block; transition: transform 200ms var(--ease-out); }
    .plaque:hover { box-shadow: 0 16px 32px -18px rgba(11,31,46,0.35); }
  }
  /* New chat bubbles slide in rather than popping. */
  .qc-msg, .qc-summary { animation: qcIn 260ms var(--ease-out) both; }
  @keyframes qcIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
  /* One gentle nudge a few seconds after load so the chat button is noticed, then still. */
  .qc-launcher:not(.is-open) { animation: qcNudge 700ms var(--ease-out) 4s 1 both; }
  @keyframes qcNudge { 0%, 100% { transform: none; } 30% { transform: translateY(-6px); } 60% { transform: translateY(0); } 80% { transform: translateY(-2px); } }
}
/* Measured claim, not a carbon figure: page weights taken 2026-09-30 (inner pages
   350-410 KB, homepage ~1.4 MB incl. map tiles) against HTTP Archive's ~2.3 MB median.
   Re-measure if pages get heavier; don't turn this into a CO2 number without a proper
   tool (e.g. Website Carbon) run on the live site. */
.footer-lean a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.footer-lean a:hover { color: #fff; }
/* WhatsApp's own glyph (used as a link to WhatsApp, as its brand guidelines allow). */
.qc-launcher .qc-wa { width: 22px; height: 22px; }
.qc-send { gap: 10px; }
.qc-send .qc-wa { width: 20px; height: 20px; flex-shrink: 0; }
.qc-send svg { width: 20px; height: 20px; flex-shrink: 0; }
.qc-send + .qc-send { margin-top: 8px; }
.qc-send.is-sent { border-color: var(--brass-deep); color: var(--brass-deep); }

/* Contact menu: the panel's first screen. One primary action (guided quote) and four
   direct routes in a 2x2 grid, all 44px+ tap targets. */
.qc-menu-main {
  display: flex; align-items: center; gap: 12px; width: 100%; padding: 14px 16px; text-align: left;
  border: none; border-radius: 14px; background: var(--navy-deep); color: #fff; cursor: pointer;
  font: 500 0.8125rem/1.3 var(--font-body);
}
.qc-menu-main svg { width: 26px; height: 26px; flex-shrink: 0; color: var(--brass); }
.qc-menu-main strong { display: block; font-size: 1rem; }
.qc-menu-main small { opacity: 0.75; font-size: inherit; }
.qc-menu-main:hover, .qc-menu-main:focus-visible { background: var(--navy); }
.qc-menu-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
.qc-menu-item, .qc-alt-item {
  display: flex; align-items: center; gap: 8px; min-height: 48px; padding: 10px 12px;
  border: 1.5px solid var(--line); border-radius: 12px; background: var(--surface);
  color: var(--ink); font: 600 0.875rem var(--font-body); text-decoration: none; cursor: pointer;
}
.qc-menu-item svg, .qc-alt-item svg { width: 20px; height: 20px; flex-shrink: 0; color: var(--navy); }
.qc-menu-item:hover, .qc-menu-item:focus-visible, .qc-alt-item:hover, .qc-alt-item:focus-visible { border-color: var(--navy); color: var(--navy); }
.qc-alt { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }
.qc-alt-item { justify-content: center; flex-direction: column; gap: 4px; font-size: 0.8125rem; }
.qc-error { margin-top: 8px; font-size: 0.8125rem; color: #C0392B; max-width: none; } /* same error red as .form-notice--error */
.qc-skip { display: block; margin: 8px auto 0; padding: 8px 12px; background: none; border: none; color: var(--navy); font: 700 0.8125rem var(--font-body); cursor: pointer; }
.qc-form input[type="date"] { min-height: 46px; }

/* ---- 404 page (generate-pages.js) ---- */
.nf-code {
  font: 800 clamp(5rem, 16vw, 9.5rem)/0.9 var(--font-display); letter-spacing: -0.04em;
  color: var(--navy); margin-bottom: 8px;
}
.nf-code span { color: var(--brass-deep); }
.nf-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }
.nf-links { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px; margin-top: 40px; }
.nf-links a {
  display: block; padding: 14px 16px; background: var(--surface); border: 1px solid var(--line);
  border-radius: 4px; color: var(--ink); font-weight: 600; text-decoration: none;
}
.nf-links a small { display: block; font-weight: 400; color: var(--ink-2); font-size: 0.8125rem; }
.nf-links a:hover { border-color: var(--navy); color: var(--navy); }

/* Phones/tablets (hamburger mode): the phone chip used to vanish into the menu, so the
   single most important action for a local service took two taps. Icon-only call
   button beside the hamburger; font-size:0 hides the number visually but keeps it as
   the link's accessible name (no markup change across the 58 inlined headers). */
@media (max-width: 1359.98px) {
  .nav-phone[href^="tel:"] {
    display: inline-flex; justify-content: center; font-size: 0; gap: 0; padding: 0;
    width: 42px; height: 42px; border-radius: 12px; color: #fff;
    background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.32);
  }
  .nav-phone[href^="tel:"] svg { width: 18px; height: 18px; }
}

/* Blog reading progress (element added by site.js on article pages only). */
.read-progress {
  position: fixed; left: 0; top: 0; z-index: 70; height: 3px; width: 100%;
  background: var(--brass); transform-origin: 0 50%; transform: scaleX(0); pointer-events: none;
}
