/*------------------------------------------------------------------
[Dooms-Deboudt — authored overrides]

Loaded LAST, after every template stylesheet, so these rules win without
editing Poolrex source. Everything here is site-specific work, not template.

All values come from the DESIGN.md scales. The two error reds are the one
deliberate addition: the incumbent #ff0000 measures 3.6:1 on the navy form
panel, below AA, so an accessible pair replaces it and is documented in
DESIGN.md alongside the legacy token.

1. Focus visibility
2. Form labels, values and errors
3. Form status block
4. Header actions at every breakpoint
5. Quote band photograph
6. Reduced motion
7. Homepage: About -> Projecten gap
8. Homepage: Projecten band
9. Services: Diensten -> Testimonials gap
10. Homepage: Projecten -> Footer gap
11. Contact: skip link, submit states, error binding, legal notice
-------------------------------------------------------------------*/

/* 1. FOCUS VISIBILITY
   custom.css:56 sets `outline: none` globally on a, button and input with no
   replacement. Restore a visible focus ring everywhere, tinted per surface. */

a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
[tabindex]:focus-visible {
    outline: 3px solid var(--primary--color);
    outline-offset: 3px;
    border-radius: 5px;
}

/* On the navy quote panel and the footer, navy-on-navy is invisible. */
.form-box a:focus-visible,
.form-box button:focus-visible,
.form-box input:focus-visible,
.form-box textarea:focus-visible,
.footer-con a:focus-visible,
.footer-con button:focus-visible,
.copyright a:focus-visible {
    outline-color: var(--white--color);
}

/* Yellow surfaces need the dark ring back. */
.generic-btn a:focus-visible,
.quote-btn a:focus-visible,
.submit-btn:focus-visible,
.read_more:focus-visible {
    outline-color: var(--primary--color);
    outline-offset: 4px;
}

/* Two elements strip their own focus at a specificity the generic rule above
   cannot reach: mobile.css:946 `.navbar-toggler:focus` (0,2,0) and
   custom.css:1641 `#back-to-top-btn:focus` (1,1,0), against (0,1,1) for
   `button:focus-visible`. These match that specificity and win on order.
   The hamburger matters most — below 992px it is the only route to the nav. */
.navbar-toggler:focus-visible {
    outline: 3px solid var(--primary--color);
    outline-offset: 4px;
}

#back-to-top-btn:focus-visible {
    outline: 3px solid var(--primary--color);
    outline-offset: 3px;
}

/* The toggler is a 24px-wide absolutely positioned box holding three 2px bars,
   which lands at roughly 24x24. Vertical padding takes it to 24x44 without
   moving the icon, since the element is right-anchored and centred. */
.navbar-toggler {
    padding: 10px 0;
}

/* 1a. STICKY HEADER
   Keeps both conversion paths — the call button and the quote pill — reachable
   at any scroll depth on a nine-section page, which previously only the mobile
   action bar (<=767px) provided. That left 768-1199px with no persistent
   access to either.

   Notes on the mechanics:
   - .header-con is `w-100 float-left`; the float is cleared because a floated
     sticky element is inconsistently handled. w-100 keeps the full width.
   - A background is mandatory: the header declares none, so it would otherwise
     be transparent over the content scrolling beneath it.
   - The shadow is the system's one documented ambient glow, not a new recipe.
   - z-index 300 clears every value in the template (max 106 outside the
     loader) and the action bar at 200. Bootstrap already sets
     `.navbar { position: relative }`, so the mobile collapse menu's containing
     block is unchanged by this. */

/* Every section in this template is `w-100 float-left` and nothing ever clears
   them, so <body> did not contain its own floats and its content height
   collapsed to roughly the header alone. A sticky element can only travel
   within its containing block, so the header unstuck immediately. flow-root
   makes <body> contain the floats without the `overflow` that would break
   sticky outright. This is the declaration that makes the rule below work. */
body {
    display: flow-root;
    /* The page background was never declared — it relied on the UA default. It
       is Paper White by design, and saying so lets any tool that resolves the
       page's own colours (the design detector among them) stop inferring a dark
       page and misreading the ambient card glow as a halo on black. */
    background: var(--white--color);
}

/* Display headings carried no tracking and no wrap control, which is what made
   them read as raw rather than set. Two corrections, both on the h1 only, so
   the homepage hero and the sub-banner titles stay identical:

   Tracking: type drawn for body sizes looks loose once it is 90px tall, because
   the sidebearings scale with it. -0.02em closes that up — well inside the
   -0.04em floor, and expressed in em so it tracks every step of the ramp down
   to 34px without a rule per breakpoint.

   Balance: "Wie zijn we?" was breaking to a lone "we?" and "Welkom in onze
   shop!" splits four words across two lines. text-wrap: balance evens the lines
   instead of filling the first and orphaning the rest. Browsers without it fall
   back to normal wrapping, which is exactly today's behaviour, so nothing
   regresses. It is limited to headings; the spec caps balancing at a few lines
   and it is wrong for running copy. */
h1 {
    letter-spacing: -0.02em;
    text-wrap: balance;
}

/* Removing the photographic help band from the about page left two Pool Mist
   sections touching — the figures band and the team band — and this system
   separates sections tonally, with no rules or dividers, so the boundary
   between them disappeared entirely.

   Scoped to the about page rather than changed on `.team-con` itself: the
   homepage uses that band too, and there it is followed by a white
   `.project-con`, so turning it white globally would move the same merged-band
   problem onto the homepage. `.services`, `.shop` and `.contact` already carry
   body classes for exactly this kind of page-scoped correction; about.html now
   carries one too. */
.about .team-con {
    background: var(--white--color);
}

/* With that band turned white, the team cards were white on white and had only
   the ambient glow holding them up — the tonal step that carries all depth in
   this system had nothing left to step against. Inverting it restores the
   relationship rather than inventing a new one: a Pool Mist tile on a white
   band is the same single step as the white-on-Pool-Mist cards everywhere else,
   just the other way up. Navy would have stood out harder, but three navy tiles
   directly above the navy footer would have run together the way the two Pool
   Mist bands did.

   All of this is scoped to .about. index.html runs identical team markup on a
   Pool Mist band, where a Pool Mist card would vanish completely and the larger
   sizes were not asked for.

   The hover border is the statistic card's device (1px transparent, resolving
   to Pool Surface Blue) rather than a new one. */
.about .team-box-item {
    flex: 0 1 300px;
    background: var(--grey--color);
    border: 1px solid transparent;
    transition: all 0.5s ease-in-out;
}

.about .team-box-item:hover {
    border-color: var(--secondary--color);
}

/* Transparent, not white: the monogram panel should read as part of the tile
   rather than punch a white rectangle into the top of it. */
.about .team-monogram {
    height: 240px;
    background: transparent;
}

/* 130px echoes the 129px white badge on the About photograph, and the initials
   move from the Stat step to the Headline step — the ramp's next stop up, not
   an invented size to match the circle's growth. */
.about .team-monogram span {
    width: 130px;
    height: 130px;
    font-size: 44px;
    line-height: 44px;
}

.about .team-content {
    padding: 34px 24px;
}

/* The 22px below the role was spacing for the social list that used to sit
   under it; nothing follows it now, so it only made the card bottom-heavy. */
.about .team-content span {
    margin-bottom: 0;
}

.header-con {
    position: sticky;
    top: 0;
    z-index: 300;
    /* Bootstrap's .float-left is `float: left !important`, so this needs the
       same weight to land. */
    float: none !important;
    background: var(--white--color);
    box-shadow: 2px 2px 67px rgb(44 162 252 / 7%);
}

/* Anchored sections would otherwise land underneath the pinned header.
   :target covers every fragment on the site — the service anchors, the project
   anchors, the contact page's skip-link target — without enumerating them. */
:target {
    scroll-margin-top: 104px;
}

@media screen and (max-width: 1199px) {
    :target {
        scroll-margin-top: 88px;
    }
}

/* 1b. HERO QUALIFIER
   Answers the two questions a cold visitor arrives with — "what exactly do you
   do?" and "komen jullie tot bij mij?" — at the moment of the primary action. */

.banner-meta {
    max-width: 46ch;
    margin: 26px 0 0;
    font-size: 16px;
    line-height: 26px;
    font-weight: 500;
}

/* 1c. TEAM CARDS
   The template shipped three identical grey "255 x 255" placeholders under the
   owners' real names. Until real photographs exist, an initial in the system's
   own medallion reads as designed rather than as broken. */

.team-intro {
    max-width: 52ch;
    margin: 16px auto 0;
}

/* The panel must be the card's own white, not the section's. .team-con is
   var(--grey--color) and so was this — identical values, so the top 220px of
   every card dissolved into the band behind it and the cards read as three
   floating circles. White restores the silhouette; the documented ambient glow
   separates the card from the pale-blue section the way every other card in
   the system does. */
.team-box-item {
    box-shadow: 2px 2px 67px rgb(44 162 252 / 7%);
}

.team-monogram {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 220px;
    border-radius: 20px 20px 0 0;
    background: var(--white--color);
}

.team-monogram span {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 110px;
    height: 110px;
    border-radius: 50%;
    background: var(--primary--color);
    color: var(--white--color);
    font-size: 32px;
    font-weight: 700;
    line-height: 32px;
    transition: all 0.4s ease-in-out;
}

.team-box-item:hover .team-monogram span {
    background: var(--secondary--color);
    transform: translateY(-5px);
}

/* 1e. CONTACT PAGE
   The contact page now uses the homepage's quote-band structure
   (.mian-form-con > .form-box > .form-item), which retires the second input
   system DESIGN.md flagged as unresolved template drift — .form_style fields
   at 25px with a blue focus border versus the pill fields at 26.5px with a
   yellow one. One form language site-wide.

   Nothing on the site renders .contact-con or .contact_info any more, so the
   template's rules for both are dead; they are left in place because they are
   template source, not authored code. */

/* The quote form's grid puts two fields per row and spans the last one. A
   sixth field — the contact page's Onderwerp — needs the same span without
   being last. */
.contact-form ul li.field-wide {
    grid-column: 1 / -1;
}

/* The contact page has no hero since its sub-banner was removed, so the quote
   band's headline is the page's <h1>. Only the tag changed — the global h1 is
   the 90px hero scale, which this panel was never built to hold, so the
   heading keeps the 44px Headline step and its six responsive drops verbatim
   from custom.css/mobile.css `h2`. Colour and margin come from `.form-con h2`,
   which the tag no longer matches, so they are restated here.

   If a hero is ever restored to this page, its title becomes the <h1> and this
   heading goes back to <h2> — delete this block with it. */
.form-con h1 {
    font-size: 44px;
    font-weight: 700;
    line-height: 53px;
    margin-bottom: 24px;
    color: var(--white--color);
}

@media screen and (max-width: 1440px) {
    .form-con h1 {
        font-size: 40px;
        line-height: 48px;
        margin-bottom: 14px;
    }
}

@media screen and (max-width: 1199px) {
    .form-con h1 {
        font-size: 36px;
        line-height: 40px;
    }
}

@media screen and (max-width: 991px) {
    .form-con h1 {
        font-size: 30px;
        line-height: 36px;
        margin-bottom: 12px;
    }
}

@media screen and (max-width: 767px) {
    .form-con h1 {
        font-size: 26px;
        line-height: 28px;
    }
}

@media screen and (max-width: 575px) {
    .form-con h1 {
        font-size: 24px;
        line-height: 26px;
    }
}

/* 2. FORM LABELS, VALUES AND ERRORS */

.field-label {
    display: block;
    font-size: 14px;
    font-weight: 700;
    line-height: 14px;
    margin-bottom: 9px;
    margin-left: 0;
    color: var(--primary--color);
    /* mobile.css centres .form-con (<=767px) and .contact_info (<=991px); a
       label must stay aligned with the text inside its own field. */
    text-align: left;
}

