/* ===========================================================================
   Blue Ocean Marketing — shared client-site stylesheet.

   ONE file, served to every client site and cached across all of them. There
   is nothing site-specific in here: each site's palette arrives as a `:root`
   override emitted inline by layout.twig from that site's `theme` row, which
   is why every color below that could vary by brand is a var() rather than
   a literal.

   The literals that remain are deliberate: white text and dark shadows over
   a darkened hero photo stay legible on any brand, so they are not themeable.

   The values here are only a fallback for a page rendered without its theme
   block. App\Utils\Content::DEFAULT_THEME is the authority and doubles as the
   allowlist of themeable tokens — keep the two in sync.
   =========================================================================== */
:root {
    --bg: #0f1419;
    --panel: #1a2332;
    /* One step darker than --panel, for insets that need to read as "sunk
       into" a dark section: code blocks and form fields. */
    --panel-deep: #0b1018;
    --text: #e7ecf3;
    --muted: #8b9bb4;
    --accent: #3d8bfd;
    /* Whatever stays readable ON --accent. Split out from a hardcoded #fff so
       a client with a pale brand (a bright yellow, say) can flip it to a dark
       value and keep every button, pill and CTA band legible. */
    --accent-contrast: #ffffff;
    --border: #2a3548;
    --error: #f66;
    /* Review stars. Not brand-derived — a gold star is a gold star. */
    --star: #f5b942;
    /* Light-section tokens — used by the alternating light bands
       (services, work, reviews, FAQ) between the dark hero/about/
       quote sections. Kept separate from the dark --bg/--panel/etc.
       above rather than swapping the whole theme. */
    --light-bg: #f7f8fa;
    --light-panel: #ffffff;
    --light-text: #1a2332;
    --light-muted: #5b6b84;
    --light-border: #e7ebf0;
    /* A third, visibly-darker-than-light-bg gray step — used for
       one section (see .section-mid) so the page isn't just a
       strict light/dark checkerboard; still light-family (dark
       text stays readable), just a clear shade down from
       --light-bg. */
    --mid-bg: #dde2e8;
    --mid-border: #c7cedb;
    /* The header band. Its own pair rather than --light-panel/--light-text so
       a site can run a brand-colored header (dark green to match a logo) while
       every card and light section stays as it is. Defaults equal the light
       panel, so an unthemed site is unchanged. */
    --header-bg: #ffffff;
    --header-text: #1a2332;
    /* Corner radii. Not theme tokens (Content::theme() only lets colors
       through); body.corners-sharp zeroes them all, which is what
       business.corners = 'sharp' sets. Every border-radius below reads one of
       these, so the two looks cannot drift apart rule by rule. */
    --radius-sm: 6px;
    --radius-md: 8px;
    --radius-lg: 10px;
    --radius-xl: 14px;
}
body.corners-sharp {
    --radius-sm: 0;
    --radius-md: 0;
    --radius-lg: 0;
    --radius-xl: 0;
}
* { box-sizing: border-box; }
body {
    margin: 0;
    font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.5;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
header {
    position: sticky;
    top: 0;
    z-index: 100;
    background: var(--header-bg);
    color: var(--header-text);
    /* Derived from the header text rather than --light-border, so the line
       stays a faint version of the text on a dark band as well as a white one. */
    border-bottom: 1px solid color-mix(in srgb, var(--header-text) 12%, transparent);
    box-shadow: 0 1px 2px rgba(15, 20, 25, 0.06);
}
.header-inner {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0.75rem 1.5rem;
    min-height: 76px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 1.5rem;
    /* The row never breaks. A long business name shrinks the brand
       and wraps its text onto a second line instead; everything
       else in the row (nav, phone pill, hamburger) keeps its width.
       This used to be flex-wrap: wrap, which put the hamburger on a
       line of its own under any long brand at phone widths. */
    flex-wrap: nowrap;
}
header .brand {
    display: inline-flex;
    align-items: center;
    gap: 0.65rem;
    /* The one flex item allowed to give. min-width: 0 overrides the
       default min-width: auto, which would otherwise hold the brand
       at its full content width and push the row past the viewport. */
    flex: 1 1 auto;
    min-width: 0;
}
header .brand:hover { text-decoration: none; }
/* brand_layout: 'stacked' puts the name under the logo instead of beside
   it, for an emblem with a long business name. The name drops a size, since
   the logo already carries the header's height. Content::business() only
   emits it when logo_display is 'both'. */
header .brand-stacked {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.2rem;
}
header .brand-stacked .brand-text {
    font-size: 1rem;
    line-height: 1.2;
}
/* logo_display: 'logo' | 'both' | 'text' (default) — see
   App\Utils\Content::business() for the fallback logic and
   content.php's business.logo/business.logo_display for how to
   set it per site. */
header .brand-logo {
    display: block;
    height: 40px;
    width: auto;
    flex-shrink: 0;
}
/* business.logo_size = 'large': twice the height, for a lockup whose
   wordmark is unreadable at 40px. The row grows with it; nothing else in the
   header changes size. Smaller again on a phone, where the hamburger and the
   phone pill share the row. */
header.logo-large .brand-logo { height: 80px; }
header .brand-text {
    font-size: 1.25rem;
    font-weight: 800;
    letter-spacing: -0.01em;
    color: var(--header-text);
    line-height: 1.25;
    /* Wraps at spaces first; break-word only splits a single word
       that is wider than the space it has, so a one-word name can
       never overflow the header either. */
    min-width: 0;
    overflow-wrap: break-word;
}
header nav {
    display: flex;
    align-items: center;
    gap: 1.75rem;
    font-size: 0.95rem;
    font-weight: 600;
    flex-shrink: 0;
}
header nav a {
    color: color-mix(in srgb, var(--header-text) 78%, transparent);
    transition: color 0.15s ease;
}
header nav a:hover {
    color: var(--header-text);
    text-decoration: none;
}
header .phone-cta {
    display: inline-flex;
    align-items: stretch;
    border-radius: var(--radius-lg);
    overflow: hidden;
    animation: rocking 2s infinite;
    animation-timing-function: ease-out;
    flex-shrink: 0;
}
header .phone-cta a {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    background: var(--accent);
    color: var(--accent-contrast);
    font-weight: 700;
    font-size: 0.9rem;
    white-space: nowrap;
    transition: background 0.15s ease;
}
header .phone-cta a:hover {
    filter: brightness(0.85);
    text-decoration: none;
}
header .phone-cta svg { flex-shrink: 0; }
header .phone-cta-call { padding: 0.7rem 1.1rem 0.7rem 1.35rem; }
header .phone-cta-text {
    padding: 0.7rem 1.1rem;
    border-left: 1px solid rgba(255, 255, 255, 0.25);
}
/* Second .phone-cta lives inside the mobile nav dropdown (see
   .nav-phone-cta below) — hidden by default so it doesn't also
   show inline in the desktop nav row; the mobile media query
   below swaps which of the two instances is visible. */
header nav .phone-cta { display: none; }
.nav-phone-cta {
    display: none;
    justify-content: center;
    padding: 1rem 1.5rem;
    border-top: 1px solid color-mix(in srgb, var(--header-text) 12%, transparent);
}
@keyframes rocking {
    0%   { transform: rotate(0deg); }
    25%  { transform: rotate(0deg); }
    50%  { transform: rotate(2deg); }
    75%  { transform: rotate(-2deg); }
    100% { transform: rotate(0deg); }
}
/* business.phone_cta_shake = false. */
header .phone-cta.phone-cta-still { animation: none; }
/* Text-only pill: the text link carries a word, and the number's padding. */
header .phone-cta-text-only .phone-cta-text {
    padding: 0.7rem 1.1rem 0.7rem 1.35rem;
    border-left: 0;
}
@media (prefers-reduced-motion: reduce) {
    header .phone-cta { animation: none; }
}

/* Mobile nav toggle — pure CSS (checkbox hack), no script/CSP nonce needed */
header .nav-toggle { display: none; }
header .nav-toggle-label {
    display: none;
    cursor: pointer;
    padding: 0.5rem;
    /* Negative margins keep the tap target generous without adding
       to the row's height; margin-left: auto pins it to the right
       edge. It never wraps below the brand: .header-inner is nowrap
       and the brand is the item that shrinks. */
    margin: -0.5rem -0.5rem -0.5rem auto;
    flex-shrink: 0;
}
header .nav-toggle-label span,
header .nav-toggle-label span::before,
header .nav-toggle-label span::after {
    display: block;
    width: 22px;
    height: 2px;
    background: var(--header-text);
    position: relative;
    transition: transform 0.15s ease, opacity 0.15s ease;
}
header .nav-toggle-label span::before { content: ''; position: absolute; top: -7px; }
header .nav-toggle-label span::after { content: ''; position: absolute; top: 7px; }
header .nav-toggle:checked ~ .nav-toggle-label span { background: transparent; }
header .nav-toggle:checked ~ .nav-toggle-label span::before { transform: translateY(7px) rotate(45deg); }
header .nav-toggle:checked ~ .nav-toggle-label span::after { transform: translateY(-7px) rotate(-45deg); }

@media (max-width: 860px) {
    header nav {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        background: var(--header-bg);
        border-bottom: 1px solid color-mix(in srgb, var(--header-text) 12%, transparent);
        box-shadow: 0 4px 8px rgba(15, 20, 25, 0.06);
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        max-height: 0;
        overflow: hidden;
        transition: max-height 0.2s ease;
    }
    header nav a {
        padding: 0.9rem 1.5rem;
        border-top: 1px solid var(--light-border);
    }
    header .nav-toggle:checked ~ nav { max-height: 400px; }
    header .nav-toggle-label { display: block; }
    /* Swap which .phone-cta shows: the header-row one (used on
       desktop, next to the inline nav) hides, and the one inside
       the dropdown (.nav-phone-cta, last item in <nav>) takes
       over — avoids fighting a second wrapped header row for
       something that already has a natural home in the dropdown. */
    .header-inner > .phone-cta { display: none; }
    header nav .phone-cta { display: flex; }
    .nav-phone-cta { display: flex; }
}
@media (max-width: 480px) {
    header .phone-cta-call span { display: none; }
    header .phone-cta-call { padding: 0.7rem 0.9rem; }
}
@media (max-width: 600px) {
    header.logo-large .brand-logo { height: 60px; }
}

.hero {
    position: relative;
    background-image: linear-gradient(135deg, var(--bg) 0%, var(--panel) 100%);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    padding: 5rem 1.5rem 4rem;
    text-align: left;
}
/* Background video (sections/hero/_hero.twig). The video fills the section
   under a scrim carrying the same darkening as the photo; the copy and the
   form sit above both. The section's own background-image (the poster plus
   the gradient) stays underneath, which is what shows until the first frame
   decodes and whenever the browser declines to play. */
.hero-has-video {
    overflow: hidden;
    min-height: min(78vh, 720px);
    display: flex;
    align-items: center;
}
.hero-has-video > .hero-inner { width: 100%; }
.hero-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 0;
    pointer-events: none;
}
.hero-scrim {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
    .hero-video { display: none; }
}
.hero-inner {
    position: relative;
    z-index: 2;
    max-width: 1200px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: 1.15fr 0.85fr;
    gap: 3rem;
    align-items: center;
}
/* No form beside the copy (hero.form = 'below' or 'hidden'): one column,
   the copy held to a readable measure. */
