/* ============================================================
   Jnane Oum El Ghite — design system
   Marrakech editorial luxury. Alabaster · olive-espresso · antique brass.
   Display: Bodoni Moda (couture didone) · Serif: Cormorant · Utility: Inter
   Signature: the segmental arch (a window onto the domaine).
   ============================================================ */

:root {
  /* Palette */
  --alabaster:  #F4F0E7;   /* page ground */
  --alabaster-2:#FBF8F1;   /* lifted panels */
  --ivory:      #FFFFFF;
  --bone:       #EFE9DC;   /* subtle blocks */
  --ink:        #22261D;   /* olive-espresso near-black */
  --ink-soft:   #4C4F44;   /* body text (AA on light) */
  --stone:      #6E6858;   /* muted / captions (AA on light) */
  --olive:      #232A1E;   /* deep sections */
  --olive-2:    #1A2016;
  --brass:      #846529;   /* precious accent — hairlines, labels, numerals (AA on cream) */
  --brass-soft: #C6A56A;
  --line:       rgba(34,38,29,.14);
  --line-2:     rgba(34,38,29,.08);
  --line-light: rgba(255,255,255,.16);
  --wa:         #1F7A54;   /* muted heritage green, not neon */

  /* Type */
  --display: "Bodoni Moda", "Playfair Display", Georgia, serif;
  --serif:   "Cormorant Garamond", Georgia, serif;
  --sans:    "Inter", ui-sans-serif, system-ui, sans-serif;

  /* Layout */
  --container: 1240px;
  --container--wide: 1460px;
  --gutter: clamp(1.5rem, 5vw, 4rem);
  --arch: 50% 50% 6px 6px / 26% 26% 0 0;   /* segmental arch radius */
  --shadow: 0 40px 90px -50px rgba(34,38,29,.55);
  --shadow-sm: 0 20px 50px -30px rgba(34,38,29,.4);
  /* ---- ONE curve. Every transition and reveal in the 300–1200 ms band uses it.
     --ease is kept as an alias so existing declarations keep working unchanged.
     (Only the ambient long loops — kenburns 22s, dotbreathe 3.8s, the 6.5s proof
     pan — and the 0s CSS failsafes stay off it: they are outside the band.) ---- */
  --ease-expo: cubic-bezier(.16, 1, .3, 1);
  --ease: var(--ease-expo);
  /* THE ONE DOCUMENTED EXEMPTION, and it is a token so it can be counted.
     A 3,8 s breathe is a symmetric loop with no arrival: the house curve is an
     entrance curve (it decelerates into a destination), and linear — the right
     answer for the 22 s Ken Burns drift, which IS constant velocity — turns a
     breathe into a metronome blink. Both « live » dots (.dot--live and the
     #domaine note dot) reference this token, so the site carries ONE entrance
     curve, one drift (linear) and one breathe, not stray easings. */
  --ease-breathe: ease-in-out;
  /* the signature masked rise, one set of numbers for every heading on the site */
  --rise-dur: .9s;
  --rise-stagger: .06s;
  /* --- Spacing scale (normalizes existing renders; tokens byte-match the previous clamps) ---
     --sp-section   section vertical padding (.section)
     --gap-cols     two-column editorial gap (df-grid + #design copy, cf-grid, faq-grid, #domaine .feature)
     --gap-cols-sm  single-column collapse gap of the shared df component (global + #design copy only)
     --gap-head     asymmetric two-part header gap (gal-head, spec-head, dist-head)
     --foot-mt / --foot-pt  hairline-topped closing rails (gal-foot, faq-foot, v-foot, dist-coda)
     --sticky-top   sticky-lede offset clearing the fixed nav (cf-head, faq-lede) */
  --sp-section: clamp(5rem, 12vw, 11rem);
  --gap-cols: clamp(2.5rem, 6vw, 6.5rem);
  --gap-cols-sm: clamp(2.4rem, 8vw, 3.4rem);
  --gap-head: clamp(1.5rem, 5vw, 5rem);
  --foot-mt: clamp(3rem, 6vw, 5rem);
  --foot-pt: clamp(2rem, 4vw, 3rem);
  --sticky-top: clamp(5rem, 10vw, 7.5rem);
  /* --gap-lede  the one h2 → lede joint, shared by every chapter. Deliberately
     smaller than the 32px eyebrow → h2 lockup, so more air always sits ABOVE a
     heading than below it and the heading belongs to what follows. */
  --gap-lede: 1.5rem;
  /* Breakpoints: 380 gallery-1col · 480 consent · 620 phone · 700 mobile-hero/video contract · 820 mobilebar · 860 tablet stack · 940 grid density · 980 nav links · 1024 motion gate */
}

/* ---------- Reset / base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
/* Lenis (desktop) / native (mobile) manage smoothing — CSS smooth would conflict with Lenis */
body {
  margin: 0; font-family: var(--sans); color: var(--ink); background: var(--alabaster);
  font-size: 17px; line-height: 1.7; font-weight: 300; letter-spacing: .002em;
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; overflow-x: hidden;
  /* elite type polish — optical sizing for the variable serifs, real kerning/ligatures, hung punctuation */
  font-optical-sizing: auto; font-feature-settings: "kern" 1, "liga" 1, "calt" 1; hanging-punctuation: first allow-end last;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
::selection { background: var(--brass); color: #fff; }

/* ---- Accessibility: visible focus + skip link ---- */
:focus-visible { outline: 2px solid var(--brass); outline-offset: 3px; }
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--brass); outline-offset: 3px; border-radius: 1px;
}
.field input:focus-visible, .field select:focus-visible { outline-offset: 4px; }
.skip-link {
  position: fixed; top: -60px; left: 1rem; z-index: 200;
  background: var(--ink); color: #F4F0E7; padding: .8rem 1.4rem;
  font-size: .74rem; letter-spacing: .16em; text-transform: uppercase;
  transition: top .3s var(--ease);
}
.skip-link:focus { top: 1rem; }

/* ---- Cookie consent (GDPR — for EU/MRE paid traffic) ---- */
/* It is revealed on the first scroll out of the hero, never at first paint — see
   wireConsent() in app.js. Nothing is tracked before consent, so the prompt costs
   nothing by waiting, and the hero's two CTAs stay tappable on landing at every width
   (at 390 the card used to sit exactly on both of them). */
.consent { position: fixed; left: 1rem; right: 1rem; bottom: 1rem; z-index: 60; max-width: 620px; margin-inline: auto; background: var(--alabaster-2); border: 1px solid var(--line); box-shadow: var(--shadow-sm); padding: 1rem 1.25rem; display: flex; flex-wrap: wrap; align-items: center; gap: .85rem 1.4rem; opacity: 0; transform: translateY(10px); transition: opacity .5s var(--ease-expo), transform .5s var(--ease-expo); }
.consent.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .consent { transition: none; } }
.consent[hidden] { display: none; }   /* the display:flex above would otherwise defeat the hidden attribute */
.consent__txt { margin: 0; font-size: .82rem; line-height: 1.5; color: var(--ink-soft); flex: 1 1 280px; }
.consent__txt a { color: var(--brass); border-bottom: 1px solid var(--line); }
.consent__actions { display: flex; gap: .6rem; flex: 0 0 auto; }
.consent .btn { padding: .72rem 1.25rem; font-size: .66rem; }
.consent .btn::before { content: ""; position: absolute; inset: -5px; }
@media (max-width: 820px) { .consent { bottom: 74px; padding: .9rem 1.05rem; gap: .7rem 1rem; } }
@media (max-width: 480px) { .consent__actions { width: 100%; } .consent__actions .btn { flex: 1; } .consent__txt { font-size: .78rem; } }

h1,h2,h3,h4 { margin: 0; font-family: var(--display); font-weight: 400; line-height: 1.15; letter-spacing: -.01em; color: var(--ink); font-optical-sizing: auto; }

/* ---------- Typography ---------- */
h1,.h1 { font: 300 clamp(2.9rem, 7vw, 6rem)/1.05 var(--display); letter-spacing: -.02em; }
h2,.h2 { font: 400 clamp(2.1rem, 4.6vw, 3.7rem)/1.10 var(--display); letter-spacing: -.02em; }
h3,.h3 { font: 400 clamp(1.3rem, 2.3vw, 1.85rem)/1.15 var(--display); }
/* MANDATORY numerals + balance — the one property that makes a didone’s figures
   read as designed rather than as leftovers. Kept after the font shorthands above
   because `font:` resets font-variant / font-feature-settings. liga+calt are carried
   through so the display serif keeps its standard ligatures. */
h1,h2,h3,.h1,.h2,.h3,.price,.stat,.fact { font-variant-numeric: lining-nums tabular-nums;
                              font-feature-settings: "lnum" 1,"tnum" 1,"kern" 1,"liga" 1,"calt" 1; }
h1,h2,h3,.h1,.h2,.h3 { text-wrap: balance; }
/* UNIT SYMBOLS. In French/SI typography a unit symbol is case-sensitive — « m² »,
   « h », « min », « km » are never capitalised, whatever the type treatment around
   them does. Any spec line set in uppercase wraps its units in .u, and the declared
   value beats the parent’s inherited text-transform without touching its tracking. */
.u { text-transform: none; }
.display { font-family: var(--display); }
.lead { font-family: var(--serif); font-size: clamp(1.25rem, 2.3vw, 1.7rem); line-height: 1.5; color: var(--ink-soft); font-weight: 400; letter-spacing: .005em; text-wrap: pretty; }
/* one relationship, one value: the chapter title and its lede are a single lockup */
.section h2 + .lead { margin-top: var(--gap-lede); }
p { margin: 0 0 1.1rem; color: var(--ink-soft); font-weight: 300; text-wrap: pretty; }
/* display figures outside the heading set keep lining figures at their natural widths */
.pricestrip .ps b { font-variant-numeric: lining-nums proportional-nums; }
.spec-row .spec b { font-variant-numeric: tabular-nums; }
.muted { color: var(--stone); }

/* Section head, one order everywhere: eyebrow → 34 px brass rule → h2 → optional italic lede.
   The rule is the eyebrow’s ::before, so the order can never drift section to section. */