/* Labels sit flush with the field's left edge rather than over its placeholder
   text. Matching the placeholder would mean tracking the input's padding, which
   both forms restyle at four breakpoints (24px->14px here, 26px->16px on the
   contact page) and which resolves differently for floated and non-floated
   field groups. Flush-left needs none of that: every label lines up with every
   field, at every width, whatever the container does. */
.contact-form .field-label{
    padding-left: 0 !important;
}

/* custom.css styles EVERY span inside these forms as an absolutely positioned
   red validation error (.contact-form span, .contact-con .contact_info span).
   The hint is part of the label, not an error, so it has to opt out of all of
   it — and needs the specificity to beat those two rules. */
.contact-form .field-label__hint{
    position: static;
    display: inline;
    height: auto;
    margin: 0;
    padding: 0;
    left: auto;
    font-size: 14px;
    line-height: 14px;
    font-weight: 400;
    color: inherit;
    opacity: 0.75;
}

/* States what happens after the button is pressed, at the moment of highest
   hesitation, and keeps the phone path open beside the form. */
.form-promise {
    margin-bottom: 32px;
    font-weight: 600;
}

.form-box .form-promise {
    color: var(--white--color);
}

/* Poolside Sun never appears as text, so the link carries its weight through
   an underline rather than a colour shift. */
.form-promise a {
    color: inherit;
    font-weight: 700;
    text-decoration: none;
    border-bottom: 1px solid currentColor;
}

.form-promise a:hover {
    border-bottom-color: transparent;
}

/* The quote form sits on the navy panel. */
.form-box .field-label {
    color: var(--white--color);
}

/* The breadcrumb is the contact page's only orientation signal: it has no
   sub-banner, and the nav item that used to carry `active` is gone, so nothing
   else names the page. It sits above the eyebrow rather than below the heading
   as it does in the sub-banner on other pages — a breadcrumb orients before the
   content. It no longer carries the page name alone: the heading below it now
   reads "Contacteer ons" too, so the two lines repeat each other.

   custom.css authored these rules for the sub-banner, where `.sub-banner-title *`
   painted every descendant white. That rule does not reach inside the quote
   band, so the link needs its own colour; the pill border, separator and active
   item are already white and carry over unchanged. Hover follows `.form-promise a`
   above — an underline, not a colour shift, because Poolside Sun never appears
   as text and Pool Surface Blue is the incumbent's own contrast defect. */
.form-con .breadcrumb-con {
    margin-bottom: 26px;
}

.form-con .breadcrumb-item a {
    color: var(--white--color);
    text-decoration: none;
}

.form-con .breadcrumb-item a:hover {
    color: var(--white--color);
    border-bottom: 1px solid currentColor;
}

/* The placeholder was set to the same colour as the value, so a filled field
   and an empty one looked identical. Now that every field carries a real
   label, the distinction moves to the value: typed text is navy and bold,
   placeholder text stays grey. Both clear AA on white. */
.contact-form ul li input,
.contact-form ul li textarea{
    color: var(--primary--color);
    font-weight: 600;
}

.contact-form ul li input::placeholder,
.contact-form ul li textarea::placeholder{
    color: var(--text--color);
    font-weight: 400;
    opacity: 1;
}

/* Validation errors were absolutely positioned and overlaid the next field.
   Let them reflow, and give them contrast on the surface they land on. */
.contact-form span.error{
    position: static !important;
    display: block;
    height: auto;
    left: auto;
    margin-top: 8px;
    padding-left: 0;
    font-size: 14px;
    line-height: 20px;
    font-weight: 600;
    text-align: left;
    color: #c81e1e;
}

.form-box .contact-form span.error {
    color: #ff8a8a;
}

.contact-form ul li{
    position: relative;
}

/* 3. FORM STATUS BLOCK
   Replaces the 5-second toast with a persistent, announced result. */

.form-status {
    display: none;
    margin-top: 24px;
    padding: 20px 24px;
    border-radius: 15px;
    font-size: 16px;
    line-height: 26px;
}

.form-status.is-visible {
    display: block;
}

.form-status--success {
    color: var(--primary--color);
    background: var(--white--color);
}

.form-status--error {
    color: var(--white--color);
    background: #c81e1e;
}

.form-status a {
    font-weight: 700;
    color: inherit;
}

/* 4. HEADER ACTIONS AT EVERY BREAKPOINT
   mobile.css hid .phone-num below 1199px and .quote-btn below 575px, which left
   the header with no contact affordance at all on phones. Both are restored in
   mobile.css; these rules make them fit. */

.header-action {
    display: none;
    width: 46px;
    height: 46px;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    font-size: 16px;
    text-decoration: none;
    transition: all 0.3s ease-in-out;
}

.header-action--call {
    color: var(--white--color);
    background: var(--primary--color);
}

.header-action--call:hover,
.header-action--call:focus {
    color: var(--white--color);
    background: var(--secondary--color);
}

/* The quote pill keeps a readable label at every width — it shortens, it never
   becomes a bare arrow. */
.quote-btn__short {
    display: none;
}

/* Below 575px the written-out number does not fit beside the logo and the
   hamburger, so it becomes a round call button and the pill shortens. Both
   paths stay reachable; only their form changes. These live here rather than in
   mobile.css because this file loads later and media queries add no
   specificity — split across files, the base rule would win. */
@media screen and (max-width: 575px) {
    .header-btn {
        gap: 10px;
        margin-left: 0;
    }

    .header-action {
        display: inline-flex;
    }

    .phone-num {
        display: none;
    }

    .quote-btn a {
        padding: 15px 20px;
        font-size: 14px;
    }

    .quote-btn a i {
        padding-left: 8px;
    }

    .quote-btn__long {
        display: none;
    }

    .quote-btn__short {
        display: inline;
    }
}

/* 5. QUOTE BAND PHOTOGRAPH
   .form-img is an empty div and both template background rules for it are
   commented out (custom.css:788, custom.css:895), leaving 46.6% of the
   highest-intent band on the page as bare navy at every width >=768px.
   DESIGN.md documents this slot as "form left, photography right, edge to
   edge"; quote_large.jpg was already in the repo, unused. mobile.css:1699
   still hides the column below 768px, which is correct. */

.form-img {
    background: url(../images/originals/quote_large.jpg) no-repeat center;
    background-size: cover;
}

/* 6. REDUCED MOTION
   The wave, the sonar pulse, the AOS reveals and three autoplaying carousels
   all ran unconditionally. Honour the OS preference. */

@media (prefers-reduced-motion: reduce) {

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    html {
        scroll-behavior: auto;
    }

    .generic-btn a:hover,
    .quote-btn a:hover,
    .submit-btn:hover {
        transform: none;
    }

}

/* ==========================================================================
   7. HOMEPAGE: ABOUT -> PROJECTEN GAP
   .about-con carries `padding-bottom: 182px !important`, and on the homepage
   .project-con's top padding is 80px (blog.css redefines .padding-top unscoped
   and loads after mobile.css, so the 140px rhythm never applies here). That put
   262px of empty page between the About block and Onze Projecten.

   The 182px is not arbitrary: .about-small-img is absolutely positioned at
   `bottom: -57px`, so the 216px circular portrait hangs below the block and
   needs clearance. The template itself allows only 70px at <=991px against that
   same 57px overhang, so 90px is more generous than its own tightest step while
   cutting the desktop gap by a third.

   Scoped with :not(.aboutpage-con) because about.html carries that extra class
   on the same section, and its About block is followed by a different section
   at a different rhythm. Every step is restated: this selector outranks
   mobile.css's .about-con at all widths, so an unrestated breakpoint would
   inherit the desktop value rather than the template's smaller one.
   ========================================================================== */

.about-con:not(.aboutpage-con) {
    padding-bottom: 90px !important;
}

@media screen and (max-width: 1440px) {
    .about-con:not(.aboutpage-con) {
        padding-bottom: 85px !important;
    }
}

@media screen and (max-width: 1199px) {
    .about-con:not(.aboutpage-con) {
        padding-bottom: 80px !important;
    }
}

/* The template's own values below here are already tight; kept as they were. */
@media screen and (max-width: 991px) {
    .about-con:not(.aboutpage-con) {
        padding-bottom: 70px !important;
    }
}

@media screen and (max-width: 767px) {
    .about-con:not(.aboutpage-con) {
        padding-bottom: 40px !important;
    }
}

/* ==========================================================================
   8. HOMEPAGE: PROJECTEN BAND
   Onze Projecten takes the same Pool Mist fill as Onze Diensten. It had no
   background rule of its own and so rendered white, directly beneath a white
   .about-con — and this system separates sections tonally, with no rules or
   dividers, so there was nothing marking where one ended and the other began.
   That adjacency appeared when the team band was removed from between them.

   With this the homepage alternates cleanly from top to bottom: photograph,
   Pool Mist, white, Pool Mist, navy footer.

   .project-con exists only on index.html, so no scoping is needed.
   ========================================================================== */

.project-con {
    background: var(--grey--color);
}

/* ==========================================================================
   9. SERVICES: DIENSTEN -> TESTIMONIALS GAP
   Two adjacent sections each contributed a full 140px rhythm unit, so 280px of
   empty white sat between the last service card and "Wat Onze Klanten Zeggen".
   Both bands are white, so nothing marked the boundary either — it read as one
   long pause rather than two sections.

   The Diensten side gives up almost all of its trailing padding. 20px sits
   below the ramp's smallest step (40px), so that one declaration holds at every
   breakpoint without restating anything.

   The testimonials side keeps the larger share, because it opens with an
   eyebrow and a heading and a heading wants more space above it than below —
   but it no longer keeps a full rhythm unit. Its steps are restated down to
   40px at <=991px, which is exactly what the ramp already gives there and at
   every width below, so the ladder ends cleanly rather than pinning small
   screens to a desktop value.

   Result: 100px at desktop down to 60px on a phone, from 280/80.
   Both classes exist only on services.html, so no scoping is needed.
   ========================================================================== */

.main-services-con {
    padding-bottom: 20px;
}

.tesimonials-con {
    padding-top: 80px;
}

@media screen and (max-width: 1440px) {
    .tesimonials-con {
        padding-top: 60px;
    }
}

@media screen and (max-width: 1199px) {
    .tesimonials-con {
        padding-top: 50px;
    }
}

/* Matches the ramp's own value here and below, so nothing is pinned. */
@media screen and (max-width: 991px) {
    .tesimonials-con {
        padding-top: 40px;
    }
}

/* ==========================================================================
   10. HOMEPAGE: PROJECTEN -> FOOTER GAP
   The projects grid is wrapped twice, and both wrappers claim the section
   rhythm: .project-con carries `padding-bottom` (index.html:355) and the
   .blog-posts grid inside it carries it again (index.html:361). index.html
   loads blog.css last, so that class resolves to 100px rather than the
   declared 140px — but it resolves to it twice. With the last card's `mb-4`
   on top, 224px of empty Pool Mist sat between the final realisation and the
   navy footer, the largest unbroken gap on the homepage.

   The inner wrapper gives up its padding entirely, top and bottom. The section
   it sits in already owns the rhythm, and a grid does not need a second one to
   separate it from its own parent's edge. Zero holds at every breakpoint
   without restating anything, so .project-con's own ramp (100 / 80 / 60 / 40)
   still steps the gap down on its own.

   The top is the same fault seen from the other side. .generic-title already
   carries its own 48px bottom margin (38 / 32 / 22 down the ramp), and every
   other band on the homepage — Onze Diensten most visibly — runs title
   straight into content on that margin alone, with no wrapper between them.
   Only this band inserted one, and it is the template's blog-listing wrapper
   rather than anything authored. Removing it makes the projects band space its
   title exactly like the services band above it.

   It also removes an inversion: blog.css sets .padding-top to 80px at base but
   100px at <=1440, so the gap under the heading was LARGER on a laptop than on
   a wide desktop. That was never a decision, just two template values crossing.
   ========================================================================== */

.project-con .blog-posts.padding-top,
.project-con .blog-posts.padding-bottom {
    padding-top: 0;
    padding-bottom: 0;
}