.hero-inner-solo { grid-template-columns: 1fr; }
.hero-inner-solo .hero-copy { max-width: 760px; }
/* The centered variant: copy centered over the picture, form beneath. */
.hero-inner-centered {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
}
.hero-inner-centered .hero-subheadline { margin-left: auto; margin-right: auto; }
.hero-inner-centered .hero-actions,
.hero-inner-centered .hero-stats,
.hero-inner-centered .hero-stats-line-row { justify-content: center; }
.hero-inner-centered .hero-stat { align-items: center; }
.hero-inner-centered .lead-form-card { width: 100%; max-width: 560px; }
/* The lead_form section: an ordinary light band anywhere on the page. */
.lead-form-section .lead-form-card-below,
.hero-form-below .lead-form-card-below {
    position: relative;
    z-index: 3;
    max-width: 560px;
    margin: 0 auto;
}
/* Directly under the hero it keeps the look it had as hero.form = 'below':
   the card floats up over the hero's bottom edge, so the picture runs clean
   and the form is still the first thing under it.

   .hero-form-below is that retired markup, and is kept here on purpose: a
   published site is static HTML on disk that a deploy does not re-render,
   while this stylesheet is shared and served live. So every site published
   before this release keeps its old class until someone republishes it, and
   dropping the rule would flatten the form on all of them in the meantime.
   Removable once every site has been republished — the same two-deploy rule
   migrations/AGENTS.md applies to columns. */