.eyebrow {
  font-family: var(--sans); text-transform: uppercase; letter-spacing: .42em;
  font-size: .68rem; font-weight: 400; color: var(--brass); margin: 0 0 1.6rem;
  display: inline-flex; align-items: center; gap: .9rem;
}
.eyebrow::before { content: ""; width: 34px; height: 1px; background: var(--brass); opacity: .6; flex: none; }
.eyebrow--center { justify-content: center; }
.eyebrow--light { color: var(--brass-soft); }

/* ---------- Layout ---------- */
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.container--wide { max-width: var(--container--wide); }
.section { padding-block: var(--sp-section); position: relative; }
.section--white  { background: var(--ivory); }
.section--cream,.section--cream2 { background: var(--alabaster); }
.section--forest { background: var(--olive); color: #E7E2D4; }
.section--forest h1,.section--forest h2,.section--forest h3 { color: #FBF8F1; }
.section--forest p,.section--forest .lead { color: #CFC9B8; }
.center { text-align: center; }
.rule { height: 1px; background: var(--line); border: 0; margin: 0; }
.rule--brass { background: linear-gradient(90deg, var(--brass), transparent); height: 1px; }

.grid { display: grid; gap: clamp(1.5rem, 3vw, 2.75rem); }
.grid-3 { grid-template-columns: repeat(3,1fr); }
.grid-4 { grid-template-columns: repeat(4,1fr); }
@media (max-width: 940px){ .grid-3,.grid-4 { grid-template-columns: repeat(2,1fr); } }
@media (max-width: 620px){ .grid-3,.grid-4 { grid-template-columns: 1fr; } }

/* ---------- Buttons (thin, tracked, restrained) ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .7rem;
  padding: 1.15rem 2.3rem; font-family: var(--sans); font-size: .74rem; font-weight: 500;
  letter-spacing: .22em; text-transform: uppercase; border: 1px solid transparent;
  border-radius: 0; transition: all .45s var(--ease); line-height: 1; white-space: nowrap; position: relative;
}
/* brass ink-sweep: the hover fills from the left like a pen stroke, not a color swap */
.btn-primary { background: linear-gradient(var(--brass), var(--brass)) no-repeat left center / 0% 100%, var(--ink); color: #F4F0E7; }
.btn-primary:hover { background-size: 100% 100%; color: #fff; }
/* dark ("forest") sections: the ink CTA is near-invisible on olive at rest — fill it brass so it reads on touch, without a hover the finger never triggers */
.section--forest .btn-primary { background: var(--brass); color: #1A2016; border-color: var(--brass); }
.section--forest .btn-primary:hover { background: #8f6f2f; color: #fff; }
.btn-outline { background: transparent; color: var(--ink); border-color: var(--line); }
.btn-outline:hover { border-color: var(--brass); color: var(--brass); }
.btn-ghost { background: transparent; color: currentColor; border: 1px solid currentColor; opacity: .9; }
.btn-ghost:hover { opacity: 1; border-color: var(--brass-soft); color: var(--brass-soft); }
/* WhatsApp — understated, NOT neon green */
.btn-whatsapp { background: transparent; color: currentColor; border: 1px solid currentColor; }
.btn-whatsapp:hover { opacity: 1; border-color: var(--wa); color: var(--wa); }
.section--forest .btn-whatsapp, .hero .btn-whatsapp { color: #F4F0E7; }
.btn-block { width: 100%; }
.btn svg { width: 1em; height: 1em; opacity: .8; }

/* ---------- Chips / labels ---------- */
.chip { display: inline-flex; align-items: center; gap: .6rem; padding: .55rem 1.1rem; border: 1px solid var(--line); font-size: .74rem; font-weight: 400; letter-spacing: .04em; color: var(--ink-soft); background: transparent; }
.chip--light { border-color: var(--line-light); color: #E7E2D4; }
.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brass); }
/* quiet, refined "live" indicator — a slow brass breathe, not a notification-badge pulse */
.dot--live { background: var(--brass); animation: dotbreathe 3.8s var(--ease-breathe) infinite; }
@keyframes dotbreathe { 0%,100%{ opacity: 1; } 50%{ opacity: .3; } }
@media (prefers-reduced-motion: reduce){ .dot--live { animation: none; } }

/* ---------- Stat blocks ---------- */
.stat { padding: 1.75rem .5rem; }
.stat .num { font-family: var(--display); font-size: clamp(1.9rem,3.6vw,2.9rem); color: var(--ink); line-height: 1; font-weight: 400; }
.section--forest .stat .num { color: #FBF8F1; }
.stat .lbl { font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; color: var(--stone); margin-top: .85rem; }
.section--forest .stat .lbl { color: #A8A895; }

/* ---------- Cards ---------- */
.card { background: transparent; border: 0; overflow: visible; transition: transform .6s var(--ease); }
.card:hover { transform: translateY(-6px); }
/* translateZ(0) promotes a compositing layer so overflow:hidden + border-radius actually
   clips the hover-zoomed <img> at the ARCH corners (without it the scaled image bleeds
   a hair past the rounded edge — the "image gets out of the box on hover" tell) */
.card__media { position: relative; aspect-ratio: 4/5; overflow: hidden; border-radius: var(--arch); background: var(--bone); transform: translateZ(0); backface-visibility: hidden; }
/* the image also rounds itself to the arch (border-radius: inherit) so the scaling
   element clips its OWN corners in every browser — no bleed past the arch edge */
.card__media img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; transition: transform 1.2s var(--ease); }
.card:hover .card__media img { transform: scale(1.06); }
.card__body { padding: 1.4rem .25rem 0; }

.spec-row { display: flex; flex-wrap: wrap; gap: .5rem 1.4rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line-2); }
.spec-row .spec { font-size: .82rem; color: var(--stone); font-weight: 300; }
.spec-row .spec b { color: var(--ink); font-weight: 500; }

/* ---------- Feature (image + text), editorial ---------- */
.feature { display: grid; grid-template-columns: 1.05fr 1fr; gap: clamp(2rem,6vw,6rem); align-items: center; }
@media (max-width: 860px){ .feature { grid-template-columns: 1fr; } }
.feature__media { position: relative; }
/* display:block removes the inline descender gap so the edge overlay hugs the image exactly */
.feature__media img { display: block; width: 100%; aspect-ratio: 4/5; object-fit: cover; box-shadow: var(--shadow-sm); }
.feature__media--arch img { border-radius: var(--arch); }

/* ---------- Elite arch edge — a permanent, whisper-fine finish on every arched image ----------
   An overlay that traces the exact arch curve above the image: a hairline of brass on
   the very edge + a faint ivory catch-light just inside it. It masks the 1px background
   fringe that curved overflow-clips can leave, and reads as a crafted, framed edge at
   rest — not only on hover. Section-level hover rings (villas/materiaux ::after) layer on
   top of this cleanly. */
.card__media, .feature__media--arch { position: relative; }
.card__media::before,
.feature__media--arch::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  border-radius: var(--arch);
  box-shadow:
    inset 0 0 0 1px rgba(132,101,41,.24),
    inset 0 0 0 3px rgba(244,240,231,.06);
  /* clean, GPU-composited edge — sharper anti-aliasing on the curve */
  backface-visibility: hidden;
}

/* ---------- Editorial framed-arch feature (shared: Design / Patrimoine / Localisation) ----------
   Promoted from the Design & architecture section so the three read as one system.
   `.df-grid` = text left / visual right; `.df-grid--rev` mirrors it (visual left / text
   right) while keeping each section's own orientation. #design keeps its own scoped copy
   (higher specificity) so it stays byte-identical; these globals drive the other two. */
.df-grid {
  display: grid;
  grid-template-columns: 1fr 1.06fr;
  gap: var(--gap-cols);
  align-items: center;
}
.df-grid--rev { grid-template-columns: 1.06fr 1fr; }   /* visual leads on the left */

.df-text { max-width: 42rem; }
.df-text h2 { font-size: var(--h2-chapter); letter-spacing: -.01em; margin-top: .4rem; }
.df-text h2 em { font-style: italic; color: var(--brass); font-family: var(--serif); font-weight: 500; }
.df-text p:not(.lead) { max-width: 36rem; }

/* grace-note divider — a whispered zellige diamond turning prose into spec */
.df-div {
  position: relative; height: 1px; max-width: 34rem; margin: 2.6rem 0 2.1rem;
  background: linear-gradient(90deg, var(--line) 0%, var(--line) 72%, transparent 100%);
}
.df-div::before {
  content: ""; position: absolute; left: 0; top: 50%;
  width: 7px; height: 7px; background: var(--brass);
  transform: translate(-3px, -50%) rotate(45deg);
}
.df-div::after {
  content: ""; position: absolute; left: 3.5px; top: 50%;
  width: 15px; height: 15px; border: 1px solid var(--brass); opacity: .4;
  transform: translate(-50%, -50%) rotate(45deg);
}

.df-text .icon-list { margin-top: 0; max-width: 34rem; }
.df-text .icon-list b { color: var(--ink); font-weight: 500; letter-spacing: .006em; }

/* refined pullquote — the brass border + guillemets carry the quote; the oversized
   didone “ ornament rendered as detached grey dots at display sizes, so it's gone */
.df-quote { position: relative; margin: 2.8rem 0 0; padding-left: 1.7rem; border-left: 2px solid var(--brass); }
.df-quote blockquote {
  margin: 0; color: var(--ink); font-style: italic;
  font-size: clamp(1.45rem, 2.4vw, 2.05rem); line-height: 1.32;
}

/* understated trust line closing the column */
.df-note {
  position: relative; margin: 1.9rem 0 0; padding-left: 1.3rem;
  font-family: var(--serif); font-style: italic; font-size: 1rem;
  color: var(--stone); letter-spacing: .01em;
}
.df-note::before {
  content: ""; position: absolute; left: 1px; top: .5em;
  width: 5px; height: 5px; transform: rotate(45deg); background: var(--brass);
}

/* the visual: arched image with an offset brass hairline frame */
.df-visual { position: relative; }
.df-frame {
  position: absolute; inset: -18px;
  border: 1px solid var(--brass); border-radius: var(--arch);
  opacity: .42; z-index: 0; pointer-events: none;
  transition: opacity .6s var(--ease);
}
.df-visual .feature__media { position: relative; z-index: 1; }
.df-visual .feature__media img { aspect-ratio: 4 / 5; transition: scale 1s var(--ease); }
/* slow breathe on hover — 5%, matching the card idiom (1.06); animates the
   independent `scale` property so it composes with the GSAP parallax (yPercent) */
.df-visual:hover .feature__media img { scale: 1.05; }
.df-visual:hover .df-frame { opacity: .68; }

@media (max-width: 860px) {
  .df-grid, .df-grid--rev { grid-template-columns: 1fr; gap: var(--gap-cols-sm); }
  .df-visual { order: -1; }                 /* image leads on mobile */
  .df-text, .df-text p:not(.lead), .df-text .icon-list, .df-div { max-width: none; }
  .df-frame { inset: -12px; }
  .df-visual .feature__media img { aspect-ratio: 4 / 3.4; max-height: 72vh; }
}
@media (max-width: 620px) {
  .df-frame { inset: -9px; }
  .df-div { margin: 2.2rem 0 1.8rem; }
  .df-quote { padding-left: 1.3rem; }
}
@media (hover: none) { .df-visual:hover .feature__media img { scale: 1; } }
@media (prefers-reduced-motion: reduce) { .df-visual:hover .feature__media img { scale: 1; } }

/* ---------- Forms (underline, not boxes) ---------- */
.form { display: grid; gap: 1.6rem; }
.field { display: grid; gap: .5rem; }
.field label { font-size: .7rem; letter-spacing: .22em; text-transform: uppercase; color: var(--stone); font-weight: 500; }
.field input, .field select {
  /* ≥48px hit height on the money form — a mis-tap here costs a lead */
  width: 100%; min-height: 48px; padding: .85rem 0; font-family: var(--sans); font-size: 1rem; color: var(--ink); font-weight: 300;
  background: transparent; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; transition: border-color .4s var(--ease);
}
/* :focus-not-:focus-visible — the mouse case only. Written as :focus:not(:focus-visible)
   because a bare `.field input:focus { outline:none }` ties on specificity with the
   global `.field input:focus-visible` rule 260 lines above and, being later, won
   the cascade: the keyboard ring was being removed from every field on the lead
   form. The brass underline stays on both paths. */
.field input:focus, .field select:focus { border-bottom-color: var(--brass); }
.field input:focus:not(:focus-visible), .field select:focus:not(:focus-visible) { outline: none; }
.field select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23A9854A' fill='none'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right .2rem center; }
.form-note { font-size: .78rem; color: var(--ink-soft); font-weight: 300; }

/* ---------- Sticky nav ---------- */
/* ad deep-links (/#contact), skip-link and hash restores use NATIVE anchoring —
   without this every section heading lands hidden under the fixed nav */
main section[id], main [id] { scroll-margin-top: 92px; }
.nav { position: fixed; top: 0; left: 0; right: 0; z-index: 50; transition: background .6s var(--ease), box-shadow .6s var(--ease), padding .6s var(--ease), color .6s var(--ease); padding: 1.6rem 0; color: #F4F0E7; }
.nav__inner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
/* the brand must never absorb the row's shrinkage — flex computed min-width:0 on the
   logo img and crushed it to 1.5px at 1200 before the actions column yielded */
.nav__inner > a:first-child, .nav__brand { flex: 0 0 auto; }
.nav__logo { height: 40px; width: auto; flex-shrink: 0; }
.nav__logo--dark { display: none; }
.nav__actions { display: flex; align-items: center; gap: 1.1rem; }
.nav--solid { color: var(--ink); background: rgba(244,240,231,.9); backdrop-filter: blur(14px); box-shadow: 0 1px 0 var(--line-2); padding: .95rem 0; }
.nav--solid .nav__logo--light { display: none; }
.nav--solid .nav__logo--dark { display: block; }
.nav--solid .nav__link { color: var(--ink); }
.nav__links { display: flex; gap: 1.7rem; }
.nav__link { font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: #F4F0E7; font-weight: 400; opacity: .85; transition: opacity .3s var(--ease-expo), color .3s var(--ease-expo); white-space: nowrap; }
.nav__link:hover { opacity: 1; color: var(--brass-soft); }
/* transparent-nav links float over the bright sunset — a whisper of shadow keeps the tracked labels above AA; auto-drops once the nav goes solid ink-on-cream */
.nav:not(.nav--solid) .nav__link, .nav:not(.nav--solid) .nav__cta-label { text-shadow: 0 1px 12px rgba(20,26,16,.55); }

/* real JOEG logo lockup (emblem + calligraphy + wordmark, ~3.14:1) */
.intro__logo { height: 76px; width: auto; opacity: 0; animation: introfade 1.6s var(--ease) forwards; }
@media (max-width: 720px) { .intro__logo { height: 60px; } }
@media (max-width: 980px){ .nav__links { display: none; } .nav__cta-label { display: none; } }

/* ---------- Sticky mobile action bar ---------- */
.mobilebar { position: fixed; bottom: 0; left: 0; right: 0; z-index: 50; display: none; grid-template-columns: 1fr 1fr 1fr; background: rgba(251,248,241,.97); backdrop-filter: blur(10px); box-shadow: 0 -1px 0 var(--line); }
.mobilebar a { display: flex; flex-direction: column; align-items: center; gap: .3rem; padding: .8rem; font-size: .62rem; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; color: var(--ink); border-right: 1px solid var(--line-2); }
.mobilebar a:last-child { border-right: none; }
.mobilebar a svg { width: 19px; height: 19px; opacity: .85; }
.mobilebar a.is-primary { background: var(--ink); color: #F4F0E7; }
/* WhatsApp is the PRIMARY action in this funnel — filled brass, the brightest, most
   tappable slot; « Appeler » and « Visite » read as neutral secondary options */
.mobilebar a.is-wa { background: var(--brass); color: #fff; }
.mobilebar a.is-wa svg { opacity: 1; }
.mobilebar a.is-wa:active { background: var(--brass-deep, #7C5E28); }
@media (max-width: 820px){ .mobilebar { display: grid; } body { padding-bottom: 68px; } /* .mobilebar renders 66.3px tall — reserve must clear it */ }

/* ---------- Floating WhatsApp (desktop) — understated ---------- */
/* .wa-float retired — the desktop-only WhatsApp disc duplicated the nav's WhatsApp
   pill at every width it was visible at. One channel, one entry point per viewport:
   the nav pill on desktop, .mobilebar below 820 px. */

/* ============================================================
   ONE MOTION LANGUAGE — the masked rise
   ------------------------------------------------------------
   Everything that enters the page enters the same way: it is held behind a
   mask and rises out of it. Headings rise line by line (the hero h1 word by
   word); blocks and images rise as a whole. One curve (--ease-expo), one
   duration (--rise-dur), one stagger (--rise-stagger), one trigger (80 % of
   the viewport), fired once.

   Three earlier systems are gone, not layered over: the 1.1 s .reveal fade-up
   with its three delay classes and three directional variants, the 1.4 s
   .img-reveal clip+scale wipe, and the hero’s own 26 px translate entrance.

   PROGRESSIVE ENHANCEMENT: no scroll target is hidden by this stylesheet.
   motion.js sets the hidden state at runtime, and only for elements still
   below the fold — so no-JS, failed-JS and reduced-motion visitors get the
   page fully visible with no flash and nothing stuck at opacity 0. The one
   CSS-held state is the hero, which must be hidden on the very first frame
   and carries its own 3.4 s failsafe (below).
   ============================================================ */

/* The mask, and the word that rises out of it. Shared by the hero h1 (CSS-driven)
   and every other heading (GSAP-driven) so the device is literally one thing.
   The descender headroom lives on the INNER element and the accent headroom on
   the mask: that keeps « prix juste » and « À dix minutes » unsliced AND makes a
   travel of exactly 110 % clear the mask completely. The negative margins give
   the pair back its natural line box, so nothing in the layout moves. */
.rw  { display: inline-block; overflow: hidden; vertical-align: bottom;
       padding: .2em .05em 0; margin: -.2em -.05em -.22em; }
.rw > i { display: inline-block; font-style: inherit; padding-bottom: .22em; }

@media (prefers-reduced-motion: reduce){
  /* opacity only: no mask, no travel, nothing that moves */
  .rw { overflow: visible; padding: 0; margin: 0; }
  .rw > i { transform: none !important; padding-bottom: 0; }
  .reveal, .img-reveal img { opacity: 1 !important; transform: none !important; clip-path: none !important; }
  .hero .h-rise { opacity: 1 !important; transform: none !important; transition: none; animation: none !important; }
  html { scroll-behavior: auto; }
  .hero__bg img { animation: none !important; }
}

/* ---------- Hero ---------- */
.hero { position: relative; min-height: 100svh; display: flex; align-items: flex-end; color: #F4F0E7; overflow: hidden; }
.hero__bg { position: absolute; inset: 0; z-index: 0; }
/* a 22 s drift is a constant-velocity move, not an eased one — `ease-out` was the last
   non-house curve on the site and read as a lurch in the first two seconds */
.hero__bg img { width: 100%; height: 100%; object-fit: cover; animation: kenburns 22s linear forwards; }
@keyframes kenburns { from { transform: scale(1.12); } to { transform: scale(1); } }
/* ONE scrim vocabulary for all three heroes (home carousel, /mre, /local), still and
   film alike: a CORNER wash, never a full frame. A left wash carries the type column, a
   floor wash carries the bottom chrome, and a short top wash keeps the nav legible over
   a pale sky — between them the photograph is left alone. Full-frame scrims are what
   made these plates read murky next to the reference at 1440. */
/* The mid-stops of the left wash (26 %/48 %) were set by eye and left the TAIL of the
   type — the last third of the eyebrow, « …DE MARRAKECH », and the end of the third
   headline line — over the pale wall at the far side of the pool. Measured off the
   composited frame at 1440 with the copy hidden, the eyebrow's brightest ground gave
   CR 3,87:1 against #F1E9D8 — below the 4,5:1 floor that 11 px text owes. The two mid
   stops are raised (.46→.50, .16→.24) so the fade is stretched, not steepened: the deep
   corner (.78 at 0 %) and the release point (0 at 66 %) are both unchanged, so the right
   third of the photograph is exactly as bright as before and nothing reads darker at a
   glance — only the band the small type actually crosses gains its 0,09 of alpha. */
.hero__bg::after,
.hero__bg.is-playing::after { content: ""; position: absolute; inset: 0; background:
  linear-gradient(96deg, rgba(16,21,13,.78) 0%, rgba(16,21,13,.50) 26%, rgba(16,21,13,.24) 48%, rgba(16,21,13,0) 66%),
  linear-gradient(0deg,  rgba(16,21,13,.88) 0%, rgba(16,21,13,.26) 30%, rgba(16,21,13,0) 56%,
                         rgba(16,21,13,0) 80%, rgba(16,21,13,.38) 100%); }
/* The two campaign heroes carry their whole argument in the LEFT column, from the
   eyebrow down to the ledger — where the home hero puts only its bottom-left block. So
   the left wash reaches further before it releases; the right third of the frame (the
   villa, the loungers) is still untouched. Same vocabulary, one stop deeper. */
html[data-page="campaign"] .hero__bg::after { background:
  linear-gradient(96deg, rgba(16,21,13,.82) 0%, rgba(16,21,13,.60) 26%, rgba(16,21,13,.34) 48%,
                         rgba(16,21,13,.10) 62%, rgba(16,21,13,0) 74%),
  linear-gradient(0deg,  rgba(16,21,13,.72) 0%, rgba(16,21,13,.20) 34%, rgba(16,21,13,0) 60%,
                         rgba(16,21,13,0) 80%, rgba(16,21,13,.38) 100%); }
/* THE FILM IS BRIGHTER THAN ANY STILL. While the ambient loop is on screen (home hero,
   frame 2 — .is-first is the class that says so, and it exists only where there is a
   carousel) the plate is a moving frame whose peak instant sits far above every still in
   the set: measured at 1440 with the copy hidden, the headline measure fell to CR 2,0 on
   that frame while the stills held 4,8–8,3. So the wash reaches one stop deeper and as
   far right as the headline measure runs (« au prix juste. » ends at 47 % of the frame),
   and no further. Same corner vocabulary, never a full frame: past 74 % the photograph
   is untouched, and the moment the loop leaves the screen the wash lifts again. */
.hero__bg.is-playing.is-first::after { background:
  linear-gradient(96deg, rgba(16,21,13,.84) 0%, rgba(16,21,13,.68) 30%, rgba(16,21,13,.46) 46%,
                         rgba(16,21,13,.22) 58%, rgba(16,21,13,0) 74%),
  linear-gradient(0deg,  rgba(16,21,13,.88) 0%, rgba(16,21,13,.26) 30%, rgba(16,21,13,0) 56%,
                         rgba(16,21,13,0) 80%, rgba(16,21,13,.38) 100%); }
/* portrait phones read the type over the FLOOR of the frame, not its left edge —
   the wash is redistributed, never deepened overall.
   MEASURED off the composited portrait frames at 390×844 with the copy hidden: the
   type band runs y ≈ 407–517 (38–53 % up from the floor), and the old stops released
   it to alpha ,12–,40 exactly there. Over the coin-repas marble that gave the h1 an
   AVERAGE ground of CR 2,33 and the eyebrow 1,67 — not shimmer, the whole line. The
   two mid stops come up (,40→,52 at 40 %, ,12→,26 at 58 %) and the release stretches
   to 74 % (was 68), so the band the type actually crosses gains ,12–,14 of alpha while
   the deep floor (,92/,70) and the frame's top half are untouched. The rest of the
   deficit is carried by the type's own layered halo (see sections/01-hero.html) —
   the photograph is not asked to pay for a near-white wall alone. */
@media (max-width: 700px) {
  .hero__bg::after,
  .hero__bg.is-playing::after,
  .hero__bg.is-playing.is-first::after { background:
    linear-gradient(0deg, rgba(16,21,13,.92) 0%, rgba(16,21,13,.70) 22%, rgba(16,21,13,.52) 40%,
                          rgba(16,21,13,.26) 58%, rgba(16,21,13,0) 74%,
                          rgba(16,21,13,0) 84%, rgba(16,21,13,.38) 100%); }
  /* the campaign heroes still carry more type than the home hero, so their floor wash
     holds harder — but it now has less to cover. With the 3-stat ledger and the film
     CTA removed the type starts at y ≈ 304 px on /mre and /local at 390×844 instead of
     185/245, so the wash releases at 82 % (y ≈ 152) instead of 92 % (y ≈ 68) and its
     mid-band comes up a stop. Before the subtraction it held ≥ .64 down from y = 219,
     which at 390 is a full-frame scrim in everything but name — the photograph the paid
     click came to see was buried. It is a FLOOR wash again. Same vocabulary as every
     other hero; only the short top wash for the nav comes back at the very edge. */
  html[data-page="campaign"] .hero__bg::after { background:
    linear-gradient(0deg, rgba(16,21,13,.90) 0%, rgba(16,21,13,.82) 18%, rgba(16,21,13,.74) 38%,
                          rgba(16,21,13,.68) 55%, rgba(16,21,13,.58) 66%,
                          rgba(16,21,13,.14) 74%, rgba(16,21,13,0) 82%,
                          rgba(16,21,13,.34) 100%); }
}
.hero__inner { position: relative; z-index: 1; padding-bottom: clamp(3rem,7vw,6rem); padding-top: 8rem; width: 100%; }
.hero h1 { color: #FBF8F1; max-width: 16ch; }
.hero .lead { color: rgba(251,248,241,.9); max-width: 44ch; margin-top: 1.6rem; }
.hero__cta { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 2.4rem; }
/* .hero__trust — the 3-stat hero ledger — was retired in the perfection pass. It only
   ever ran on /mre and /local, where every figure it carried was already in the eyebrow,
   the lede or the réassurance line, and where it forced the floor wash up to .93 at 390
   (a full-frame scrim in all but name). Its rules are deleted rather than left dormant:
   dead CSS is how a retired device comes back. */

/* DESKTOP RHYTHM — the two campaign heroes only.
   These values existed to squeeze SIX blocks (eyebrow → h1 → lede → three CTAs →
   réassurance → 3-stat ledger) above the fold: at the home hero’s rhythm that budget ran
   to 1033 px and, at 1440×900, the fold cut through the ledger’s numerals. The
   perfection pass removed the ledger and the third CTA instead — every figure the ledger
   carried is already in the eyebrow, the lede or the réassurance line, so the subtraction
   costs no information (see the scope note in sections/01-hero.{mre,local}.html).
   What remains is a four-block hero, so the tightening is relaxed rather than deleted:
   the interlines come back up toward the home hero’s, and the display size keeps a small
   step down because a campaign headline sits above a much longer lede. Still
   proportional to the viewport (vh inside a clamp) rather than gated on a height
   breakpoint, so nothing snaps between two viewport heights. */
@media (min-width: 821px) {
  html[data-page="campaign"] .hero__inner {
    padding-top: clamp(5.6rem, 12.6vh, 7.2rem);
    padding-bottom: clamp(3rem, 6.4vh, 5.5rem);
  }
  html[data-page="campaign"] .hero h1 { font-size: clamp(3rem, 5.6vw, 5.4rem); }
  html[data-page="campaign"] .hero .lead { margin-top: clamp(1.1rem, 2.8vh, 1.6rem); }
  html[data-page="campaign"] .hero__cta { margin-top: clamp(1.9rem, 4.2vh, 2.4rem); }
  html[data-page="campaign"] .hero__reassure { margin-top: clamp(1.2rem, 2.6vh, 1.5rem); }
}

/* ---------- Price strip ---------- */
.pricestrip { background: var(--olive-2); color: #E7E2D4; }
.pricestrip .container { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 1rem 3rem; text-align: center; }
.pricestrip .ps { display: flex; align-items: center; gap: .7rem; font-size: .84rem; letter-spacing: .04em; font-weight: 300; }
.pricestrip .ps b { color: #FBF8F1; font-weight: 500; font-family: var(--display); font-size: 1.05rem; }
.pricestrip .muted { color: #C4BEAF; }   /* AA on the near-black strip */

/* ---------- icon list ---------- */
.icon-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 1.1rem; }
.icon-list li { display: flex; gap: .9rem; align-items: flex-start; color: var(--ink-soft); font-weight: 300; border-bottom: 1px solid var(--line-2); padding-bottom: 1.1rem; }
.icon-list li:last-child { border-bottom: 0; }
.icon-list li svg { width: 19px; height: 19px; color: var(--brass); flex: none; margin-top: 3px; }
.section--forest .icon-list li { color: #D3CDBD; border-color: var(--line-light); }
.section--forest .icon-list li svg { color: var(--brass-soft); }
.section--forest .chip { color: #E7E2D4; border-color: var(--line-light); }

/* ---------- Preloader / intro ---------- */
/* on phones the splash only delays the LCP hero + first CTA behind deferred JS — skip it; the hero itself is the brand statement (desktop keeps the choreography) */
@media (max-width: 820px){ .intro { display: none !important; } }
.intro { position: fixed; inset: 0; z-index: 100; background: var(--olive); display: flex; align-items: center; justify-content: center; transition: opacity 1s var(--ease), visibility 1s; }
.intro img { height: 64px; width: auto; opacity: 0; animation: introfade 1.6s var(--ease) forwards; }
@keyframes introfade { 0%{opacity:0;transform:translateY(10px);} 40%{opacity:1;transform:none;} 100%{opacity:1;} }
.intro.is-done { opacity: 0; visibility: hidden; }
/* JS-independent safety: the overlay always clears even if app.js fails to load */
.intro { animation: introfail 0s linear 3.6s forwards; }
@keyframes introfail { to { opacity: 0; visibility: hidden; } }

/* ---------- utilities ---------- */
.stack > * + * { margin-top: 1.1rem; }
.mt-1{margin-top:.5rem}.mt-2{margin-top:1rem}.mt-3{margin-top:1.75rem}.mt-4{margin-top:2.5rem}
details { border-top: 1px solid var(--line); }
details summary { list-style: none; cursor: pointer; padding: 1.5rem 0; font-family: var(--display); font-size: clamp(1.05rem,1.9vw,1.3rem); color: var(--ink); display: flex; justify-content: space-between; gap: 1rem; align-items: center; }
details summary::-webkit-details-marker { display: none; }
details summary::after { content: "+"; color: var(--brass); font-family: var(--sans); font-weight: 300; font-size: 1.4rem; transition: transform .4s var(--ease); }
details[open] summary::after { transform: rotate(45deg); }
/* sections that ship their own toggle sign (e.g. #faq .sign) must not get the global marker too */
details summary:has(.sign)::after { content: none; }

/* ============================================================
   V4 — Ambient video hero · direct-response · film lightbox
   ============================================================ */
/* ambient muted loop covers the hero, fades in over the poster (which is the LCP) */
.hero__bg iframe {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  /* the iframe MUST stay a true 16:9 box so YouTube fills it edge-to-edge (no
     letterbox bars). JS (wireAmbientHero) sizes it in px to cover the hero exactly;
     this rule is the pre-JS fallback — 16:9, large enough to cover the viewport. */
  width: max(100vw, 177.78vh); aspect-ratio: 16 / 9; height: auto;
  border: 0; pointer-events: none; opacity: 0; transition: opacity 1.6s var(--ease); z-index: 0;
  /* softer + darker: a dreamier blurred film that lets the headline sit cleanly on top */
  filter: blur(5px) saturate(1.02) brightness(.78);
}
/* self-hosted ambient video — covers the hero, a touch less blur than the youtube
   fallback so the golden-hour footage reads while the headline stays crisp on top */
.hero__bg video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  border: 0; pointer-events: none; opacity: 0; transition: opacity 1.6s var(--ease); z-index: 0;
  filter: blur(3px) saturate(.96) brightness(.6);
}
.hero__bg.is-playing iframe,
.hero__bg.is-playing video { opacity: 1; }              /* video fully covers the poster */
.hero__bg.is-playing .hero__poster { opacity: 0; transition: opacity 1s var(--ease); }  /* retire the still once the film is in */
/* keep text legible over the moving footage (paint order already puts ::after above the iframe) */
/* (the .is-playing scrim is now the same corner wash as the still state — see .hero__bg::after) */
.hero__bg .hero__poster { position: absolute; inset: 0; display: block; }
.hero__bg .hero__poster img { width: 100%; height: 100%; object-fit: cover; }

/* ---- Choreographed hero entrance — the same rise, fired by the intro lift ----
   The hero is the one place the trigger is page entrance rather than scroll: it is
   already in view on arrival. Curve, duration and 60 ms stagger are the site’s. */
.hero .h-rise { opacity: 0; transform: translateY(22px); transition: opacity var(--rise-dur) var(--ease-expo), transform var(--rise-dur) var(--ease-expo); will-change: opacity, transform; }
.hero.is-ready .h-rise { opacity: 1; transform: none; }
.hero .hd1 { transition-delay: 0s; }
.hero .hd2 { transition-delay: .06s; }
.hero .hd3 { transition-delay: .12s; }
.hero .hd4 { transition-delay: .18s; }
.hero .hd5 { transition-delay: .24s; }
.hero .hd6 { transition-delay: .30s; }
/* failsafe: if JS never sets .is-ready (e.g. app.js fails to load), reveal anyway */
.hero:not(.is-ready) .h-rise { animation: heroRiseFail .01s linear 3.4s forwards; }
@keyframes heroRiseFail { to { opacity: 1; transform: none; } }

/* ---- The signature, at its loudest: the hero H1 rises WORD by WORD out of its mask.
        app.js wraps the words before first paint, so the transform lives on the word
        and the h1 itself never leaves opacity 1 — if the wrap never happens (no JS,
        JS error) the headline simply paints as plain text, LCP untouched. #hero keeps
        this ahead of the .is-ready / failsafe rules above. ---- */
#hero h1.h-rise { opacity: 1; transform: none; animation: none; transition: none; will-change: auto; }
#hero h1 .rw > i { transform: translateY(110%); will-change: transform; }
#hero.is-ready h1 .rw > i { transform: none;
  transition: transform var(--rise-dur) var(--ease-expo);
  transition-delay: calc(.06s + var(--wi, 0) * var(--rise-stagger)); }
#hero:not(.is-ready) h1 .rw > i { animation: heroWordFail .01s linear 3.4s forwards; }
@keyframes heroWordFail { to { transform: none; } }
@media (prefers-reduced-motion: reduce) {
  #hero h1 .rw > i, #hero.is-ready h1 .rw > i { transform: none !important; animation: none !important; transition: none; }
}

/* « Voir le film » — the home hero's second CTA. Its circular play disc (.hero__film-ic)
   and its « avec le son » qualifier (.hero__film-sub) went with the campaign heroes' third
   CTA in the perfection pass; the label carries the action, and the rules are deleted
   rather than left dormant. */
.hero__film { display: inline-flex; align-items: center; justify-content: center; gap: .8rem; background: none; border: 0; color: #F4F0E7; font-family: var(--sans); font-size: .74rem; letter-spacing: .2em; text-transform: uppercase; cursor: pointer; padding: 1.05rem .4rem; transition: color .4s var(--ease); }
.hero__film:hover { color: var(--brass-soft); }

.hero__reassure { margin: 1.5rem 0 0; font-family: var(--serif); font-size: clamp(1rem, 1.6vw, 1.15rem); color: rgba(244,240,231,.82); max-width: 46ch; }
.hero__reassure strong { color: #fff; font-weight: 600; }

/* V6: the floating hero scarcity flag was retired — the trust-row stat carries "9 / 16"
   in the same viewport, so scarcity now appears once per ask instead of twice. */

/* film lightbox (full sound) */
.film-lb { position: fixed; inset: 0; z-index: 120; background: rgba(18,22,15,.95); display: flex; align-items: center; justify-content: center; padding: 6vw var(--gutter); opacity: 0; visibility: hidden; transition: opacity .5s var(--ease), visibility .5s; }
.film-lb.is-open { opacity: 1; visibility: visible; }
.film-lb__frame { position: relative; width: min(100%, 1180px); aspect-ratio: 16/9; box-shadow: var(--shadow); background: #000; }
.film-lb__frame iframe, .film-lb__frame video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.film-lb__frame video { object-fit: contain; background: #000; }
.film-lb__close { position: absolute; top: .6rem; right: .8rem; z-index: 2; padding: 1rem; color: #F4F0E7; background: none; border: 0; font-family: var(--sans); font-size: .7rem; letter-spacing: .22em; text-transform: uppercase; cursor: pointer; display: inline-flex; gap: .55rem; align-items: center; opacity: .85; transition: opacity .3s var(--ease-expo); }
.film-lb__close:hover { opacity: 1; }
.film-lb__close svg { width: 15px; height: 15px; }

@media (max-width: 620px) { .hero__cta .hero__film { width: 100%; } }
@media (prefers-reduced-motion: reduce) { .hero__bg iframe, .hero__bg video { display: none; } }

/* ---- GSAP parallax on editorial images ---- */
.feature__media.px { overflow: hidden; box-shadow: var(--shadow-sm); }
.feature__media.px img { scale: 1.16; box-shadow: none; will-change: transform; }
.feature__media--arch.px { border-radius: var(--arch); }
/* magnetic buttons: let GSAP own transform, keep colour transitions */
.magnetic { transition-property: background-color, color, border-color; will-change: transform; }

/* ---- Immersive navigable image lightbox (gallery + villas) ----
   `visibility .45s` was a transition on BOTH edges, and the opening edge was
   costing the dialog its focus. A transitioned visibility does not compute as
   `visible` on the tick the class flips — measured, getComputedStyle still read
   `hidden` there — and a hidden element cannot take focus, so app.js's same-tick
   .focus() no-opped and the keyboard reader sat on <body> inside an inert page
   until a later frame rescued it. The two edges want different things and now
   say so: opening flips visibility instantly (0s, no delay) so the dialog is
   focusable in the same tick as the click; closing delays the flip by the full
   .45s so the fade-out still plays before the element leaves the a11y tree.
   Visually identical, one race gone. */
/* PALETTE HYGIENE: the scrim was rgb(18,22,15) — a fourth dark, neither --olive
   (35,42,30) nor --olive-2 (26,32,22). Same value as the gallery's caption plate
   now, so the overlay and the lightbox are one ground. */
.img-lb { position: fixed; inset: 0; z-index: 120; background: rgba(26,32,22,.95); display: flex; align-items: center; justify-content: center; padding: clamp(2.5rem,6vh,5rem) clamp(3.5rem,8vw,6rem); opacity: 0; visibility: hidden; transition: opacity .45s var(--ease), visibility 0s linear .45s; }
.img-lb.is-open { opacity: 1; visibility: visible; transition: opacity .45s var(--ease), visibility 0s linear 0s; }
.img-lb__stage { margin: 0; position: relative; display: flex; flex-direction: column; align-items: center; max-width: min(92vw, 1300px); }
.img-lb__stage img { max-width: min(92vw, 1300px); max-height: 80vh; object-fit: contain; box-shadow: var(--shadow); border-radius: 2px; transform: scale(.97); transition: transform .5s var(--ease), opacity .35s var(--ease); }
.img-lb.is-open .img-lb__stage img { transform: scale(1); }
/* ONE CAPTION FAMILY, ONE WHITE. This is the master the gallery's tile overlay is
   now brought to (Cormorant title, Inter .18em provenance) — so the two whites it
   used to state, #EDE7DA and rgba(237,231,218,.6), become the one alabaster the
   overlay paints: clicking a tile no longer reformats the caption you were reading. */
.img-lb__cap { margin-top: 1.15rem; text-align: center; color: var(--alabaster); font-family: var(--serif); font-size: 1.1rem; }
.img-lb__cap span { display: block; margin-top: .25rem; font-family: var(--sans); font-size: .64rem; letter-spacing: .18em; text-transform: uppercase; color: rgba(244,240,231,.78); }
.img-lb__nav { position: absolute; top: 50%; transform: translateY(-50%); width: 56px; height: 56px; border-radius: 50%; border: 1px solid rgba(244,240,231,.4); background: rgba(20,25,16,.35); color: #F4F0E7; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background .3s var(--ease), border-color .3s var(--ease); z-index: 2; }
.img-lb__nav:hover { background: rgba(244,240,231,.14); border-color: var(--brass-soft); }
.img-lb__nav svg { width: 20px; height: 20px; }
.img-lb__prev { left: clamp(.5rem, 3vw, 2.5rem); }
.img-lb__next { right: clamp(.5rem, 3vw, 2.5rem); }
/* the house rule sets lining/tabular figures on h1–h3/.price/.stat/.fact; a counter
   is nothing but figures and had neither, so a centred « 2 / 10 » re-centred itself
   as the index crossed into two digits. Tabular + the zero-padded « 02 / 10 » string
   app.js now writes (the reference's own 01 / 05 idiom) hold it still. */
.img-lb__counter { position: absolute; left: 50%; bottom: clamp(1rem, 3vh, 2rem); transform: translateX(-50%); color: rgba(244,240,231,.7); font-size: .7rem; letter-spacing: .22em; font-variant-numeric: lining-nums tabular-nums; font-feature-settings: "lnum" 1, "tnum" 1; }
.img-lb__close { position: absolute; top: 1rem; right: 1.2rem; z-index: 3; padding: .6rem; background: none; border: 0; color: #F4F0E7; cursor: pointer; opacity: .85; transition: opacity .3s var(--ease-expo); display: inline-flex; }
.img-lb__close:hover { opacity: 1; }
.img-lb__close svg { width: 26px; height: 26px; }
@media (max-width: 620px) {
  .img-lb { padding: 3.5rem .8rem; }
  .img-lb__nav { width: 46px; height: 46px; }
  .img-lb__prev { left: .4rem; } .img-lb__next { right: .4rem; }
  .img-lb__stage img { max-height: 68vh; }
}

/* ---------- Overflow safety ---------- */
html, body { max-width: 100%; overflow-x: clip; }
/* grid & flex children may exceed their track when they contain wide/nowrap content */
.grid > *, .feature > *, .hero__cta > *, .icon-list li { min-width: 0; }

/* ---- 941–1379px: the full desktop nav does not fit (measured +105px overflow at
   1024, +14px at 1120; with the brand made unshrinkable the row is still ~75px
   short at 1280 when WhatsApp is present). The WhatsApp button yields across the
   whole tight band (it exists again in the contact chapter and the mobile bar),
   and the link rail tightens; the brand and the primary CTA never clip. ---- */
@media (min-width: 941px) and (max-width: 1379px) {
  .nav__actions .btn-whatsapp { display: none; }
  .nav__links { gap: 1.1rem; }
}

/* ---------- Mobile refinements (10/10 on phones) ---------- */
@media (max-width: 820px) {
  /* declutter the top bar — the sticky bottom bar already carries WhatsApp */
  .nav__actions .btn-whatsapp { display: none; }
  .nav { padding: 1.1rem 0; }
  .nav__logo { height: 34px; }
  .nav__actions .btn { padding: .8rem 1.25rem; font-size: .68rem; letter-spacing: .14em; min-height: 44px; }
}
@media (max-width: 620px) {
  :root { --gutter: 1.4rem; }
  /* wrap-safe, comfortable CTAs */
  .btn { white-space: normal; padding: 1.05rem 1.4rem; letter-spacing: .12em; font-size: .72rem; line-height: 1.35; text-align: center; }
  /* content CTAs go full-width (bigger tap target + cleaner luxury stacking) */
  .hero__cta { flex-direction: column; align-items: stretch; }
  /* nav (header.nav) and pricestrip are NOT .section descendants, so they're excluded */
  .hero__cta .btn, .section .btn, .feature .btn, .stack .btn, .form .btn { width: 100%; }
  .nav__actions .btn, .pricestrip .btn { width: auto; }
  /* tighten oversized display leading a touch on phones */
  .hero h1 { line-height: 1.04; }
  /* keep big numerals from breaking mid-unit */
  .stat .num { font-size: clamp(1.7rem, 7vw, 2.4rem); }
  /* generous tap targets for inline + footer links */
  footer a, .icon-list a { display: inline-block; padding-block: 8px; min-height: 24px; }
}

/* ============================================================
   V6 — « La visite privée » : elite craft layer
   The page is one continuous guided tour. This layer adds the
   narrative spine + the last 1% of interaction craft. Placed at
   end-of-file on purpose: equal-specificity overrides win here.
   ============================================================ */

/* ---- Le fil de la visite — fixed chapter rail (desktop only, decorative) ---- */
.tour-rail { display: none; }
@media (min-width: 1024px) and (pointer: fine) {
  .tour-rail {
    position: fixed; left: clamp(14px, 1.6vw, 26px); top: 50%; transform: translateY(-50%);
    z-index: 40; display: flex; flex-direction: column; align-items: center; gap: 1.15rem;
    pointer-events: none; opacity: 0; transition: opacity .9s var(--ease);
  }
  .tour-rail.is-live { opacity: 1; }
  .tour-rail__label {
    writing-mode: vertical-rl; transform: rotate(180deg);
    font-family: var(--sans); font-size: .58rem; letter-spacing: .34em; text-transform: uppercase;
    font-weight: 500; color: var(--brass); white-space: nowrap;
    display: inline-flex; align-items: center; gap: .8rem;
    transition: opacity .24s var(--ease);
  }
  .tour-rail__num { font-family: var(--display); font-size: .76rem; letter-spacing: .1em; }
  .tour-rail.is-switching .tour-rail__label { opacity: 0; }
  .tour-rail__track { width: 1px; height: 24vh; background: rgba(132,101,41,.22); position: relative; overflow: hidden; }
  .tour-rail__thread { position: absolute; inset: 0; background: linear-gradient(var(--brass), var(--brass-soft)); transform: scaleY(0); transform-origin: top; will-change: transform; }
}

/* ---- Nav links: brass underline draws from center ---- */
.nav__link { position: relative; }
.nav__link::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -7px; height: 1px;
  background: currentColor; opacity: .85;
  transform: scaleX(0); transform-origin: center; transition: transform .45s var(--ease);
}
.nav__link:hover::after, .nav__link:focus-visible::after { transform: scaleX(1); }

/* ---- Form fields: the underline draws in brass, the label answers ---- */
.field { position: relative; }
.field label { transition: color .4s var(--ease); }
.field::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: var(--brass); transform: scaleX(0); transform-origin: left;
  transition: transform .55s var(--ease); pointer-events: none;
}
.field:focus-within::after { transform: scaleX(1); }
.field:focus-within label { color: var(--brass); }

/* ---- The invisible 1% ---- */
:root { accent-color: var(--brass); }
html { scrollbar-width: thin; scrollbar-color: rgba(132,101,41,.5) var(--alabaster); }
::-webkit-scrollbar { width: 11px; }
::-webkit-scrollbar-track { background: var(--alabaster); }
::-webkit-scrollbar-thumb { background: rgba(132,101,41,.42); border: 3px solid var(--alabaster); }
::-webkit-scrollbar-thumb:hover { background: rgba(132,101,41,.62); }
body { -webkit-tap-highlight-color: transparent; }
/* touch feedback replaces the removed tap highlight */
@media (hover: none) {
  .mobilebar a:active { background: var(--bone); }
  .mobilebar a.is-primary:active { background: var(--olive-2); }
  .btn:active { transform: translateY(1px); }
}

/* (the .reveal--left / --right / --scale choreography variants are deleted:
    three extra directions were three extra motion languages, and none of the
    section markup ever used them.) */

@media (prefers-reduced-motion: reduce) {
  .tour-rail, .tour-rail__label, .nav__link::after, .field::after { transition: none; }
}

/* ---- Lead-form resolution: form bows out, confirmation rises, the check draws ---- */
.form.is-out { opacity: 0; transform: translateY(-8px); transition: opacity .4s var(--ease), transform .4s var(--ease); }
#form-success { opacity: 0; transform: translateY(14px); }
#form-success.is-in { opacity: 1; transform: none; transition: opacity .7s var(--ease), transform .7s var(--ease); }
#form-success svg path { stroke-dasharray: 60; stroke-dashoffset: 60; }
#form-success.is-in svg path { animation: joeg-check-draw 1s var(--ease) .25s forwards; }
@keyframes joeg-check-draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
  .form.is-out { transition: none; }
  #form-success, #form-success.is-in { opacity: 1; transform: none; transition: none; }
  #form-success svg path { stroke-dasharray: none; stroke-dashoffset: 0; animation: none !important; }
}

/* ---- Safe areas: the mobile bar breathes under the iPhone home indicator ---- */
.mobilebar { padding-bottom: env(safe-area-inset-bottom, 0px); }
@media (max-width: 820px) {
  body { padding-bottom: calc(68px + env(safe-area-inset-bottom, 0px)); }
  .consent { bottom: calc(74px + env(safe-area-inset-bottom, 0px)); }
}

/* ---- Print / save-as-PDF: buyers and their notaires print property pages ---- */
@media print {
  .reveal, .img-reveal, .h-rise { opacity: 1 !important; transform: none !important; }
  .img-reveal img { clip-path: none !important; transform: none !important; }
  .rw { overflow: visible !important; padding: 0 !important; margin: 0 !important; }
  .rw > i { transform: none !important; padding-bottom: 0 !important; }
  .nav, .mobilebar, .consent, .intro, .tour-rail,
  .hero__bg iframe, .hero__bg video, .film-lb, .img-lb, .skip-link { display: none !important; }
  .section--forest, footer.section--forest, .pricestrip { background: #fff !important; color: #22261D !important; }

  /* The dark ground flattens to white, so the TEXT has to flatten with it.
     Recolouring only h1–h3/p/.lead left every <a>, <b>, <span>, <em> and button
     label sitting on its on-dark light colour over white paper: the footer
     printed its phone number and its e-mail address white-on-white — invisible
     on all sixteen routes — and #visio printed its slot row, its closing line
     and « Convenir d’un créneau » the same way. .pricestrip was never in the
     reset at all, so « dès 2,25 MDH » vanished too.
     !important is required here: the footer sets colour inline (style="color:#fff"). */
  .section--forest *, .pricestrip * { color: #22261D !important; }
  .section--forest .eyebrow, .section--forest em,
  .pricestrip .eyebrow, footer.section--forest .foot-seg { color: #7C5E28 !important; }

  /* …EXCEPT where the text overlays a photograph. An <img> DOES print, so these
     compositions keep their light colour and are told to print their scrim —
     coercing them to ink is what turned « En visio, le matin » into ink on a
     dark interior. Order matters: this must follow the flatten rule above. */
  .hero__fact, .hero__fact *, .vp-full, .vp-full *, .vp-spine, .vp-spine *,
  .vx-body, .vx-body *, .vx-status, .vx-status *,
  .vs-state, .vs-state * { color: #FBF8F1 !important; }
  .vp-scrim, .vx-veil, .vs-veil, .vs-state, .hero__fact {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }

  /* A button on paper is information, not an affordance: no fill, a hairline box. */
  .section--forest .btn, .pricestrip .btn, footer .btn {
    background: transparent !important; box-shadow: none !important;
    border: 1px solid rgba(34, 38, 29, .45) !important;
  }
  .vs-state .btn, .vp-full .btn, .vx-body .btn { border-color: rgba(251, 248, 241, .55) !important; }

  .hero { min-height: auto; color: #22261D; }
  a[href^="tel:"]::after, a[href^="mailto:"]::after { content: none; }
  #contact .lead-card { box-shadow: none; }
}

/* ============================================================
   V11 REFINEMENT LAYER — contrast, tokens, mobile nav, pause
   controls, touch resets. Appended so it wins the cascade at
   equal specificity; keep new rules in this block.
   ============================================================ */
:root {
  /* dark-section neutrals, unified (were six near-duplicate hexes) */
  --cream:      #EDE7DA;   /* display text on olive */
  --cream-soft: #CFC9BC;   /* body on olive */
  --cream-mute: #A8A895;   /* captions on olive */
  /* interaction brass, unified */
  --brass-deep:   #7C5E28; /* hover on light grounds */
  --brass-bright: #E0C489; /* hover on dark grounds */
  /* single chapter-title scale — now identical to the base h2 spec, so the site has
     ONE h2 size rather than a base scale and a chapter scale drifting apart */
  --h2-chapter: clamp(2.1rem, 4.6vw, 3.7rem);
}

/* screen-reader-only utility */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* ---- WCAG AA: primary CTA text on brass fill (3.07:1 → 5.4:1) ---- */
.section--forest .btn-primary { color: #fff; }
.section--forest .btn-primary:hover { background: var(--brass-deep); color: #fff; }

/* ---- WCAG non-text contrast: brass ring is invisible on olive — use bright brass there ---- */
.section--forest :focus-visible, .pricestrip :focus-visible, .hero :focus-visible,
footer :focus-visible, .film-lb :focus-visible, .img-lb :focus-visible, .mnav :focus-visible,
.section--forest a:focus-visible, .section--forest button:focus-visible,
.hero a:focus-visible, .hero button:focus-visible,
footer a:focus-visible, .mnav a:focus-visible, .mnav button:focus-visible {
  outline-color: var(--brass-soft);
}

/* ---- inline body link, ONE anatomy ----
   The site had no shared treatment for a link set inside running prose, so each
   one inherited its paragraph's colour and shipped with no affordance at all.
   This is #pricestrip's .ps-why anatomy — brass + a 1 px brass hairline that goes
   solid on hover — promoted to a token and set in the light-ground brass (#846529
   passes AA on alabaster; .ps-why keeps brass-soft because it sits on olive). */
.link-inline {
  color: var(--brass);
  border-bottom: 1px solid rgba(132, 101, 41, .45);
  padding-bottom: 1px;
  transition: border-color .35s var(--ease), color .35s var(--ease);
}
.link-inline:hover { border-bottom-color: var(--brass); color: var(--brass-deep); }
.section--forest .link-inline, .pricestrip .link-inline, footer .link-inline {
  color: var(--brass-soft); border-bottom-color: rgba(198, 165, 106, .55);
}
.section--forest .link-inline:hover, .pricestrip .link-inline:hover, footer .link-inline:hover {
  border-bottom-color: var(--brass-soft); color: var(--brass-bright);
}

/* ---- one H2 scale for every chapter ---- */
h2, .h2 { font-size: var(--h2-chapter); }
.df-text h2 { font-size: var(--h2-chapter); }

/* ---- eyebrow → H2 lockup, one gap everywhere ---- */
.eyebrow + h2, .eyebrow + .h2 { margin-top: .4rem; }

/* ---- touch devices: no sticky hover states (cards jump on tap) ---- */
@media (hover: none) {
  .card:hover { transform: none; }
  .card:hover .card__media img { transform: none; }
  #galerie .gal-item:hover .gal-thumb img { transform: none; }
  #galerie .gal-item:hover .gal-thumb::after { box-shadow: inset 0 0 0 1px rgba(132,101,41,0); }
}

/* ---- hero ambient pause (WCAG 2.2.2) — quiet brass hairline disc, bottom-right ---- */
.hero__pause {
  position: absolute; right: 1.4rem; bottom: 1.4rem; z-index: 5;
  width: 44px; height: 44px; border-radius: 50%;
  background: rgba(18, 22, 15, .38); border: 1px solid rgba(244, 240, 231, .38);
  color: #F4F0E7; display: flex; align-items: center; justify-content: center;
  backdrop-filter: blur(4px); opacity: .66;
  transition: opacity .45s var(--ease), border-color .45s var(--ease);
}
.hero__pause:hover { opacity: 1; border-color: var(--brass-soft); }
.hero__pause svg { width: 14px; height: 14px; }
.hero__pause .ic-play { display: none; }
.hero__pause.is-paused .ic-pause { display: none; }
.hero__pause.is-paused .ic-play { display: block; }
/* the ambient loop now lives at EVERY width — the pause control follows it (WCAG 2.2.2);
   on phones it sits top-right in the sky zone, clear of the trust ledger and the mobilebar */
@media (max-width: 820px) {
  .hero__pause { display: flex; top: calc(64px + .8rem); right: .9rem; bottom: auto; width: 34px; height: 34px; opacity: .5; }
}

/* ---- mobile chapter menu ---- */
.nav__burger {
  display: none; flex-direction: column; justify-content: center; gap: 5px;
  width: 44px; height: 44px; padding: 10px;
  background: none; border: 0;
}
.nav__burger span { display: block; height: 1px; background: currentColor; transition: opacity .3s var(--ease); }
.nav .nav__burger { color: #F4F0E7; }
.nav--solid .nav__burger { color: var(--ink); }
@media (max-width: 980px) { .nav__burger { display: flex; } }

.mnav {
  position: fixed; inset: 0; z-index: 120;
  background: var(--olive-2); color: var(--cream);
  display: flex; flex-direction: column; justify-content: center;
  padding: clamp(2rem, 8vw, 4rem);
  opacity: 0; transition: opacity .35s var(--ease);
}
.mnav.is-open { opacity: 1; }
.mnav[hidden] { display: none; }
.mnav__close {
  position: absolute; top: 1.1rem; right: 1.1rem;
  width: 46px; height: 46px; border-radius: 50%;
  background: none; border: 1px solid var(--line-light); color: var(--cream);
  display: flex; align-items: center; justify-content: center;
}
.mnav__close svg { width: 18px; height: 18px; }
.mnav__links { display: flex; flex-direction: column; gap: .1rem; }
.mnav__links a {
  display: flex; align-items: baseline; gap: 1.1rem;
  padding: .78rem .2rem; border-bottom: 1px solid rgba(255, 255, 255, .08);
  font-family: var(--display); font-size: clamp(1.45rem, 6vw, 2rem); line-height: 1.1;
  color: var(--cream);
}
.mnav__links a i {
  font-family: var(--sans); font-style: normal; font-size: .62rem;
  letter-spacing: .22em; color: var(--brass-soft); min-width: 2.2em;
  font-variant-numeric: lining-nums;
}
.mnav__links a.mnav__contact span { color: var(--brass-soft); font-style: italic; }
.mnav__foot { display: flex; gap: 1.8rem; margin-top: 2.2rem; }
.mnav__foot a {
  font-size: .74rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--cream-soft); border-bottom: 1px solid rgba(255, 255, 255, .22);
  padding-bottom: .35rem;
}
@media (prefers-reduced-motion: reduce) { .mnav { transition: none; } }

/* ---- mobilebar yields to the form keyboard ---- */
@media (max-width: 820px) {
  body.kb-open .mobilebar { transform: translateY(110%); }
  .mobilebar { transition: transform .3s var(--ease); }
}

/* ---- pull-quote kicker: marks the house's own conviction (not an attributed quote) ---- */
.df-quote__kicker {
  display: block; font-family: var(--sans); font-size: .62rem; font-weight: 500;
  letter-spacing: .26em; text-transform: uppercase; color: var(--brass);
  margin-bottom: .7rem;
}

/* ---- hero ambient state ---- the scrim is the SAME corner wash as the still state
   (see .hero__bg::after): a film that darkens when it starts is a film that announces
   itself as chrome. Only the footage keeps its own soft treatment. */
.hero__bg video { filter: blur(2px) brightness(.82) saturate(.99); }

/* ---- mobile CTA hierarchy: where the primary is demoted to a quiet brass text-link
   (localisation, domaine), the boxed WhatsApp button must not outweigh it —
   same quiet register, one voice per tier.
   #patrimoine is NOT in this list any more: the subtractive sweep left that chapter with
   a single ask — the black button under the villa-témoin coda — and no .hero__cta pair
   at all, so every #patrimoine selector here was matching nothing. ---- */
@media (max-width: 820px) {
  #localisation .hero__cta .btn-whatsapp,
  #domaine .hero__cta .btn-whatsapp {
    border: 0; background: none; width: auto; align-self: flex-start;
    justify-content: flex-start; opacity: 1;
    min-height: 44px; padding: 0 .15rem .55rem;
    color: var(--stone); font-size: .72rem; letter-spacing: .2em;
    text-decoration: underline; text-decoration-thickness: 1px;
    text-underline-offset: .55em; text-decoration-color: rgba(34, 38, 29, .22);
  }
  #localisation .hero__cta .btn-whatsapp:hover, #localisation .hero__cta .btn-whatsapp:active,
  #domaine .hero__cta .btn-whatsapp:hover, #domaine .hero__cta .btn-whatsapp:active {
    color: var(--wa); text-decoration-color: var(--wa); background: none;
  }
  /* domaine's primary joins the quiet tier too (patrimoine/localisation already have scoped demotions) */
  #domaine .hero__cta .btn-primary {
    background: none; border: 0; border-bottom: 1px solid rgba(132, 101, 41, .35);
    color: var(--brass); width: auto; align-self: flex-start;
    min-height: 44px; padding: 0 .15rem .55rem;
    align-items: flex-end; justify-content: flex-start; gap: .6rem;
    font-size: .72rem; letter-spacing: .24em;
  }
  #domaine .hero__cta .btn-primary::after { content: "\2192"; font-size: 1.05em; line-height: 1; }
  #domaine .hero__cta .btn-primary:hover, #domaine .hero__cta .btn-primary:active {
    background: none; color: var(--brass); border-bottom-color: var(--brass);
  }
  #domaine .hero__cta { flex-direction: column; align-items: flex-start; gap: 1.1rem; }
}

/* ---- #patrimoine's desktop CTA demotion (24 lines, ≥821px) lived here. It was the
   desktop twin of the ≤820 block above, and it died the same way: the chapter's
   .df-text column no longer carries a .hero__cta pair — the sweep left ONE ask in the
   whole section, the full-weight button under the villa-témoin coda, which is exactly
   what these rules existed to outrank. Nothing to demote, nothing to declare. ---- */

/* ---- #confiance rejoins the section rhythm (was a private padding token) ---- */
#confiance { padding-block: calc(var(--sp-section) * .85); }

/* ---- close the dead void between the contact card and the footer (same forest ground) ---- */
#contact { padding-bottom: clamp(3rem, 6vw, 5.5rem); }

/* ---- FAQ in-answer CTAs: quiet tracked brass text-links, not dark slabs inside a
   hairline accordion (anchors + data-cta attributes untouched) ---- */
#faq .faq-a .btn-primary {
  background: none; border: 0; border-bottom: 1px solid rgba(132, 101, 41, .35);
  color: var(--brass); width: auto; padding: 0 .15rem .55rem; min-height: 44px;
  align-items: flex-end; justify-content: flex-start; gap: .6rem;
  font-size: .72rem; letter-spacing: .24em; white-space: normal; text-align: left;
}
#faq .faq-a .btn-primary::after { content: "\2192"; font-size: 1.05em; line-height: 1; transition: transform .45s var(--ease); }
#faq .faq-a .btn-primary:hover { background: none; color: var(--ink); border-bottom-color: var(--brass); }
#faq .faq-a .btn-primary:hover::after { transform: translateX(4px); }
@media (prefers-reduced-motion: reduce) {
  #faq .faq-a .btn-primary::after { transition: none; }
  #faq .faq-a .btn-primary:hover::after { transform: none; }
}

/* ---- feature media: the CSS aspect-ratio must govern, never the height attribute
   (with width:100% an unmatched height="" pins the box — the stretched-arch bug) ---- */
.feature__media img { height: auto; }

/* ---- distance ledger: body copy starts on one shared line across the 4 columns ---- */
@media (min-width: 941px) {
  #distance .dist-step h3 { min-height: 2.3em; }
}

/* ---- contact demand chips: equal heights when one label wraps ---- */
#contact .visite-mode__opt span { height: 100%; }

/* ---- ≤620px: the nav carries only logo + WhatsApp + burger — the mobilebar already
   owns VISITE, and the dark button crowded the hero eyebrow on the first screen ---- */
@media (max-width: 620px) {
  .nav__actions .btn-primary { display: none; }
}

/* ---- touch: villas/matériaux hover states must not stick after tap ---- */
@media (hover: none) {
  #villas .card:hover .card__media::after { box-shadow: inset 0 0 0 1px rgba(132, 101, 41, 0); }
  #villas .card:hover .v-rule::after { width: 34px; }
  #materiaux .card:hover .card__media::after { opacity: 0; transform: scale(1.015); }
  #materiaux .card:hover .card__body::before { width: 28px; opacity: .5; }
  #materiaux .card:hover .card__body h3 { color: var(--ink); }
}

/* ---- #patrimoine's arch used to be pinned sticky « while the LONG text column
   scrolls ». That column is no longer long: the sweep cut it to a headline and one
   40-word lede, and MEASURED at 861/940/1024/1200/1440/1680 on all four routes that
   carry the chapter, the arch is the TALLER of the two everywhere (502–636 px against
   243–450). A sticky element can only travel inside its own scroll container, so the
   rule could never fire once the premise flipped — it was inert CSS carrying a note
   that described the opposite layout, and a note about `.temoin-visual`, a class the
   section no longer contains. Removed, not corrected. `.df-visual` keeps
   `position: relative` from its base rule, so `.df-frame` still hangs off it. ---- */

/* ---- inline ambient loops: the video covers its media box inside the arch and
   dissolves in only once it actually plays — the photograph never flashes away ---- */
[data-loop-src] { position: relative; }
[data-loop-src] .inline-loop {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: var(--arch);
  opacity: 0; transition: opacity 1.4s var(--ease);
  pointer-events: none; z-index: 0;
}
[data-loop-src].is-looping .inline-loop { opacity: 1; }
/* keep the crafted brass edge above the footage */
[data-loop-src].feature__media--arch::before { z-index: 1; }
@media (prefers-reduced-motion: reduce), print {
  [data-loop-src] .inline-loop { display: none; }
}

/* ---- arch caption: the museum provenance line under an editorial arch (shared idiom
   with #domaine .dom-cap — brass em carries the provenance claim) ---- */
.arch-cap {
  margin: 1.05rem .2rem 0; font-family: var(--serif); font-style: italic;
  font-size: .98rem; letter-spacing: .01em; color: var(--stone);
}
.arch-cap em { font-style: italic; color: var(--brass); }

/* ---- hero primary CTA: a breath of brass depth for tap-magnetism (paid traffic) ---- */
#hero .hero__cta .btn-primary { box-shadow: 0 16px 38px -16px rgba(132, 101, 41, .6); }

/* ---- footer on phones: the ADDRESS's dot-separated items stack — a separator can
   never strand at a line edge in either direction. Scoped to the address on purpose:
   applied to the legal nav too it turned three tiers into one undifferentiated ladder
   of underlined grey lines. The legal links keep their single dot-separated wrap. ---- */
@media (max-width: 620px) {
  footer address .foot-item { display: block; margin-top: .4rem; white-space: normal !important; }
  footer address .foot-dot { display: none; }
  /* The legal tier keeps its compact wrap (never the one-link-per-line ladder), but it
     wrapped onto three lines each opening on a stranded « · ». The gap carries the
     separation the dots used to; the dots go. */
  footer nav[aria-label="Liens légaux"] { display: flex; flex-wrap: wrap; gap: 0 1.1rem; }
  footer nav[aria-label="Liens légaux"] .foot-dot { display: none; }
  /* …and the legal links themselves measured 36,4px tall. Same negative-margin technique
     as #confiance p a, calibrated to the 8px padding `footer a` already carries above:
     +4px each side is exactly absorbed, so the target reaches 44px and the tier keeps
     its 93,2px height and its leading rhythm with the address and site-nav rows. */
  footer nav[aria-label="Liens légaux"] a { padding-block: 12px; margin-block: -4px; min-height: 44px; }
}

/* ---- tap target: the trust chapter's inline WhatsApp link — the first conversion
   opportunity after the hero — measured 19px tall. Same treatment as footer/icon-list
   links, sized to the 44px minimum; the negative margin absorbs the added height so
   the paragraph's rhythm is unchanged. ---- */
#confiance p a { display: inline-block; padding-block: 12px; margin-block: -12px; min-height: 44px; }

/* ===== Campaign landing pages (mre.html / local.html) — reduce nav leaks =====
   One page = one goal = one CTA. Keep logo + WhatsApp + Visite actions; drop the
   section menu + burger + chapter rail. data-page is set synchronously in <head>
   (PAGE_CONFIG) so nothing ever flashes before it hides. */
html[data-page="campaign"] .nav__links { display: none; }
html[data-page="campaign"] .nav__burger { display: none; }
html[data-page="campaign"] #tour-rail { display: none; }
/* One goal in the header: the circular WhatsApp float is already on screen at these
   widths, so the outline WhatsApp pill was the same ask twice, at near-equal weight
   with the VISITE block it was competing against. */
html[data-page="campaign"] .nav__actions .btn-whatsapp { display: none; }
/* The footer handed back everything the reduced header removed. The four content
   routes duplicate chapters already on the page, and the two segment links label an
   exit to the page written for the OTHER segment (both noindex, so no SEO value
   either). The whole site nav goes; the brand line, address, legal tier and the
   mobilebar remain. .foot-seg stays defined so the segment links can never return
   here even if the nav is one day restored. */
/* !important: the nav carries display:flex as an inline style (footer is inline-styled
   throughout), which no stylesheet rule can outrank without it. */
html[data-page="campaign"] footer nav[aria-label="Le site"] { display: none !important; }
html[data-page="campaign"] .foot-seg { display: none; }
/* One axis, top to bottom. The header rides container--wide (1460) while every section
   and the footer sit on --container (1240) — the same wordmark 94,8px apart at 1440,
   110px above 1588. On the campaign pages the header holds a logo and one button, so
   the wide track buys nothing. index.html keeps its 1460: six nav links + two buttons
   measure 1302px and would not fit 1112px. */
html[data-page="campaign"] .nav__inner.container--wide { max-width: var(--container); }
/* ≤620 the campaign header holds nothing but the logo (WhatsApp hidden ≤820, Visite
   ≤620, burger by the rule above) yet kept a permanently fixed ~69px band — 8% of the
   viewport on the pages that most need it. It scrolls away after the first screen;
   the mobilebar owns the actions. */
@media (max-width: 620px) {
  html[data-page="campaign"] .nav { position: absolute; }
  /* …and with the nav no longer fixed here, the 92px it was reserving became dead
     space: an ad landing on #preuve-prix opened a third of the first screen above
     its own target. Just enough left to breathe under the viewport edge.
     !important: several anchor targets carry an inline scroll-margin-top (96px),
     which no ordinary rule can outrank. */
  html[data-page="campaign"] main section[id],
  html[data-page="campaign"] main [id] { scroll-margin-top: 20px !important; }
}
/* The first screen fits at 390×844 and nowhere under it: the hero stack is ~890–905px
   intrinsic, so at 360×800 the money ledger renders entirely behind the mobilebar and
   at 375×667 it falls 81px below the fold. The film row (78px + its 21px gap) is the
   one element the first screen can lose without losing an argument — #galerie still
   carries « REVOIR LE FILM DU DOMAINE » at 280×44. Height-gated: at 390×844 nothing
   changes and the film row survives. */
@media (max-width: 620px) and (max-height: 820px) {
  html[data-page="campaign"] #hero .hero__film { display: none; }
}