/* ==========================================================================
   11. CONTACT: SKIP LINK, SUBMIT STATES, ERROR BINDING, LEGAL NOTICE
   Everything here serves the site's only form. Ordered as a visitor meets it:
   getting to it, the copy that removes the risk, the moment of the press, and
   what a failed field looks like.
   ========================================================================== */

/* The header puts six interactive stops before the first field. The skip link
   targets #main, which carries tabindex="-1" so the jump moves focus rather
   than only scrolling. Visible only when focused.

   It points at <main> rather than the form section because the section's
   #offerte id was removed sitewide — every quote CTA now lands on the page
   itself, so no fragment appears in the address bar. This link is the one
   remaining fragment, and it appears only when a keyboard user activates it. */

.skip-link {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 100000;
    padding: 14px 24px;
    border-radius: 0 0 15px 0;
    font-size: 14px;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--black--color);
    background: var(--accent--color);
    transform: translateY(-120%);
    transition: transform 0.3s ease-in-out;
}

.skip-link:focus {
    transform: translateY(0);
    color: var(--black--color);
}

/* The word that removes the risk — vrijblijvend — sat once, mid-paragraph,
   and never where the decision is made. It sits under the button now, and the
   service area sits under the promise, because "komen jullie tot bij mij?" is
   answered nowhere else on this page. */

.form-area {
    margin-top: -18px;
    margin-bottom: 32px;
    font-size: 16px;
    line-height: 26px;
    color: rgba(255, 255, 255, 0.75);
}

.form-reassure {
    margin-top: 18px;
    margin-bottom: 8px;
    font-size: 16px;
    line-height: 26px;
    font-weight: 700;
    color: var(--white--color);
}

/* Google's terms allow hiding the badge (custom.css:3451) only when this text
   is shown. It was hidden with nothing in its place, and the site carries no
   privacy statement at all — so this is also the only notice a visitor gets
   before handing over a name, phone, e-mail and home address. */

.form-legal {
    margin-bottom: 0;
    font-size: 14px;
    line-height: 22px;
    color: rgba(255, 255, 255, 0.75);
}

.form-legal a {
    font-weight: 600;
    color: var(--white--color);
    text-decoration: underline;
}

/* The submit button's three broken states.

   Hover flipped the fill to Pool Surface Blue and the label to white, which is
   2.73:1 — the same measurement DESIGN.md's Blue-Is-Not-Text Rule already
   condemns, landing on the one control that completes the task. Keeping the
   label black through the flip reads as 7.68:1 and is calmer besides: only the
   fill changes, and the label never moves colour. This is a deviation from the
   button-primary-hover token and belongs in DESIGN.md.

   Disabled had no styling at all, so a button that could not be pressed kept
   its pointer cursor and still lifted 5px under the cursor. */

.submit-btn:hover,
.submit-btn:focus-visible {
    color: var(--black--color);
}

.submit-btn:hover i,
.submit-btn:focus-visible i {
    color: var(--black--color);
}

.submit-btn:disabled,
.submit-btn[disabled] {
    opacity: 0.65;
    cursor: not-allowed;
    transform: none;
    background: var(--accent--color);
    color: var(--black--color);
}

.submit-btn:disabled:hover,
.submit-btn[disabled]:hover {
    transform: none;
    background: var(--accent--color);
    color: var(--black--color);
}

/* jQuery Validation adds class="error" to the invalid input as well as
   emitting the message, and nothing in any stylesheet targeted it — so the
   broken field looked exactly like a correct one, and the only signal was
   14px of text beneath it.

   Two things decide the treatment. The token is Error Red on Light, not the
   #ff8a8a the message beside it uses: the message sits on the navy panel, but
   this ring sits on the field's own white fill, where #ff8a8a measures 2.27:1
   and fails WCAG 1.4.11's 3:1 for non-text. #c81e1e reads 5.74:1 there.

   And it is an inset ring rather than a border because the fields carry
   `border: none` at rest (custom.css:829), so a real border would grow every
   invalid field by 2px and shift the row. This is a state ring, not elevation
   — the Same-Glow Rule governs depth, and nothing here is being raised. */

.contact-form ul li input.error,
.contact-form ul li textarea.error {
    box-shadow: inset 0 0 0 2px #c81e1e;
}

/* Focus on a field had one indicator: a 1px Poolside Sun border, which is
   1.35:1 against the white fill and fails WCAG 1.4.11's 3:1 outright. The 3px
   white ring beside it is `:focus-visible`, which does not fire for touch — so
   on a phone, where most of this audience is, a tapped field had effectively
   no focus indicator at all.

   :focus is used deliberately, not :focus-visible, because touch is the case
   that was uncovered. Deep Water Navy reads 14.28:1 on the white fill and is
   already the colour of the typed value, so an active field simply firms up
   rather than changing hue. Inset so the row cannot shift. */

.contact-form ul li input:focus,
.contact-form ul li textarea:focus {
    box-shadow: inset 0 0 0 2px var(--primary--color);
}

/* An invalid field that is also focused stays red — the error is the more
   urgent fact, and 5.74:1 clears the same threshold. */
.contact-form ul li input.error:focus,
.contact-form ul li textarea.error:focus {
    box-shadow: inset 0 0 0 2px #c81e1e;
}

/* iOS Safari zooms the viewport whenever a focused input is under 16px and
   does not zoom back, which pushed the second column off-screen mid-form. 16px
   is the exact threshold — 15px does not help. The padding brings the control
   from roughly 39px to 48px, over the 44px touch minimum, and the two-column
   grid collapses so no field is ever half-width on a phone. */

@media screen and (max-width: 991px) {

    .contact-form ul li input,
    .contact-form ul li textarea {
        font-size: 16px;
        line-height: 20px;
        padding: 13px 16px 14px;
    }
}

@media screen and (max-width: 767px) {
    .contact-form ul {
        grid-template-columns: 100%;
    }
}

/* The status block receives focus after a send so the next Enter does not
   re-run the validators against a form that was just emptied. It is not in
   the tab order, so it must not paint a ring when focused programmatically. */

#form_result:focus {
    outline: none;
}

#form_result:focus-visible {
    outline: 2px solid var(--white--color);
    outline-offset: 4px;
}

/* ==========================================================================
   12. CLOSING BAND (index, about, services, projects)
   Four pages ran the whole persuasion sequence and then handed the visitor a
   sitemap: index had three links to contact.html and none between the hero and
   the footer, services ended in 217px of empty white, projects had no call to
   action anywhere in 5291px of scroll, and about ended on a monogram. This is
   the closing move all four were missing, built once and reused.

   Full-bleed navy is deliberate. DESIGN.md's Full-Bleed Break Rule allows two
   container breaks per page and each of these pages spends one on its hero, so
   the band is inside its budget and gives the page a second break — which the
   same rule names as the lever for a page that reads as a list of boxes.

   No `data-aos` here on purpose. `[data-aos^=fade]` sets `opacity: 0`, so any
   section that carries it is invisible when aos.js fails to arrive. That is an
   acceptable risk on decoration and not on the only conversion prompt in the
   page body.

   The promise line is the point of the whole band. "Vrijblijvend" and "je
   hoort binnen 3 werkdagen van ons" previously existed only on contact.html —
   in other words only *after* the click they are supposed to cause.
   ========================================================================== */

/* The band was Deep Water Navy in its first form, which put it directly against
   the navy footer (custom.css:1364 — the same --primary--color) with no
   boundary at all: the two read as one ~700px block, and the closing prompt
   lost its status as a distinct moment by being absorbed into footer furniture.

   There is no fix for that inside the navy family. Deep Water Tint measures
   1.13:1 against the footer and Deep Water Shade 1.06:1 — both imperceptible
   across a field this size. DESIGN.md's rule is that separation is always
   tonal, so the band steps to the light side instead: Pool Mist under a navy
   footer restores the footer's role as the page's floor without a divider, and
   puts the band back into the white/mist alternation the rest of the page runs.

   Poolside Sun is louder here, not quieter — it is the only warm thing on a
   pale field rather than one bright object among several on a dark one. */
.closing-con {
    background-color: var(--grey--color);
    padding: 100px 0;
}

/* index.html is the exception: .project-con above it is already Pool Mist
   (site.css:709), so a mist band there would reproduce the merge in light.
   White steps against Pool Mist above and against the navy footer below. */
.project-con+.closing-con {
    background-color: var(--white--color);
}

.closing-inner {
    max-width: 660px;
    margin: 0 auto;
    text-align: center;
}

/* Deep Water Navy, not Pool Surface Blue. The system's eyebrow colour measures
   2.57:1 on Pool Mist and 2.73:1 on white — the failure DESIGN.md's
   Blue-Is-Not-Text Rule says to correct rather than copy. The eyebrow stays
   distinct from the headline by size and weight (16/400 against 44/700), which
   is what the pattern actually depends on. */
.closing-inner .closing-eyebrow {
    font-size: 16px;
    font-weight: 400;
    line-height: 16px;
    margin-bottom: 16px;
    display: inline-block;
    color: var(--primary--color);
}

.closing-inner h2 {
    font-size: 44px;
    font-weight: 700;
    line-height: 53px;
    margin-bottom: 18px;
    text-wrap: balance;
    color: var(--primary--color);
}

/* Body Grey: 4.92:1 on Pool Mist, 5.23:1 on white. Both clear AA. */
.closing-inner .closing-promise {
    font-size: 16px;
    font-weight: 400;
    line-height: 26px;
    margin-bottom: 34px;
    color: var(--text--color);
}

/* Both doors, side by side, at every width — PRODUCT.md treats the form and
   the phone as equally first-class and neither may be routed through the
   other. They wrap rather than shrink so the phone number is never truncated. */
.closing-actions {
    gap: 30px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
}

.closing-actions .generic-btn {
    margin: 0;
}

/* mobile.css:132 gives every `.generic-btn a` a 20-25px bottom margin inside
   its <=1440px block, and zeroing the wrapper above did not reach the anchor —
   so the button's optical centre sat 10-12px above the phone number's at every
   width from 768px up. Measured 12px at 1440/1200/992 and 10px at 768. */
.closing-actions .generic-btn a {
    margin-bottom: 0;
}

/* .generic-btn's global hover flips the label to white on Pool Surface Blue =
   2.73:1, which DESIGN.md records as a live AA failure. Rather than inherit it
   onto a brand-new element, this reuses the correction already proven on
   .submit-btn: the label stays black through the yellow-to-blue flip (7.68:1)
   and only the fill changes. */
.closing-actions .generic-btn a:hover,
.closing-actions .generic-btn a:focus-visible {
    color: var(--black--color);
    background: var(--secondary--color);
}

.closing-actions .generic-btn a:hover i,
.closing-actions .generic-btn a:focus-visible i {
    color: var(--black--color);
}

/* Navy on the light band: 13.42:1 on Pool Mist, 14.28:1 on white. This is the
   same 18px/700 treatment the header gives the phone number. */
.closing-call {
    gap: 10px;
    display: inline-flex;
    align-items: center;
    font-size: 18px;
    font-weight: 700;
    line-height: 28px;
    text-decoration: none;
    color: var(--primary--color);
    transition: color 0.3s ease-in-out;
}

/* The same icon the header uses beside the same number, at its natural size
   and with the header's 10px separation — one phone treatment, not two.
   The header's own hover (Pool Surface Blue, 2.73:1) is deliberately not
   copied; the corrected hover below stays. */
.closing-call__icon {
    width: 34px;
    height: 30px;
    flex: 0 0 auto;
}

/* Poolside Sun would be 1.3:1 here — the yellow-on-light failure DESIGN.md
   lists under Don'ts. The hover underlines instead of changing hue, which is
   the pattern already used on .form-promise a and the service card links. */
.closing-call:hover,
.closing-call:focus-visible {
    text-decoration: underline;
    text-underline-offset: 4px;
}