.lead-form-section.lead-form-after-hero,
.hero-form-below { padding-top: 0; }
.lead-form-section.lead-form-after-hero .lead-form-card-below,
.hero-form-below .lead-form-card-below { margin-top: -3rem; }
.hero-eyebrow {
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.8rem;
    font-weight: 700;
    color: #fff;
    margin: 0 0 1rem;
    text-shadow: 0 0 8px rgba(0, 0, 0, 0.7), 0 2px 4px rgba(0, 0, 0, 0.6);
}
.hero-headline {
    font-size: clamp(2rem, 4.5vw, 3.25rem);
    line-height: 1.15;
    font-weight: 800;
    margin: 0 0 1.25rem;
    color: #fff;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.55);
}
.hero-subheadline {
    font-size: 1.1rem;
    line-height: 1.6;
    color: #fff;
    max-width: 560px;
    margin: 0 0 2rem;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
}
.hero-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 1rem;
    margin-bottom: 2.5rem;
}
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.85rem 1.75rem;
    border-radius: var(--radius-lg);
    font-weight: 700;
    font-size: 1rem;
    white-space: nowrap;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.btn:hover { text-decoration: none; }
.btn-primary { background: var(--accent); color: var(--accent-contrast); }
/* Derived from --accent rather than `filter: brightness()`: brightness
   darkens a mid-tone brown or green into mud, which is why the Grinstead
   site under _template had to replace this rule with a hand-picked hex and
   lost the automatic hover shade. color-mix keeps one --accent change
   re-toning the whole site. */
