/* The single `@layer` order statement for the theme lives at the top of tokens.css, which is
   enqueued before this file (see inc/assets.php). Declaring it twice would be redundant — the
   first statement wins and later ones only append unknown names. */
@layer reset {
  *,*::before,*::after{box-sizing:border-box}
  body,h1,h2,h3,h4,p,figure,ul,ol{margin:0}
  /* No display:block here (used to be img,svg,video{display:block;...}). Third-party JS
     widgets (Magnific Popup / PhotoBlocks) inject their own <img class="mfp-img"> at runtime
     and size/center it with their OWN unlayered CSS (img.mfp-img{display:block;...}) — an
     unlayered normal declaration always beats a layered one regardless of specificity or
     source order, so this reset's display:block was never actually reaching that element
     anyway. What it DID reach were the theme's own images: every one now sets display:block
     itself (see .vn-hero__img, .vn-formula-card__img, .vn-gallery__grid img, .vn-region__img,
     .vn-logo/.vn-footer__logo svg, .vn-prose img) so removing it here changes nothing for
     theme markup and stops this layer from shadowing plugin-owned elements for no benefit. */
  img,svg,video{max-width:100%;height:auto}
  /* Imagify wraps uploads that have a .webp sibling in <picture> (production only). inc/media.php
     keeps the theme's classes on the <img> and marks the wrapper; `display:contents` then removes the
     wrapper from the layout, so the <img> is a grid/flex/positioned child exactly as without Imagify. */
  picture.vn-picture{display:contents}
  /* …and its <source> must make no box. With `display:contents` on the wrapper its children take
     part in the PARENT's layout, and browsers give <source> a box there: in a grid each one took a
     cell, so the three thumbnails of the Over ons photo grid landed in cells 2, 4 and 6 of a
     3-column grid — one alone on the first row, two with a hole between them on the second
     (Joe, 2026-10-05; only visible with Imagify's <picture> output, i.e. after the launch). */
  picture.vn-picture>source{display:none}
  button,input,select,textarea{font:inherit;color:inherit}
  :focus-visible{outline:3px solid var(--vn-accent);outline-offset:2px}
  @media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}
}
@layer base {
  /* html MUST stay at 100% (= the browser's 16px default). Every --vn-step-* token is a clamp()
     expressed in `rem`, i.e. relative to THIS font-size — and theme.json separately emits an
     unlayered `body{font-size:var(--vn-step-0)}`. Setting the step on <html> as well made the rem
     unit itself 18px, so every clamp resolved against an 18px root and the whole type scale
     compounded by 12.5% (measured on dev at 1440px: body 20.25px, h1 67.5px, h2 49.5px). That is
     the root cause of "font sizes not correct" in Joe's 2026-09-07 review. Keep the step on body
     only; the duplicate declaration here is deliberate insurance in case theme.json's styles
     block is ever trimmed. */
  html{background:var(--vn-bg);color:var(--vn-text);font-family:var(--vn-font-body);font-size:100%;line-height:1.6;-webkit-text-size-adjust:100%}
  body{background:var(--vn-bg);font-size:var(--vn-step-0)}
  h1,h2,h3{font-family:var(--vn-font-display);font-weight:500;line-height:1.15;text-wrap:balance;font-variant-numeric:lining-nums}
  h1{font-size:var(--vn-step-4)} h2{font-size:var(--vn-step-3)} h3{font-size:var(--vn-step-2)}
  a{color:var(--vn-accent)} a:hover{color:var(--vn-accent-hover)}
  ul[class],ol[class]{list-style:none;padding:0}
}
@layer layout {
  .vn-container{width:min(100% - 2*var(--vn-space-5),var(--vn-max));margin-inline:auto}
  .vn-stack>*+*{margin-block-start:var(--vn-stack-gap,var(--vn-space-5))}
  .vn-grid{display:grid;gap:var(--vn-space-5);grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr))}
  .vn-section{padding-block:var(--vn-space-7)}
}
@layer components {
  .vn-btn{display:inline-flex;align-items:center;gap:.5em;padding:.75em 1.25em;border-radius:var(--vn-radius);border:1px solid var(--vn-accent);font-weight:600;text-decoration:none;min-height:44px}
  /* No !important on the button colours below — they are plain layered declarations.
     History: primary-button text briefly rendered accent-on-accent on dev and was patched
     with five `color:#fff!important`s blamed on plugin CSS. The real culprit was THIS theme's
     own theme.json, which emitted an unlayered `a{color:var(--wp--preset--color--accent)}`
     element rule into global-styles-inline-css; it was removed in c3b08a3, which made the
     !importants redundant, so they are gone.
     Still true in general: per the CSS Cascade Layers spec ANY unlayered normal declaration
     beats ANY layered one regardless of specificity, so unlayered CSS shipped by the ~35
     active plugins CAN still override anything in these layers. Plan 03 decides the layers
     strategy (wrap third-party CSS in a low-priority layer, or scope with @scope/revert-layer).
     If a plugin regression shows up before then, fix it at the layer level — do not re-add
     !important here. */
  .vn-btn--primary{background:var(--vn-accent);color:#fff} .vn-btn--primary:hover{background:var(--vn-accent-hover);color:#fff}
  /* Compact outline CTA (desktop header) */
  .vn-btn--outline{min-height:40px;padding:0 16px;border:1.5px solid var(--vn-accent);color:var(--vn-accent);background:transparent}
  .vn-btn--outline:hover{background:var(--vn-accent);color:#fff}
  /* Ghost secondary button (hero, on photographic backgrounds) */
  .vn-btn--ghost{border:1.5px solid rgb(255 255 255 / .85);color:#fff;background:rgb(0 0 0 / .12)}
  .vn-btn--ghost:hover{background:rgb(255 255 255 / .25);color:#fff}

  /* Every inline icon villanatica_icon() (inc/icons.php) prints. The helper emits no width/height
     attributes on purpose, so the size is decided here and one call site can differ from another
     by overriding these two lines on its own selector. `flex:0 0 auto` because every current call
     site puts the icon in a flex row beside a text label, where the default `flex-shrink:1` would
     let a long label squeeze the glyph out of shape. This also has to restate width/height at all
     because @layer reset's `img,svg,video{max-width:100%;height:auto}` would otherwise collapse an
     <svg> that carries only a viewBox. */
  .vn-icon{display:block;flex:0 0 auto;width:28px;height:28px}

  /* ── Front page ────────────────────────────────────────────────────────────────────────────
     Section rhythm is taken straight off the approved mockups (dev/canvas/Main.dc.html at
     1440px, HomeSandMobile.dc.html at 390px). Each section owns its padding because the
     mockup deliberately pairs sections (e.g. formulas 72/24 then highlights 48/48 then
     gallery 24/72) rather than repeating one uniform block — the generic .vn-section (48px,
     used by the highlights band) stays the default for every other template.
     Mobile-first: the unprefixed values are the 390px mockup, the >=768px block is the
     1440px mockup. */
  .vn-eyebrow{font-size:12px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;color:var(--vn-accent);margin:0}
  .vn-home__title{font-size:var(--vn-step-3)}
  .vn-home__lead{color:var(--vn-muted);margin:0}

  /* Hero — full screen since task 16 (Joe picked the transparent-header variant A of
     dev/screenshots/hero-fullscreen-A.png). Two lines per breakpoint, `vh` first and `svh`
     second: `svh` is the SMALL viewport height, i.e. the height with the mobile browser's URL bar
     EXPANDED, so the hero never grows taller than the first screen and then shrinks under the
     visitor as the bar retracts. A browser that does not know the unit drops the second
     declaration and keeps the `vh` line, which is the pre-2022 behaviour (correct on desktop,
     slightly too tall on a phone) rather than no height at all.
     `min-height`, not `height`: on a short landscape phone the copy is taller than the viewport
     and must push the section open instead of being clipped by the overflow:hidden below.
     Desktop subtracts the top bar so hero + bar together are exactly one screen; below 1024px
     the bar is display:none and the hero owns the whole viewport. */
  .vn-hero{position:relative;min-height:100vh;min-height:100svh;display:flex;align-items:flex-end;overflow:hidden}
  /* top/left rather than `inset:0`: the motion block further down re-points the top edge and the
     height (the photo's box is taller than the hero so the parallax lag has something to slide),
     and inset's own `bottom:0` would be a third, over-constraining value on the same box. */
  /* object-position 60% 45%: on a phone the 1600x1198 photo is HEIGHT-constrained in a 390x844
     box (`cover` scales by max(390/1600, 844/1198) = 0.70), so the whole frame is visible top to
     bottom and only the horizontal 60% actually crops — it drops the empty field on the left and
     keeps the villa. The 45% is what bites on a tablet or a short landscape window, where there
     IS vertical slack: it favours the house over the sky. The >=1024px parallax box overrides
     both (see the motion block). */
  .vn-hero__img{display:block;position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;object-position:60% 45%;z-index:0}
  /* Below 1024px the hero is a PORTRAIT box (390x844 on a phone), so `cover` scales the 4:3 aerial
     to fit its height and the entire frame is on screen top to bottom — a quarter of it sky. No
     object-position can fix that: with the photo exactly as tall as its box there is no vertical
     slack to slide, which is why the 45% above only bites on a tablet or a short landscape window.
     What does work is giving the photo's own box 18% more height than the hero and pulling it up by
     the same 18%, so its bottom edge still lands on the hero's: the hero's overflow:hidden then
     clips the top of the photo instead of the browser fitting all of it in.
     Measured at 390x844 — sky drops from about 25% of the hero to about 10%, the horizontal window
     is unchanged (still centred by the 60%), and the villa keeps its whole roofline. Joe's note on
     the approved mock, dev/screenshots/hero-fullscreen-A.png: prefer the house over the sky.
     Only below 1024px: from there up the motion block owns this box (a 130px-taller frame for the
     parallax) and the hero is landscape again, where the photo already fits by width. */
  @media (max-width:1023.98px){
    .vn-hero__img{top:-18%;height:118%}
  }
  /* Corner wash — on `.vn-hero-wash`, a class BOTH heroes carry (the front page's .vn-hero and
     page-team.php's .vn-page-hero), because the two need exactly the same treatment for exactly
     the same reason and a second copy of a two-gradient rule is a second thing to keep in step.
     Everything else about the two heroes differs (height, photo framing, the parallax that is
     bound to .vn-hero alone), so only this is shared.
     Joe picked option A off dev/screenshots/hero-options.png (2026-09-08); the
     frosted-panel options were rejected because a panel covers the villa, which is the one thing
     the photo is there to show. The problem it solves is real and measurable: the white h1 sits on
     lawn, road and roof edges whose mid-tones run right through the letterforms — sampled on
     dev/screenshots/hero-current.png at (140,420) the pixel under the headline is srgb(72,82,39),
     a mid olive.
     Two gradients in one box, which is why this is a pseudo-element and not another <div>:
       - `background` darkens from the LEFT edge and is fully gone by 68% of the width, so the lake
         (top left, above the mask) and the villa/right-hand houses keep their own contrast;
       - `mask-image` fades the whole thing IN from the top, so the sky and the lake at the top of
         the frame are untouched and the wash only exists where the copy is.
     Both are needed together: the horizontal gradient alone would veil the top-left lake, the
     vertical one alone would be the full-width band this deliberately is not (at >=860px).
     -webkit-mask-image is kept beside the unprefixed property for Safari < 15.4, which shipped
     masks prefixed only. z-index 1 puts it above .vn-hero__img (0) and below the copy (2).
     COLOUR (task 22): `--vn-wash` is set inline on the hero section by inc/media.php, sampled from
     the bottom-left of that hero's own photograph and darkened to 12% lightness with its hue kept
     (Joe approved the "derived" column of dev/screenshots/wash-options.png, 2026-09-08). So the
     wash reads as the picture going into shadow rather than as a grey film. The `18 28 34` in every
     var() below is the charcoal this used to be, and is what renders when no colour could be
     derived — a missing file, a PNG, GD switched off.
     REACH (2026-09-08, Joe: "it goes too far into the photo"). Every edge was pulled back by
     roughly a tenth of the hero and NOT a single alpha was touched — the wash is as dark where it
     starts as it ever was, it simply stops sooner: horizontally 30/55/78% -> 20/50/68%, and the
     mask's fade-in 22/45% -> 32/55% so it begins lower down the frame. Reducing alpha instead
     would have been the wrong lever: the contrast under the h1 is the whole reason the wash is
     here.
     The MIDDLE stop is 50%, not the 45% the other two implied. Measured on `/` at 1440x900 by
     sampling the pixel 4px under the tail of the second title line — the site's contrast floor,
     44% across the viewport — a straight 20/45/68 came out at 4.98:1 against the white type where
     the old 30/55/78 gives 6.91:1 at that same pixel; holding the middle stop at 50% recovers it
     to 5.63:1. That pixel is still under the 7:1 target and was ALREADY marginal before this
     change (see the task-23 wash note in the SDD report) — it is a property of where the corner
     wash fades out, not of the colour, and the fix is to shorten the headline or move the copy
     left, not to darken the wash further over the villa. */
  .vn-hero-wash::before{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
    background:linear-gradient(90deg,rgb(var(--vn-wash, 18 28 34)/.92) 0%,rgb(var(--vn-wash, 18 28 34)/.84) 20%,rgb(var(--vn-wash, 18 28 34)/.5) 50%,rgb(var(--vn-wash, 18 28 34)/0) 68%);
    -webkit-mask-image:linear-gradient(to bottom,rgb(0 0 0/0) 0%,rgb(0 0 0/.5) 32%,#000 55%);
    mask-image:linear-gradient(to bottom,rgb(0 0 0/0) 0%,rgb(0 0 0/.5) 32%,#000 55%)}
  /* Below 860px the copy spans the full width of the hero, so a wash that runs out at 68% would
     leave the last words of every line on bare photo. There the corner becomes a plain bottom-up
     band and the vertical mask is dropped — the band's own gradient already does the fading, and
     masking it a second time would wash it out. 860px, not the 768px used elsewhere in this file:
     the breakpoint is set by where the h1 stops fitting the left column, not by the section grid.
     860-1023px keeps the desktop corner values. The band was pulled back with the rest: its
     mid-point 50% -> 40% and it is fully transparent by 90% rather than only at the very top, so
     the sky above the copy is the photograph again. */
  @media (max-width:859.98px){
    .vn-hero-wash::before{background:linear-gradient(to top,rgb(var(--vn-wash, 18 28 34)/.86) 0%,rgb(var(--vn-wash, 18 28 34)/.6) 40%,rgb(var(--vn-wash, 18 28 34)/0) 90%);-webkit-mask-image:none;mask-image:none}
  }
  /* Header legibility scrim (task 16). The corner wash above fades IN from the top, i.e. it
     deliberately leaves the top of the frame untouched — which is exactly where the header now
     sits, in white, over sky one moment and over a dark treeline the next. This is a second,
     independent 180px band at the top that only has to carry the header; it is short enough that
     it never reaches the wash's own 32% fade-in, so the two do not compound into a full-height
     veil. Same z-index as the wash (1): they never overlap, and both stay under the copy (2).
     inset shorthand `0 0 auto 0` = top/left/right pinned, bottom auto, so `height` decides.
     220px -> 180px in the same 2026-09-08 pull-back as the wash above; the stops are untouched, so
     the header sits on exactly the density it did, over less of the photograph. */
  .vn-hero::after{content:"";position:absolute;inset:0 0 auto 0;height:180px;z-index:1;pointer-events:none;
    background:linear-gradient(to bottom,rgb(var(--vn-wash, 18 28 34)/.62),rgb(var(--vn-wash, 18 28 34)/0))}
  /* Bottom padding is a clamp (not a space token) so the hero buttons float clear of the
     section's bottom edge at every width — 40px floor, 88px ceiling, and the middle term is `7vh`
     rather than a `vw`: the hero is a viewport-HEIGHT box now, so what has to scale with it is the
     viewport's height. (Joe's design-review feedback: buttons sat flush on the edge.) The gradient
     background is painted on this same box, so it automatically grows to keep covering the
     text/buttons. That gradient is much lighter than it was (.45 fading out at 60%, was .72 → .1
     over the whole box): the corner wash above carries the contrast for the copy, so this one only
     has to stop the buttons floating on nothing. Left as it was it would double up with the wash
     and grey out the bottom of the villa. The 70% -> 60% end point is part of the 2026-09-08
     pull-back; the .45 it starts at is deliberately unchanged. */
  .vn-hero__overlay{position:relative;z-index:2;width:100%;padding-block:var(--vn-space-7) clamp(40px, 7vh, 88px);background:linear-gradient(to top,rgb(var(--vn-wash, 18 28 34) / .45),rgb(var(--vn-wash, 18 28 34) / 0) 60%)}
  .vn-hero__content{display:flex;flex-direction:column;gap:14px;color:#fff}
  .vn-hero__eyebrow{color:#fff;opacity:.9;letter-spacing:.14em}
  .vn-hero__content h1{color:#fff;line-height:1.08;margin:0}
  .vn-hero__sub{margin:0;opacity:.95}
  .vn-hero__actions{display:flex;flex-direction:column;gap:var(--vn-space-3);margin-block-start:4px}
  .vn-hero__actions .vn-btn{min-height:48px;padding:0 20px;justify-content:center}
  /* The sticky mobile enquiry bar is fixed to the bottom of the viewport, and the hero is now
     exactly one viewport tall — so without this the second hero button sits UNDER the bar, on the
     one screen every visitor sees first. Reserve the bar's 72px on the hero itself: it is a flex
     column with align-items:flex-end, so bottom padding lifts the copy clear instead of scrolling
     it. Keyed on the same `has-cta-bar` body class the footer reserve uses (inc/setup.php mirrors
     villanatica_show_cta_bar(), so pages without a bar get no dead space) and cut off at the same
     1024px where the bar itself is hidden. On .vn-hero and not on body for the reason spelled out
     at the footer reserve below: theme.json emits an UNLAYERED body{padding:0} that beats any
     layered body rule regardless of specificity. */
  @media (max-width:1023.98px){body.has-cta-bar .vn-hero{padding-bottom:72px}}

  /* Intro — an editorial photo pair beside the copy (Joe, 2026-09-08: "ok for the pair", the TOP
     panel of dev/screenshots/intro-options.png). It replaces one centred 70ch column of text on
     bare sand, which is why the old `max-width:70ch` and the centring are gone: the section is
     back to the full 1200px .vn-container and the measure is now set by .vn-intro__text's own
     52ch, in the body font, on the one element that needs it.
     Mobile first as everywhere in this file, but the breakpoint is 1024px rather than the 768px
     the rest of the front page uses — the same kind of reasoning as the hero's 860px: it is set
     by where two columns each still hold a readable line beside a 520px photo, not by the section
     grid. From 768 to 1023px the pair simply stacks above the text, which is the approved phone
     panel at a wider width. */
  .vn-intro{display:grid;grid-template-columns:1fr;gap:36px;align-items:center;padding-block:48px}
  /* The media column is ONE positioned box, not a two-cell grid: the small photo overlaps the main
     photo's lower-right corner and hangs outside the box, which no grid track can express. Two
     consequences worth knowing before editing it:
       - both photos are absolutely positioned, so nothing inside gives the box a height —
         `aspect-ratio` is what does, and it is also what stops the section reflowing as the two
         images decode;
       - the right margin reserves exactly the distance the small photo hangs out by (36px here,
         and the desktop block below swaps the 48px it uses there for a `max-width` that leaves
         the same slack inside the grid column). Without it the overhang is horizontal PAGE
         overflow on a phone, i.e. a scrollbar on the whole site. */
  .vn-intro__media{position:relative;aspect-ratio:4/3;margin-inline:0 36px}
  .vn-intro__photo{display:block;border-radius:var(--vn-radius);object-fit:cover}
  .vn-intro__photo--main{position:absolute;inset:0;width:100%;height:100%}
  /* The 6px border is the page background, not a colour of its own: it is what separates the two
     photographs where they overlap, and it is why the small photo needs a slightly larger radius
     than the main one. `rgb(31 42 51/.18)` is --vn-text's own colour spelled out in channels —
     there is no --vn-shadow token, and box-shadow cannot take the hex through a var() and add an
     alpha to it. If a shadow token is ever added, this is one of the two places to change. */
  .vn-intro__photo--small{position:absolute;inset:auto -36px -28px auto;width:46%;height:52%;border:6px solid var(--vn-bg);border-radius:10px;box-shadow:0 18px 40px rgb(31 42 51/.18)}
  .vn-intro__text{display:flex;flex-direction:column;gap:20px}
  .vn-intro__rule{display:block;width:56px;height:2px;background:var(--vn-accent)}
  /* Not var(--vn-step-3) like the other section headings: this h2 is the page's editorial opening
     and carries the pair, so it runs one step larger than the rest of the front page. */
  .vn-intro__title{font-size:32px;line-height:1.08;margin:0}
  .vn-intro__lead{font-size:var(--vn-step-1);color:var(--vn-text);margin:0}
  .vn-intro__body{color:var(--vn-muted);margin:0}
  @media (min-width:1024px){
    .vn-intro{grid-template-columns:1fr 1fr;gap:64px;padding-block:72px}
    /* 520px inside a (1200-64)/2 = 568px column leaves 48px of slack to the right — exactly what
       the small photo hangs out by below, so its right edge lands on the column edge and never
       reaches the text column. Change one of the three and re-measure the other two. */
    .vn-intro__media{aspect-ratio:4/5;max-width:520px;margin-inline:0}
    .vn-intro__photo--small{inset:auto -48px -40px auto;width:52%;height:46%}
    .vn-intro__text{max-width:52ch}
    .vn-intro__title{font-size:clamp(34px,3.4vw,50px)}
  }

  /* Formulas */
  .vn-formulas{padding-block:40px 8px;display:flex;flex-direction:column;gap:18px}
  /* Single stacked block at every width — heading, then lead, left-aligned (task 12 fix round 1,
     owner request). Previously this split row/space-between at >=768px, heading left / lead right,
     which read as "heading belongs to the left card, lead to the right card" since the two cards
     sit directly beneath it. align-items:flex-start (rather than the default stretch) is what
     keeps the lead at its own 60ch measure instead of stretching to the section's full width — see
     .vn-region__body for the same pattern. */
  .vn-formulas__head{display:flex;flex-direction:column;gap:12px;align-items:flex-start}
  .vn-formulas__head .vn-home__lead{max-width:60ch}
  .vn-formulas__grid{display:grid;grid-template-columns:1fr;gap:16px}
  .vn-formula-card{display:flex;flex-direction:column;background:var(--vn-surface);border:1px solid var(--vn-line);border-radius:var(--vn-radius);overflow:hidden;text-decoration:none;color:var(--vn-text);transition:transform .15s ease,box-shadow .15s ease}
  .vn-formula-card:hover,.vn-formula-card:focus-visible{transform:translateY(-2px);box-shadow:0 8px 24px rgb(0 0 0 / .1);color:var(--vn-text)}
  .vn-formula-card__img{display:block;width:100%;height:180px;object-fit:cover}
  .vn-formula-card__body{display:flex;flex-direction:column;gap:8px;padding:22px 24px 24px}
  .vn-formula-card h3{font-size:28px;font-weight:600;line-height:1.15;margin:0}
  .vn-formula-card__facts{margin:0;color:var(--vn-muted);font-size:15px}
  .vn-formula-card__go{display:flex;justify-content:flex-end;color:var(--vn-accent);margin-block-start:6px}
  /* One text link under the two cards, to the team-building page (task 14). Not a third card —
     see the note in template-parts/home/formulas.php. Centred under the two-card grid since task
     16 (Joe): left-aligned it read as a caption belonging to the left card. `margin:0` stays —
     the spacing above comes from .vn-formulas' own flex `gap`, not from a margin, so there is no
     margin-top to preserve and none to add.
     inline-BLOCK, not the inline-flex it was: as a flex container the arrow was a second flex item
     of its own, so at 390px the sentence wrapped to two lines and the arrow stayed pinned to the
     right edge, vertically centred against both of them, visibly detached from the last word. As
     an inline-block the arrow is just the last thing in the text flow and the `&nbsp;` in front of
     it (template-parts/home/formulas.php) is what keeps it on the same line as the last word — a
     translation-safe glue, since the four .po strings differ in what that last word is.
     The 44px tap target moves from `min-height` (which a flex container needed) to padding: 10px
     top and bottom on a 1.6 line-height 16px line is 45.6px of hit area, and on the two-line phone
     layout it is far more. The box shrink-wraps its text, so the target is still the link and not
     the full width of the flex column. */
  .vn-formulas__more{margin:0;text-align:center}
  .vn-formulas__more a{display:inline-block;padding-block:10px;color:var(--vn-accent);font-weight:600;text-decoration:none}
  .vn-formulas__more a:hover{color:var(--vn-accent-hover);text-decoration:underline}
  /* -3px, not `middle`: the arrow is an 18px box on a ~26px line, and `middle` aligns it to the
     x-height midpoint, which sits noticeably above the stroke of the arrow itself. */
  .vn-formulas__more svg{vertical-align:-3px}

  /* Highlights (uses the generic .vn-section — 48px, exactly the mockup's value) */
  .vn-highlights__grid{display:grid;grid-template-columns:1fr;gap:22px}
  .vn-highlight{display:flex;gap:14px;align-items:flex-start}
  .vn-highlight__icon{color:var(--vn-accent);flex:0 0 auto;display:inline-flex;padding-block-start:2px}
  .vn-highlight__text{display:flex;flex-direction:column;gap:4px}
  .vn-highlight__title{font-weight:600;font-size:17px;color:var(--vn-text)}
  .vn-highlight__desc{color:var(--vn-muted);font-size:15px}

  /* ── Amenity strip (task 19) ───────────────────────────────────────────────────────────────
     Sits directly under the highlights band and shares its section rhythm — both use the generic
     .vn-section (48px padding-block), so the pair reads as one block rather than as two sections
     with different spacing. There is no mockup for this section; the shape is the brief's.

     Its own media queries live here rather than in the shared >=768/>=1024 blocks further down,
     the same way .vn-intro keeps its 1024px rules next to the ones they override: the list swaps
     display mode (grid -> flex) between breakpoints, and a reader who cannot see all three states
     in one place will eventually change one of them alone.

     Three states, mobile-first:
       phone   2-column grid — long labels wrap to 2-4 lines in a ~163px column at 390px, which is
               why the items are top-aligned rather than centred;
       >=768   3 columns, so a tablet does not stretch two columns across 700px;
       >=1024  the strip proper: a wrapping flex row where each item is only as wide as its own
               label, gap 12px 28px.
     The -2px on the icon is optical, not structural: the glyph box is 28px against a 24px line
     box, so top-aligning the two leaves the icon's centre 2px below the first line's. */
  .vn-amenities__title{font-size:var(--vn-step-2);margin-block-end:var(--vn-space-5)}
  .vn-amenities__list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 16px}
  .vn-amenities__item,.vn-amenities__link{display:flex;align-items:flex-start;gap:10px}
  .vn-amenities__item{font-size:15px;color:var(--vn-muted)}
  .vn-amenities__item .vn-icon{color:var(--vn-accent);margin-block-start:-2px}
  /* The dog item is a link and must still look like its thirteen neighbours until it is hovered
     — @layer base's `a{color:var(--vn-accent)}` would otherwise single it out in teal. */
  .vn-amenities__link{color:inherit;text-decoration:none}
  .vn-amenities__link:hover{color:var(--vn-accent-hover);text-decoration:underline}
  @media (min-width:768px){
    .vn-amenities__list{grid-template-columns:repeat(3,minmax(0,1fr));gap:14px 28px}
  }
  @media (min-width:1024px){
    .vn-amenities__list{display:flex;flex-wrap:wrap;gap:12px 28px}
  }

  /* Gallery */
  .vn-gallery{padding-block:8px 40px;display:flex;flex-direction:column;gap:16px}
  .vn-gallery__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
  /* The tile exists to CLIP: the hover in the motion block scales the photo inside it, and a bare
     grid <img> has nothing to be clipped by — it would simply spill over its neighbours. It also
     keeps the grid's direct children countable for the reveal stagger (6 tiles, not 6 images plus
     whatever a future caption adds). Since task 29 the tile is also the lightbox LINK, hence the
     zoom-in cursor: the click enlarges the photo rather than navigating away. */
  .vn-gallery__tile{display:block;overflow:hidden;border-radius:var(--vn-radius);cursor:zoom-in}
  .vn-gallery__grid img{display:block;width:100%;height:130px;object-fit:cover;border-radius:var(--vn-radius)}
  /* Nine tiles in two columns leave one alone on the last row; below 768px it takes the full width
     instead of sitting half-empty (the >=768px block resets it for the 3-column grid). */
  .vn-gallery__tile:last-child:nth-child(odd){grid-column:1 / -1}

  /* Availability */
  .vn-availability{background:var(--vn-surface);border-block:1px solid var(--vn-line)}
  .vn-availability__inner{padding-block:40px;display:grid;grid-template-columns:1fr;gap:28px;align-items:start}
  .vn-availability__copy{display:flex;flex-direction:column;gap:16px;align-items:flex-start}
  .vn-availability__legend{display:flex;gap:18px;align-items:center;color:var(--vn-muted);font-size:14px}
  .vn-availability__legend span{display:inline-flex;align-items:center;gap:8px}
  /* 6px, matching the radius the calendar cells themselves now carry in the skin (task 15 §E). */
  .vn-availability__swatch{width:14px;height:14px;border-radius:6px;display:inline-block}
  /* A legend whose swatches don't match the grid right next to it is worse than no legend at all,
     so these MUST equal what the calendar actually paints. Until 2026-09-08 that meant hard-coded
     #00D025 / #FF5500, because uploads/css/skins/multidays.css (the skin, shared with production)
     still carried Booking Calendar's stock colours. That skin was repainted to the design palette
     in the same change, so the two now agree on --vn-avail / --vn-booked and the tokens can be
     used directly. Repainted again on 2026-09-08 to Joe's chosen "Deep" palette V3 — available
     #3E9C56, booked #C9552A (pending unchanged at #E5B84B) — in tokens.css AND in the skin, in
     lockstep. Re-check against the skin if it is ever restyled again: it is a plugin-scoped file
     in uploads, not part of this theme, and it is shared with production. */
  .vn-availability__swatch--free{background:var(--vn-avail)}
  .vn-availability__swatch--busy{background:var(--vn-booked)}
  /* The calendar is plugin markup (Booking Calendar 11.7). Only ever constrain it — never
     restyle its internals from here, or the shared uploads/css/skins/multidays.css skin and this
     file start fighting. (The plugin's duplicate legend is suppressed in
     template-parts/home/availability.php, NOT here — a layered display:none loses to the
     plugin's unlayered CSS; the reasoning is written out in that file.) */
  .vn-availability__calendar{min-width:0;max-width:100%;overflow-x:auto}
  /* Shown only when the bookingcalendar shortcode is not registered (plugin deactivated, or a
     future build renamed it) — see the guard in template-parts/home/availability.php. Keeps the
     two-column section balanced instead of leaving an empty right-hand column. */
  .vn-availability__fallback{margin:0;color:var(--vn-muted)}

  /* Testimonials */
  .vn-testimonials{padding-block:40px;display:flex;flex-direction:column;gap:18px}
  .vn-testimonials__grid{display:grid;grid-template-columns:1fr;gap:14px}
  .vn-testimonial{display:flex;flex-direction:column;gap:12px;margin:0;padding:24px;background:var(--vn-surface);border:1px solid var(--vn-line);border-radius:var(--vn-radius)}
  .vn-testimonial__stars{display:flex;gap:2px;color:var(--vn-booked)}
  .vn-testimonial blockquote{margin:0;font-family:var(--vn-font-display);font-size:20px;line-height:1.4;color:var(--vn-text)}
  .vn-testimonial blockquote::before{content:"\201C"} .vn-testimonial blockquote::after{content:"\201D"}
  .vn-testimonial figcaption{color:var(--vn-muted);font-size:14px;font-weight:600}

  /* Region */
  .vn-region{padding-block:0 40px;display:grid;grid-template-columns:1fr;gap:16px;align-items:center}
  /* The photo moved into a frame (see template-parts/home/region.php) so the motion block can give
     it a box 40px taller than what is on screen and slide it inside — the frame owns the height,
     the radius and the clipping, the image just fills it. Without the frame a parallax translate
     would move the visible edges of the photo instead of its content. */
  .vn-region__frame{position:relative;overflow:hidden;height:220px;border-radius:var(--vn-radius)}
  .vn-region__img{display:block;position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover}
  .vn-region__body{display:flex;flex-direction:column;gap:16px;align-items:flex-start}
  .vn-btn--quiet{background:var(--vn-surface);border:1px solid var(--vn-line);color:var(--vn-text)}
  .vn-btn--quiet:hover{background:var(--vn-bg);color:var(--vn-text)}

  /* Enquiry band */
  .vn-enquiry-band{background:var(--vn-accent);color:#fff}
  .vn-enquiry-band__inner{padding-block:40px;display:flex;flex-direction:column;gap:12px;align-items:flex-start}
  .vn-enquiry-band__copy{display:flex;flex-direction:column;gap:8px}
  .vn-enquiry-band__title{font-family:var(--vn-font-display);font-size:30px;font-weight:500;line-height:1.1}
  .vn-enquiry-band__sub{opacity:.9}
  .vn-btn--onaccent{background:#fff;border-color:#fff;color:var(--vn-accent);flex:0 0 auto}
  .vn-btn--onaccent:hover{background:var(--vn-bg);color:var(--vn-accent-hover)}
  .vn-enquiry-band :focus-visible{outline-color:#fff}

  @media (min-width:768px){
    /* No .vn-hero height here any more — it is `100svh` from the base rule up to 1024px and
       `100svh - the top bar` above it (the block right after this one). The old fixed 620px was
       what the pre-task-16 design used. */
    .vn-hero__content{gap:20px}
    .vn-hero__eyebrow{font-size:14px}
    .vn-hero__content h1{max-width:820px;line-height:1.05}
    .vn-hero__sub{font-size:20px;max-width:640px}
    .vn-hero__actions{flex-direction:row;flex-wrap:wrap;gap:12px;margin-block-start:6px}

    /* No .vn-intro rule here on purpose — that section breaks at 1024px and owns its own media
       query above, next to the rules it overrides. */
    .vn-formulas{padding-block:72px 24px;gap:28px}
    .vn-formulas__grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:24px}
    .vn-formula-card__img{height:220px}

    .vn-highlights__grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:32px}

    .vn-gallery{padding-block:24px 72px;gap:24px}
    .vn-gallery__grid{grid-template-columns:repeat(3,minmax(0,1fr))}
    .vn-gallery__tile:last-child:nth-child(odd){grid-column:auto}
    .vn-gallery__grid img{height:250px}

    .vn-availability__inner{padding-block:64px;grid-template-columns:repeat(2,minmax(0,1fr));gap:48px}

    .vn-testimonials{padding-block:72px;gap:28px}
    .vn-testimonials__grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:24px}

    .vn-region{padding-block:0 72px;grid-template-columns:repeat(2,minmax(0,1fr));gap:48px}
    .vn-region__frame{height:400px}

    .vn-enquiry-band__inner{padding-block:56px;flex-direction:row;justify-content:space-between;align-items:center;gap:32px}
    .vn-enquiry-band__title{font-size:36px}
  }

  /* From 1024px the top bar exists (40px, --vn-topbar-h) and the header floats over the photo, so
     "full screen" means the bar plus the hero, not the hero alone — subtract the token or the
     first screen is 40px too tall and the page starts with a scrollbar it does not need. Same
     vh-then-svh pair as the base rule. */
  @media (min-width:1024px){
    .vn-hero{min-height:calc(100vh - var(--vn-topbar-h));min-height:calc(100svh - var(--vn-topbar-h))}
  }

  /* ── Sub-page hero (template-parts/page-hero.php) ──────────────────────────────────────────
     A shorter, quieter version of the front-page hero: 300px on a phone, 420px from 768px up.
     Since task 16 it is the hero of EVERY page and post that has a featured image, not just the
     team page — page.php, index.php and page-team.php all render the same template part.
     It shares exactly ONE thing with .vn-hero — the corner wash, through the `vn-hero-wash` class
     both sections carry — and deliberately nothing else. In particular it must not reuse
     .vn-hero__img: the motion block below gives THAT class a box 130px taller than its hero and
     slides it with a --vn-py that only motion.js's `.vn-hero` lookup ever writes, so on a page
     with no `.vn-hero` the photo would silently be re-cropped by 130px and then never move.
     `center 45%` is the generic framing: the featured images are ordinary landscape photographs
     of the house, the lake and the region, `cover` in a 1440x420 box throws away most of their
     height, and a little above centre is where the subject of such a photo usually sits (sky
     above, foreground below). Individual pages that need a different crop get a modifier class —
     see .vn-page-hero--team right below, the one framing that was tuned by hand. */
  .vn-page-hero{position:relative;min-height:300px;display:flex;align-items:flex-end;overflow:hidden}
  .vn-page-hero__img{display:block;position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;object-position:center 45%;z-index:0}
  /* The team page's own photo (assets/img/home/meeting-setup, not a featured image), framed by
     hand: `cover` on that 4:3 shot in a 1440x420 box crops ~61% of the height away. Task 14 set
     55% for the first meeting photo; the 2026-09-15 version (new chairs, the presenter's laptop
     mirrored on the TV) puts the story higher in the frame — TV 26-33%, flip chart 27-49%, the
     laptop on its stand 41-54% — and at 55% the band started below the TV and cut the flip chart
     in half. 40% keeps all three plus the front of the table with the laptops; 34% only traded
     table for ceiling lamps (compared at 1440 and 1024, dev/screenshots/task31-team-hero-1440.png).
     The 60% horizontal only bites on a phone, where the box is barely wider than the photo — there
     it drops the kitchen on the left first, the least interesting corner for this page, and the
     vertical value makes no difference at all. */
  .vn-page-hero--team .vn-page-hero__img{object-position:60% 40%}
  /* Privacy / Algemene voorwaarden / Impressum. They get a hero like every other page — dropping
     it there would make them look broken rather than restrained — but a legal text does not earn
     420px of photograph, so the band is shorter. Keyed on a body class fed by
     villanatica_legal_pages() (inc/setup.php), never on `body.page-id-N`: the ids differ per
     language and per environment, and that list is already the single place they are written
     down. One rule at (0,2,1) beats both the base and the >=768px height regardless of order. */
  body.has-short-hero .vn-page-hero{min-height:280px}
  /* THE PLATE (2026-10-03, Joe: "a semi-transparent background just on the title text and leave
     the rest of the image in original form"). Until then this hero carried the front page's corner
     wash plus a bottom-up gradient on the overlay; on a 420px band the two together veiled most of
     the photograph. Now nothing is painted over the picture except one box that hugs the copy
     (`width:fit-content`), in the photograph's own shadow colour (--vn-wash, inc/media.php).
     .72 is the contrast floor, not a taste value: over a WHITE wall it leaves white type at about
     6.5:1; lower it and the subline on the team page drops under 4.5:1. The blur is light on
     purpose — it calms edges running through the letterforms without turning the plate into a
     frosted panel. The front-page hero keeps its wash: its copy is five lines and two buttons, and
     a plate that size would cover the villa. */
  .vn-page-hero__overlay{position:relative;z-index:2;width:100%;padding-block:var(--vn-space-7) clamp(20px, 3.5vw, 44px)}
  .vn-page-hero__plate{display:flex;flex-direction:column;gap:14px;width:fit-content;max-width:100%;padding:4px 24px;color:#fff;background:rgb(var(--vn-wash, 18 28 34) / .72);border-radius:var(--vn-radius);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}
  .vn-page-hero__plate h1{color:#fff;line-height:1.08;margin:0}
  /* A plate that carries more than the title (a subline, buttons: the team page) needs room to
     breathe — at 4px the buttons sit on the plate's bottom edge. `:has()` keeps the title-only
     plate at the tight 4px Joe asked for (2026-10-04). */
  .vn-page-hero__plate:has(.vn-page-hero__sub,.vn-page-hero__actions){padding-block:16px}
  .vn-page-hero__sub{margin:0;opacity:.95}
  /* 3D tour call to action at the top of the house page (inc/tour.php, 2026-10-04): the button and
     one quiet line beside it; stacked on a phone, where the button takes the full width like the
     other primary buttons. `!important`-free: .vn-prose a colours links, .vn-btn--primary is more
     specific through the two classes on the same element. */
  /* News list teasers and pagination (index.php, 2026-10-04). */
  .vn-post-teaser__date{margin-block:-8px 8px;color:var(--vn-muted);font-size:var(--vn-step--1)}
  .vn-post-teaser__more{font-weight:600}
  .vn-prose h2>a,.vn-prose h1>a{color:inherit;text-decoration:none}
  .vn-prose h2>a:hover,.vn-prose h1>a:hover{color:var(--vn-accent);text-decoration:underline}
  .vn-pagination .nav-links{display:flex;flex-wrap:wrap;gap:8px;margin-block:var(--vn-space-5) var(--vn-space-7)}
  .vn-pagination .page-numbers{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;padding:0 12px;border:1px solid var(--vn-line);border-radius:var(--vn-radius);text-decoration:none;color:var(--vn-text);background:var(--vn-surface)}
  .vn-pagination .page-numbers.current{background:var(--vn-accent);border-color:var(--vn-accent);color:#fff}
  .vn-pagination .screen-reader-text,.vn-prose .screen-reader-text{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
  .vn-tour-cta{display:flex;flex-direction:column;align-items:stretch;gap:8px;margin-block:0 var(--vn-space-6)}
  .vn-prose .vn-tour-cta .vn-btn{justify-content:center;min-height:48px;color:#fff;text-decoration:none}
  .vn-tour-cta__sub{color:var(--vn-muted);font-size:var(--vn-step--1)}
  @media (min-width:640px){.vn-tour-cta{flex-direction:row;align-items:center;gap:16px}}
  .vn-page-hero__actions{display:flex;flex-direction:column;gap:var(--vn-space-3);margin-block-start:4px}
  .vn-page-hero__actions .vn-btn{min-height:48px;padding:0 20px;justify-content:center}
  @media (min-width:768px){
    .vn-page-hero{min-height:420px}
    .vn-page-hero__plate{gap:20px}
    .vn-page-hero__plate h1{max-width:820px;line-height:1.05}
    .vn-page-hero__sub{font-size:20px;max-width:640px}
    .vn-page-hero__actions{flex-direction:row;flex-wrap:wrap;gap:12px;margin-block-start:6px}
  }

  /* ── Editorial page body (.vn-prose--wide) and the three block styles it adds ───────────────
     .vn-prose caps every top-level block at 76ch, which is right for a page that IS one column of
     text. The team page is not: two of its four sections are card grids that want the container's
     full 1200px. So the cap moves off the container and onto the elements that still need a
     measure — 70ch for the running text and the bulleted lists, none for the card grids.
     Written as `.vn-prose.vn-prose--wide>…` rather than `.vn-prose--wide>…`: the latter ties on
     specificity with .vn-prose's own `>` rules further up and would win on source order alone,
     which is exactly the kind of dependency that breaks when a stylesheet is reordered. */
  .vn-prose.vn-prose--wide>h1,.vn-prose.vn-prose--wide>h2,.vn-prose.vn-prose--wide>h3,
  .vn-prose.vn-prose--wide>h4,.vn-prose.vn-prose--wide>p,
  .vn-prose.vn-prose--wide>ul.vn-list,.vn-prose.vn-prose--wide>ol{max-width:70ch}
  .vn-prose.vn-prose--wide>ul.vn-cards{max-width:none}

  /* `<ul class="vn-cards">` — a grid of cards written as a plain list in the editor, so the copy
     stays editable without a page builder and degrades to a readable list with no CSS at all.
     1 column below 640px, 2 below 1024, 3 above. */
  .vn-prose .vn-cards{display:grid;grid-template-columns:1fr;gap:16px;list-style:none;padding:0;margin-block:0 var(--vn-space-5)}
  .vn-prose .vn-cards>li{margin:0;padding:20px;background:var(--vn-surface);border:1px solid var(--vn-line);border-radius:var(--vn-radius)}
  /* The card title is the <strong> the editor writes first inside the <li>: display:block is what
     puts the rest of the item on its own line, so a card without a photo needs no wrapper element.
     A card WITH one does (see .vn-card__body below), and the second selector is what keeps the two
     kinds identical apart from the image — without it the titles in the programme grid would fall
     back to the browser's inline bold in Source Sans while the facts grid above them stayed
     Cormorant. 600, not the browser's bold(700): Cormorant Garamond ships 500 and 600 here and 700
     would be synthesised. */
  .vn-prose .vn-cards>li>strong,
  .vn-prose .vn-cards>li>.vn-card__body>strong{display:block;font-family:var(--vn-font-display);font-weight:600;font-size:var(--vn-step-1);line-height:1.2;margin-block-end:6px}
  @media (min-width:640px){.vn-prose .vn-cards{grid-template-columns:repeat(2,minmax(0,1fr))}}
  @media (min-width:1024px){.vn-prose .vn-cards{grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}}

  /* ── Task 18: photographs on the team page ──────────────────────────────────────────────────
     Two separate things that happen to be photographs on one page.

     1. `.vn-photo-strip` — four theme photos of the house, printed by page-team.php after the
        first card grid. 2x2 on a phone, 4 across from 1024px, 3:2 crops. `aspect-ratio` +
        `object-fit:cover` rather than four re-exports: the sources are 1600x1200 (pool-green) and
        1600x900 (the other three), so a single ratio here is the only way to get one clean row.
        The `margin:0` and `border-radius:8px` on the img are not redundant — @layer components'
        legacy `.vn-prose img` rule further down sets display:inline-block and the theme's larger
        --vn-radius, and this grid is inside .vn-prose.

     2. `.vn-card__img` / `.vn-card__body` — a photo header on a `ul.vn-cards` card. Unlike (1)
        this markup lives in the PAGE CONTENT (dev/task18-team-photos.php put it there, in all four
        languages) so Catherine can swap a photo in the editor, which is why the padding has to
        move: the <li> loses its own 20px so the image can reach the card's edges, and the text
        that follows gets it back through the .vn-card__body wrapper the content now carries.
        `:has()` rather than a class on the <li> — the condition IS "this card has a photo", and
        keeping it out of the content means an editor deleting the image cannot leave a card
        claiming a padding it no longer needs. Cards without an image are untouched by all three
        rules and keep the plain 20px above.
        `overflow:hidden` clips the photo to the card's radius; `border-radius:0` on the image
        itself undoes the legacy `.vn-prose img` radius, which would otherwise round the photo
        INSIDE the already-rounded card and show the card's background in the two top corners. */
  .vn-photo-strip{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-block:0 var(--vn-space-5)}
  .vn-photo-strip img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;border-radius:8px;margin:0}
  /* Since 2026-09-15 each strip photo is a lightbox link (page-team.php); the link is the grid item. */
  .vn-photo-strip__item{display:block;border-radius:8px;cursor:zoom-in}
  @media (min-width:1024px){.vn-photo-strip{grid-template-columns:repeat(4,minmax(0,1fr))}}
  .vn-prose .vn-cards>li:has(>.vn-card__img){padding:0;overflow:hidden}
  .vn-prose .vn-cards>li>.vn-card__img{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;border-radius:0;margin:0}
  .vn-prose .vn-cards>li>.vn-card__body{padding:20px}

  /* `<ul class="vn-list">` — an ordinary bulleted list. It needs a rule at all because @layer
     base strips list-style and padding from EVERY classed list (`ul[class]`), and this layer is
     later, so it wins without needing extra specificity. */
  .vn-prose .vn-list{list-style:disc;padding-inline-start:var(--vn-space-5);margin-block:0 var(--vn-space-4)}
  .vn-prose .vn-list>li+li{margin-block-start:.35rem}

  /* `<p class="vn-note">` — the one caveat paragraph per page (the house rules on the team page).
     (0,2,0) so it beats .vn-prose p's own margin rule without depending on source order. */
  .vn-prose .vn-note{background:var(--vn-surface);border-inline-start:3px solid var(--vn-accent);border-radius:0 var(--vn-radius) var(--vn-radius) 0;padding:16px 20px;margin-block:0 var(--vn-space-5)}

  /* ── Team enquiry band + its Contact Form 7 form (page-team.php) ────────────────────────────
     Surface-coloured, not the front page's teal: that band is a CTA that sends you somewhere
     else, this one CONTAINS the form, and form fields on an accent background would need a whole
     second set of colours to stay readable.
     scroll-margin: the header is sticky (64px, 88px from 1024px up), so a jump to #aanvraag from
     the hero button would otherwise land the section's first line underneath it. */
  .vn-team-enquiry{background:var(--vn-surface);border-block:1px solid var(--vn-line);margin-block-start:var(--vn-space-7);scroll-margin-block-start:76px}
  .vn-team-enquiry__inner{padding-block:48px;display:flex;flex-direction:column;gap:24px}
  .vn-team-enquiry__copy{display:flex;flex-direction:column;gap:8px}
  .vn-team-enquiry__title{margin:0}
  .vn-team-enquiry__sub{margin:0;color:var(--vn-muted);max-width:60ch}
  .vn-team-enquiry__form{width:100%;max-width:900px}

  /* The fields. Layout and skin only — nothing here fights the plugin, and it could not: CF7's
     own styles.css is UNLAYERED, so per the cascade-layers spec it beats every rule in this file
     regardless of specificity. What it actually sets is `.wpcf7-form-control-wrap{position:
     relative}`, `.wpcf7-not-valid-tip{color:#dc3232;display:block}` and the response-output box's
     2px status-coloured border (blue/green/red/yellow). None of those collide with the properties
     below, and all three are left as they are on purpose: they are conventional, they work, and
     overriding them would take an !important out of a budget of five (see the .vn-btn--primary
     note at the top of this layer). */
  .vn-team-enquiry .vn-form__note{margin:0 0 20px;color:var(--vn-muted);font-size:var(--vn-step--1)}
  .vn-team-enquiry .vn-form__grid{display:grid;grid-template-columns:1fr;gap:16px}
  /* The <label> wraps its control (no `for`/`id` pair to keep in sync, and CF7 generates no ids
     of its own), so the label text is a bare text node and the control sits in CF7's own
     .wpcf7-form-control-wrap span — which is what gets the 6px, not the label.
     `.vn-form__row label`, NOT `>label`: wpcf7_autop wraps the label of each row in a <p> of its
     own (verified in the rendered markup on dev), so the label is a grandchild. That <p> then has
     to lose its margin, or the grid gap doubles. */
  .vn-team-enquiry .vn-form__row p{margin:0}
  .vn-team-enquiry .vn-form__row label{display:block;font-weight:600}
  .vn-team-enquiry .wpcf7-form-control-wrap{display:block;margin-block-start:6px}
  /* A <select> is as wide as its longest <option>. The contact pages' "aanvraag" select carries
     "Villa Natica S: 2-6 personen zonder zwembad/wellness" — 388px in Dutch, 404px in German — inside
     a 342px column on a 390px phone, so the page scrolled sideways (found by the launch rehearsal,
     2026-10-03; English and French happen to fit). Any Contact Form 7 control stops at its column. */
  .wpcf7 select,.wpcf7 input,.wpcf7 textarea{max-width:100%}
  .vn-team-enquiry input[type="text"],.vn-team-enquiry input[type="email"],
  .vn-team-enquiry input[type="tel"],.vn-team-enquiry input[type="number"],
  .vn-team-enquiry select,.vn-team-enquiry textarea{
    width:100%;min-height:44px;padding:10px 12px;font-weight:400;color:var(--vn-text);
    background:var(--vn-bg);border:1px solid var(--vn-line);border-radius:var(--vn-radius)}
  .vn-team-enquiry textarea{min-height:132px;resize:vertical}
  .vn-team-enquiry .vn-form__actions{margin:24px 0 0;display:flex;align-items:center;flex-wrap:wrap;gap:12px}
  .vn-team-enquiry input[type="submit"]{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:0 24px;border:1px solid var(--vn-accent);border-radius:var(--vn-radius);background:var(--vn-accent);color:#fff;font-family:var(--vn-font-body);font-weight:600;cursor:pointer}
  .vn-team-enquiry input[type="submit"]:hover{background:var(--vn-accent-hover);border-color:var(--vn-accent-hover)}
  .vn-team-enquiry .vn-form__legal{margin:16px 0 0;max-width:70ch;color:var(--vn-muted);font-size:var(--vn-step--1)}
  @media (min-width:768px){
    .vn-team-enquiry{scroll-margin-block-start:100px}
    .vn-team-enquiry__inner{padding-block:64px;gap:32px}
    .vn-team-enquiry .vn-form__grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}
    /* The long free-text fields and the two selects with long option labels run the full width;
       the short ones pair up. */
    .vn-team-enquiry .vn-form__row--wide{grid-column:1/-1}
  }

  /* ── Motion (task 12, the set Joe approved 2026-09-08) ─────────────────────────────────────
     Rules of engagement for everything in this block:
       1. transform/opacity only — and via the INDIVIDUAL `translate` / `scale` properties, never
          the `transform` shorthand. The hero photo carries a settle (scale) and a parallax lag
          (translate) at the same time; as one shorthand each would overwrite the other, as two
          properties they compose. Ease-out, <= 620ms, every reveal fires once.
       2. Every hidden or displaced state is scoped under `html.is-motion`, added by the inline
          <script> villanatica_motion_flag() (inc/setup.php) prints at the top of <head> — before
          this stylesheet is even fetched, so hidden is the FIRST painted state and an on-screen
          block can animate on load without a flash. Scripting off ⇒ nothing is hidden and the
          page is exactly the no-JS page. Nothing here may hide content on its own. The flip side
          of hiding that early is that a broken motion.js would hide content for good, which is
          what `html.is-reveal-all` at the end of this block exists for.
       3. `html.is-reduced` (added beside is-motion when prefers-reduced-motion matches) removes
          the position change and keeps the reveal — content appears rather than sliding. It
          works WITH @layer reset's global prefers-reduced-motion rule, which already zeroes every
          duration with !important, instead of adding !important of its own: the budget
          dev/check-dev.sh enforces stays at exactly 5.
       4. Nothing moves on the calendar, the booking form, the header, the footer, the off-canvas
          panel or the legal row — the parts people are trying to READ or operate. */

  /* Reveal on entry. A plain [data-vn-in] animates itself; one that also carries [data-vn-stagger]
     stays put and animates its direct children instead, each on the --vn-delay motion.js writes
     once at load. The `.is-in` class is added by an IntersectionObserver that then unobserves, so
     a section that has appeared never re-hides on the way back up. */
  html.is-motion [data-vn-in]{opacity:0;translate:0 14px;transition:opacity .62s var(--vn-ease),translate .62s var(--vn-ease)}
  html.is-motion [data-vn-in].is-in{opacity:1;translate:none}
  html.is-motion [data-vn-in][data-vn-stagger]{opacity:1;translate:none;transition:none}
  /* The reveal transition is declared as a VALUE (--vn-reveal) as well as applied, because
     `transition` is a single property: whatever wins the cascade for it replaces the whole list.
     This selector is (0,3,1) — three attribute/class components plus the html element — so it beat
     `.vn-formula-card`'s own (0,1,0) `transition:transform…,box-shadow…` permanently, for the whole
     life of the page and not just during the reveal. The card's hover lift therefore SNAPPED
     instead of easing (task 13 review, medium). A component that carries its own transition has to
     re-declare BOTH lists at matching-or-higher specificity; --vn-reveal is what makes that a
     one-token copy instead of a hand-duplicated bezier.
     --vn-delay-base is a per-block head start added to every child's own --vn-delay, so a block
     can be told to wait for something else to finish without touching the stagger step. Only
     .vn-intro__text sets it (see the pair choreography below); everywhere else it falls back to 0s
     and the calc is the bare stagger delay. Mixed s/ms inside one calc() is legal — both are
     <time> — and is what keeps the two authoring units natural on each side. */
  html.is-motion [data-vn-in][data-vn-stagger]>*{--vn-reveal:opacity .62s var(--vn-ease) calc(var(--vn-delay-base,0s) + var(--vn-delay,0ms)),translate .62s var(--vn-ease) calc(var(--vn-delay-base,0s) + var(--vn-delay,0ms));opacity:0;translate:0 14px;transition:var(--vn-reveal)}
  /* (0,4,1), so it wins over the rule above; durations/easings copied verbatim from
     .vn-formula-card. The formula grid is the ONLY staggered container whose children carry a
     transition of their own — checked 2026-09-08: .vn-gallery__tile has none (the scale lives on
     the <img> INSIDE it, a descendant rather than a stagger child, so it is untouched),
     .vn-testimonial, .vn-highlight and .vn-intro__photo have none either. Add a line here if that
     ever changes. */
  html.is-motion [data-vn-in][data-vn-stagger]>.vn-formula-card{transition:var(--vn-reveal),transform .15s ease,box-shadow .15s ease}
  html.is-motion [data-vn-in].is-in>*{opacity:1;translate:none}
  html.is-reduced [data-vn-in],html.is-reduced [data-vn-in]>*{translate:none}
  /* The plain rule above ties on specificity with `[data-vn-in]`/`[data-vn-in].is-in` above and
     wins on source order (later in the file) — fine. But a STAGGERED child's pre-reveal rule,
     `[data-vn-in][data-vn-stagger]>*` two lines up, has THREE attribute/class components against
     this rule's two, so it out-specifies the line above regardless of order: the reduced branch
     never actually wins for staggered children, and was only invisible because @layer reset's
     global prefers-reduced-motion rule zeroes the transition duration anyway (see rule 3 above).
     Match its specificity so this rule wins on its own, independent of that global rule. */
  html.is-reduced [data-vn-in][data-vn-stagger]>*{translate:none}

  /* ── Intro pair choreography (task 15, Joe 2026-09-08) ────────────────────────────────────
     The two photos are not a stagger container: they are one positioned box (see intro.php), the
     small photo is absolutely placed against the big one's lower-right corner, and they should
     read as ONE gesture — the dining table settles, then the garden print is laid on top of it.
     `data-vn-stagger` on the media block would have animated the two <img>s as equal siblings on
     a linear 120ms tick, which is why it is gone from the markup and this is written by hand.

     The media block itself must not fade: it is the container, and fading it would cross-fade the
     photos' own animation with a second opacity ramp on their parent. The generic
     `html.is-motion [data-vn-in]` rule is (0,2,1); this is (0,3,1), so it wins without having to
     carve `:not(.vn-intro__media)` out of the generic selector (which would also break the
     dev/check-dev.sh assertion that greps for that selector verbatim). */
  html.is-motion .vn-intro__media[data-vn-in]{opacity:1;translate:none;transition:none}
  /* 0ms: the main photo rises 20px and fades in over .7s.
     350ms: the small photo follows — further (24px right and down) and out of a .94 scale, so it
     reads as being placed ON the pair rather than arriving beside it. `scale` and `translate` are
     separate properties on purpose (see rule 1 at the top of this block). */
  html.is-motion .vn-intro__media[data-vn-in] .vn-intro__photo--main{opacity:0;translate:0 20px;transition:opacity .7s var(--vn-ease),translate .7s var(--vn-ease)}
  html.is-motion .vn-intro__media[data-vn-in] .vn-intro__photo--small{opacity:0;translate:24px 24px;scale:.94;transition:opacity .7s var(--vn-ease) .35s,translate .7s var(--vn-ease) .35s,scale .7s var(--vn-ease) .35s}
  html.is-motion .vn-intro__media[data-vn-in].is-in .vn-intro__photo--main,
  html.is-motion .vn-intro__media[data-vn-in].is-in .vn-intro__photo--small{opacity:1;translate:none;scale:none}
  /* 450ms: the text block's stagger starts, i.e. just after the small photo has begun. The base
     delay is declared here rather than as an inline style so the whole choreography is readable
     in one place; motion.js only writes the per-child --vn-delay. */
  html.is-motion .vn-intro__text[data-vn-in]{--vn-delay-base:.45s}
  /* Reduced motion keeps the fades and drops both position changes. Specificity has to MATCH the
     hidden rules above — (0,4,1) — for the same reason the staggered-children override two rules
     up had to: a shorter `html.is-reduced .vn-intro__photo--main` is (0,2,1) and would lose to
     them outright, leaving the photos to slide anyway wherever @layer reset's global duration
     reset does not apply. */
  html.is-reduced .vn-intro__media[data-vn-in] .vn-intro__photo--main,
  html.is-reduced .vn-intro__media[data-vn-in] .vn-intro__photo--small{translate:none;scale:none}

  /* ── Safety net (task 15) ─────────────────────────────────────────────────────────────────
     `is-motion` now comes from an inline head script, so the hidden state is painted BEFORE
     motion.js has had any chance to run. That is the whole point — and it also means a motion.js
     that 404s, is mangled by an optimiser or throws would leave this content hidden permanently
     rather than merely un-animated. The inline script therefore arms a 2s timeout that adds
     `is-reveal-all` unless motion.js has set `documentElement.dataset.motionReady`.
     One selector per hidden rule above, at MATCHING specificity and later in source order, so
     each wins on order alone: (0,2,1) for the plain reveal and its children, (0,3,1) for a
     staggered child, (0,4,1) for the two intro photos. A single short selector would silently
     fail to un-hide exactly the elements this exists for. No !important — the budget stays at 5. */
  html.is-reveal-all [data-vn-in],
  html.is-reveal-all [data-vn-in]>*,
  html.is-reveal-all [data-vn-in][data-vn-stagger]>*,
  html.is-reveal-all .vn-intro__media[data-vn-in] .vn-intro__photo--main,
  html.is-reveal-all .vn-intro__media[data-vn-in] .vn-intro__photo--small{opacity:1;translate:none;scale:none}

  /* Hero, plane one: the photo settles out of a 6% overscale on load. `both` so the from-state is
     applied before the first frame (no flash at 1:1) and the end-state sticks afterwards. */
  @keyframes vn-settle{from{scale:1.06}to{scale:1}}
  html.is-motion:not(.is-reduced) .vn-hero__img{animation:vn-settle 1.2s var(--vn-ease) both;transform-origin:60% 50%}
  /* Hero, plane two: the photo lags the page by scrollY*0.2px, capped at 130px (motion.js). Its
     box is 130px taller than the hero and starts 130px above it, so the lag slides reserved image
     down into view instead of opening a gap at the bottom edge.
     >=1024px only. Below that the hero is a portrait box and the rule right under .vn-hero__img
     already owns this element for a different reason (cropping the sky) — two rules writing `top`
     and `height` on one box at the same width would be a coin toss, and the phone hero not moving
     is the kinder default under a scrolling thumb anyway.
     Re-measured for the full-screen hero (task 16): at 1440x900 the hero is 860px, the photo's box
     990px, and `cover` scales the 1600x1198 photo by 0.9 to 1440x1078 — 88px of vertical crop and
     none horizontally, so essentially the whole frame is on screen, lake and villa both. At
     1440x1200 the box is 1290px and the scale flips to height-driven (1.077), which crops 283px
     horizontally; 60% keeps the lake in the top-left corner (verified in
     dev/screenshots/home-1440x1200.png). object-position 60% 30% is kept from the 620px hero: with
     only 88px of vertical slack left it barely moves the frame, and what it still does is favour
     the top of the photo, which is where the lake is. */
  @media (min-width:1024px){
    html.is-motion:not(.is-reduced) .vn-hero__img{top:-130px;height:calc(100% + 130px);object-position:60% 30%;translate:0 var(--vn-py,0px)}
  }

  /* Region photo: 40px of travel across the whole pass through the viewport, inside the
     overflow-hidden frame, with 20px reserved above and below so no edge is ever exposed. The copy
     beside it stays still — one moving plane per section, or it reads as a broken layout.
     Below 1024px and under .is-reduced this rule simply never applies, which leaves `translate` at
     its initial `none` and the frame showing the photo 1:1. */
  @media (min-width:1024px){
    html.is-motion:not(.is-reduced) .vn-region__img{top:-20px;height:calc(100% + 40px);translate:0 var(--vn-py,0px)}
  }

  /* Hover and press. Pointer-gated so a touch device never lands in a stuck hover state; the press
     deliberately is NOT gated, because :active means the same thing under a thumb. `transition` is
     always on the one property that changes — never `all`, which makes every unrelated style
     change on the element an animation too. */
  @media (hover:hover) and (pointer:fine){
    .vn-gallery__tile img{transition:scale .18s ease-out}
    .vn-gallery__tile:hover img,.vn-gallery__tile:focus-visible img{scale:1.03}
    .vn-formula-card__go{transition:translate .16s ease-out}
    .vn-formula-card:hover .vn-formula-card__go{translate:4px 0}
    .vn-formulas__more svg{transition:translate .16s ease-out}
    .vn-formulas__more a:hover svg{translate:4px 0}
  }
  .vn-btn:active{translate:0 1px}

  /* Top bar (desktop only — phone, mail, language switcher) */
  .vn-topbar{display:none;background:var(--vn-text);color:#fff}
  /* --vn-topbar-h on all three, not just on the bar: the links and the language switcher are what
     actually set the bar's height (they are its only content), so a token used on the bar alone
     would be a number the layout ignores. The front page's absolute header and the hero's
     `calc(100svh - …)` both offset by this same token — see tokens.css. */
  .vn-topbar__bar{display:flex;align-items:center;justify-content:space-between;gap:var(--vn-space-5);min-height:var(--vn-topbar-h);font-size:14px}
  .vn-topbar__links{display:flex;gap:var(--vn-space-6);align-items:center}
  .vn-topbar__link{display:inline-flex;align-items:center;gap:8px;min-height:var(--vn-topbar-h);color:#fff;text-decoration:none}
  .vn-topbar__link:hover{color:#fff;opacity:.85}
  .vn-topbar .vn-lang a{min-height:var(--vn-topbar-h);color:rgb(255 255 255 / .65)}
  .vn-topbar .vn-lang a[aria-current="true"]{color:#fff}
  @media (min-width:1024px){
    .vn-topbar{display:flex}
  }

  /* Header */
  .vn-header{position:sticky;top:0;background:var(--vn-surface);border-bottom:1px solid var(--vn-line);z-index:40}
  /* Mobile gap is 12px, not --vn-space-5 (24px): below 1024px the bar carries the long
     wordmark + a 44px phone button + a 44px toggle, and at 360px the container is only
     ~312px wide — 24px gaps overflowed it and gave the whole site a horizontal scrollbar. */
  .vn-header__bar{display:flex;align-items:center;gap:12px;min-height:64px}
  .vn-logo{display:inline-flex;align-items:center;min-height:44px;padding:6px 0;color:var(--vn-brand);flex-shrink:0}
  /* Below 1024px the wordmark is sized by WIDTH against the viewport (min(55vw,216px)),
     not by height: it is a ~10.8:1 long lockup, so a fixed height silently makes it
     260px+ wide and blows out the mobile bar. Height-based tiers resume at >=1024px,
     where the bar has room — those blocks must reset width:auto to undo this rule. */
  .vn-logo svg{display:block;width:min(55vw,216px);height:auto}
  .vn-header__cta{display:none}
  .vn-header__phone{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;margin-inline-start:auto;border-radius:var(--vn-radius);border:1px solid var(--vn-line);color:var(--vn-accent);flex-shrink:0}

  /* Desktop nav (>= 1024px).
     The "Villa Natica" (home) item is NOT hidden. It used to be — `#primary-nav
     .menu-item-home{display:none}`, on the reasoning that the logo already links home — but on
     this site the two formula pages ("Vakantiehuis tot 6/18 personen") are CHILDREN of that home
     item in the WP menu, so hiding it deleted the only dropdown in the whole navigation. That is
     the "submenus don't exist" defect from Joe's 2026-09-07 review. The width it costs is paid
     for by the tighter link padding/gap/logo tiers below instead. */
  .vn-nav{display:none}
  .vn-nav__list{display:flex;gap:var(--vn-space-5);align-items:center}
  .vn-nav__list li{position:relative}
  .vn-nav__list a{display:inline-flex;align-items:center;min-height:44px;text-decoration:none;color:var(--vn-text);font-weight:600;white-space:nowrap}
  .vn-nav__list a:hover,.vn-nav__list a:focus-visible{color:var(--vn-accent)}
  /* Caret on parent items. A CSS border triangle rather than an icon: no extra request, no mask
     prefixing, and it inherits the link colour on hover/open for free. */
  .vn-nav__list .menu-item-has-children>a::after{content:"";flex:0 0 auto;margin-inline-start:6px;border:5px solid transparent;border-block-end:0;border-block-start:6px solid currentColor;transition:transform .15s ease}
  .vn-nav__list .menu-item-has-children.is-open>a::after{transform:rotate(180deg)}
  .vn-nav__list .sub-menu{display:none;position:absolute;top:100%;left:0;flex-direction:column;gap:0;min-width:240px;background:var(--vn-surface);border:1px solid var(--vn-line);border-radius:var(--vn-radius);padding:var(--vn-space-2) 0;box-shadow:0 8px 24px rgb(0 0 0 / .08)}
  /* Three ways in: hover, keyboard focus (no JS needed for either) and the JS click/tap toggle
     that adds .is-open — touch devices in the 1024px+ range get no hover, so the toggle is the
     only path there. */
  .vn-nav__list li:hover>.sub-menu,.vn-nav__list li:focus-within>.sub-menu,.vn-nav__list li.is-open>.sub-menu{display:flex}
  .vn-nav__list .sub-menu a{padding-inline:var(--vn-space-4);width:100%;font-weight:400}

  /* Mobile nav toggle (< 1024px). margin-inline-start:auto here too (not just on
     .vn-header__phone) so the toggle still lands at the right edge on a header render
     with no phone number configured (vn_phone empty ⇒ .vn-header__phone doesn't render
     at all — see header.php). Harmless when the phone button IS present: it absorbs the
     free space first, so the toggle's own auto margin has nothing left to do. */
  .vn-nav-toggle{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;margin-inline-start:auto;padding:0;border:0;background:transparent;cursor:pointer;flex-shrink:0}
  .vn-nav-toggle__bars,.vn-nav-toggle__bars::before,.vn-nav-toggle__bars::after{display:block;width:24px;height:2px;background:var(--vn-text);border-radius:1px}
  .vn-nav-toggle__bars{position:relative}
  .vn-nav-toggle__bars::before,.vn-nav-toggle__bars::after{content:"";position:absolute;left:0}
  .vn-nav-toggle__bars::before{top:-7px} .vn-nav-toggle__bars::after{top:7px}

  /* Desktop breakpoint overrides — must come after the base .vn-nav / .vn-nav-toggle rules
     above so they win the cascade (same specificity, later source order), and the >=1280px
     block must come after the >=1024px one for the same reason.
     Six top-level items now (the home item is back — see the note above the nav rules) plus
     the wordmark and the CTA. The wordmark is a ~10.8:1 lockup, so its HEIGHT is what drives
     the width: 40px => ~434px, 36px => ~390px. Budget at 1280px (container 1200px):
     390 + nav + CTA + 2x24px header gaps. Hence the two tiers below — nothing is hidden to
     make it fit, which was the mistake the old menu-item-home rule made. */
  @media (min-width:1024px){
    /* 16px, not --vn-space-5 (24px), between logo / nav / CTA: measured on dev at exactly
       1024px the GERMAN menu ("Unser Haus · Über uns · Unsere Region · Kontakt · Dokumente")
       overflowed the bar by 6px at 24px gaps. Dutch fits either way — always re-measure the
       longest language, not the default one. */
    .vn-header__bar{min-height:88px;gap:16px}
    /* 30px, was 36px, since the seventh item "3D Tour" (2026-10-04): at 36px (~390px wide) the
       French and German bars overflowed a 1024px window by up to 30px and pushed the CTA out of
       it. 30px => ~325px. From 1280px up the lockup is 40px again (next block). */
    .vn-logo svg{width:auto;height:30px}
    .vn-header__cta{display:inline-flex;padding:0 12px;font-size:14px}
    .vn-header__phone{display:none}
    .vn-nav{display:flex;flex:1 1 auto;justify-content:flex-end}
    .vn-nav__list{gap:12px;font-size:14px}
    .vn-nav__list .sub-menu{font-size:15px}
    .vn-nav-toggle{display:none}
  }
  @media (min-width:1280px){
    .vn-header__bar{gap:var(--vn-space-5)}
    .vn-logo svg{height:40px}
    .vn-nav__list{gap:18px;font-size:15px}
    .vn-nav__list>li>a{padding-inline:4px}
    .vn-header__cta{padding:0 14px;font-size:15px}
  }

  /* ── Header over the hero (front page only) ────────────────────────────────────────────────
     Task 16, Joe's variant A off dev/screenshots/hero-fullscreen-A.png: on the front page the
     header has no bar of its own — it sits ON the photograph in white, and only becomes the solid
     white bar once the hero has scrolled past. Gated on the `has-hero-overlay` body class
     (inc/setup.php, is_front_page()) and not on `body.home`, because `body.home` is also true for
     a blog index if one is ever assigned to a page — and that page has no .vn-hero to sit on.

     TWO states, split by `html:not(.has-scrolled-past-hero)` / `html.has-scrolled-past-hero`
     (motion.js toggles the class from the rAF scroll handler it already runs):
       - over the hero: this block;
       - past the hero: the short block after it. It says only `position:fixed` and the entry
         animation, because every colour comes back BY ITSELF — none of the white rules below
         match any more. That is why they are written with :not() rather than overridden one by
         one: an override list has to be kept in step with the list it overrides, and the day one
         line of it is forgotten the site ships a white logo on a white bar.
     With scripting off the class is never added, the header simply stays over the hero, and the
     page is entirely usable — the sticky bar is the enhancement, not the baseline.

     `position:absolute`, not fixed, while over the hero: the header has to scroll AWAY with the
     photo rather than ride over the content beneath it. Nothing between it and <html> is
     positioned, so its containing block is the initial one and `top` is measured from the top of
     the document — 0 below 1024px, and the top bar's own height above it, where that bar is in
     flow directly above. left/right:0 because an out-of-flow box otherwise shrink-wraps.
     Both states are out of flow, and that is what makes the hand-over safe: adding the class
     cannot change the hero's geometry, so the threshold motion.js measured cannot move underneath
     it and the class cannot oscillate around the switch point. */
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-header{position:absolute;left:0;right:0;top:0;background:transparent;border-bottom:0}
  @media (min-width:1024px){
    html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-header{top:var(--vn-topbar-h)}
  }
  /* `>li>a` and not a bare descendant `a`: the dropdown panel keeps its own white surface and
     dark text (it opens over the photograph, where it needs the contrast), and a descendant
     selector here would paint its links white on white. */
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-logo,
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-nav__list>li>a,
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-header__phone,
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-nav-toggle{color:#fff}
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-nav__list>li>a:hover,
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-nav__list>li>a:focus-visible{color:rgb(255 255 255 / .78)}
  /* The burger is three painted bars, so what carries its colour is `background`, not `color`. */
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-nav-toggle__bars,
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-nav-toggle__bars::before,
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-nav-toggle__bars::after{background:#fff}
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-header__phone{border-color:rgb(255 255 255 / .7)}
  /* The outline CTA. A hair of white fill (.08) so the button reads as a button over a busy
     photograph without becoming a panel; on hover it fills to .75 and the label goes teal, which
     is the only pair that stays readable once the background is nearly white. */
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-header__cta{color:#fff;border-color:rgb(255 255 255 / .7);background:rgb(255 255 255 / .08)}
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-header__cta:hover{background:rgb(255 255 255 / .75);border-color:transparent;color:var(--vn-accent)}
  /* Focus ring: @layer reset paints it in the accent teal, which disappears into the photo. White
     reads on it — except inside the dropdown, which is a white panel, so the second rule puts the
     teal back there. Equal specificity, later source order: the order of these two lines is
     load-bearing. */
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .vn-header :focus-visible{outline-color:#fff}
  html:not(.has-scrolled-past-hero) body.has-hero-overlay .sub-menu :focus-visible{outline-color:var(--vn-accent)}

  /* Past the hero. Everything the solid bar looks like is already on `.vn-header` at the top of
     this section; the one thing that cannot simply be inherited is the positioning, because the
     base rule's `sticky` would put the header back IN the flow and shove the whole page down by
     its own 88px at the exact moment the class lands. `fixed` keeps it out of flow, so nothing
     moves. 160ms ease-out on opacity and translate only, `both` so the from-state is applied
     before the first frame. Reduced motion swaps the keyframes for a fade rather than leaning on
     @layer reset's global duration reset, so the intent is readable here rather than implied
     three hundred lines away. */
  @keyframes vn-header-in{from{opacity:0;translate:0 -8px}to{opacity:1;translate:none}}
  @keyframes vn-header-fade{from{opacity:0}to{opacity:1}}
  html.has-scrolled-past-hero body.has-hero-overlay .vn-header{position:fixed;left:0;right:0;top:0;animation:vn-header-in .16s ease-out both}
  html.is-reduced.has-scrolled-past-hero body.has-hero-overlay .vn-header{animation-name:vn-header-fade}

  /* Off-canvas nav */
  .vn-offcanvas{position:fixed;inset:0;visibility:hidden;z-index:50}
  html[data-nav-open] .vn-offcanvas{visibility:visible}
  html[data-nav-open]{overflow:hidden}
  .vn-offcanvas__backdrop{position:absolute;inset:0;background:rgb(0 0 0 / .4)}
  .vn-offcanvas__panel{position:absolute;inset:0 0 0 auto;width:min(88vw,360px);height:100%;background:var(--vn-surface);padding:var(--vn-space-6);display:flex;flex-direction:column;gap:var(--vn-space-5);overflow-y:auto;transform:translateX(100%);transition:transform .25s}
  html[data-nav-open] .vn-offcanvas__panel{transform:none}
  .vn-offcanvas__close{align-self:flex-end;width:44px;height:44px;border:0;background:transparent;font-size:1.5rem;line-height:1;cursor:pointer;color:var(--vn-text)}
  .vn-offcanvas__list{display:flex;flex-direction:column;gap:0}
  .vn-offcanvas__list li{border-bottom:1px solid var(--vn-line)}
  .vn-offcanvas__list a{display:flex;align-items:center;min-height:44px;text-decoration:none;color:var(--vn-text);font-weight:600}
  .vn-offcanvas__list .sub-menu{display:flex;flex-direction:column;gap:0;padding-inline-start:var(--vn-space-4)}
  .vn-offcanvas__cta{align-self:flex-start}
  /* .vn-lang is shared by the top bar copy (.vn-topbar, a row — muted colours overridden
     above for the dark background), the off-canvas copy (.vn-offcanvas__panel, a column)
     and the footer contact column (.vn-footer__contact, a row) — push-to-bottom only
     applies in the off-canvas column context. */
  .vn-offcanvas__panel .vn-lang{margin-top:auto}
  .vn-lang{display:flex;gap:var(--vn-space-4)}
  .vn-lang a{display:inline-flex;align-items:center;min-height:44px;padding:0 var(--vn-space-2);text-decoration:none;color:var(--vn-muted);font-weight:600}
  .vn-lang a[aria-current="true"]{color:var(--vn-text)}

  /* Footer */
  .vn-footer{background:var(--vn-surface);border-top:1px solid var(--vn-line);margin-top:var(--vn-space-8)}
  /* ...but not on the front page, which ends in the full-bleed teal enquiry band. The mockup
     runs the footer flush against that band; the 80px of page background the margin inserts
     read as a gap someone forgot to close. Sub pages keep the margin — their content ends on
     the page background, where the separation is needed. */
  .vn-home+.vn-footer{margin-top:0}
  /* Three columns from 1024px (brand+contact · Navigatie · languages+social), TWO below it with
     the languages/social row spanning the full width underneath — Joe's 2026-09-08 layout. The
     separate "Contact" column is gone: phone and e-mail now sit under the address in the brand
     column, where they read as one block of contact details instead of a duplicate heading. */
  .vn-footer__grid{display:grid;gap:var(--vn-space-6);padding-block:var(--vn-space-7);grid-template-columns:repeat(2,minmax(0,1fr))}
  .vn-footer__meta{grid-column:1/-1;display:flex;flex-direction:column;gap:var(--vn-space-4);align-items:flex-start}
  @media (min-width:1024px){
    .vn-footer__grid{grid-template-columns:1.6fr 1fr 1fr;gap:var(--vn-space-7)}
    .vn-footer__meta{grid-column:auto}
  }
  .vn-footer__brand{display:flex;flex-direction:column;gap:var(--vn-space-4)}
  .vn-footer__logo{display:inline-flex;color:var(--vn-brand)}
  /* Width-based like the header wordmark. A fixed height plus the reset's `max-width:100%`
     is the classic SVG distortion trap: once the 10.8:1 lockup is wider than its column the
     max-width shrinks the width while the fixed height stays put, squashing the aspect ratio.
     300px, not 520px: the wordmark is meant to sit at the address line's width (Joe's review) —
     at 520px it dwarfed the two lines of text under it. */
  .vn-footer__logo svg{display:block;width:min(100%,300px);height:auto}
  .vn-footer__address{font-style:normal;color:var(--vn-muted);line-height:1.8;max-width:300px}
  .vn-footer__nav,.vn-footer__contact{display:flex;flex-direction:column;gap:var(--vn-space-3);align-items:flex-start}
  /* Social row. 44px box = the same tap target the phone button and the language links use. */
  .vn-footer__social{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:var(--vn-radius);border:1px solid var(--vn-line);color:var(--vn-text)}
  .vn-footer__social:hover{color:var(--vn-accent);border-color:var(--vn-accent)}
  .vn-footer__social svg{display:block;width:18px;height:18px}
  .vn-footer__heading{font-weight:600;color:var(--vn-text)}
  .vn-footer__list{display:flex;flex-direction:column;gap:0}
  .vn-footer__list a{display:inline-flex;align-items:center;min-height:44px;text-decoration:none;color:var(--vn-text)}
  .vn-footer__list a:hover{color:var(--vn-accent)}
  /* Must come AFTER the unconditional 44px rule above (same selector/specificity — cascade
     order decides, and a media query adds no specificity of its own) or this is dead code
     at >=1024px. Denser list is fine once pointers (not thumbs) are the primary input. */
  @media (min-width:1024px){
    .vn-footer__list a{min-height:36px}
  }
  .vn-footer__contact-link{display:inline-flex;align-items:center;gap:8px;min-height:44px;color:var(--vn-text);text-decoration:none}
  .vn-footer__contact-link:hover{color:var(--vn-accent)}
  .vn-footer__legal{display:flex;flex-wrap:wrap;justify-content:space-between;gap:var(--vn-space-4);padding-block:var(--vn-space-5);border-top:1px solid var(--vn-line);color:var(--vn-muted);font-size:var(--vn-step--1)}
  /* Privacy / Terms / Imprint links. Row stays visually compact (font-size inherited from
     .vn-footer__legal, gap kept tight) while each link still meets the 44px tap target via
     padding rather than line-height, matching .vn-lang a's approach just above. */
  .vn-footer__legal-links{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--vn-space-2)}
  .vn-footer__legal-link{display:inline-flex;align-items:center;min-height:44px;padding:0 4px;color:var(--vn-muted);text-decoration:none}
  .vn-footer__legal-link:hover{color:var(--vn-accent)}

  /* Sticky mobile enquiry bar */
  .vn-cta-bar{position:fixed;inset:auto 0 0 0;padding:var(--vn-space-3);background:var(--vn-surface);border-top:1px solid var(--vn-line);display:flex;justify-content:center;z-index:30}
  @media (min-width:1024px){.vn-cta-bar{display:none}}
  /* Reserve the bar's height ONLY where it actually renders: the `has-cta-bar` body class
     (inc/setup.php) mirrors the same guard cta-bar.php uses, and the bar itself is hidden
     from 1024px up. The old rule padded EVERY body, including the enquiry page, which is the
     one page that never shows the bar.
     The reserve sits on .vn-footer, not on `body`: WordPress emits an UNLAYERED
     `body{padding:0px}` into global-styles-inline-css (theme.json root layout), and per the
     cascade-layers spec an unlayered normal declaration beats ANY layered one regardless of
     specificity — so `body.has-cta-bar{padding-bottom:72px}` computes to 0px on the live page
     (measured on dev: the previous unconditional `body{padding-bottom:72px}` was dead code for
     the same reason, so the bar had been overlapping the footer all along). .vn-footer is the
     theme's own class, is the last element in the document, and nothing unlayered targets it —
     so this reserves the same space, keeps the footer's own surface colour behind the bar, and
     needs no !important. */
  @media (max-width:1023.98px){body.has-cta-bar .vn-footer{padding-block-end:72px}}

  /* Focus ring: the reset paints :focus-visible with the accent colour, which is a dark
     teal — invisible against the dark top bar and against the hero's photo/gradient.
     White reads on both. */
  .vn-topbar :focus-visible,.vn-hero :focus-visible,.vn-page-hero :focus-visible{outline-color:#fff}

  /* ── legacy ────────────────────────────────────────────────────────────────────────────────
     Baseline for the sub pages, whose content is still the pre-redesign editor/SiteOrigin
     markup until plan 04 migrates it. Two rules of engagement:
       1. SiteOrigin's own `.panel-grid` / `.panel-grid-cell` layout is LEFT ALONE — it ships
          its own inline CSS, and an unlayered inline rule beats anything in this file anyway.
       2. Nothing here may need !important. The budget in dev/check-dev.sh is exactly 5.
     The single biggest visible defect this fixes: the parent theme used to supply WordPress's
     `.alignleft`/`.alignright` float rules and a `.gallery` layout. This theme registers html5
     gallery support, which switches OFF WordPress's own inline gallery stylesheet
     (gallery_shortcode() only emits it when `use_default_gallery_style` is true, and that
     defaults to `! $html5`) — so with no theme rules at all, every gallery item and every
     floated thumbnail became a full-width block. That is "galleries are image lists" from
     Joe's 2026-09-07 review. */
  .vn-prose{padding-block:var(--vn-space-7)}
  /* The stand-in `<h1>` index.php prints above a post list whose posts page has no featured image
     (and therefore no hero to carry the title). It is a .vn-prose block of its own, so without
     this its 48px bottom padding would stack with the first list item's 48px top padding. */
  .vn-prose--list-title{padding-block-end:0}
  .vn-prose::after{content:"";display:table;clear:both}
  .vn-prose>h1,.vn-prose>h2,.vn-prose>h3,.vn-prose>h4,.vn-prose>p,.vn-prose>ul,.vn-prose>ol,.vn-prose>dl,.vn-prose>blockquote{max-width:76ch}

  /* ── Vertical rhythm ──────────────────────────────────────────────────────────────────────
     DESCENDANT selectors, not the `>` child combinator this block used until 2026-09-08: the sub
     pages' content is SiteOrigin panel markup, so essentially nothing is a direct child of
     .vn-prose — every heading sits four or five divs deep inside a panel cell
     (.panel-grid > .panel-grid-cell > .so-panel > .so-widget-sow-editor > .textwidget > h2). With
     `.vn-prose>h2` these rules matched nothing at all, so @layer reset's
     `h1,h2,h3,h4,p,figure,ul,ol{margin:0}` had the last word and every heading sat flush against
     the paragraph under it. That is the "headings crammed against the text" defect from Joe's
     2026-09-08 desktop review (screenshots of /vakantiehuis/ and /vakantiehuis-6-personen/).
     Values at the 16px root: h2 32/12, h3 24/8, h4 20/8, flow blocks 0/16, li+li 4. Adjacent
     margins collapse, so a paragraph (16 below) followed by an h2 (32 above) yields 32px, not 48;
     a heading (12 below) followed by a paragraph (0 above) yields 12px. */
  .vn-prose h1{margin-block:0 var(--vn-space-5)}
  .vn-prose h2{margin-block:var(--vn-space-6) var(--vn-space-3)}
  .vn-prose h3{margin-block:var(--vn-space-5) var(--vn-space-2)}
  .vn-prose h4{margin-block:1.25rem var(--vn-space-2)}
  .vn-prose p{margin-block:0 var(--vn-space-4)}
  /* The `:not()` guards keep this off markup that owns its own spacing: `.gallery` is a <figure>
     under html5 gallery support and `.gallery-item` its <figure> children (the grid below sets
     both), and the Booking Calendar's month grids are plugin <table>s that carry classes. Plain
     editor lists/tables — which is all the legacy content uses — have no class and are styled. */
  .vn-prose ul:not([class]),.vn-prose ol:not([class]){padding-inline-start:var(--vn-space-5);margin-block:0 var(--vn-space-4)}
  .vn-prose figure:not(.gallery):not(.gallery-item),.vn-prose table:not([class]){margin-block:0 var(--vn-space-4)}
  .vn-prose li+li{margin-block-start:var(--vn-space-1)}
  .vn-prose>:first-child{margin-block-start:0}

  /* ── Images ───────────────────────────────────────────────────────────────────────────────
     Prose images are INLINE by default. The "Samenvatting" blocks on /vakantiehuis/ and
     /vakantiehuis-6-personen/ put a ~30px PNG icon and its sentence in two cells of a legacy
     .rTable (see the .rTable rules below); a `display:block` image there is a line of its own, so
     every row read icon / newline / text — Joe's 2026-09-08 defect. (Task 21 has since replaced
     those fourteen icons with inline SVG, so the summary table is no longer the example; the rule
     stays because /ardennen/'s 26x32 PDF glyphs and the classless photos elsewhere still need it.)
     max-width:100%;height:auto
     duplicates @layer reset's global img rule on purpose: it is what keeps every prose image with
     no size-* class (thumbnails, those icons, the 26x32px PDF glyphs) at its intrinsic size
     instead of the stretched width:100% below. */
  .vn-prose img{display:inline-block;vertical-align:middle;max-width:100%;height:auto;border-radius:var(--vn-radius)}
  /* ...block only where the image genuinely IS a block: a WP size-full/size-large image, a
     <figure>'s image, or a core [gallery] item. */
  .vn-prose img.size-full,.vn-prose img.size-large,.vn-prose figure img,.vn-prose .gallery img{display:block}
  /* Only "big" images fill the 76ch column: a WP size-full/size-large image, a core [gallery]
     item, a Gutenberg wide/full-bleed image, or an explicit alignfull/alignwide block.
     Anything smaller — size-thumbnail/size-medium, or (like the region page's PDF/icon
     images) no size class at all — stays intrinsic, capped by max-width:100% above. Before
     this rule the sub-600px media query below forced width:100% on EVERY floated image with
     no size check at all, which blew the /ardennen/ page's 26x32px PDF icons up to the full
     column width on phones — Joe's "giant images" defect. */
  .vn-prose img.size-full,.vn-prose img.size-large,.vn-prose .gallery img,
  .vn-prose figure.wp-block-image img,.vn-prose .alignfull img,.vn-prose .alignwide img{width:100%}

  /* WordPress alignment + caption classes */
  .vn-prose .alignleft{float:left;margin:0 var(--vn-space-5) var(--vn-space-4) 0}
  .vn-prose .alignright{float:right;margin:0 0 var(--vn-space-4) var(--vn-space-5)}
  /* An image the editor inserted at its own intrinsic size — no `size-*` class, so WordPress never
     resized it — is usually an icon here (the /ardennen/ 26x32 PDF glyphs). 24px of clearance
     detaches such a glyph from the sentence it labels, so those get a tight margin instead. */
  .vn-prose img.alignleft:not([class*="size-"]){margin:0 .6em .2em 0}
  .vn-prose img.alignright:not([class*="size-"]){margin:0 0 .2em .6em}
  .vn-prose .aligncenter{display:block;margin-inline:auto;margin-block-end:var(--vn-space-4)}
  .vn-prose .alignnone{margin-block-end:var(--vn-space-4)}
  .vn-prose .wp-caption{max-width:100%;margin-block-end:var(--vn-space-4)}
  .vn-prose .wp-caption img{margin-block-end:var(--vn-space-2)}
  .vn-prose .wp-caption-text,.vn-prose figcaption{color:var(--vn-muted);font-size:var(--vn-step--1);line-height:1.5}
  @media (max-width:600px){
    /* Everything aligned unfloats below 600px — including the small icons. The 2026-09-08 brief
       asked for those to KEEP floating so their text stays beside them, and the obvious selector
       for "small" (`:not(.size-full):not(.size-large)`) was measured on dev and is wrong for this
       content: it also matches /over-ons/'s 150px size-thumbnail portraits, its 300px size-medium
       photo and /contact/'s 200px classless photo, all of which then float in a 342px column —
       and a lone floated image collapses its own <p> to height 0 (measured) and overlaps whatever
       follows. CSS cannot compare the width attribute numerically, so there is no honest "≤64px"
       selector to write.
       It is also unnecessary: with the image inline-block (rule above) rather than block, an
       unfloated 26x32 glyph sits on the SAME line as the start of the sentence after it — text
       beside the icon, which was the point. Only the wrap-around is lost, and a 32px-tall icon
       has nothing to wrap. Sizing still comes from the size-based rule above, never from a
       blanket width:100% here — that blanket rule was the actual "giant images" bug. */
    .vn-prose .alignleft,.vn-prose .alignright{float:none;margin-inline:0}
  }
  /* ...and below 640px a content PHOTO fills the column outright (Joe, 2026-09-08). The legacy
     pages insert their photos as `alignleft wp-image-N size-medium`, i.e. a 300px intrinsic image
     (inline-block per the rule above) inside a 342px column at 390px — so every one of them
     stopped ~42px short of the text's right edge and read as a mis-set image rather than a choice.
     Keying on the size-* class is safe here for exactly the reason the sub-600px note above
     spells out in the other direction: an icon carries NO size class, so it keeps its intrinsic
     inline sizing and stays on its sentence's line. (The "Samenvatting" icons that made that case
     were bare `<img width="53" height="30">` inside .rTableCell; since task 21 they are inline SVG
     and out of this rule's reach entirely, but /ardennen/'s PDF glyphs are still images.)
     640px rather than reusing the 600px block above: that one is about un-floating, this one is
     about the column being too narrow to set an image beside text at all. Above 640px the float
     behaviour is untouched — a size-medium photo is still 300px with text beside it. */
  @media (max-width:639.98px){
    .vn-prose img.size-medium,.vn-prose img.size-medium_large,.vn-prose img.size-large,.vn-prose img.size-full{float:none;display:block;width:100%;height:auto;margin:0 0 var(--vn-space-4)}
    /* Except [gallery] items, which are `attachment-medium size-medium` too but are laid out by
       the grid further down (2 columns at this width). The sizing above is what they already
       compute to; the margin is not — it would add 16px inside every cell on top of the grid's
       own gap. Same specificity as the rule above, so source order decides: keep this second. */
    .vn-prose .gallery img{margin:0}
  }

  /* ── Legacy .rTable pseudo-table ──────────────────────────────────────────────────────────
     WordPress stores Appearance > Customize > Additional CSS PER STYLESHEET — `wp_get_custom_css()`
     keys on `get_stylesheet()` — and this site's copy is attached to `vantage`. Under this theme WP
     prints no `<style id="wp-custom-css">` at all (verified 2026-09-08: the block is present on
     www.villanatica.be and absent from dev.villanatica.be?vn_theme=1). Almost everything in it is
     vantage chrome that must stay dead — it also restyles `body`, `h2` and `p` globally with 12px
     Tahoma — but three declarations in it are LAYOUT for editor content that is still live: the
     .rTable / .rTableRow / .rTableCell pseudo-table the "Samenvatting" icon lists are built from.
     Without them those cells are plain divs and every row stacks. Ported here (scoped to .vn-prose)
     so the icon and its sentence share a row, deliberately WITHOUT the rest of that stylesheet.
     line-height is 1.4 rather than the original 120% — that value was written for 12px Tahoma.

     TASK 21 REWROTE THIS BLOCK rather than adding a second one beside it, so there is exactly one
     place that lays the summary table out. The icons are now inline SVG
     (villanatica_replace_legacy_icons in inc/setup.php swaps the fourteen 2018/2021 PNGs for the
     theme's own icon set), and every value ported in task 11 was shaped around the bitmaps:

       - `width:100%` on the table. As a shrink-to-fit `display:table` it was only as wide as its
         longest sentence, so the block ended on a ragged right edge inside the 76ch column.
       - `vertical-align:middle`, not `top`. A 28px glyph beside a one-line sentence sat proud of
         its own text with `top`, and beside a two-line one it sat at the first line's cap height.
         Centred, a row reads as one object whether it wraps or not.
       - a fixed 44px first column (28px glyph + 16px gutter) instead of `padding-inline-end:10px`
         on both cells. The PNGs were 25 to 53px wide, so with a shrink-to-fit column the sentences
         started at fourteen different x positions; the SVGs do not vary, so the column should not.
       - the 16px `padding-block-end` became a symmetric 6px plus a hairline: air under every row
         and none above it read as fourteen separate lines rather than as one table.
     `border-collapse:collapse` so a row boundary is one pixel and not two touching ones. Mobile
     needs nothing of its own — the table is two narrow columns at every width. */
  .vn-prose .rTable{display:table;width:100%;border-collapse:collapse}
  .vn-prose .rTableBody{display:table-row-group}
  .vn-prose .rTableRow{display:table-row}
  .vn-prose .rTableCell{display:table-cell;vertical-align:middle;padding:6px 0;line-height:1.4}
  .vn-prose .rTableCell:first-child{width:44px;padding:6px 8px 6px 0}
  /* The hairline is drawn on the CELLS, not on the row: a `border` on a `display:table-row` box is
     inert under border-collapse. `:not(:last-child)` keeps the last row from underlining nothing. */
  .vn-prose .rTableRow:not(:last-child) .rTableCell{border-bottom:1px solid var(--vn-line)}
  /* The swapped-in glyph. `display:block` removes the inline baseline gap that would otherwise make
     the cell ~4px taller than the icon; `color` is what the SVG's fill="currentColor" resolves
     against. Sized here as well as by `.vn-icon` so the summary table keeps its 28px if the home
     strip's icon size is ever changed. */
  .vn-legacy-icon svg{display:block;width:28px;height:28px;color:var(--vn-accent)}

  /* WordPress [gallery] as a responsive grid. `.gallery` is a <figure> under html5 support and
     `.gallery-item` its <figure> children; `br.clear` is legacy float-clearing WP still emits. */
  .gallery{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-block:var(--vn-space-5)}
  .gallery-columns-1{grid-template-columns:1fr}
  .gallery-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}
  .gallery-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}
  .gallery-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}
  .gallery-columns-5,.gallery-columns-6,.gallery-columns-7,.gallery-columns-8,.gallery-columns-9{grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
  .gallery-item{margin:0;width:auto;max-width:none;float:none}
  .gallery-item img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;border-radius:var(--vn-radius);border:0}
  .gallery .gallery-caption{color:var(--vn-muted);font-size:var(--vn-step--1);margin-block-start:var(--vn-space-2)}
  .gallery br,br.clear{display:none}
  /* Columns 7-9 already self-adjust via auto-fit,minmax(140px,1fr) above and need no media
     query. Columns 3-6 use a fixed column count instead (so a 4-column gallery reliably shows
     4, not "however many 140px tiles fit"), so THOSE need an explicit phone/tablet cap or a
     390px screen renders 4-6 slivers per row. */
  @media (max-width:639.98px){
    .gallery,.gallery-columns-3,.gallery-columns-4,.gallery-columns-5,.gallery-columns-6{grid-template-columns:repeat(2,minmax(0,1fr))}
  }
  @media (min-width:640px) and (max-width:1023.98px){
    .gallery-columns-3,.gallery-columns-4,.gallery-columns-5,.gallery-columns-6{grid-template-columns:repeat(3,minmax(0,1fr))}
  }

  /* Documents-style link lists: a plain <ul> whose items are nothing but a link (the
     Documents page's PDF lists) renders as tappable rows instead of bare underlined text —
     easier to hit on a phone, and the row itself signals "this opens a file". `:only-child`
     ALONE is not enough to scope this to pure-link rows: it only counts ELEMENT children, so
     an <li> like `<a><strong>Tourist Info Bütgenbach</strong></a>, Marktplatz 13a, 4750
     Bütgenbach` — a link followed by a plain TEXT node (the address, no wrapping element) —
     still matches `a:only-child`, even though visually it is a mixed link+text row (found on
     /ardennen/, task 12 fix round 1). The `[href$=".pdf" i]` requirement is what actually
     limits this to file links: the Documents page's rows are exactly that, "Pub24 – (Online
     Shop)"'s two-link row and a mixed link+address row are not. */
  .vn-prose li>a:only-child[href$=".pdf" i]{display:flex;align-items:center;gap:8px;min-height:44px;padding:8px 12px;border:1px solid var(--vn-line);border-radius:var(--vn-radius);background:var(--vn-surface);color:var(--vn-text);text-decoration:none}
  .vn-prose li>a:only-child[href$=".pdf" i]:hover{color:var(--vn-accent);border-color:var(--vn-accent)}
  .vn-prose li:has(>a:only-child[href$=".pdf" i]){list-style:none}
  /* File icon, PDF rows only — a mask-image data URI so it recolors via currentColor like
     every other icon in this theme instead of shipping a separate asset for one glyph.
     Font Awesome Free 7 "file-pdf" (solid), CC BY 4.0. */
  .vn-prose li>a:only-child[href$=".pdf" i]::before{
    content:"";flex:0 0 auto;width:16px;height:16px;background-color:currentColor;
    -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512'%3E%3Cpath d='M96 0C60.7 0 32 28.7 32 64l0 384c0 35.3 28.7 64 64 64l80 0 0-112c0-35.3 28.7-64 64-64l176 0 0-165.5c0-17-6.7-33.3-18.7-45.3L290.7 18.7C278.7 6.7 262.5 0 245.5 0L96 0zM357.5 176L264 176c-13.3 0-24-10.7-24-24L240 58.5 357.5 176zM240 380c-11 0-20 9-20 20l0 128c0 11 9 20 20 20s20-9 20-20l0-28 12 0c33.1 0 60-26.9 60-60s-26.9-60-60-60l-32 0zm32 80l-12 0 0-40 12 0c11 0 20 9 20 20s-9 20-20 20zm96-80c-11 0-20 9-20 20l0 128c0 11 9 20 20 20l32 0c28.7 0 52-23.3 52-52l0-64c0-28.7-23.3-52-52-52l-32 0zm20 128l0-88 12 0c6.6 0 12 5.4 12 12l0 64c0 6.6-5.4 12-12 12l-12 0zm88-108l0 128c0 11 9 20 20 20s20-9 20-20l0-44 28 0c11 0 20-9 20-20s-9-20-20-20l-28 0 0-24 28 0c11 0 20-9 20-20s-9-20-20-20l-48 0c-11 0-20 9-20 20z'/%3E%3C/svg%3E");
    mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512'%3E%3Cpath d='M96 0C60.7 0 32 28.7 32 64l0 384c0 35.3 28.7 64 64 64l80 0 0-112c0-35.3 28.7-64 64-64l176 0 0-165.5c0-17-6.7-33.3-18.7-45.3L290.7 18.7C278.7 6.7 262.5 0 245.5 0L96 0zM357.5 176L264 176c-13.3 0-24-10.7-24-24L240 58.5 357.5 176zM240 380c-11 0-20 9-20 20l0 128c0 11 9 20 20 20s20-9 20-20l0-28 12 0c33.1 0 60-26.9 60-60s-26.9-60-60-60l-32 0zm32 80l-12 0 0-40 12 0c11 0 20 9 20 20s-9 20-20 20zm96-80c-11 0-20 9-20 20l0 128c0 11 9 20 20 20l32 0c28.7 0 52-23.3 52-52l0-64c0-28.7-23.3-52-52-52l-32 0zm20 128l0-88 12 0c6.6 0 12 5.4 12 12l0 64c0 6.6-5.4 12-12 12l-12 0zm88-108l0 128c0 11 9 20 20 20s20-9 20-20l0-44 28 0c11 0 20-9 20-20s-9-20-20-20l-28 0 0-24 28 0c11 0 20-9 20-20s-9-20-20-20l-48 0c-11 0-20 9-20 20z'/%3E%3C/svg%3E");
    -webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center
  }

  /* Legacy green submit buttons (Booking Calendar form + the old theme's .btn_apply) restyled
     to the redesign's primary button. Selector list mirrors the markup those plugins emit. */
  .vn-prose input[type="submit"],.booking_form_div button,.booking_form_div input[type="submit"],.btn_apply,button.btn_apply{
    display:inline-flex;align-items:center;justify-content:center;gap:.5em;min-height:44px;padding:.75em 1.25em;
    border-radius:var(--vn-radius);border:1px solid var(--vn-accent);background:var(--vn-accent);color:#fff;
    font-family:var(--vn-font-body);font-weight:600;text-decoration:none;text-shadow:none;cursor:pointer
  }
  .vn-prose input[type="submit"]:hover,.booking_form_div button:hover,.booking_form_div input[type="submit"]:hover,.btn_apply:hover,button.btn_apply:hover{
    background:var(--vn-accent-hover);border-color:var(--vn-accent-hover);color:#fff
  }

  /* ── Prose sidebar: any page whose content carries a .vn-aside (task 17) ───────────────────
     Joe's approved mock, dev/screenshots/about-aside-mock.png. /over-ons/ is one column of prose
     with the family photographs floated into it and stacked at its foot, so at desktop widths the
     right half of the page is empty. The photographs move into an <aside> at the END of the
     content — the classic editor can only author a linear document, there is no way to write two
     columns in it — and these rules put that aside in a second column beside the WHOLE text.

     Nothing below names /over-ons/: the condition is "a .vn-prose with a direct .vn-aside child",
     so any page grows a sidebar by ending its content with the same block.

     :has() is a SELECTOR, not a media query and not script — Baseline since Firefox 121
     (Dec 2023), i.e. every current browser. A browser that does not know it drops the rule and
     gets the single-column page that exists below 1024px anyway: the right fallback, not a broken
     one.

     This block sits at the END of @layer components, after the legacy .vn-prose rules, because
     two of those TIE with it on specificity and source order is what settles the tie — see the
     notes on the two rules concerned. Moving it earlier silently un-does both.

     Phone/tablet first: the aside is a stack of figures under the text, capped at 480px so that a
     single photograph never becomes an 850px slab on a tablet (the container is 952px wide at a
     1000px viewport). A TWO-column arrangement was built first and measured at 390px and
     rejected: the figures are portrait / 3-up band / 3-up band / wide, the two bands have to span
     both columns, and grid auto-placement therefore leaves each of the two SINGLE photographs
     alone in a half-width cell with an empty cell beside it. Pairing them would need a CSS
     `order` that contradicts the reading order of the markup. */
  .vn-aside{display:grid;gap:18px;max-width:480px;margin-block:var(--vn-space-6) 0}
  /* (0,3,1), to MATCH `.vn-prose figure:not(.gallery):not(.gallery-item)` above — which gives
     every prose figure a 16px bottom margin. Here the flex/grid `gap` IS the spacing model, so
     that margin has to go, and only an equal-or-higher specificity later in the file removes it.
     Hence also the class on every figure in the content: `.vn-prose .vn-aside figure` alone is
     (0,2,1) and would lose outright. */
  .vn-prose .vn-aside figure.vn-aside__figure{margin:0}
  /* `margin:0` for the same kind of reason: the <=639.98px legacy rule gives every size-medium
     image a 16px bottom margin, which would open a gap inside each 3-up grid on a phone. That
     rule is (0,2,1); this one ties and comes later. */
  .vn-prose .vn-aside img{display:block;width:100%;height:auto;margin:0;border-radius:var(--vn-radius)}
  .vn-prose .vn-aside figcaption{margin-block-start:6px;font-size:14px;line-height:1.4;color:var(--vn-muted)}
  .vn-aside__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
  .vn-prose .vn-aside__grid img{aspect-ratio:1;object-fit:cover}
  /* The two single photographs are CROPPED, not letterboxed: the portrait to a 4:5 card (its
     source is a 4:3 landscape, and `center 15%` is what keeps heads clear of the top edge) and
     the bus to 16:9. Both are modifier classes rather than :first-child/:last-child, so the order
     of the figures inside an aside can change without silently re-cropping two of them. */
  .vn-prose .vn-aside__portrait img{aspect-ratio:4/5;object-fit:cover;object-position:center 15%}
  .vn-prose .vn-aside__wide img{aspect-ratio:16/9;object-fit:cover}

  @media (min-width:1024px){
    /* column-gap, NEVER the `gap` shorthand. The aside spans 999 implicit rows (see below), so a
       64px ROW gap is 998 gaps of 64px inside this one element: measured on dev before this was
       split, /over-ons/ computed to a 64,673px tall prose block. The vertical rhythm between the
       text blocks is their own margins, exactly as in the single-column layout, so the row gap
       has to be 0 here anyway. */
    .vn-prose:has(>.vn-aside){max-width:var(--vn-max);display:grid;grid-template-columns:minmax(0,1fr) 360px;column-gap:64px;align-items:start}
    /* Editor content is a FLAT list of blocks, so there is no wrapper element to put the text in:
       every direct child except the aside is placed in column 1 by hand. 62ch rather than the
       76ch the base rule gives a full-width prose column — the measure has to come down with the
       column, or the two columns simply meet in the middle. */
    .vn-prose:has(>.vn-aside)>:not(.vn-aside){grid-column:1;max-width:62ch}
    /* .vn-prose's clearfix ::after would be a grid ITEM here, i.e. an empty extra row in column 1.
       A grid item establishes its own formatting context, so a float inside one cannot escape it
       and there is nothing left for a clearfix to do. */
    .vn-prose:has(>.vn-aside)::after{content:none}
    /* `1 / span 999`: the aside must own the whole height of the text column, and the rows here
       are IMPLICIT (one per content block, count unknown at authoring time), so there is no `-1`
       line to span to — `-1` resolves against the explicit grid, which has exactly one row. The
       span is also what gives position:sticky room to travel: a sticky box is clamped to its
       containing block, and a grid item's containing block is its grid area.
       top = 88px (.vn-header__bar's own >=1024px min-height) + its 1px border + 16px of air. The
       header is position:sticky at top:0 on every page except the front page, so the aside has to
       clear it; dev/check-dev.sh asserts both numbers so the pair cannot drift apart unnoticed. */
    .vn-prose:has(>.vn-aside)>.vn-aside{grid-column:2;grid-row:1 / span 999;position:sticky;top:105px;display:flex;flex-direction:column;gap:18px;margin-block:0}
  }

  /* ── "Samen": a short film of the house with music, click to play (2026-09-28) ───────────────────
     template-parts/home/together.php (replaces the 2026-09-15 silhouette mockup). The stage keeps
     the mockup's shapes: square on phones, 16:9 from 1024px. The film exists in exactly those two
     cuts (<source media> picks one), so `object-fit: cover` only ever trims rounding, never people.
     Until the button is pressed only the poster exists; the <video> is [hidden] and preload=none. */
  .vn-together{padding-block:40px}
  .vn-together__head{display:flex;flex-direction:column;gap:10px;margin-block-end:18px}
  .vn-together__lead{margin:0;max-width:62ch;color:var(--vn-muted)}
  .vn-together__film{margin:0}
  .vn-together__stage{position:relative;aspect-ratio:1;overflow:hidden;border-radius:var(--vn-radius);background:var(--vn-line)}
  .vn-together__poster,.vn-together__video{position:absolute;inset:0;display:block;width:100%;height:100%;max-width:none;object-fit:cover}
  /* `display:block` above would beat the UA's [hidden]{display:none}; the video must stay out
     until the click, so the hidden state is restated at higher specificity. */
  .vn-together__video[hidden],.vn-together__play[hidden]{display:none}
  .vn-together__play{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:#fff;text-decoration:none;background:linear-gradient(rgb(0 0 0 / 0) 35%,rgb(0 0 0 / .38))}
  .vn-together__play-disc{display:grid;place-items:center;width:76px;height:76px;border-radius:50%;background:rgb(255 255 255 / .92);color:var(--vn-accent);box-shadow:0 6px 24px rgb(0 0 0 / .25);transition:scale .2s var(--vn-ease)}
  /* The triangle's optical centre sits left of its box: nudge it right so the disc looks centred. */
  .vn-together__play-disc .vn-icon{width:26px;height:26px;margin-inline-start:5px}
  .vn-together__play:hover .vn-together__play-disc{scale:1.06}
  .vn-together__play:focus-visible{outline:3px solid #fff;outline-offset:-8px}
  /* A pill, not bare text: the label sits mid-frame, over whatever the poster shows there. */
  .vn-together__play-label{padding:6px 14px;border-radius:999px;background:rgb(0 0 0 / .5);font-size:var(--vn-step--1);font-weight:600}
  .vn-together__note{margin-block-start:8px;font-size:var(--vn-step--1);color:var(--vn-muted)}
  @media (min-width:1024px){
    .vn-together{padding-block:72px}
    .vn-together__stage{aspect-ratio:16/9}
  }
}
@layer utilities {
  .vn-visually-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
  /* Skip-link reveal. MUST live in this layer, after .vn-visually-hidden: utilities is the
     last layer, so a rule in `components` would lose to the utility's width/height/clip no
     matter how specific it is. Within this layer the extra :focus-visible beats it on
     specificity — except for `position`, which the utility marks !important; that is the ONE
     justified !important in this stylesheet (only an !important can beat an !important). */
  .vn-skip:focus-visible{position:fixed!important;top:8px;left:8px;width:auto;height:auto;clip:auto;padding:8px 12px;background:var(--vn-surface);color:var(--vn-accent);z-index:100}
}

/* ── SiteOrigin widget colours (UNLAYERED — read this before moving it) ───────────────────────
   The only block in this file that sits outside @layer, and it has to. Each SiteOrigin widget
   instance ships its OWN generated stylesheet in uploads — e.g.
   wp-content/uploads/siteorigin-widgets/sow-tabs-default-c1ed8dfad016-594.css, which paints the
   Contact page's tab bar `background-color:#1e73be` — and those <link>s are UNLAYERED. Per the
   CSS Cascade Layers spec any unlayered normal declaration beats ANY layered one regardless of
   specificity, so a rule in `components` (where the rest of the legacy CSS lives, and where it
   stays) loses to them by definition. This is the "fix it at the layer level, do not reach for
   !important" case the note above .vn-btn--primary anticipates: the fix is to move OUT of the
   layers, not to spend from the 5-!important budget dev/check-dev.sh enforces.

   Everything here is scoped to `.vn-prose` (so it can only reach legacy page content) and carries
   one class more than the plugin's own selector, so it also wins on specificity — not merely on
   source order, which would be fragile if an optimiser ever reordered the stylesheets. The stable
   `so-widget-sow-tabs` / `so-widget-sow-accordion` classes are used, never the per-instance
   `…-c1ed8dfad016-594` hash, which changes whenever the widget is re-saved.

   Palette: the widget's blue chrome becomes the theme's charcoal (--vn-text) with white text; the
   selected/hovered tab becomes the surface colour so it reads as the active panel's continuation.
   All of this retires with the legacy content in plan 04. */
.vn-prose .so-widget-sow-tabs .sow-tabs .sow-tabs-tab-container{background-color:var(--vn-text)}
.vn-prose .so-widget-sow-tabs .sow-tabs .sow-tabs-tab-container .sow-tabs-tab{color:#fff}
.vn-prose .so-widget-sow-tabs .sow-tabs .sow-tabs-tab-container .sow-tabs-tab.sow-tabs-tab-selected,
.vn-prose .so-widget-sow-tabs .sow-tabs .sow-tabs-tab-container .sow-tabs-tab:hover{background-color:var(--vn-surface);color:var(--vn-text)}
.vn-prose .so-widget-sow-tabs .sow-tabs .sow-tabs-panel-container{background-color:var(--vn-surface)}
.vn-prose .so-widget-sow-accordion .sow-accordion .sow-accordion-panel .sow-accordion-panel-header,
.vn-prose .so-widget-sow-accordion .sow-accordion .sow-accordion-panel .sow-accordion-panel-header:hover,
.vn-prose .so-widget-sow-accordion .sow-accordion .sow-accordion-panel .sow-accordion-panel-header:focus{background-color:var(--vn-text);color:#fff}

/* Legacy lime SiteOrigin buttons (task 12 fix round 1, owner request) — the SiteOrigin Button
   widget (e.g. "NU BOEKEN" under the calendar on /vakantiehuis/, /vakantiehuis-6-personen/, all
   four languages) ships its OWN generated per-instance stylesheet, same as the tabs/accordion
   above, e.g. wp-content/uploads/siteorigin-widgets/sow-button-atom-fc004d6a6245-650.css, painting
   the plugin's default lime green (#81d742). No !important needed: its base-colour rule is
   `.so-widget-sow-button-atom-<hash> .ow-button-base .sowb-button` — 3 class/attribute selectors —
   and its hover/active rule adds `.ow-button-hover:hover`/`:active` for 5. Both are unlayered AND
   load after main.css in <head> (confirmed on all 8 pages), so an equal-specificity rule here would
   still lose on source order. Using the STABLE `.so-widget-sow-button` wrapper class (never the
   per-instance hash, which changes on every re-save — same reasoning as the tabs override) plus
   `.vn-prose` and the widget's own inner classes gives 4 for the base rule and 6 for hover/active —
   enough to win outright on specificity, order irrelevant. Verified: all 8 pages (both listing
   pages x 4 languages) use this same "atom" widget style with an identical rule shape, only the
   instance hash differs. */
.vn-prose .so-widget-sow-button .ow-button-base .sowb-button{
  background:var(--vn-accent);background-image:none;color:#fff;border:0;
  border-radius:var(--vn-radius);padding:.75em 1.25em;min-height:44px;
  font-weight:600;text-shadow:none;box-shadow:none
}
.vn-prose .so-widget-sow-button .ow-button-base .sowb-button.ow-button-hover:hover,
.vn-prose .so-widget-sow-button .ow-button-base .sowb-button.ow-button-hover:active{
  background:var(--vn-accent-hover);background-image:none;color:#fff
}

/* ── Deliberately no .mfp-img override here ───────────────────────────────────────────────────
   Considered and REJECTED: `.mfp-content img.mfp-img{max-width:none;height:auto}` (an unlayered
   rule, to out-specificity Magnific's own unlayered img.mfp-img{width:auto;max-width:100%;
   height:auto;...} shipped in wp-content/plugins/photoblocks-grid-gallery/public/css/
   magnific-popup.css). Measured on dev at 390x844: Magnific's JS sets ONLY an inline
   `max-height` on .mfp-img (never an inline width) and relies on its own CSS's
   width:auto;max-width:100% to cap the horizontal side while the image's natural aspect ratio
   does the rest — that IS the library's fit algorithm, not a conflict with this theme. Setting
   max-width:none removes the only thing capping the width, so the image scales to fill the
   inline max-height and overflows sideways (measured: 1018px wide in a 390px viewport). So
   nothing in this stylesheet touches .mfp-img at all: per the cascade-layers spec an unlayered
   plugin rule already beats every @layer above regardless of !important or specificity, and
   here that is correct — Magnific's own sizing already centers and fits the popup correctly
   (measured: 378x363.5 centered in the 390x844 viewport with the reset's display:block removed
   above). If a real centering/sizing defect reappears, diagnose the ACTUAL computed styles and
   inline attributes on .mfp-img/.mfp-content first (see this comment) before overriding either
   — don't re-add a blanket max-width:none. Both plugins retire in plan 05. */