/* The Headline ramp, restated so the band steps with the rest of the system
   rather than holding 44px into a 390px viewport. */

@media screen and (max-width: 1440px) {
    .closing-inner h2 {
        font-size: 40px;
        line-height: 48px;
    }
}

@media screen and (max-width: 1199px) {
    .closing-con {
        padding: 90px 0;
    }

    .closing-inner h2 {
        font-size: 36px;
        line-height: 44px;
    }
}

@media screen and (max-width: 991px) {
    .closing-con {
        padding: 80px 0;
    }

    .closing-inner h2 {
        font-size: 30px;
        line-height: 38px;
    }
}

@media screen and (max-width: 767px) {
    .closing-con {
        padding: 64px 0;
    }

    .closing-inner h2 {
        font-size: 26px;
        line-height: 34px;
    }

    .closing-actions {
        gap: 22px;
    }
}

@media screen and (max-width: 575px) {
    .closing-inner h2 {
        font-size: 24px;
        line-height: 32px;
    }

    /* Stacked, and the button goes full-width so the primary action is the
       largest target on the screen rather than a centred pill. */
    .closing-actions {
        gap: 20px;
        flex-direction: column;
        align-items: stretch;
    }

    .closing-actions .generic-btn a {
        display: block;
        text-align: center;
    }

    /* Stacked, the phone row stretches to the button's width, so its content
       needs centring or it reads as hanging off the left edge. */
    .closing-call {
        justify-content: center;
    }
}

/* ==========================================================================
   13. SERVICES: TESTIMONIAL TAIL
   .tesimonials-con reserved 217px of bottom padding (custom.css:1088) for the
   #thumbs thumbnail row, whose markup is commented out — so the page ended in
   a void, and .customer-status was absolutely positioned outside its own card
   (custom.css:1191-1197, bottom: -50px; mobile.css:199 takes it to -164px) to
   float in that void. On a phone the attribution landed nearer the footer than
   the quote it attributes, with the carousel arrows between them.

   Both are undone here: the attribution returns to normal flow inside the
   card, and the reserved padding drops to the section rhythm. Section 12's
   closing band now occupies the space the void used to.
   ========================================================================== */

.tesimonials-con .customer-status {
    left: auto;
    bottom: auto;
    position: static;
    transform: none;
    margin-top: 26px;
}

/* The quotes are the last thing above the closing band, and .closing-con opens
   with 100px of its own, so a full section padding here made 200px of dead
   space between the last testimonial and "Laat van je horen". Same seam, same
   ladder as index.html (section 25) and about.html: 132 / 122 / 104 / 88. The
   1440 and 767 steps this used to carry are gone -- two values cover it once
   the number is this small, and .closing-con is doing the stepping. */
.tesimonials-con {
    padding-bottom: 32px;
}

/* The speech-bubble tail pointed down at the thumbnail row that no longer
   exists (custom.css:1223-1234), so it aimed at nothing. */
.tesimonials-con .slider-box .review-details-box::after {
    display: none;
}

@media screen and (max-width: 991px) {
    .tesimonials-con {
        padding-bottom: 24px;
    }
}

/* ==========================================================================
   14. PROJECTS: PHOTOGRAPH ASPECT RATIO
   blog.css hard-codes a pixel height on every realisation photograph and
   steps it down a five-rung ladder — 462px (blog.css:1823), then 387
   (2615), 287 (3075), 212 (3526) and 200 (3941) — while blog.css:260-263
   forces `width: 100%`. There is no `object-fit` anywhere, so the rendered
   ratio is whatever the two happen to produce at that width.

   The source crops are 1920x800, i.e. 2.40:1. The ladder lands near that at
   every rung except the phone one: at 390px the card is 360px wide and the
   image is forced to 200px tall, so a 2.40:1 photograph renders at 1.80:1 —
   a 33% vertical stretch. Measured on a 390px render: 358x199. Across the
   <=575 range the ratio swings from 2.73:1 down to 1.80:1, crossing correct
   somewhere near 480px by accident.

   PRODUCT.md puts this audience on mobile and calls the completed
   realisations the argument, so every phone visitor was judging the work
   from misshapen photographs of it. One declaration retires all five rungs
   and cannot drift again, because the ratio no longer depends on width.
   ========================================================================== */

.blogpage-section .onecolumn-blog .post-item-wrap img,
.blogpage-section .onecolumn-blog .post-item-wrap .carousel-item img {
    height: auto;
    aspect-ratio: 12 / 5;
    object-fit: cover;
}

/* ==========================================================================
   15. PROJECTS: CAPTION BAR AND CARD SHELL
   Seven realisations shipped with no name, no service, no scope and no size,
   and four of them rendered a location pin with nothing after it — an empty
   pin advertises a missing fact rather than hiding one. The pin is now dropped
   entirely on the four with no confirmed place (PRODUCT.md confirms only
   Bever, Pamel and Meerbeke), and every card carries a heading and one line
   describing what is visibly in the photograph. Nothing about scope, date,
   budget or dimension is claimed, because none of it is on hand.

   The heading also repairs the outline: the page ran h1 straight to the
   footer's h4, so navigating by heading produced three stops and not one of
   them was a project.

   The card shell is corrected to the system at the same time, since this rule
   set already owns the element. blog.css:512-524 is blog-listing styling that
   the projects markup inherits by accident: a 15px radius against the
   documented 20px card corner, a grey offset shadow against the system's
   single blue ambient glow, and a 1px hairline under .lower-portion — a
   divider, in a system DESIGN.md describes as having none, running dead
   straight across the bottom of a rounded card because .blog-box has no
   overflow clip.
   ========================================================================== */

.blogpage-section .blog-box {
    border-radius: 20px;
    box-shadow: 2px 2px 67px rgb(44 162 252 / 7%);
}

/* The photograph and its card share one silhouette, per DESIGN.md's card
   image treatment. The template's own commented-out line wanted this. */
.blogpage-section .blog-box img {
    border-radius: 20px 20px 0 0 !important;
}

.blogpage-section .blog-box .lower-portion {
    padding: 26px 30px 30px;
    border-bottom: none;
}

.blogpage-section .blog-box .lower-portion .project-title {
    font-size: 22px;
    font-weight: 700;
    line-height: 28px;
    margin-bottom: 8px;
    color: var(--primary--color);
}

.blogpage-section .blog-box .lower-portion .project-note {
    font-size: 16px;
    font-weight: 400;
    line-height: 26px;
    max-width: 68ch;
    margin-bottom: 0;
    color: var(--text--color);
}

@media screen and (max-width: 767px) {
    .blogpage-section .blog-box .lower-portion {
        padding: 20px 20px 24px;
    }

    .blogpage-section .blog-box .lower-portion .project-title {
        font-size: 20px;
        line-height: 26px;
    }
}

/* ==========================================================================
   16. SERVICES: CARDS CARRY THE WORK
   The four service cards argued for a large, irreversible purchase using
   adjectives and a ~35px clip-art glyph, on a page that showed no photography
   at all below the hero — while seven realisations sat unused in the repo and
   the homepage presented these same four services *with* photographs. Clicking
   "Meer weten?" made the experience visually poorer.

   Each card now opens on a real realisation with the system's 99px Pool
   Surface Blue medallion straddling the seam, which is the anatomy the
   homepage already uses and DESIGN.md calls "what turns a plain image-and-text
   card into something recognizably this system's".

   Two icon defects go with it: main-services-icon3.png was used for BOTH
   Aanleg and Onderhoud, so two of four cards were visually identical at a
   glance, and Uitrusting wore main-services-icon2.png — the spray-bottle glyph
   belonging to the deleted Reiniging service. Four distinct Font Awesome 6
   Free Solid marks replace them: one library, one stroke weight.
   ========================================================================== */

/* Four items in a three-column grid left one card alone in the last row with
   68% of it empty — on a page whose subject is finish quality. Four services
   are a 2x2. */