.btn-primary:hover { background: color-mix(in srgb, var(--accent) 85%, black); }
.btn-outline {
    background: transparent;
    color: #fff;
    border: 1.5px solid var(--border);
}
.btn-outline:hover { border-color: var(--accent); color: var(--accent); }
.hero-stats {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 2.5rem;
}
.hero-stat { display: flex; flex-direction: column; align-items: flex-start; }
.hero-stat-value {
    font-size: 1.5rem;
    font-weight: 800;
    color: #fff;
    text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.hero-stat-label {
    font-size: 0.8rem;
    color: #fff;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-top: 0.25rem;
    text-shadow: 0 1px 5px rgba(0, 0, 0, 0.55);
}
/* hero.stats_style = 'line' (the default): the stats as one line of phrases
   at one size, a bar between each. Every item carries its bar in a
   fixed-width ::before, and the row is pulled left by exactly that width
   under an overflow:hidden outer - so the bar at the start of every row,
   including a row a phone wraps onto, is clipped out of view. A bar only on
   `+ .hero-stat-item` would wrap along with its item and open the new row. */
.hero-stats-line {
    overflow: hidden;
    /* Sits between the subheadline and the button. */
    margin: 0 0 2rem;
    color: #fff;
    /* Same size and weight as the subheadline: the bars alone set it apart
       as a proof strip. Jason tried bold and semibold first and wanted it
       quieter next to the button. */
    font-size: 1.1rem;
    font-weight: 400;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
}
.hero-stats-line-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem 0;
    margin-left: -1.75rem;
}
.hero-stat-item::before {
    content: "|";
    display: inline-block;
    width: 1.75rem;
    text-align: center;
    opacity: 0.5;
}

/* Reusable light-card lead-capture form — used in the hero's Contact
   Us card and on /contact-us (see app/views/_lead_form.twig). No
   existing light-on-dark component to reuse (unlike .section-light,
   which is a full-width band, not a floating card), so this is
   purpose-built. Reuses the --light-* tokens for consistency with
   the rest of the site's light surfaces. */
.lead-form-card {
    background: var(--light-panel);
    color: var(--light-text);
    border-radius: var(--radius-xl);
    padding: 2rem;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
}
.lead-form-heading { font-size: 1.5rem; font-weight: 800; margin: 0 0 0.5rem; color: var(--light-text); }
.lead-form-intro { font-size: 0.9rem; color: var(--light-muted); margin: 0 0 1.25rem; }
.lead-form { display: flex; flex-direction: column; gap: 1rem; }
/* Honeypot — see _lead_form.twig and App\Utils\FormGuard. Taken out of the
   layout entirely rather than hidden with visibility/opacity, so it can never
   flash on a slow paint or occupy space. Not `display:none` on the input
   itself: some bots specifically skip those, and the point is that a bot
   believes this field is real. */
.lead-form-hp {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}
.lead-form label {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--light-text);
}
.lead-form-optional { font-weight: 400; color: var(--light-muted); }
.lead-form input,
.lead-form textarea {
    background: var(--light-bg);
    border: 1px solid var(--light-border);
    border-radius: var(--radius-md);
    padding: 0.65rem 0.75rem;
    color: var(--light-text);
    font: inherit;
}
.lead-form input::placeholder,
.lead-form textarea::placeholder { color: var(--light-muted); }
.lead-form .error { color: var(--error); font-size: 0.85rem; margin: 0; }
/* Rendered hidden and revealed by beacon.js after a ?lead_error=1 redirect. */
.lead-form .error[hidden], .contact-form .error[hidden] { display: none; }
.lead-form-submit { width: 100%; }
.lead-form-disclaimer {
    font-size: 0.75rem;
    color: var(--light-muted);
    margin: 1rem 0 0;
    text-align: center;
}
/* Standalone /contact-us page: no card chrome (see contact.twig) —
   just the bare form, same width the card used to constrain it to,
   centered in the section. */
.lead-form-wrap { max-width: 480px; margin: 0 auto; }
/* .lead-form's inputs default to --light-bg for contrast against
   .lead-form-card's white (--light-panel) background. On a bare
   .section-light band (--light-bg itself), invert that so inputs
   still stand out against the section. */
.section-light .lead-form input,
.section-light .lead-form textarea { background: var(--light-panel); }
/* .contact-form defaults to dark-panel colors (its usual home is a
   dark .card); override for use inside a light section. */
.section-light .contact-form label { color: var(--light-muted); }
.section-light .contact-form input,
.section-light .contact-form textarea {
    background: var(--light-panel);
    border-color: var(--light-border);
    color: var(--light-text);
}
@media (max-width: 900px) {
    .hero-inner { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
    .hero { padding: 3.5rem 1.25rem 3rem; }
    .hero-actions { flex-direction: column; align-items: stretch; }
    .hero-stats { gap: 1.5rem 2rem; }
}

/* Full-bleed homepage sections below the hero — alternating light/dark
   bands, each a direct sibling of main (see the "sections" Twig
   block below), not constrained by main's narrow 720px column. */
.section { padding: 5rem 1.5rem; }
.section-inner { max-width: 1200px; margin: 0 auto; }
.section-light { background: var(--light-bg); color: var(--light-text); }
.section-dark { background: var(--bg); color: var(--text); }
.section-mid { background: var(--mid-bg); color: var(--light-text); }
.section-header {
    max-width: 640px;
    margin: 0 auto 3rem;
    text-align: center;
}
.section-header-split {
    max-width: none;
    margin: 0 0 2.5rem;
    text-align: left;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1.5rem;
}
.section-header-split .section-heading { margin-bottom: 0; }
.section-eyebrow {
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--accent);
    margin: 0 0 0.75rem;
}
.section-heading {
    font-size: clamp(1.75rem, 3.5vw, 2.5rem);
    font-weight: 800;
    line-height: 1.2;
    margin: 0 0 1rem;
}
.section-intro { font-size: 1.05rem; line-height: 1.6; opacity: 0.72; margin: 0; }
/* .btn-outline defaults to white/dark-border for the hero (see above);
   override for use on a light section so it reads against a light bg. */
.section-light .btn-outline,
.section-mid .btn-outline { color: var(--light-text); border-color: var(--light-border); }
@media (max-width: 640px) {
    .section { padding: 3.5rem 1.25rem; }
    .section-header-split { flex-direction: column; align-items: flex-start; }
}
.section-inner.section-inner-narrow { max-width: 760px; }
/* Body text a client typed, anywhere it appears: a service, product,
   location or project page, the two policy pages, and the Text homepage
   section. `.rich` is the wrapper App\Utils\RichText's output goes in — see
   that class for the markers, and note it is the ONLY thing in this
   stylesheet allowed to style bare h2/h3/p/ul/ol/li, because it is the only
   place tags arrive without classes on them.

   The .section-inner-narrow half of each selector is what these rules were
   before .rich existed, and it stays: it also carries the plain <p> on the
   FAQ, contact, thank-you and expired pages and on every index, none of
   which is a .rich block. .rich is listed alongside rather than replacing it
   so the Text section can render the same type in a two-column layout, which
   is not narrow. */
.section-inner-narrow h2,
.rich h2 { font-size: 1.3rem; font-weight: 700; margin: 2rem 0 0.75rem; }
.section-inner-narrow h3,
.rich h3 { font-size: 1.05rem; font-weight: 700; margin: 1.5rem 0 0.5rem; }
.section-inner-narrow p,
.rich p { line-height: 1.6; margin: 0 0 1rem; }
.section-inner-narrow ul,
.rich ul,
.rich ol { margin: 0 0 1rem; padding-left: 1.25rem; line-height: 1.6; }
.section-inner-narrow li,
.rich li { margin-bottom: 0.4rem; }
/* The first block in a body sits against whatever is above it — a heading,
   a banner photo — so it must not add the top margin a mid-body heading
   needs. The last one must not push the section's own padding out. */
.rich > :first-child { margin-top: 0; }
.rich > :last-child { margin-bottom: 0; }
.rich a { color: var(--accent); text-decoration: underline; }
.rich a:hover { text-decoration: none; }
/* "Last Updated" on the two legal pages. Quieter than the body it introduces:
   it is a fact about the document, not the first thing to read in it. */
.policy-updated { color: var(--muted); font-size: 0.9rem; margin: -0.5rem 0 1.5rem; }
.section-cta { text-align: center; margin: 2rem 0 0; }

/* Bottom-of-homepage CTA band (see home.twig) — solid accent fill,
   distinct from both --bg/--light-bg, so it reads as a deliberate
   "stop scrolling, take action" band after the rest of the page. */
.cta-section { background: var(--accent); color: var(--accent-contrast); }
.cta-section .section-heading,
.cta-section .section-intro { color: var(--accent-contrast); }
.cta-section .section-intro { opacity: 0.92; }
.cta-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 1rem;
    margin-top: 2rem;
}
/* The "or call" line under the button on expired.twig. Its own rule rather
   than reusing .section-intro because that is margin: 0 and would sit flush
   against the button it follows. Scoped to the container it lives in on
   purpose: a bare `.expired-note` loses to `.section-inner-narrow p` on
   specificity and the margin-top below silently does nothing. */
.section-inner-narrow .expired-note {
    margin: 1rem 0 0;
    text-align: center;
    font-size: 0.95rem;
    opacity: 0.72;
}
/* Same .phone-cta component as the header (see header .phone-cta
   above and _phone_cta.twig), inverted: white pill/accent text
   instead of accent pill/white text, since this section's own
   background is already the accent color. No rocking animation
   here (that's scoped to `header .phone-cta` only) — a bottom-of-
   page CTA doesn't need the attention-grabbing motion. */
.cta-section .phone-cta {
    display: inline-flex;
    align-items: stretch;
    border-radius: var(--radius-lg);
    overflow: hidden;
}
.cta-section .phone-cta a {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    /* Inverted pill sitting on the accent band: the two colors are each
       other's opposites, so both come from the pair rather than one being a
       literal white that stops matching once --accent-contrast changes. */
    background: var(--accent-contrast);
    color: var(--accent);
    font-weight: 700;
    font-size: 0.9rem;
    white-space: nowrap;
    transition: background 0.15s ease;
}
.cta-section .phone-cta a:hover { background: color-mix(in srgb, var(--accent) 10%, var(--accent-contrast)); text-decoration: none; }
.cta-section .phone-cta svg { flex-shrink: 0; }
.cta-section .phone-cta-call { padding: 0.7rem 1.1rem 0.7rem 1.35rem; }
.cta-section .phone-cta-text {
    padding: 0.7rem 1.1rem;
    border-left: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
}
.cta-section .phone-cta-text-only .phone-cta-text {
    padding: 0.7rem 1.1rem 0.7rem 1.35rem;
    border-left: 0;
}
/* cta.buttons = 'form' | 'both': a button to the enquiry form on the band.
   Inverted like the pill, so the two read as one set. */