@media screen and (min-width: 992px) {
    .servicespage-con .main-services-box {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* No `overflow: hidden` here. It would have been belt-and-braces for the
   photograph's corners, which the image already rounds itself — and a clipping
   container wrapping an absolutely-positioned child is exactly the setup that
   silently truncates the medallion the moment its offset changes. */
.servicespage-con .main-services-item {
    padding: 0 0 34px;
    border-radius: 20px;
}

/* The photograph runs to the card edges and takes the card's own top corners,
   so image and card share one silhouette. 12/5 matches the source crops, so
   nothing is stretched at any width. */
.servicespage-con .service-photo {
    position: relative;
    margin-bottom: 62px;
}

.servicespage-con .service-photo img {
    width: 100%;
    height: auto;
    display: block;
    object-fit: cover;
    aspect-ratio: 12 / 5;
    border-radius: 20px 20px 0 0;
}

/* --- The icon medallion, one component for both pages ---
   This was scoped to .servicespage-con while the homepage ran a near-identical
   copy of it in custom.css built on a <figure> and six PNGs -- of which
   services-icon3.png was used for BOTH Aanleg and Onderhoud, so two of the
   four homepage cards carried the same mark. The 2x2 grid then stacked those
   two in the same column, one directly above the other, which is what made a
   long-standing duplicate impossible to miss. Both pages now draw the same
   four Font Awesome glyphs from the same rules; the custom.css copy and its
   mobile.css steps are deleted.

   The size is a variable because the offset is derived from it: the medallion
   sits exactly half above the seam and half below at every step. The template
   paired a 99px circle with a -46px offset, which is 3.5px shy of half and is
   the gap DESIGN.md describes as "half above and half below". */
.service-medallion {
    --medallion: 99px;
    left: 50%;
    bottom: calc(var(--medallion) / -2);
    width: var(--medallion);
    height: var(--medallion);
    display: flex;
    position: absolute;
    border-radius: 50%;
    align-items: center;
    justify-content: center;
    transform: translateX(-50%);
    background: var(--secondary--color);
    transition: all 0.4s ease-in-out;
}

/* 32px is the Stat step, so the glyph sits on the documented ramp rather than
   introducing a size of its own. It scales with the circle. */
.service-medallion i {
    font-size: calc(var(--medallion) * 0.323);
    color: var(--white--color);
}

/* The steps custom.css and mobile.css used to carry for .service-small-icon.
   .service-content's top padding is 58px at <=1199 and 48px at <=767, so the
   circle has to come down with it or the heading collides with the glyph. */
@media screen and (max-width: 1199px) {
    .service-medallion {
        --medallion: 90px;
    }
}

@media screen and (max-width: 767px) {
    .service-medallion {
        --medallion: 78px;
    }
}

/* The Lift-On-Touch Rule: the medallion rises with its card. */
.main-services-item:hover .service-medallion,
.service-item:hover .service-medallion {
    transform: translateX(-50%) translateY(-5px);
}

.servicespage-con .main-services-item h3,
.servicespage-con .main-services-item p {
    padding-left: 37px;
    padding-right: 37px;
}

.servicespage-con .main-services-item>a[href] {
    margin-left: 37px;
    margin-right: 37px;
}

/* The card's only forward action measured 2.73:1 at rest (#2ca2fc on white)
   and flipped to #fddc57 on hover — 1.35:1, i.e. the link vanished at the
   instant the visitor reached for it. DESIGN.md names both as defects. Navy
   is 14.28:1; the hover now underlines instead of changing hue, reusing the
   pattern already established on .form-promise a. */
.servicespage-con .main-services-item a[href]:not(.anchor) {
    color: var(--primary--color);
}

.servicespage-con .main-services-item a[href]:not(.anchor):hover,
.servicespage-con .main-services-item a[href]:not(.anchor):focus-visible {
    color: var(--primary--color);
    text-decoration: underline;
    text-underline-offset: 4px;
}

.servicespage-con .main-services-item a[href]:not(.anchor) i {
    color: var(--secondary--color);
}

@media screen and (max-width: 767px) {
    .servicespage-con .main-services-item h3,
    .servicespage-con .main-services-item p {
        padding-left: 24px;
        padding-right: 24px;
    }

    .servicespage-con .main-services-item>a[href] {
        margin-left: 24px;
        margin-right: 24px;
    }
}

/* ==========================================================================
   17. ABOUT: THE OWNERS ARE BACKED BY THE WORK
   PRODUCT.md's first principle is that the realisations are the argument, and
   its fourth is that the owners are the brand. The About page carried neither:
   three grey monograms, no faces, and not one project photograph — while the
   homepage's About teaser, which links here promising "Meer weten?", had more
   human content than the destination.

   Real portraits are the actual fix and need a photographer. Until they exist,
   the names are backed by the work rather than by initials: three realisations
   the visitor can click straight through to, two of them in places PRODUCT.md
   confirms (Bever, Pamel). The third carries no place name, because four of
   the seven have none and inventing one is forbidden.

   The band is white so it steps tonally against the Pool Mist team band above
   it, per the system's tonal separation rather than a divider.
   ========================================================================== */

/* 32px, not 110px. .closing-con opens with 100px of its own, so two full
   section paddings met at this seam and neither knew about the other -- 210px
   of empty white between "Dit hebben wij gebouwd" and the conversion prompt,
   at the point the page has the most conviction to spend. The band below is
   also a tonal step (white to Pool Mist), and DESIGN.md's rule is that
   separation is tonal: where the colour already separates, the space does not
   have to do it twice.

   Only this side moves. .closing-con is shared with three other pages and its
   100px is doing real work under a different band on each. The resulting gap
   is 132 / 122 / 104 / 88, which is deliberately the same ladder index.html's
   projects -> closing seam runs (section 25), so the two pages that put work
   above the closing band now hand off to it identically. */
.work-con {
    padding-bottom: 32px;
    background: var(--white--color);
}

.work-grid {
    gap: 30px;
    padding: 0;
    display: grid;
    margin-bottom: 44px;
    grid-template-columns: repeat(3, 1fr);
}

.work-grid a {
    display: block;
    text-decoration: none;
}

.work-grid figure {
    overflow: hidden;
    border-radius: 20px;
    box-shadow: 2px 2px 67px rgb(44 162 252 / 7%);
}

/* 12/5 matches the source crops, so no frame is stretched at any width. */
.work-grid img {
    width: 100%;
    height: auto;
    display: block;
    object-fit: cover;
    aspect-ratio: 12 / 5;
    transition: transform 0.4s ease-in-out;
}

.work-grid .work-caption {
    display: block;
    font-size: 16px;
    font-weight: 400;
    line-height: 26px;
    padding-top: 14px;
    color: var(--text--color);
}

/* The Lift-On-Touch Rule. The caption goes navy rather than Pool Surface Blue,
   which measures 2.73:1 on white and is the site's most repeated AA failure. */
.work-grid a:hover figure,
.work-grid a:focus-visible figure {
    transform: translateY(-5px);
    transition: transform 0.4s ease-in-out;
}

.work-grid a:hover .work-caption,
.work-grid a:focus-visible .work-caption {
    color: var(--primary--color);
}

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

.work-more a {
    font-size: 16px;
    font-weight: 700;
    line-height: 26px;
    text-decoration: none;
    color: var(--primary--color);
}

.work-more a i {
    font-size: 14px;
    padding-left: 8px;
    color: var(--secondary--color);
}

.work-more a:hover,
.work-more a:focus-visible {
    text-decoration: underline;
    text-underline-offset: 4px;
}

@media screen and (max-width: 991px) {
    .work-con {
        padding-bottom: 24px;
    }

    .work-grid {
        gap: 24px;
        margin-bottom: 34px;
        grid-template-columns: repeat(2, 1fr);
    }
}

/* No .work-con step here: 24px already carries down from 991, and .closing-con
   takes its own step to 64px at this width. */
@media screen and (max-width: 767px) {
    .work-grid {
        grid-template-columns: 100%;
    }
}

/* ==========================================================================
   18. FOOTER COLUMN HEADINGS
   The footer's two column headings ("Handige Links", "Onze Diensten") were
   <h4> on every page, directly under an <h2> section heading — one shared
   heading-outline break the detector reported once per page, and the reason a
   screen-reader user navigating by heading could not reach the site map at a
   sensible level. They are now <h3>.

   That promotion orphaned custom.css:1425 `.site-map h4`, which is where the
   white colour lived, so the headings fell back to Body Grey on Deep Water
   Navy — 2.7:1. This restores the treatment on the new tag and carries the
   two responsive margin steps (mobile.css:211 and :1948) with it.

   The size is held at the h4 value on purpose: DESIGN.md's Subtitle step
   (20px/24px) is documented as the footer column heading, and the tag change
   is about outline semantics, not about making these headings bigger.
   ========================================================================== */

.site-map h3 {
    font-size: 20px;
    font-weight: 700;
    line-height: 24px;
    margin-bottom: 22px;
    color: var(--white--color);
}

@media screen and (max-width: 1600px) {
    .site-map h3 {
        margin-bottom: 18px;
    }
}

@media screen and (max-width: 575px) {
    .site-map h3 {
        margin-bottom: 14px;
    }
}

/* ==========================================================================
   19. HOMEPAGE: PROJECTEN GRID CONTINUITY
   Three of the seven cards carried a .button-portion and four did not, because
   the caption bar existed only to hold a location and four realisations have
   none. Those four rendered as a bare photograph, so they were shorter than
   their neighbours and the two-column grid went ragged — the row broke, and
   the band stopped reading as one set of seven.

   Every card now carries the bar and a title describing what is visibly in the
   photograph, matching the captions on projects.html so the two surfaces agree.
   The bar holds the title and nothing else: the location pin on Bever, Pamel
   and Meerbeke and the photo count on the three multi-frame realisations were
   both removed at the owner's request, from this page and projects.html alike.
   All seven captions are now identical in structure, which is the simplest
   version of the continuity this section was written to get.

   Equal height is enforced structurally rather than by matching string lengths:
   the cards stretch to their row and the caption bar is pushed to the bottom,
   so a title that wraps at one breakpoint cannot pull a row out of alignment.

   The photographs take the same aspect-ratio correction as projects.html.
   blog.css pins .twocolumn-blog images to 287px and 212px against 1920x800
   sources, so the homepage grid was stretching them exactly as the projects
   page was.
   ========================================================================== */

.project-con .blog-box {
    float: none;
    height: 100%;
    display: flex;
    margin-bottom: 30px;
    flex-direction: column;
}

.project-con .blog-box .post-item-wrap {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
}

/* Pushes the caption to the card's bottom edge, so uneven title lengths spend
   the difference above the bar instead of changing the card's height. */
.project-con .blog-box .button-portion {
    margin-top: auto;
    padding: 20px 24px;
    align-items: flex-start;
    flex-direction: column;
    justify-content: flex-start;
}

.project-con .blog-box .post-item-wrap img,
.project-con .blog-box .post-item-wrap .carousel-item img {
    height: auto;
    aspect-ratio: 12 / 5;
    object-fit: cover;
}

/* 20px/700 is the Subtitle step. Navy is 14.28:1 on the white card. */
.project-con .blog-box .button-portion .project-title {
    font-size: 20px;
    font-weight: 700;
    line-height: 26px;
    margin-bottom: 0;
    color: var(--primary--color);
}

@media screen and (max-width: 767px) {
    .project-con .blog-box .button-portion {
        padding: 16px 20px;
    }

    .project-con .blog-box .button-portion .project-title {
        font-size: 18px;
        line-height: 24px;
    }
}

/* ==========================================================================
   19a. HOMEPAGE: PROJECTEN GRID, EQUAL CARDS
   Section 19 gave every card a caption but only equalised heights *within* a
   Bootstrap row, because .row is a flex line and its columns stretch to each
   other and no further. Measured at 1440px that left cards carrying a location
   at 319px and cards without at 285px — a 34px step, exactly the height of the
   .date line — so rows 1 and 4 stood taller than rows 2 and 3 and the band
   still read as ragged. The same delta was 30px at 390px.

   Two rules, belt and braces:

   1. The caption bar takes a min-height. It was sized to the tallest variant
      back when some cards carried a meta line and some did not; now that every
      caption is title-only it is a floor rather than an equaliser, and it still
      earns its place — it keeps a one-line title from producing a visibly
      shallower bar than a title that wraps.

   2. The row becomes a grid with `grid-auto-rows: 1fr`, which equalises every
      row against the tallest in the whole grid rather than pairwise. That is
      the safety net: if a title wraps to two lines at some viewport the cards
      still cannot go ragged, whereas a min-height alone would be defeated.
      With the meta line gone this rule is doing most of the work.

   `offset-xl-3` centres the seventh card and stops applying once the row is a
   grid, so that centring is restated explicitly.
   ========================================================================== */

.project-con #blog>.row {
    gap: 30px;
    margin: 0;
    display: grid;
    grid-auto-rows: 1fr;
    grid-template-columns: repeat(2, 1fr);
}

.project-con #blog>.row>[class*="col-"] {
    width: auto;
    flex: none;
    padding: 0;
    max-width: none;
}

.project-con #blog>.row>[class*="col-"]:last-child:nth-child(odd) {
    margin: 0 auto;
    grid-column: 1 / -1;
    width: calc(50% - 15px);
}

.project-con .blog-box {
    margin-bottom: 0;
}

/* Content stays top-aligned against the photograph rather than pinned to the
   card's bottom edge: with equal rows, bottom-pinning would push the caption
   away from its own image by a different amount on every card. */
.project-con .blog-box .button-portion {
    margin-top: 0;
    min-height: 100px;
}

@media screen and (max-width: 767px) {
    .project-con #blog>.row {
        gap: 24px;
        grid-template-columns: 100%;
    }

    .project-con #blog>.row>[class*="col-"]:last-child:nth-child(odd) {
        width: 100%;
    }

    .project-con .blog-box .button-portion {
        min-height: 86px;
    }
}

/* ==========================================================================
   20. MOBILE MENU: THE TWO DOORS STAY OPEN
   mobile.css:956 pinned the collapse panel to `top: 63px` inside a 123px-tall
   header, with `.navbar` supplying the containing block. Measured at 390x844,
   `document.elementFromPoint` at the centre of the header phone button and of
   the yellow contact pill both returned `li.nav-item`: the panel did not merely
   cover the two conversion controls, it swallowed their clicks, and the tap
   landed on an inert list item. PRODUCT.md Principle 2 says a visitor is always
   one action away from the form or a call, so this is the one interaction that
   must never close both doors.

   The panel now hangs off the header itself (`.navbar` goes static, so the
   sticky header becomes the containing block) and opens at `top: 100%` — the
   header's own bottom edge, whatever its height at that breakpoint.
   ========================================================================== */

@media screen and (max-width: 991px) {

    .header-con .navbar {
        position: static;
    }

    .header-con .navbar-collapse.collapse,
    .header-con .navbar-collapse.collapsing {
        top: 100%;
        left: 0;
        right: 0;
        width: 100%;
        overflow: hidden;
        border-top: 0;
        /* DESIGN.md: soft rectangles for content, and one shadow recipe. This
           panel sits above the page rather than in it, so it takes the glow at
           its emphasis opacity — not a second recipe, the same one one step up. */
        border-radius: 0 0 20px 20px;
        box-shadow: 2px 2px 67px rgb(44 162 252 / 13%);
    }

    /* mobile.css:966 put a grey drop shadow on the list inside the panel: a
       neutral shadow in a system whose separation is tonal. The panel now
       carries the elevation, so the list carries none. */
    .header-con .navbar-expand-lg .navbar-nav {
        box-shadow: none;
    }

    /* 26px rows were the smallest targets on the page. */
    .header-con .navbar-collapse .nav-link {
        display: flex;
        align-items: center;
        min-height: 44px;
        padding: 0 24px;
    }

    .header-con .navbar-collapse ul.navbar-nav:first-child .nav-item:first-child {
        padding-top: 8px;
    }

    .header-con ul.navbar-nav .nav-item:last-child {
        padding-bottom: 8px;
    }

}

/* The menu panel used to carry its own two rows for the phone number and the
   contact CTA, shown from 991px down. There is no width where they help, and
   one where they hurt:

     >=992   desktop nav, header cluster visible          rows hidden already
     768-991 hamburger AND header cluster still visible   rows were a DUPLICATE
     <=767   hamburger, cluster gone, action bar carries  rows hidden already
             both doors

   .header-btn only hides at <=767, deliberately, because the cluster still fits
   on one row at 768. So between 768 and 991 -- every iPad in portrait -- opening
   the menu printed "Contacteer ons" and the phone number a second time, directly
   beneath the copies already in the header. The <=767 block further down used to
   hide the rows again for the same reason against the action bar; both overrides
   are gone and this single rule is now the whole story.

   The two <li class="nav-item--mobile"> in each page's header are inert. */
.nav-item--mobile {
    display: none;
}

/* ==========================================================================
   21. HOMEPAGE: THE REALISATIONS LEAD SOMEWHERE
   The seven project cards held no link at all — measured `cursor: auto`,
   `transform: none`, no hover response — so the only route to projects.html on
   the whole page was the nav item. PRODUCT.md Principle 1 makes the
   realisations the argument; an argument the visitor cannot follow is
   decoration. Each caption now carries the link, and on the four cards without
   a carousel the link covers the whole card, so the photograph is the target.
   Carousel cards keep a text-sized link, because a full-card overlay would eat
   their prev/next controls.
   ========================================================================== */

.project-con .project-title a {
    color: inherit;
    text-decoration: none;
    transition: color 0.3s ease-in-out;
}

/* Pool Surface Blue on white is 2.73:1 — under the 3:1 large-text floor even at
   22px/700 (The Blue-Is-Not-Text Rule). The card's own lift carries the hover;
   the underline carries it for anyone who cannot see the lift. */
.project-con .project-title a:hover,
.project-con .project-title a:focus-visible {
    color: var(--primary--color);
    text-decoration: underline;
    text-underline-offset: 4px;
}

.project-con .project-link--cover::after {
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    content: "";
    position: absolute;
}

/* The Lift-On-Touch Rule, applied to the card rather than to the link inside
   it: the whole surface answers, which is also the honest signal that the whole
   surface is the target. */
.project-con .post-item-wrap {
    transition: transform 0.4s ease-in-out, box-shadow 0.4s ease-in-out;
}

.project-con .blog-box:hover .post-item-wrap,
.project-con .blog-box:focus-within .post-item-wrap {
    transform: translateY(-5px);
    box-shadow: 2px 2px 67px rgb(44 162 252 / 13%);
}

/* The button is a grid item, not a block after the section. Everything in this
   template is `float-left` and nothing clears, so the cards overflow their own
   wrapper by 76px and anything placed after it lands inside the last card. As a
   full-width row of the same grid, the button cannot overlap anything.
   `width` and `grid-column` are restated because section 19's
   `:last-child:nth-child(odd)` rule now matches this element instead of a card. */
/* The cards carry Bootstrap's `.float-left` utility while their columns are
   grid items, so each column's height collapsed to zero and the floated card
   spilled ~30px past the grid row that owns it — which is why anything placed
   after the grid landed inside the last card. The grid does the placing now, so
   the float is a leftover. `!important` because the utility it unsets is
   itself `!important` in bootstrap.min.css. */
.project-con .blog-box {
    float: none !important;
}

/* The route into the gallery sits under the section heading rather than after
   the grid: everything below is `float-left` inside a grid whose cards overflow
   their own rows, so anything placed after them lands inside the last card.
   Above the grid it is also seen before the scroll, not after it. */
.projects-more {
    margin-top: 24px;
}

/* ==========================================================================
   22. HOMEPAGE: EVIDENCE AT THE DECISION POINT
   The closing band asked for a five-figure commitment carrying one sentence of
   reassurance and no evidence at all, while three real testimonials sat on
   services.html and the three confirmed positioning claims — 25+ years, the
   owners on site, build-and-maintain-for-life — appeared nowhere on the page.
   Quote and credentials are both verbatim product truth; nothing here is a new
   claim.
   ========================================================================== */

.closing-proof {
    margin: 32px auto 24px;
    max-width: 620px;
}

/* Bootstrap's print stylesheet gives blockquote a 1px #adb5bd border. It never
   applies on screen, but a grey box around a customer's words is not how this
   band should print either. */
.closing-quote {
    border: 0;
    margin: 0 0 8px;
    font-size: 20px;
    font-weight: 500;
    line-height: 33px;
    color: var(--primary--color);
}

/* Bootstrap sets figcaption to #adb5bd (2.3:1 on white) — off-palette and
   illegible. The attribution is a name, so it takes the Label step in navy. */
.closing-attribution {
    font-size: 14px;
    font-weight: 700;
    line-height: 14px;
    color: var(--primary--color);
}

.closing-attribution::before {
    content: "— ";
}

.closing-credentials {
    margin: 0 auto 32px;
    max-width: 620px;
    font-size: 16px;
    line-height: 26px;
    color: var(--text--color);
}

@media screen and (max-width: 767px) {
    .closing-quote {
        font-size: 18px;
        line-height: 29px;
    }
}

/* ==========================================================================
   23. SUB-BANNERS CARRY OUR OWN WORK
   custom.css:1687 pointed every sub-banner at img_secondary.jpg — a stock
   photograph of an American suburban backyard: lap siding, asphalt shingles, a
   vinyl privacy fence, a palm tree. On projects.html it measured as the LCP
   element at both viewports, so the largest and first-painted image on the page
   that exists to prove "these are our pools" was somebody else's pool.

   Both replacements are real realisations. project_7_3 is also 197 KB against
   the stock file's 543 KB, so the projects hero gets lighter as well as true.
   services.html and shop.html still carry the stock frame — a separate pass.
   ========================================================================== */

.about .sub-banner {
    background-image: url(../images/originals/project_1_1_1920x800.jpg);
}

.projects .sub-banner {
    background-image: url(../images/originals/project_7_3_1920x800.jpg);
}

/* ==========================================================================
   24. PROJECTS: THE PHOTOGRAPH OPENS
   The anchors that carry the lightbox are new; the images they wrap already
   filled their cards, so the link takes the image's box exactly and adds the
   one affordance that says "this enlarges".
   ========================================================================== */

.project-photo {
    display: block;
    cursor: zoom-in;
}

.project-photo img {
    display: block;
}

/* The carousel arrows sit above the photograph and must stay clickable. */
.carousel-item .project-photo {
    position: relative;
    z-index: 0;
}

/* Magnific's own chrome, brought into the system: navy rather than black, the
   card radius on the image, and the caption set like a caption. */
.mfp-project.mfp-bg {
    opacity: 0.94;
    background: var(--primary--color);
}

/* Contained rather than edge-to-edge, so the card radius reads and the close
   control does not sit on the photograph. */
.mfp-project .mfp-content {
    max-width: 1180px;
}

.mfp-project .mfp-img {
    padding: 0;
    border-radius: 20px;
}

.mfp-project .mfp-figure::after {
    display: none;
}

/* The caption sits over the bottom of the photograph, so it needs its own
   ground: white on a bright sky or pale coping is the same failure the hero
   scrim exists to prevent. Navy, fading up, and clipped to the image radius. */
.mfp-project .mfp-bottom-bar {
    margin-top: -64px;
    padding: 32px 16px 12px;
    background: linear-gradient(to bottom, rgb(10 38 99 / 0%), rgb(10 38 99 / 85%));
    border-radius: 0 0 20px 20px;
}

.mfp-project .mfp-title {
    padding: 0;
    font-size: 16px;
    line-height: 26px;
    color: var(--white--color);
}

.mfp-project .mfp-counter {
    font-size: 14px;
    font-weight: 700;
    color: var(--white--color);
}

.mfp-project .mfp-arrow,
.mfp-project .mfp-close {
    opacity: 1;
    color: var(--white--color);
}

.mfp-project .mfp-arrow:focus-visible,
.mfp-project .mfp-close:focus-visible {
    outline: 3px solid var(--white--color);
    outline-offset: 3px;
}

/* ==========================================================================
   25. CONTACT: THE ERRORS ARE WHERE THE VISITOR IS
   Two halves of one defect. The fields now clear the sticky header when
   anything scrolls them into view — site.css:212 granted that to `:target`
   only, so a focus-driven scroll still parked the field underneath the header.
   And the summary the invalidHandler writes into #form_result names every
   failing field at once, which is what the visitor was previously expected to
   remember.
   ========================================================================== */

.contact-form ul li input,
.contact-form ul li textarea,
#contact-submit {
    scroll-margin-top: 104px;
}

@media screen and (max-width: 1199px) {

    .contact-form ul li input,
    .contact-form ul li textarea,
    #contact-submit {
        scroll-margin-top: 139px;
    }
}

.form-status__lead {
    margin: 0 0 8px;
    font-weight: 700;
}

.form-status__list {
    margin: 0;
    padding: 0 0 0 18px;
    list-style: disc;
}

.form-status__list li + li {
    margin-top: 4px;
}

.form-status__list a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 3px;
}

.form-status__list a:hover,
.form-status__list a:focus-visible {
    text-decoration-thickness: 2px;
}

/* ==========================================================================
   26. MOBILE: THE TWO DOORS MOVE TO THE THUMB
   Measured at 320/360/390px: the header ran 123px tall — 14.6% of an 844px
   viewport, permanently sticky — because the call button and the quote pill
   could not fit beside the logo and wrapped onto a second row. Above 430px they
   fit and the header is 77px.

   Both conversion paths move to a fixed bar at the bottom edge instead. That
   returns 46px of every screen, puts both actions in the thumb zone rather than
   at the top corner a one-handed visitor cannot reach, and keeps them visible
   at every scroll depth — which is what the sticky header was doing the hard
   way. PRODUCT.md Principle 2: never force one path through the other, so the
   bar carries both, side by side, and the yellow one stays the only warm thing.

   Also: the hamburger sat flush against the viewport edge. `.navbar` is static
   below 992px (section 20), so the absolutely positioned toggler resolves
   against `.header-con`'s border box and `right: 0` landed on the screen edge
   rather than inside the header's 20px padding.
   ========================================================================== */

@media screen and (max-width: 991px) {
    .navbar-toggler {
        right: 20px;
    }
}

.action-bar {
    display: none;
}

/* 767, not 575: the cluster still wrapped to a second row between 576 and
   639px, and a bar that appears on one phone size and not the next is worse
   than a bar that covers every phone. At 768 the cluster fits on one row. */
@media screen and (max-width: 767px) {

    /* The header keeps the logo and the menu; the doors are downstairs. */
    .header-btn {
        display: none;
    }

    .action-bar {
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 250;
        display: flex;
        gap: 10px;
        position: fixed;
        padding: 10px 15px calc(10px + env(safe-area-inset-bottom));
        background: var(--white--color);
        border-radius: 20px 20px 0 0;
        box-shadow: 2px 2px 67px rgb(44 162 252 / 13%);
    }

    .action-bar a {
        flex: 1 1 0;
        min-width: 0;
        display: flex;
        gap: 8px;
        min-height: 48px;
        align-items: center;
        justify-content: center;
        border-radius: 29px;
        font-size: 14px;
        font-weight: 700;
        line-height: 14px;
        text-align: center;
        text-transform: uppercase;
        text-decoration: none;
        transition: all 0.3s ease-in-out;
    }

    /* The call is navy on white — a second yellow would break the One Warm
       Thing Rule, and the form is the action the page is arguing for. */
    .action-bar__call {
        color: var(--white--color);
        background: var(--primary--color);
    }

    /* The header's own call button flips to white on Pool Surface Blue = 2.73:1,
       which DESIGN.md records as a live AA failure. The flip is the system's
       idiom and stays; the label goes black, which is the correction DESIGN.md
       already documents as button-submit-hover (7.68:1). */
    .action-bar__call:hover,
    .action-bar__call:focus {
        color: var(--black--color);
        background: var(--secondary--color);
    }

    /* The form CTA takes the larger share: two doors, not two equals.
       Scoped to .action-bar so it outweighs `.action-bar a`'s own flex. */
    .action-bar .action-bar__cta {
        flex: 1.35 1 0;
        color: var(--black--color);
        background: var(--accent--color);
    }

    .action-bar__cta:hover,
    .action-bar__cta:focus {
        color: var(--black--color);
        background: var(--secondary--color);
    }

    .action-bar--single .action-bar__call {
        flex: 1 1 100%;
    }

    .action-bar__short {
        display: none;
    }

    /* Nothing may sit under the bar: the footer's copyright strip, and the
       back-to-top button — which the bar makes redundant anyway, since the
       thumb zone now belongs to the two actions that matter. */
    body {
        padding-bottom: 78px;
    }

    #back-to-top-btn {
        display: none !important;
    }

}