.cta-section .btn-form {
    background: var(--accent-contrast);
    color: var(--accent);
    padding: 0.7rem 1.35rem;
    font-size: 0.9rem;
}
.cta-section .btn-form:hover { background: color-mix(in srgb, var(--accent) 10%, var(--accent-contrast)); }

/* Services grid */
.services-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 320px));
    justify-content: center;
    gap: 1.5rem;
}
.service-card {
    background: var(--light-panel);
    border: 1px solid var(--light-border);
    border-radius: var(--radius-xl);
    padding: 2rem 1.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.service-card:hover { box-shadow: 0 8px 24px rgba(15, 20, 25, 0.08); transform: translateY(-2px); }
.service-icon {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-lg);
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    color: var(--accent);
    display: flex;
    align-items: center;
    justify-content: center;
}
.service-icon svg { width: 24px; height: 24px; }
.service-card-image {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: var(--radius-md);
    display: block;
}
.service-card h3 { margin: 0.25rem 0 0; font-size: 1.15rem; font-weight: 700; }
.service-card p { margin: 0; color: var(--light-muted); font-size: 0.95rem; line-height: 1.55; flex: 1; }
.btn-block { width: 100%; margin-top: 0.25rem; }

/* Services grid, photo-card variant (sections/services/photos.twig): the
   picture, a brand-colored band carrying the name, and an arrow tile. The
   whole card is the link. Wider minimum than the icon grid because the
   picture is the point. */
.services-photos {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1.5rem;
}
.photo-card {
    display: flex;
    flex-direction: column;
    background: var(--light-panel);
    border: 1px solid var(--light-border);
    border-radius: var(--radius-xl);
    overflow: hidden;
    color: var(--light-text);
    transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.photo-card:hover {
    text-decoration: none;
    box-shadow: 0 8px 24px rgba(15, 20, 25, 0.12);
    transform: translateY(-2px);
}
.photo-card-image {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    display: block;
}
.photo-card-band {
    display: flex;
    align-items: stretch;
    justify-content: space-between;
    gap: 1rem;
    background: var(--accent);
    color: var(--accent-contrast);
    min-height: 64px;
}
.photo-card-title {
    align-self: center;
    padding: 0.75rem 1.25rem;
    font-size: 1.2rem;
    font-weight: 800;
    line-height: 1.2;
}
.photo-card-arrow {
    flex-shrink: 0;
    width: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    color: #fff;
    transition: background 0.15s ease;
}
.photo-card:hover .photo-card-arrow { background: color-mix(in srgb, var(--bg) 80%, var(--accent)); }
.photo-card-arrow svg { width: 24px; height: 24px; }
.photo-card-summary {
    padding: 1rem 1.25rem;
    color: var(--light-muted);
    font-size: 0.95rem;
    line-height: 1.55;
}
/* The service page's own picture (services/show.twig). */
.service-banner {
    display: block;
    width: 100%;
    max-height: 420px;
    object-fit: cover;
    border-radius: var(--radius-xl);
    margin: 0 0 2rem;
}

/* About */
.about-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 3rem;
    align-items: center;
}
.about-media {
    aspect-ratio: 4 / 3;
    border-radius: var(--radius-xl);
    background: var(--panel) center/cover no-repeat;
    border: 1px solid var(--border);
}
/* A project page's photo strip sits a little below its copy. A class rather
   than a style="" attribute: client sites run a stylesheet policy without
   'unsafe-inline'. */
.section-inner-spaced { margin-top: 2rem; }
.about-copy .section-heading { margin-bottom: 1.25rem; }
.about-copy .section-intro { opacity: 0.85; }
@media (max-width: 800px) {
    .about-grid { grid-template-columns: 1fr; }
    .about-media { order: -1; }
}

/* Text section (sections/text/default.twig) — a client's own words, with or
   without a picture beside them. No typography of its own: the `.rich` rules
   above carry every heading, list and paragraph inside it, which is the whole
   point of the section existing. */
.text-with-image {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 3rem;
    align-items: center;
}
/* Source order is copy then picture, so a phone reads the words first; the
   left variant swaps them on wide screens only. */
.text-with-image .text-media { grid-column: 2; grid-row: 1; }
.text-with-image .text-copy { grid-column: 1; grid-row: 1; }
.text-image-left .text-media { grid-column: 1; }
.text-image-left .text-copy { grid-column: 2; }
.text-media img {
    display: block;
    width: 100%;
    border-radius: var(--radius-xl);
}
/* Centering a whole body of text only reads when there is no picture pulling
   the eye sideways, which is why `align` is offered on its own. Headings and
   paragraphs center; a list keeps its left edge, because ragged bullets are
   unreadable. */
.text-center { text-align: center; }
.text-center .rich ul,
.text-center .rich ol { display: inline-block; text-align: left; }
@media (max-width: 800px) {
    .text-with-image { grid-template-columns: 1fr; gap: 2rem; }
    .text-with-image .text-media,
    .text-with-image .text-copy,
    .text-image-left .text-media,
    .text-image-left .text-copy { grid-column: 1; grid-row: auto; }
}