/* At 320px the two labels cannot both be spelled out: flex items do not shrink
   below their content. The label shortens rather than the button. */
@media screen and (max-width: 360px) {
    .action-bar__long {
        display: none;
    }

    .action-bar__short {
        display: inline;
    }
}

/* ==========================================================================
   27. RECAPTCHA BADGE: HIDDEN WITHOUT THE LAYOUT BOX
   custom.css:3330 hides the badge with `visibility: hidden`, which removes the
   paint but not the box. Google positions it `fixed; right: -186px`, so the
   document measured 576px wide inside a 390px viewport — and a fixed element
   with left/right: 0, like the mobile action bar, then sized itself to that
   576px layout viewport and ran off the screen.

   The badge stays rendered (visibility, not display, so nothing about the
   invisible challenge changes) and is pinned inside the viewport with a zero
   footprint. Google's terms are satisfied by the attribution sentence in
   .form-legal, which stays.
   ========================================================================== */

.grecaptcha-badge {
    right: 0 !important;
    width: 0 !important;
    overflow: hidden !important;
    visibility: hidden;
}

/* The header's call button and the action bar's are one component in two
   places, so they hover the same way: Pool Surface Blue with a black label
   (7.68:1) rather than the white one, which measures 2.73:1 and is the AA
   failure DESIGN.md records against button-primary-hover. */
.header-action--call:hover,
.header-action--call:focus {
    color: var(--black--color);
}

/* ==========================================================================
   28. THE BLUE-IS-NOT-TEXT RULE, ENFORCED
   Pool Surface Blue is fixed at 2.73:1 on white, 2.57:1 on Pool Mist, and
   2.73:1 under white text. DESIGN.md already names this: blue is safe as a
   background, an icon fill or a border, and never as small text. The template
   uses it as text in three places and as a hover destination for white labels
   in four more, so the same failure repeats on every page.

   Two corrections, both already proven elsewhere in this file:
   - a yellow button flipping to blue keeps its BLACK label (7.68:1), the
     treatment DESIGN.md records as button-submit-hover and .closing-actions
     already uses;
   - a text link that flipped to blue stays navy and underlines instead. Colour
     was never carrying the state on its own anyway — the buttons lift, the
     cards lift and gain a border.

   The footer contact column (white on Pool Surface Blue, 2.73:1) is not here:
   that one needs a decision about the column's fill, not a colour swap.
   ========================================================================== */

/* --- Buttons: the flip keeps a black label --- */

.generic-btn a:hover,
.generic-btn a:focus,
.generic-btn a:hover i,
.generic-btn a:focus i,
.quote-btn a:hover,
.quote-btn a:focus,
.quote-btn a:hover i,
.quote-btn a:focus i,
.submit-btn:hover,
.submit-btn:focus,
.read_more:hover,
.read_more:focus {
    color: var(--black--color);
}

/* --- Text links: navy, with an underline carrying the hover --- */

.header-con .navbar-light .navbar-nav .nav-link:hover,
.phone-num a:hover,
.team-box-item:hover h5 {
    color: var(--primary--color);
}

.header-con .navbar-light .navbar-nav .nav-link:hover,
.phone-num a:hover {
    text-decoration: underline;
    text-underline-offset: 6px;
}

/* The current page was marked with 2.73:1 blue and nothing else. Weight and an
   underline carry it now; the `!important` matches custom.css:225, which sets
   the colour that way. */
.header-con .navbar-light .navbar-nav .active > a {
    color: var(--primary--color) !important;
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 6px;
}

/* "Meer weten?" was 2.73:1 at rest and flipped to Poolside Sun on hover —
   1.3:1, effectively invisible, which DESIGN.md lists under Don'ts. */
.service-content a,
.service-content a i {
    color: var(--primary--color);
}

.service-content a:hover,
.service-content a:focus,
.service-content a:hover i {
    color: var(--primary--color);
    text-decoration: underline;
    text-underline-offset: 4px;
}

/* --- Blue used as running text --- */

/* Section eyebrows: 2.73:1 on white, 2.57:1 on Pool Mist. The closing band
   already sets these in navy with the reason documented; this extends the same
   correction to the three the template still owns. */
.generic-title span.d-inline-block,
.about-content span.d-block,
.about-static-content span.d-block,
.staic-sign {
    color: var(--primary--color);
}

/* The team card's social icons flip to white on Pool Surface Blue when the card
   is hovered — 2.73:1 against the 3:1 floor for a non-text control. The card
   already answers with a lift and a blue border, so the icons keep the navy
   fill they carry at rest. */
.team-box-item:hover li a i {
    background: var(--primary--color);
}

/* ==========================================================================
   29. TWO TEMPLATE LEFTOVERS, CORRECTED AT THE SOURCE
   Both were flagged by the detector and both are real.

   The back-to-top button's `spineer` animation is a zero-offset chromatic halo
   expanding to a 30px spread ring, running infinitely: the least important
   control on the page was the only continuously animating object on it, and on
   mobile the ring reached under the action bar. The button keeps its fill, its
   hover and its focus ring; it simply stops pulsing.
   ========================================================================== */

/* mobile.css:859 drops the textarea to 18.5px below 992px while the five
   inputs beside it keep the 26.5px field token — two radii inside one field
   group. One token, one shape. */
.contact-form ul li textarea {
    border-radius: 26.5px;
}

/* ==========================================================================
   30. AUTHORED BEHAVIOUR REPLACES FOUR LIBRARIES
   AOS, counter.js and Magnific Popup shipped 190 KB between them to reveal
   elements on scroll, count three figures and open a photograph. site.js does
   all three; these are the styles it needs.
   ========================================================================== */

/* --- Reveal on scroll ---
   The hidden state is scoped to `.js-reveal`, which site.js puts on <html>
   itself. AOS hid first and revealed later, so a JS failure left whole sections
   blank; here a failure leaves the page as written. The travel is 24px rather
   than AOS's 100px — at 100px a section arrives from off-screen, which reads as
   a page still loading. */

.js-reveal [data-aos] {
    opacity: 0;
    transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
                transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
    will-change: opacity, transform;
}

.js-reveal [data-aos="fade-up"] {
    transform: translateY(24px);
}

.js-reveal [data-aos="zoom-in"] {
    transform: scale(0.96);
}

.js-reveal [data-aos].is-revealed {
    opacity: 1;
    transform: none;
    will-change: auto;
}

/* --- index.html: the four services are a grid, not a carousel ---
   Owl showed three of four at >=992px and one of four on a phone, with
   Renovatie reachable only through 4x14px dots. Four items do not need a
   carousel; they need a grid that shows all four.

   Two across, two down: at four columns each card was a quarter of the
   container. A 2x2 block gives every card twice the width -- 540px against
   255px -- and reads as one square instead of a row scanned end to end. It
   only earns that width because the photographs are now realisations rendered
   at 1080x450 -- 2x the box -- instead of 350x230 stock upscaled 1.54x. */

.service-grid {
    display: grid;
    gap: 30px;
    grid-template-columns: repeat(2, 1fr);
}

.service-grid .item {
    display: flex;
}

/* The photographs are the realisations services.html already maps to each
   service. The four they replaced were stock, and three of them showed a
   different service than their own heading: Uitrusting and Onderhoud were
   holding each other's photograph, and Renovatie was illustrated by Reiniging,
   which is not a service this business offers. The 2x2 had doubled all four to
   540px from 350x230 sources -- a 1.54x upscale, 3.09x on a 2x display -- on
   the band that exists to prove finish quality.

   12/5 is the ratio of the source crops and the ratio every other card image
   on the site already uses, so nothing is cropped and the site has one
   card-image ratio. The four _card.jpg derivatives are those crops at 1080x450
   -- 2x the rendered box -- because shipping the 1920x800 originals into a
   540px card cost 2.1 MB for a band that sits below the fold; the derivatives
   cost 496 KB. display: block removes the inline-level gap under the image,
   which had been pushing the medallion a pixel or two off the seam. */
.service-grid .service-img > figure > img {
    width: 100%;
    height: auto;
    display: block;
    object-fit: cover;
    aspect-ratio: 12 / 5;
}

@media screen and (max-width: 1199px) {
    .service-grid {
        gap: 24px;
    }
}

@media screen and (max-width: 575px) {
    .service-grid {
        grid-template-columns: 1fr;
    }
}

/* --- services.html: the three quotes stand side by side ---
   They were one-at-a-time behind two 35x35px arrows whose accessible name was a
   Private Use Area glyph. Three cards fit; nothing needs to be hidden. */

/* Stacked, not three across. The card's own padding is 63px either side —
   DESIGN.md designed it as one wide card — so in a third of the container the
   quote ran at about 28 characters a line, half the readable minimum. One
   column at 720px gives roughly 70, and all three are still visible at once,
   which was the point of retiring the carousel. */
.reviews-grid {
    display: grid;
    gap: 24px;
    max-width: 720px;
    margin: 0 auto;
    grid-template-columns: 1fr;
}

@media screen and (max-width: 767px) {
    .reviews-grid {
        gap: 20px;
        max-width: none;
    }
}

/* --- The lightbox ---
   <dialog> brings the focus trap, the Escape key, the inert background and the
   focus return with it, so the CSS only has to dress it. */

.lightbox {
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    padding: 0;
    border: 0;
    background: transparent;
    overflow: hidden;
}

.lightbox::backdrop {
    background: rgb(10 38 99 / 94%);
}

.lightbox__figure {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 40px 72px;
}

.lightbox__img {
    display: block;
    max-width: 100%;
    max-height: calc(100% - 52px);
    object-fit: contain;
    border-radius: 20px;
}

.lightbox__caption {
    padding-top: 16px;
    max-width: 68ch;
    font-size: 16px;
    line-height: 26px;
    text-align: center;
    color: var(--white--color);
}

.lightbox__close,
.lightbox__nav {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgb(255 255 255 / 10%);
    color: var(--white--color);
    cursor: pointer;
    transition: background 0.3s ease-in-out;
}

.lightbox__close:hover,
.lightbox__nav:hover {
    background: var(--secondary--color);
}

.lightbox__close:focus-visible,
.lightbox__nav:focus-visible {
    outline: 3px solid var(--white--color);
    outline-offset: 3px;
}

.lightbox__close {
    top: 16px;
    right: 16px;
    font-size: 28px;
    line-height: 1;
}

.lightbox__nav {
    top: 50%;
    font-size: 32px;
    line-height: 1;
    transform: translateY(-50%);
}

.lightbox__nav--prev {
    left: 12px;
}

.lightbox__nav--next {
    right: 12px;
}

.lightbox__counter {
    position: absolute;
    right: 20px;
    bottom: 16px;
    margin: 0;
    font-size: 14px;
    font-weight: 700;
    color: var(--white--color);
}

@media screen and (max-width: 767px) {
    .lightbox__figure {
        padding: 64px 16px 24px;
    }

    .lightbox__nav {
        top: auto;
        bottom: 16px;
        transform: none;
    }

    .lightbox__counter {
        left: 0;
        right: 0;
        bottom: 30px;
        text-align: center;
    }
}