/* Service area map */
.map-embed {
    aspect-ratio: 16 / 9;
    border-radius: var(--radius-xl);
    overflow: hidden;
    border: 1px solid var(--border);
}
.map-embed iframe { display: block; width: 100%; height: 100%; border: 0; }
.section-light .map-embed { border-color: var(--light-border); }
.section-mid .map-embed { border-color: var(--mid-border); }

main { max-width: 720px; margin: 2rem auto; padding: 0 1.5rem; flex: 1; width: 100%; }
[id] { scroll-margin-top: 96px; }
.card {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 1.5rem;
    margin-bottom: 1rem;
}
h1 { margin-top: 0; font-size: 1.75rem; }
.muted { color: var(--muted); font-size: 0.9rem; }
ul.link-list { list-style: none; padding: 0; margin: 0; }
ul.link-list li { padding: 0.75rem 0; border-bottom: 1px solid var(--border); }
ul.link-list li:last-child { border-bottom: none; }
.section-light ul.link-list li { border-color: var(--light-border); }
.product-list-item { display: flex; gap: 1rem; align-items: flex-start; }
.product-thumb {
    width: 64px;
    height: 64px;
    object-fit: cover;
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
    flex-shrink: 0;
    display: block;
}
.section-light .product-thumb { border-color: var(--light-border); }
.product-hero-image {
    width: 100%;
    max-height: 520px;
    object-fit: contain;
    background: var(--light-panel);
    border-radius: var(--radius-md);
    border: 1px solid var(--light-border);
    display: block;
    margin-bottom: 1rem;
}
code {
    background: var(--panel-deep);
    padding: 0.15rem 0.4rem;
    border-radius: var(--radius-sm);
    font-size: 0.9em;
}
.contact-form { display: flex; flex-direction: column; gap: 0.85rem; }
.contact-form label {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    font-size: 0.9rem;
    color: var(--muted);
}
.contact-form input,
.contact-form textarea {
    background: var(--panel-deep);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.6rem;
    color: var(--text);
    font: inherit;
}
.contact-form button {
    align-self: flex-start;
    background: var(--accent);
    color: var(--accent-contrast);
    border: none;
    border-radius: var(--radius-sm);
    padding: 0.6rem 1.25rem;
    font: inherit;
    cursor: pointer;
}
.contact-form .error { color: var(--error); font-size: 0.9rem; margin: 0; }
.gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 0.75rem;
}
.gallery-item { color: inherit; }
.gallery-item:hover { text-decoration: none; }
.gallery-item img {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
    display: block;
}
/* Photo gallery — the flat wall used by the gallery section and /gallery.
   Wider cells than .gallery-grid's 140px because these are the photos
   themselves rather than thumbnails standing in for a page, and a shop's
   work deserves to be legible without opening it. 4:3 rather than 1:1 so
   a long barn door or a run of cabinets isn't cropped to a square. */
.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 0.75rem;
}
.photo-tile {
    display: block;
    color: inherit;
    line-height: 0;
    border-radius: var(--radius-md);
    overflow: hidden;
    border: 1px solid var(--border);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.photo-tile:hover, .photo-tile:focus-visible {
    text-decoration: none;
    transform: translateY(-2px);
    box-shadow: 0 8px 24px rgba(15, 20, 25, 0.18);
}
.photo-tile img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    display: block;
}
.section-light .photo-tile { border-color: var(--light-border); }
/* Two across on a phone rather than one. 200px cells fall to a single column
   at 375px, which turns a gallery of any size into an endless scroll of
   full-width photos — the point of a grid is being able to skim it. */
@media (max-width: 560px) {
    .photo-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.5rem; }
}

/* Lightbox — built by public/assets/gallery.js, styled here so the viewer
   picks up each client's palette like everything else. */
.lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 3.5rem 1rem;
    background: rgba(11, 16, 24, 0.92);
}
.lightbox[hidden] { display: none; }
/* Stop the page behind the overlay scrolling under the photo. */
body.lightbox-open { overflow: hidden; }
.lightbox-figure {
    margin: 0;
    max-width: 100%;
    max-height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
}
.lightbox-image {
    max-width: 100%;
    /* Leaves room for the caption and counter without the photo ever
       pushing them off screen on a short viewport. */
    max-height: calc(100vh - 9rem);
    object-fit: contain;
    border-radius: var(--radius-sm);
    display: block;
}
.lightbox-caption {
    margin: 0;
    color: var(--text);
    text-align: center;
    max-width: 60ch;
}
.lightbox-caption[hidden] { display: none; }
.lightbox-counter {
    position: absolute;
    bottom: 1rem;
    left: 50%;
    transform: translateX(-50%);
    margin: 0;
    color: var(--muted);
    font-size: 0.85rem;
}
.lightbox-counter[hidden] { display: none; }
.lightbox button {
    position: absolute;
    background: rgba(11, 16, 24, 0.6);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 50%;
    /* Comfortably past the 44px touch target minimum — these sit over a
       photo, so a near miss means closing the viewer by accident. */
    width: 48px;
    height: 48px;
    font-size: 1.75rem;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
}
.lightbox button:hover { background: var(--accent); color: var(--accent-contrast); border-color: var(--accent); }
.lightbox button[hidden] { display: none; }
.lightbox-close { top: 1rem; right: 1rem; }
.lightbox-prev { left: 1rem; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 1rem; top: 50%; transform: translateY(-50%); }

.review-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 1.25rem;
}
.review-card {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 1.25rem;
}
.review-card p { margin: 0.4rem 0; }
.stars { color: var(--star); letter-spacing: 0.1em; }
.faq-list details {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 0.85rem 1rem;
    margin-bottom: 0.6rem;
}
.faq-list summary { cursor: pointer; font-weight: 600; }
.faq-list p { margin: 0.6rem 0 0; color: var(--muted); }
/* Light-section overrides — these components default to the dark
   theme's colors (used as-is on the projects index/show pages;
   reviews and FAQ render on the homepage); override
   scoped to .section-light so they read correctly on the homepage's
   light bands too. */
.section-light .gallery-item img { border-color: var(--light-border); }
/* Homepage "See Our Work" teaser — sized like .service-card (not
   the small .gallery-item thumbnail used on /projects and project
   photo galleries). auto-fill (not .services-grid's auto-fit) so
   a single project sits at its natural card width instead of
   stretching to fill the whole row. */
.project-teaser-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1.5rem;
}
.project-teaser-card {
    display: block;
    color: inherit;
    text-decoration: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    padding: 1.5rem;
    transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.project-teaser-card:hover { box-shadow: 0 8px 24px rgba(15, 20, 25, 0.08); transform: translateY(-2px); text-decoration: none; }
.project-teaser-card img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: var(--radius-md);
    display: block;
    margin-bottom: 1rem;
}
.project-teaser-card h3 { margin: 0; font-size: 1.15rem; font-weight: 700; }
.section-light .project-teaser-card { background: var(--light-panel); border-color: var(--light-border); }
.section-light .review-card { background: var(--light-panel); border-color: var(--light-border); }
.section-light .muted { color: var(--light-muted); }
.section-light .faq-list details { background: var(--light-panel); border-color: var(--light-border); }
.section-light .faq-list summary { color: var(--light-text); }
.section-light .faq-list p { color: var(--light-muted); }
footer {
    border-top: 1px solid var(--border);
    background: var(--panel);
    padding: 3.5rem 1.5rem 0;
    font-size: 0.9rem;
    color: var(--muted);
}
/* White footer variant — every rule below already reads --panel/
   --border/--text/--muted, so redefining those four custom
   properties on the footer element itself re-themes the whole
   block (including .footer-col h3, .footer-logo, etc.) without
   duplicating each rule for a second, light-footer selector. */
footer.footer-light {
    --panel: var(--light-panel);
    --border: var(--light-border);
    --text: var(--light-text);
    --muted: var(--light-muted);
}
.footer-inner { max-width: 1200px; margin: 0 auto; }
.footer-grid {
    display: grid;
    /* Column count varies — up to 5 (Business/Services/Products/
       Service Areas/Hours) when every module is populated, fewer
       when some are left empty (see nav_* globals). auto-fit keeps
       this correct regardless of how many actually render. */
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 2.5rem;
    padding-bottom: 3rem;
}
.footer-col h3 {
    color: var(--text);
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin: 0 0 1rem;
}
/* "Services" heading is a link to #services (see footer markup) —
   keep it looking like a plain heading rather than turning
   accent-blue like a normal link. */
.footer-col h3 a { color: inherit; text-decoration: none; }
.footer-col h3 a:hover { text-decoration: underline; }
.footer-logo { color: var(--text); font-weight: 800; font-size: 1.1rem; margin-bottom: 0.75rem; }
.footer-col p { margin: 0 0 0.5rem; }
.footer-col ul { list-style: none; margin: 0; padding: 0; }
.footer-col li { margin-bottom: 0.6rem; }
.footer-hours li { line-height: 1.4; }
/* Profile links under the address. 40px targets because this is the one row
   in the footer a thumb aims at rather than reads, and `currentColor` on the
   marks means the light-footer variant above re-themes them along with
   everything else. */
.footer-social { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 1rem; }
.footer-social a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    color: var(--muted);
    transition: color 0.15s ease;
}
.footer-social a:hover { color: var(--text); }
.footer-social svg { width: 22px; height: 22px; display: block; }
.footer-bottom {
    border-top: 1px solid var(--border);
    padding: 1.5rem 0;
    font-size: 0.8rem;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 1.5rem;
}
.footer-bottom p { margin: 0; }
.footer-legal-links { display: flex; flex-wrap: wrap; gap: 1.25rem; }
.footer-legal-links a { color: var(--muted); }
.footer-legal-links a:hover { color: var(--text); }
@media (max-width: 800px) {
    .footer-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 480px) {
    .footer-grid { grid-template-columns: 1fr; gap: 2rem; }
}