/* ==========================================================================
   31. HEADING LEVELS MATCH WHAT THE ELEMENTS ARE
   Two <h5>s under an <h2>, with no h3 or h4 between them, so the document
   outline dropped three levels at both places a screen reader navigates by
   heading.

   The three owners are subheadings of "Ons professioneel team" and become h3.
   A customer's name under their quote is not a heading at all — it is the
   attribution of the quotation above it — so it becomes a paragraph and keeps
   the type it had.
   ========================================================================== */

/* Both keep the metrics the <h5> had — 8px below, the same size, weight and
   colour — so the swap is semantic only and the layout does not move. */
.about .team-content h3 {
    font-size: 18px;
    font-weight: 700;
    line-height: 20px;
    margin-bottom: 8px;
    color: var(--primary--color);
    transition: all 0.3s ease-in-out;
}

.customer-status .customer-name {
    font-size: 20px;
    font-weight: 600;
    line-height: 20px;
    margin-bottom: 8px;
    color: var(--primary--color);
}

/* mobile.css steps the attribution down twice (18px at <=767, 16px at <=575).
   Those rules name the old <h5>, so the ramp is restated here for the element
   that replaced it — without this the quotes grew 12px taller on a phone. */
@media screen and (max-width: 767px) {
    .customer-status .customer-name {
        font-size: 18px;
        line-height: 18px;
    }
}

@media screen and (max-width: 575px) {
    .customer-status .customer-name {
        font-size: 16px;
        line-height: 16px;
    }
}

/* ==========================================================================
   32. CAROUSEL INDICATORS AND CONTROLS
   The controls are Bootstrap's markup driven by site.js now. Two corrections
   the Bootstrap version needed anyway: the dots did not exist, so a card with
   two photographs looked identical to one with five, and the arrows painted no
   focus ring at all while every other link on the page got one.
   ========================================================================== */

.carousel-indicators {
    position: static;
    display: flex;
    gap: 0;
    justify-content: center;
    margin: 16px 0 0;
    padding: 0;
    list-style: none;
}

.carousel-indicators li {
    display: block;
    width: auto;
    height: auto;
    margin: 0;
    text-indent: 0;
    background: none;
    opacity: 1;
}

/* The target is 44px and the dot inside it is 10px. The size lives on the
   button rather than on a pseudo-element of the <li>: an overlay on the parent
   covers the button, so the click lands on the list item and never reaches the
   handler — the dots looked fine and did nothing. */
.carousel-indicators button {
    position: relative;
    display: block;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
}

.carousel-indicators button::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #ccd8e1;
    transform: translate(-50%, -50%);
    transition: all 0.3s ease-in-out;
}

.carousel-indicators li.active button::after {
    width: 14px;
    height: 14px;
    background: var(--secondary--color);
}

.carousel-indicators button:focus-visible {
    outline: 3px solid var(--primary--color);
    outline-offset: 4px;
}

/* The arrows sit on photography, so their ring is white. */
.carousel-control-prev:focus-visible,
.carousel-control-next:focus-visible {
    outline: 3px solid var(--white--color);
    outline-offset: -6px;
}

/* ==========================================================================
   33. THE HERO CONTENT SITS ABOVE THE SCRIM
   `.banner-con::before` (custom.css:350) is an absolutely positioned overlay
   across the whole hero — the scrim, whose backgrounds are commented out, so it
   is invisible but still there. Being positioned, it paints above the
   non-positioned content beneath it and swallowed clicks on the hero CTA.

   It never showed while AOS was in charge: AOS animates to translate3d(0,0,0)
   and leaves the transform in place, which creates a stacking context and
   lifted the content over the overlay by accident. This file's reveal ends at
   `transform: none`, so the accident stopped happening.

   `.sub-banner .container` already carries exactly this pair (custom.css:1691);
   the homepage hero simply never got it.
   ========================================================================== */

.banner-con .container {
    position: relative;
    z-index: 1;
}

/* ==========================================================================
   24. HERO: THE SOCIAL ROW STOPS APPEARING AT 1440px
   The template shipped two placements for one list: an absolute rail pinned at
   `left: 140px; top: 366px` for very wide screens, and an inline row under the
   CTA below 1440. This site has a single <ul>, and `.banner-con ul` matches it
   too -- so `custom.css` hid it above 1440 for a rail that no longer exists,
   while `mobile.css` showed it below. Dragging a window across 1439px made two
   social buttons appear out of nothing, with no design reason for the step.

   The inline row is the live treatment: it is the one that is actually styled,
   sized (42px), and given the system's 5px hover lift, and it is what every
   viewport under 1440 has always rendered. It now renders at every width.
   ========================================================================== */

.banner-title ul {
    top: 0;
    left: 0;
    display: block;
    position: relative;
}

.banner-title ul li {
    margin: 0 5px;
    display: inline-block;
}

.banner-title ul li:first-child {
    margin-left: 0;
}

.banner-title ul li:last-child {
    margin-right: 0;
}

/* The Lift-On-Touch Rule. mobile.css carried the lift inside the <=1440 block,
   so above it the buttons were the one interactive thing on the hero that did
   not answer the pointer. :focus-within brings the keyboard along. */
.banner-title ul li:hover,
.banner-title ul li:focus-within {
    transform: translateY(-5px);
}

/* ==========================================================================
   25. HOMEPAGE: PROJECTEN -> CLOSING BAND GAP
   Two full section paddings met at this seam and neither knew about the other.
   .project-con carries `padding-bottom`, which blog.css resolves to 100px on
   this page, and .closing-con opens with 100px of its own -- so 200px of empty
   Pool Mist sat between the last realisation and the conversion prompt, at the
   exact point the page has the most conviction to spend.

   The band below is also a tonal step (white against Pool Mist, section 8), and
   DESIGN.md's own rule is that separation is tonal. Where the colour already
   does the separating, the space does not have to do it twice.

   Only .project-con's side is reduced. .closing-con is shared with about,
   services and projects, where the band above it is a different colour and its
   100px is doing real work. .project-con exists on the homepage alone, so
   scoping the change to it cannot reach another page. Two classes on one
   element outweighs blog.css's single-class rule at every breakpoint, so this
   holds inside the media queries below without !important.

   Resulting gap: 132 / 122 / 104 / 88, against 200 / 190 / 180 / 164.
   ========================================================================== */

.project-con.padding-bottom {
    padding-bottom: 32px;
}

@media screen and (max-width: 991px) {
    .project-con.padding-bottom {
        padding-bottom: 24px;
    }
}

/* ==========================================================================
   26. HOMEPAGE HERO: ONE 1920px BACKGROUND FOR EVERY VIEWPORT
   custom.css:256 set a single 1920x1033 file as .banner-con's background at
   every width, so a 390px phone downloaded 580 KB to paint a box a fifth that
   wide -- on the LCP element, above the fold, for the audience PRODUCT.md says
   arrives on mobile.

   The hero is full-bleed, so the viewport width is the box and the breakpoints
   can be read literally. Three steps, re-encoded at the same quality as the
   card derivatives:

     >1199   1920x1033   413 KB   (was 580 KB -- the original was encoded high)
     <=1199  1600x861    270 KB
     <=767   1200x646    163 KB   (a 390px phone at 3x asks for 1170)

   Only background-image is set, so `no-repeat center` and `background-size:
   cover` still come from custom.css and the crop is unchanged.

   project_2_1_home.jpg itself is deliberately left alone: it is the og:image
   and twitter:image, where a social scraper wants the higher-quality file and
   nothing is on a critical path. Changing it would also invalidate whatever
   Facebook has already cached for pages that have been shared.

   index.html preloads the matching step in <head>. A CSS background is invisible
   to the preload scanner -- it is not discovered until the stylesheet has
   parsed -- and `media` on the link means only one of the three is ever
   fetched.
   ========================================================================== */

.banner-con {
    background-image: url(../images/originals/project_2_1_home_1920.jpg);
}

@media screen and (max-width: 1199px) {
    .banner-con {
        background-image: url(../images/originals/project_2_1_home_1600.jpg);
    }
}

@media screen and (max-width: 767px) {
    .banner-con {
        background-image: url(../images/originals/project_2_1_home_1200.jpg);
    }
}

/* ==========================================================================
   27. PROJECTS PAGE: REALISATIES -> CLOSING BAND GAP
   The fourth and last instance of the same seam. The realisations sit in
   .blog-posts.blogpage-section, which claims the generic .padding-bottom that
   blog.css resolves to 100px, and .closing-con opens with 100px of its own --
   200px between the final realisation and the conversion prompt.

   index.html carries the same two classes on the band inside .project-con, but
   `.project-con .blog-posts.padding-bottom` already zeroes it there and is the
   more specific selector, so that page is untouched by this.

   Resulting gap 132 / 122 / 104 / 88, matching sections 25 (index),
   .work-con (about) and .tesimonials-con (services). All four pages that end
   in a closing band now approach it identically.
   ========================================================================== */

.blogpage-section.padding-bottom {
    padding-bottom: 32px;
}

@media screen and (max-width: 991px) {
    .blogpage-section.padding-bottom {
        padding-bottom: 24px;
    }
}

/* ==========================================================================
   28. THE HERO SCRIM
   Both heroes ship a full-cover `::before` with every background declaration
   commented out — `custom.css:350` on the homepage, `custom.css:1696` on the
   sub-banner — so white text sat directly on photography. Sampled against the
   real backdrop, with the text hidden and the pixels behind it measured: the
   homepage's lifetime-partnership line ran 1.93:1 at desktop and 1.69:1 on a
   phone, and the sub-banner breadcrumb had 62-69% of its pixels below 4.5:1,
   bottoming out at 1.00:1 — white on white, over the pool coping.

   Navy rather than black, so the scrim belongs to the palette instead of
   greying the water. It is directional because the type is: both titles run at
   60% width on the left, so the gradient clears out by 78% and the right-hand
   side of every photograph stays untouched. Below 992px the title goes
   full-width and the gradient turns vertical, weighted to the bottom where the
   lead and the breadcrumb sit.
   ========================================================================== */

.banner-con::before,
.sub-banner::before {
    background: linear-gradient(100deg,
            rgb(10 38 99 / 80%) 0%,
            rgb(10 38 99 / 64%) 46%,
            rgb(10 38 99 / 0%) 82%);
}

@media screen and (max-width: 991px) {

    .banner-con::before,
    .sub-banner::before {
        background: linear-gradient(180deg,
                rgb(10 38 99 / 34%) 0%,
                rgb(10 38 99 / 62%) 50%,
                rgb(10 38 99 / 80%) 100%);
    }
}

/* ==========================================================================
   29. THE FOOTER CONTACT COLUMN
   The column carrying the phone number, the WhatsApp link, the e-mail address
   and the postal address sat on Pool Surface Blue with 14px white text: 2.73:1,
   the failure DESIGN.md's own Blue-Is-Not-Text Rule describes, on the one block
   in the footer a visitor actually needs to read. Its hover was worse -- 
   Poolside Sun on that blue is 2.03:1, so the links got harder to read the
   moment a pointer arrived.

   Deep Water Tint rather than Deep Water Navy. Navy is what the columns beside
   it already use, so the panel would have dissolved into them and the footer
   would have gone from three tonal layers to two; the tint is the same navy
   stepped one shade up and is already the fill behind the footer's social
   buttons, so the column stays a distinct block and the Three-Layer Footer Rule
   survives. White on it measures 12.64:1.

   Below 992px mobile.css already drops the fill entirely, so this changes
   nothing there.
   ========================================================================== */

.footer-info {
    background: #122f6f;
}

/* mobile.css drops the fill below 992px, where the column spans the full width
   and sits on the footer's own navy. This file loads later, so that has to be
   restated or the tint would follow it down and band the whole footer. */
@media screen and (max-width: 991px) {
    .footer-info {
        background: transparent;
    }
}
