/*
Theme Name: Electrone Security Systems
Theme URI: https://electrone.be
Author: Electrone SRL
Author URI: https://electrone.be
Description: Full Site Editing block theme for Electrone Security Systems, a Belgian electronic security systems installer. Built around theme.json for global colors, typography and layout so the design stays editable in the Site Editor.
Requires at least: 6.5
Tested up to: 6.7
Requires PHP: 7.4
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: electrone-security-systems
Tags: full-site-editing, block-patterns, custom-colors, custom-menu, one-page

This is a block theme. Global colors, typography and layout live in theme.json;
the CSS below only covers presentation that has no equivalent block support
(decorative backgrounds, absolute-positioned map elements, custom form fields,
grid layouts) so it survives Site Editor round-trips intact.

Note: Group/Columns blocks deliberately avoid the "layout" and "border" block
attributes. Those supports make the block editor generate instance-specific
classes/CSS that hand-authored markup can't reliably reproduce, which trips
"Block contains unexpected or invalid content" on re-parse. All flex/grid/gap/
border treatment below is done with plain classNames instead so saved markup
always matches what the block's own attributes (className, backgroundColor,
textColor, padding, margin) predict.
*/

html {
	scroll-behavior: smooth;
	scroll-padding-top: 48px;
	/* Belt-and-suspenders against a horizontal-scroll "white strip" on
	   mobile: some content (a long untranslated word, a Weglot-swapped
	   string longer than the French original) can occasionally end up
	   wider than the viewport. overflow-wrap makes the actual overflow far
	   less likely to happen in the first place, by letting any single long
	   word break instead of forcing its container wider; overflow-x below
	   is the backstop for whatever still gets through.

	   "clip", deliberately not "hidden": shipped a "hidden" version of this
	   first and broke the header's position:sticky on every page, mobile
	   and desktop both — confirmed live (getBoundingClientRect().top went
	   to -1000 after scrolling 1000px, i.e. the header just scrolled away
	   like a normal element). "hidden" (like "auto"/"scroll") makes the
	   browser treat the element as an actual scroll container, which is
	   exactly the footgun the previous version of this comment reasoned
	   its way around and still got wrong in practice — every html/body
	   overflow-x/overflow-y combination tried while debugging this either
	   broke sticky again or silently stopped clipping (confirmed both ways
	   live, by scrolling an injected 3000px-wide test element into view).
	   "clip" is the one value that hides the overflow without turning the
	   element into a scroll container at all, so there's no scrolling
	   ancestor to break sticky's viewport-relative "stuck" calculation in
	   the first place. Needs Chrome 90+ / Safari 16+ / Firefox 113+, all
	   already implied by this theme's other CSS (:has(), grid, clamp()). */
	overflow-x: clip;
	overflow-wrap: break-word;
}

body {
	/* Set on both html and body — see the html rule above for why "clip"
	   specifically, and assets/js/nav-scrollspy.js / the sticky header rule
	   further down for the position:sticky this once broke. */
	overflow-x: clip;
}

@media (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}
}

/* Layout utilities — replace Group/Columns "layout" attribute usage */
.site-container {
	max-width: 1440px;
	margin-left: auto;
	margin-right: auto;
	width: 100%;
}

.flex-row {
	display: flex;
	align-items: center;
}

.flex-col {
	display: flex;
	flex-direction: column;
}

.wrap {
	flex-wrap: wrap;
}

.nowrap {
	flex-wrap: nowrap;
}

.justify-between {
	justify-content: space-between;
}

.justify-center {
	justify-content: center;
}

.gap-8 { gap: 8px; }
.gap-12 { gap: 12px; }
.gap-14 { gap: 14px; }
.gap-16 { gap: 16px; }
.gap-18 { gap: 18px; }
.gap-20 { gap: 20px; }
.gap-22 { gap: 22px; }
.gap-24 { gap: 24px; }

.hero-columns {
	gap: clamp(32px, 4vw, 72px);
}

.hero-columns > .wp-block-column {
	align-self: center;
}

/* Plays automatically on page load (no JS/IntersectionObserver involved,
   unlike the scroll-triggered .reveal-in elements below) since these are
   all above the fold and already visible on arrival — a scroll trigger
   would never fire until the visitor actually scrolls, which for content
   already on screen reads as "briefly missing" rather than an intentional
   reveal. Ends at each element's own "to" keyframe (opacity:1), so
   everything is guaranteed visible even if the animation is interrupted or
   unsupported. Staggered animation-delay makes the heading lead and the
   two cards follow in sequence instead of all three snapping in at once.

   ".hero-columns" scoping on the cards matters: .offer-card is unique to
   the hero, but .testimonials-card is reused by the Évaluations card in
   the confiance section, which already has its own separate scroll-
   triggered .reveal-in animation and must not also get this load-triggered
   one (it's below the fold, so this one would never even play until
   scrolled to — better to leave it solely on .reveal-in).

   Delays follow DOM/visual order (testimonials-card now comes first,
   offer-card second — see homepage-content.php), not which card is which,
   so the cascade always reads top-to-bottom regardless of which card that
   happens to be. */
.hero-columns h1 {
	animation: hero-fade-in 0.9s ease both;
}

.hero-columns .testimonials-card {
	animation: hero-fade-in 0.9s ease both;
	animation-delay: 0.15s;
}

.hero-columns .offer-card {
	animation: hero-fade-in 0.9s ease both;
	animation-delay: 0.3s;
}

@keyframes hero-fade-in {
	from { opacity: 0; transform: translateY(16px); }
	to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
	.hero-columns h1,
	.hero-columns .offer-card,
	.hero-columns .testimonials-card {
		animation: none;
	}
}

/* Section titles (h2 — "Nos solutions", "Nos services", "Confiance &
   conformité", etc.) use theme.json's x-large size, a clamp() that
   already floors at 1.625rem (26px) on phones and roughly 35-38px on
   tablets. That's still too big once Weglot translates to a language with
   long compound words — confirmed from a real phone screenshot of the
   Dutch translation ("...praktijkervaringen.") wrapping to 5 lines with a
   single orphaned "n." on the last line. clamp()'s own floor can't go
   any lower without editing the shared x-large token (used elsewhere
   too), so this overrides font-size directly in the two ranges that need
   it. !important because theme.json's generated global-styles rule and
   this stylesheet's own print order isn't reliably one direction — same
   reasoning as other forced overrides of core-generated CSS in this file
   (e.g. .site-header-grid's display:grid). 1100px matches the same
   tablet-range upper bound used elsewhere in this file for .service-card
   h3. Reasoned from
   available-width math, not verified live (browser access paused per
   prior instruction while Cloudflare is live) — worth a follow-up
   screenshot after deploying. */
@media (max-width: 781px) {
	h2 {
		font-size: 22px !important;
	}
}

@media (min-width: 782px) and (max-width: 1100px) {
	h2 {
		font-size: 28px !important;
	}
}

@media (max-width: 781px) {
	/* First pass here just added margin above/below the h1, which still read
	   as cramped right under the header. Centering the heading within a
	   chunk of screen height (minus the sticky header, already tracked in
	   --site-header-height for the mobile nav dropdown — see style.css's
	   nav-scrollspy comments) reads as an actual hero moment instead, with
	   the offer card starting fresh on the next scroll. flex-direction:
	   column (not row) is deliberate: it keeps the default
	   align-items:stretch on the cross axis, so the heading still spans the
	   column's full width exactly as it did as a plain block element —
	   only its vertical position changes. Duplicated min-height with vh
	   first, svh second: browsers without the newer svh unit just ignore
	   that line and keep the vh fallback instead of dropping the whole
	   rule.

	   49 (not 100, not 66): a first pass used the full screen height, then
	   a second pass halved it to 66svh — both read as too much empty space
	   above/below the heading, so this has now been halved again from the
	   66svh version. Halving this gap isn't a clean formula (it depends on
	   the heading's own rendered height, which CSS has no way to read back
	   into this calc()), so each pass was tuned empirically instead:
	   measured live against the previous version's gap at common phone
	   widths (320-414px). 49svh lands almost exactly on half of 66svh's
	   gap at 375px, though the error widens at the extremes of that range
	   (roughly -25% at 320px, +17% at 414px) since halving an already-small
	   gap is more sensitive to per-device viewport-height differences than
	   the first halving was. */
	.hero-columns > .wp-block-column:first-child {
		display: flex;
		flex-direction: column;
		justify-content: center;
		min-height: calc(49vh - var(--site-header-height, 76px));
		min-height: calc(49svh - var(--site-header-height, 76px));
	}
}

.services-columns {
	gap: 20px;
	align-items: stretch;
}

.services-columns > .wp-block-column {
	display: flex;
}

.confiance-columns {
	gap: 24px;
	align-items: stretch;
}

.confiance-columns > .wp-block-column {
	display: flex;
	/* Neither column has an explicit width, so core only gives them equal
	   flex-grow — flex-basis stays content-based, and the testimonials
	   card's nested content is enough wider than the legal card's plain
	   text rows that "equal growth" from unequal starting sizes still
	   lands unequal. Forcing flex-basis:0 makes both grow from the same
	   starting point, for a genuine 50/50 split. */
	flex-basis: 0;
	flex-grow: 1;
}

.equipment-columns {
	gap: clamp(32px, 5vw, 64px);
}

.equipment-col-divider {
	padding-left: clamp(32px, 5vw, 64px);
}

@media (max-width: 781px) {
	.equipment-col-divider {
		padding-left: 0;
		padding-top: clamp(32px, 5vw, 64px);
	}
}

.equipment-image {
	margin: 0;
}

.equipment-image img {
	display: block;
	width: 100%;
	height: auto;
	border-radius: 12px;
	border: 1px solid rgba(255, 255, 255, 0.12);
}

/* Same gap as .confiance-columns, and both columns now use explicit 50%
   widths (see homepage-content.php) instead of 45/55 — so the right column
   starts at exactly the same x position as .testimonials-card above it. */
.contact-columns {
	gap: 24px;
}

/* Thin divider line (replaces core/separator's color style, same reason as above) */
.card-divider {
	background-color: rgba(20, 22, 26, 0.08);
}

/* Sticky header stacking + centered nav grid */
/* WordPress wraps every template part in its own auto-generated container
   (header.wp-block-template-part) sized to fit its content exactly. Sticky
   positioning needs room to travel within its containing block, so it has to
   go on that wrapper, not on .site-header itself (whose wrapper is exactly
   as tall as the header, leaving no room to stick). */
header.wp-block-template-part:has(.site-header) {
	position: sticky;
	top: 0;
	z-index: 50;
}

.site-header {
	border-bottom: 1px solid rgba(255, 255, 255, 0.09);
}

.site-header-grid {
	display: grid !important;
	grid-template-columns: 1fr auto 1fr;
	align-items: center;
	gap: 1.25rem;
	max-width: 1440px;
	margin-left: auto;
	margin-right: auto;
}

.site-header-grid > * {
	min-width: 0;
}

.site-header-grid .wp-block-navigation {
	justify-self: center;
	width: auto !important;
	flex-wrap: wrap;
}

.site-header-grid .wp-block-navigation__container {
	gap: clamp(20px, 3vw, 36px) !important;
}

.site-header-grid .wp-block-navigation .wp-block-navigation-item__content {
	color: #f2f3f5 !important;
}

.site-header-grid .wp-block-navigation .wp-block-navigation-item__content:hover {
	color: #b01030 !important;
}

/* Set by assets/js/nav-scrollspy.js while the matching section is in view. */
.site-header-grid .wp-block-navigation .wp-block-navigation-item__content.is-active-link {
	color: var(--wp--preset--color--primary) !important;
	font-weight: 700;
}

/* WordPress keeps .wp-block-navigation__responsive-container in the DOM at
   all times — inline/relative on desktop, fixed-position full-screen overlay
   on mobile once .is-menu-open is toggled by the hamburger button. Scoping
   to .is-menu-open (confirmed via live inspection to only ever appear on the
   mobile overlay, never on desktop) targets the overlay specifically without
   the previous attempt's bug of also matching the always-present desktop
   wrapper. Core defaults this to a white background; make it match the
   header instead so the existing white nav-link colors above stay legible
   here too, without needing a separate text-color override. */
.wp-block-navigation__responsive-container.is-menu-open {
	background-color: var(--wp--preset--color--contrast) !important;
}

/* Core also makes the open menu cover the full viewport (position:fixed;
   inset:0). Pin it to just below the header instead and let it size to its
   own content, so it reads as a compact dropdown rather than a fullscreen
   takeover. max-height + overflow-y is a safety net in case the menu ever
   grows past what's left of the viewport on a very short screen.

   top reads --site-header-height, set by assets/js/nav-scrollspy.js from
   the header's own actual live getBoundingClientRect() — a hardcoded px
   value here previously went stale once already (44px, left over from
   before the header was made taller), and even freshly measured on one
   device it can be a couple px off on another (font rendering, DPI). The
   99px fallback covers the rare case JS hasn't run yet. */
.wp-block-navigation__responsive-container.is-menu-open {
	top: var(--site-header-height, 99px) !important;
	bottom: auto !important;
	height: auto !important;
	max-height: calc(100vh - var(--site-header-height, 99px)) !important;
	overflow-y: auto !important;
	/* Core reserves this (a clamp of the root padding vars, 1rem here since
	   we don't set those) to keep the dialog's content clear of the close
	   button drawn in its own top-right corner. Now that the close button is
	   repositioned into the header itself instead (see below), that reserved
	   space is just dead black area between the header and the menu links. */
	padding-top: 0 !important;
}

/* Core's default overlay also reserves calc(24px + 2rem) of top padding on
   the content wrapper specifically to clear its own close button — same
   reasoning as above, no longer needed once the close button lives in the
   header instead of inside this content flow. 20px just gives the menu
   links a little breathing room below the header, not a button to avoid. */
.wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
	padding-top: 20px !important;
}

/* Core renders the open (hamburger) and close (X) buttons as two entirely
   separate elements — the open button stays in the header's own grid, the
   close button lives inside the overlay dialog. Tried moving the close
   button on top of the hamburger via position:fixed, but the dialog itself
   has a core-applied (identity, but non-"none") transform, which makes IT
   the containing block for any fixed-position descendant instead of the
   viewport — the close button ended up offset by exactly the header's
   height, stuck low inside the dialog no matter what top/right we gave it.
   Simpler and more robust: never move the close button at all. Just hide it
   (nothing to reposition, nothing to leave dead space for) and instead
   morph the hamburger's own two SVG bars into an X with plain CSS transforms
   when the menu is open — same persistent element, no containing-block
   fight, no dead space. Core only wires the close button's own click to
   actually close the overlay, so assets/js/nav-scrollspy.js delegates a
   click on this (now X-shaped) open button through to the hidden close
   button whenever the menu is already open — one visible control that both
   opens and closes, instead of two. */
.wp-block-navigation__responsive-container-close {
	display: none !important;
}

.has-modal-open .site-header-grid .wp-block-navigation__responsive-container-open svg path:nth-child(1) {
	transform: translate(0, 3.75px) rotate(45deg);
	transform-box: fill-box;
	transform-origin: center;
}

.has-modal-open .site-header-grid .wp-block-navigation__responsive-container-open svg path:nth-child(2) {
	transform: translate(0, -3.75px) rotate(-45deg);
	transform-box: fill-box;
	transform-origin: center;
}

/* The hamburger (open) and X (close) icons have no color of their own by
   default — they inherit the page's normal dark heading color, which is
   invisible against both the black header bar and the now-black overlay. */
.site-header-grid .wp-block-navigation__responsive-container-open,
.site-header-grid .wp-block-navigation__responsive-container-close {
	color: #f2f3f5 !important;
}

/* Logo left, hamburger right. grid-row:1 is set explicitly on both —
   without it, the logo (first in DOM, explicit column 2) advances the
   auto-placement cursor past column 1 while filling row 1, and sparse
   packing (the grid default) won't backfill an earlier column once the
   cursor has moved on, so the nav (explicit column 1) silently gets pushed
   onto row 2 instead of sitting beside the logo.

   1120px, not core's own 600px hamburger breakpoint: at true tablet widths
   (confirmed live, e.g. 820px and even 1024px — iPad Pro portrait) the
   inline nav plus the logo genuinely don't fit side by side and overlap,
   since .wp-block-navigation is centered in an "auto" grid column sized to
   its own natural width regardless of what's left after the logo. The
   overlap only fully clears around ~1105px, so 1120px is used here (and
   below, to force the hamburger on) with a small safety margin. */
@media (max-width: 1120px) {
	.site-header-grid {
		grid-template-columns: 1fr auto;
	}

	.site-header-grid .logo-badge {
		grid-column: 1;
		grid-row: 1;
		justify-self: start;
	}

	.site-header-grid .wp-block-navigation {
		grid-column: 2;
		grid-row: 1;
		justify-self: end;
	}
}

/* Below 600px core already shows the hamburger and hides the inline nav
   on its own. Above that, core's own CSS keeps the open button hidden
   (".wp-block-navigation__responsive-container-open:not(.always-shown)"
   at min-width:600px) and the inline nav container visible unconditionally
   — there's no built-in way to push that switchover to a wider breakpoint,
   so this forces both sides of it directly for the added tablet range. */
@media (min-width: 600px) and (max-width: 1120px) {
	.site-header-grid .wp-block-navigation__responsive-container-open {
		display: flex !important;
	}

	.site-header-grid .wp-block-navigation__responsive-container:not(.is-menu-open) {
		display: none !important;
	}
}

.logo-badge {
	display: inline-flex;
	align-items: center;
	justify-self: start;
}

.logo-badge img {
	display: block;
	height: 40px;
	width: auto;
}

/* .site-header has no padding of its own in this stylesheet, so it falls
   back to core's default block padding for any background-colored group
   (":where(.wp-block-group.has-background) { padding: 1.25em 2.375em; }"),
   which was rendering the mobile header at 95px tall — measured live
   against a reference header (a competitor's Elementor site, 75.59375px at
   375px wide) that the client wants matched.

   First pass used 15.3px padding with the logo left at 40px and hit
   75.59375px exactly — but that alone didn't visibly change anything once
   deployed (a caching gap, not a CSS problem: confirmed live after the
   fact that the padding was still the old 25px). While waiting on that,
   the client separately asked to also match the reference's own logo
   height (its img renders at 28.09px tall on the actual page, not its
   350x100 native size) — so .logo-badge img is now 28.09px on mobile too,
   and 21.26px is the padding recalculated for THAT shorter logo to still
   land on 75.59375px overall (confirmed live: shrinking the logo without
   repadding undershot the target at ~63.7px).

   This block must come AFTER the base .logo-badge img rule above (not just
   nested in a narrower media query) — confirmed live that the mobile
   height:28.09px was silently losing to the base rule's height:40px at
   every width, since both selectors have identical specificity and equal-
   specificity rules resolve by source order regardless of which media
   query actually matches. */
@media (max-width: 599px) {
	.site-header {
		padding-top: 21.26px;
		padding-bottom: 21.26px;
	}

	.logo-badge img {
		height: 28.09px;
	}
}

/* Dotted background used on light sections */
.dotted-surface {
	background-image: radial-gradient(rgba(20, 22, 26, 0.12) 1px, transparent 1.6px);
	background-size: 26px 26px;
	background-attachment: fixed;
}

/* Small uppercase eyebrow label used above section headings */
.eyebrow {
	display: inline-block;
	text-transform: uppercase;
	letter-spacing: 0.14em;
	font-weight: 700;
	font-size: 0.75rem;
	color: var(--wp--preset--color--primary);
}

/* Sizing by height alone (the original approach here) left Ajax's logo —
   a flat wide wordmark — much wider than Verisure/Securitas/IBZ, which are
   all a squarer icon-over-text shape: matching height ignores how much
   each logo's own canvas actually uses width-wise. Constraining every logo
   to the same width+height box via object-fit:contain instead means each
   one scales to fill whichever dimension it's proportionally closer to
   filling, capped in both — no single logo can dominate the row regardless
   of its own aspect ratio. */
.partner-badge-row img {
	display: block;
	width: 160px;
	height: 68px;
	object-fit: contain;
}

/* At the 599px phone breakpoint used elsewhere in this theme, the card's own
   padding leaves roughly 264-279px of inner width. All 3 logos need to sit
   on one row (not just 2, as a first pass here previously allowed) — at
   108px each even a tight gap doesn't leave room for a third, so both the
   box and the gap come down further. 80x34 keeps the same aspect ratio as
   the 160x68 desktop box. ".hero-columns .partner-badge-row" (two classes)
   deliberately outranks the "gap-24" utility class's single-class
   specificity, so this wins regardless of source order — see the earlier
   .logo-badge img bug in this file for why that matters here specifically.
   10px (not the original 8px): confirmed live that's the widest gap that
   still leaves all 3 logos on one row at the narrowest common phone width
   (360px) — 12px already wraps Ajax to a second line there. */
@media (max-width: 599px) {
	.hero-columns .partner-badge-row {
		gap: 10px;
	}

	.partner-badge-row img {
		width: 80px;
		height: 34px;
	}
}

/* Hero offer card */
.offer-card {
	border: 1px solid rgba(255, 255, 255, 0.12);
	border-radius: 8px;
}

.offer-card-header {
	border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

/* Confiance & agréments card */
.legal-card {
	box-sizing: border-box;
	width: 100%;
	border: 1px solid rgba(20, 22, 26, 0.12);
	border-radius: 8px;
}

.legal-card-header {
	border-bottom: 1px solid rgba(20, 22, 26, 0.07);
}

.testimonials-card {
	box-sizing: border-box;
	width: 100%;
	border: 1px solid rgba(20, 22, 26, 0.12);
	border-radius: 8px;
}

.testimonials-card-header {
	border-bottom: 1px solid rgba(20, 22, 26, 0.07);
}

/* Contact section */
.contact-section {
	border-top: 1px solid rgba(255, 255, 255, 0.07);
}

.solutions-section {
	border-top: 1px solid rgba(255, 255, 255, 0.07);
	border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}

.max-w-640 {
	max-width: 640px;
}

.max-w-480 {
	max-width: 480px;
}

/* Services section: a dotted "trunk" line runs above the whole row, and
   each card's numbered badge hangs a short dotted "branch" down from it —
   .step-badge is positioned absolutely (not via negative margin), so its
   own ::before branch can anchor to it with bottom:100%, reaching exactly
   from the badge's top up to the trunk line with no manual offset math to
   keep the two in sync. Both lines are drawn with radial-gradient dot
   patterns rather than hand-positioned elements. */
.services-cards {
	position: relative;
	margin-top: 150px;
	/* The bookend icons (.services-rail-icon) sit outside this element's own
	   box via negative top/bottom offsets — confirmed on a real Android
	   phone (not reproducible in a desktop browser resized to match) that
	   whichever icon is the last to settle into place during a scroll
	   gesture intermittently fails to paint, in either direction, sometimes
	   both at once. That's consistent with a known class of mobile Chrome
	   tile-rasterization bug: content positioned outside its containing
	   block's own box can get excluded from the region the browser decides
	   to (re)paint during scroll, especially once animation is involved.
	   Forcing this element onto its own compositor layer makes the browser
	   treat it and its overflowing children as one atomic paint region
	   instead of trying to independently cull the overflow. */
	transform: translateZ(0);
}

.services-cards::before {
	content: "";
	position: absolute;
	top: -76px;
	left: 80px;
	right: 80px;
	height: 4px;
	background-image: radial-gradient(circle, var(--wp--preset--color--primary) 1.6px, transparent 1.6px);
	background-size: 14px 4px;
	background-repeat: repeat-x;
	background-position: 0 0;
	z-index: 0;
}

/* Bookend icons: an empty/unprotected house where the line starts, the
   full locked Electrone mark where it ends — same "before/after" story the
   4 numbered steps tell, just visualized at the ends of the rail. Both are
   position:absolute (not flex children of .services-cards, which is a
   4-column flex row) so they can't disturb the card grid's widths, and
   both are centered on the line via top + translate. Inset (32px) is half
   the icon's own width, so each icon's outer edge lands flush with the
   row's edge instead of overhanging it. The line's own inset (80px) clears
   each icon's footprint (32px inset + 32px half-width) plus a 16px gap.
   top is -83px rather than -74 (the line's own center): the empty-house
   artwork has a lot of transparent padding above the house shape and none
   below, so centering the full image box on the line actually left the
   line crossing well above the house's visual middle — shifting the whole
   box up moves the house shape itself back into place. */
.services-rail-icon {
	position: absolute;
	top: -83px;
	width: 64px;
	height: 64px;
	z-index: 1;
}

.services-rail-icon.is-start {
	left: 32px;
	transform: translate(-50%, -50%);
}

.services-rail-icon.is-end {
	right: 32px;
	transform: translate(50%, -50%);
}

/* The trunk line draws itself left to right on first reveal (scaleX rather
   than width so it's a compositor-friendly transform, not a
   layout-triggering property), then once fully drawn its dots keep flowing
   left to right on a seamless loop — one animation cycle shifts the
   background exactly one dot-pattern period (14px), so it wraps invisibly.
   The end icon fades in once the line reaches it. All triggered by the
   same .is-revealed class (see assets/js/services-reveal.js). */
.has-js .services-cards::before {
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 0.7s ease;
}

.has-js .services-cards.is-revealed::before {
	transform: scaleX(1);
	animation: services-dots-flow-x 1.333s linear infinite;
}

.has-js .services-rail-icon.is-start {
	opacity: 0;
	transition: opacity 0.3s ease;
}

.has-js .services-cards.is-revealed .services-rail-icon.is-start {
	opacity: 1;
}

.has-js .services-rail-icon.is-end {
	opacity: 0;
	transition: opacity 0.3s ease;
	transition-delay: 0.7s;
}

.has-js .services-cards.is-revealed .services-rail-icon.is-end {
	opacity: 1;
	/* Half the cycle length as delay, so this one pulses exactly when the
	   start icon is resting, and vice versa — an alternating "taking
	   turns" rhythm rather than both breathing in sync. */
	animation: services-rail-pulse-end 4s ease-in-out 2s infinite;
}

@keyframes services-dots-flow-x {
	from { background-position: 0 0; }
	to { background-position: 14px 0; }
}

@keyframes services-rail-pulse-end {
	0%, 100% { transform: translate(50%, -50%) scale(1); }
	50% { transform: translate(50%, -50%) scale(1.15); }
}

@media (max-width: 781px) {
	.services-cards::before,
	.services-rail-icon {
		display: none;
	}
}

.service-card {
	position: relative;
	z-index: 1;
	border: 1px solid rgba(20, 22, 26, 0.12);
	border-radius: 8px;
}

/* Matches the equipment section's h3 styling (Systèmes d'alarme / Caméras
   de surveillance — 22px, see homepage-content.php) so both sets of card
   titles read as the same visual language, just swapped for pure black
   since these sit on a light card background instead of the dark one. */
.service-card h3 {
	font-size: 22px;
	color: #000000;
}

/* Above ~1100px the 4-across grid is wide enough per column for 22px to
   still fit in ~2 lines. Below that, 4 columns side by side (or, on
   phones, stacked to full width but with less room than that number
   alone suggests — see .services-cards' margin-left:100px reserved for
   the vertical rail below) leaves less room per title than 22px wants.
   First pass here only touched the tablet range and landed on 16px,
   confirmed too tight from a second real iPad screenshot — the Dutch
   translation "implementatie" still broke as "implementati" / "e" at
   16px. Sized down further (14px tablet) and added a phone value too
   (20px) as asked, both with a bit more margin this time. Reasoned from
   column-width math, not verified live (browser access paused per prior
   instruction while Cloudflare is live). */
@media (max-width: 781px) {
	.service-card h3 {
		font-size: 20px;
	}
}

@media (min-width: 782px) and (max-width: 1100px) {
	.service-card h3 {
		font-size: 14px;
	}
}

/* Solid filled square (not a circle) with an 8px border-radius, matching
   the same rounded-corner language used on .service-card/.equipment-image
   elsewhere in this theme. Same ~40px footprint and white-on-red fill as
   the original circle badge. Centered exactly on the card's own top border
   (top:-20px on a 40px box puts its vertical center at 0), so half sits
   inside the card, half outside. */
.step-badge {
	position: absolute;
	top: -20px;
	left: 50%;
	transform: translateX(-50%);
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	border-radius: 8px;
	background-color: var(--wp--preset--color--primary);
	color: var(--wp--preset--color--base);
	font-size: 14px;
	font-weight: 700;
	z-index: 2;
}

/* Dotted branch connecting the trunk line down to this badge specifically.
   bottom:100% pins it flush to the badge's own top edge; height is a touch
   short of the full gap to the trunk line's center (-74px), leaving a
   small visible gap at the top instead of touching the trunk line exactly.
   Same dot radius and spacing as the horizontal trunk line
   (.services-cards::before), just rotated 90° — 14px 4px there, 4px 14px
   here — so the two read as one continuous dot pattern. */
.step-badge::before {
	content: "";
	position: absolute;
	left: 50%;
	bottom: 100%;
	transform: translateX(-50%);
	width: 4px;
	height: 48px;
	background-image: radial-gradient(circle, var(--wp--preset--color--primary) 1.6px, transparent 1.6px);
	background-size: 4px 14px;
	background-repeat: repeat-y;
}

/* Cards pop in sequentially as the row scrolls into view (see
   assets/js/services-reveal.js). Gated behind .has-js (added inline, see
   functions.php) so the cards stay visible by default and only animate once
   JS has actually confirmed it's running — no permanently-invisible content
   if a script fails to load or the visitor has JS disabled. Each card's
   --reveal-delay custom property (set by the reveal script) is inherited by
   its own .step-badge, so the badge pops in lockstep with its card. */
.has-js .services-cards .service-card {
	opacity: 0;
	transform: translateY(28px);
	transition: opacity 0.5s ease, transform 0.5s ease;
	transition-delay: var(--reveal-delay, 0ms);
}

.has-js .services-cards .service-card.is-visible {
	opacity: 1;
	transform: translateY(0);
}

.has-js .step-badge {
	transform: translateX(-50%) scale(0);
	transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
	transition-delay: var(--reveal-delay, 0ms);
}

.has-js .service-card.is-visible .step-badge {
	transform: translateX(-50%) scale(1);
}

/* The branch grows up from the badge shortly after the badge itself pops
   in, so the sequence reads as badge-then-branch rather than everything
   appearing at once. Once fully grown, its dots start flowing bottom to
   top (from the node up toward the trunk line) on the same seamless-loop
   technique as the trunk line — one cycle shifts the background exactly
   one dot-pattern period (14px). The flow's own delay (150ms + 300ms) is
   timed to start right as the grow-in transition above finishes. */
.has-js .step-badge::before {
	transform: translateX(-50%) scaleY(0);
	transform-origin: bottom;
	transition: transform 0.3s ease;
	transition-delay: calc(var(--reveal-delay, 0ms) + 150ms);
}

.has-js .service-card.is-visible .step-badge::before {
	transform: translateX(-50%) scaleY(1);
	animation: services-dots-flow-y 1.333s linear infinite;
	animation-delay: calc(var(--reveal-delay, 0ms) + 450ms);
}

@keyframes services-dots-flow-y {
	from { background-position: 0 14px; }
	to { background-position: 0 0; }
}

@media (prefers-reduced-motion: reduce) {
	.has-js .services-cards .service-card {
		opacity: 1;
		transform: none;
		transition: none;
	}

	.has-js .step-badge {
		transform: translateX(-50%);
		transition: none;
	}

	.has-js .step-badge::before {
		transform: translateX(-50%);
		transition: none;
		animation: none;
	}

	.has-js .services-cards::before {
		transform: none;
		transition: none;
		animation: none;
	}

	.has-js .services-rail-icon.is-start,
	.has-js .services-rail-icon.is-end {
		opacity: 1;
		transition: none;
		animation: none;
	}
}

/* Generic scroll-reveal for section titles/cards outside the services rail
   above (which has its own bespoke badge/branch-line choreography — left
   untouched). Same .has-js gating rationale as that system: cards stay
   visible by default and only animate once JS has actually confirmed it's
   running, so nothing is permanently hidden if a script fails to load or
   JS is disabled. See assets/js/scroll-reveal.js. */
.has-js .reveal-in {
	opacity: 0;
	transform: translateY(24px);
	transition: opacity 0.6s ease, transform 0.6s ease;
	transition-delay: var(--reveal-delay, 0ms);
}

.has-js .reveal-in.is-shown {
	opacity: 1;
	transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
	.has-js .reveal-in {
		opacity: 1;
		transform: none;
		transition: none;
	}
}

/* .map-placeholder skips the translateY slide every other .reveal-in
   element uses. assets/js/contact-map-height.js measures this element's
   own getBoundingClientRect() on page load to set its height so it lines
   up with the contact form's bottom edge — and that transform (still
   active right up until this element's first scroll-triggered reveal)
   shifts where the browser reports its top edge, throwing off that very
   first measurement by exactly the 24px the slide moves — matches a
   reported real "map misaligned with the form until the next resize" bug
   exactly, traced here by reading contact-map-height.js rather than a
   live repro (browser access paused per prior instruction while
   Cloudflare is live). Opacity alone doesn't affect getBoundingClientRect(), so
   keeping just the fade (dropping the slide) avoids the whole class of
   transform-vs-measurement timing issues here. Higher specificity than
   ".has-js .reveal-in" (3 classes vs. 2) so this wins regardless of
   source order. */
.has-js .map-placeholder.reveal-in {
	transform: none;
}

.has-js .map-placeholder.reveal-in.is-shown {
	transform: none;
}

/* Below 782px — NOT the header/hamburger's own 600px breakpoint (see the
   nav override above), but the wider threshold at which WordPress core's
   Columns block itself actually stops stacking .services-cards to 100%
   width (confirmed live: still one-per-row at 781px, back to 4-across at
   782px). Using 599px here used to leave a broken 600-781px gap where the
   cards were already stacked by core but this rail still applied its
   horizontal desktop geometry — no bookend icons in the right place, a
   trunk line spanning nothing. The whole rail rotates 90° here: a
   vertical trunk line runs down the left side of the stacked cards instead
   of a horizontal one above them, each badge straddles its own card's left
   edge instead of its top edge, and each branch reaches left toward the
   trunk instead of up. Every rule below is a direct axis-swap of its
   desktop counterpart earlier in this file — same distances, same gaps,
   same reveal choreography, just rotated. This block has to come after
   every desktop rule it overrides (not just be nested inside a narrower
   media query) — equal-specificity selectors resolve by source order
   regardless of which media query matches, so placed any earlier the
   desktop declarations further down the file would still win even at
   mobile widths. Numbers (margin-left, the -52px trunk offset, etc.)
   assume the 4 cards end up close to equal height, same as the desktop
   version assumed equal-width columns; if card heights ever diverge a
   lot, the trunk won't hit each badge dead-on. */
@media (max-width: 781px) {
	.services-cards {
		margin-top: 80px;
		margin-bottom: 90px;
		margin-left: 100px;
		/* .site-container (also applied to this element) sets width:100%,
		   so margin-left alone would just push the whole row 100px past its
		   container's right edge instead of shrinking it — there'd be no
		   actual reserved space for the rail, and the cards would still
		   occupy it. */
		width: calc(100% - 100px);
	}

	/* top/bottom are small (16px) rather than the desktop-mirroring 72px
	   this started as — now that the icons properly clear the row's own
	   top/bottom edge (see .services-rail-icon.is-start/is-end below)
	   there's no longer a large icon footprint inside the row to route
	   around, so the trunk can start right where the icon's own gap ends,
	   reading as one continuous rail instead of two disconnected pieces. */
	.services-cards::before {
		display: block;
		left: -52px;
		right: auto;
		top: 16px;
		bottom: 16px;
		width: 4px;
		height: auto;
		background-size: 4px 14px;
		background-repeat: repeat-y;
	}

	/* -50px, matching the trunk line's own center (left:-52px + 4px width
	   above) exactly, so the icons read as sitting right on the line
	   instead of floating off to one side of it. An earlier version used
	   -70px to dodge overlapping the badges, but that's moot now that the
	   icons sit fully above/below the row (a different axis) rather than
	   alongside it — the vertical separation alone already rules out any
	   badge overlap regardless of horizontal position. */
	.services-rail-icon {
		display: block;
		left: -50px;
		top: auto;
	}

	/* top/bottom are negative here (not the +32px "small inset" desktop
	   uses for left/right) because on desktop the icons clear the cards
	   via the *other* axis (a large negative top pushes them above the
	   row entirely) — on mobile the rail's along-the-line axis IS the
	   card-stacking axis, so the icons have to actually clear the row's
	   own top/bottom edge to avoid sitting on top of card 1 / card 4,
	   the same way the horizontal ones clear the cards sideways. -40px
	   center means (with the 64px box) they clear by 8px net.

	   left is repeated here (matching the shared .services-rail-icon rule
	   above) because desktop's OWN .is-start/.is-end selectors each set
	   left/right too (32px/32px, 2 classes chained) — same specificity as
	   this block's own .is-start/.is-end, so without restating "left"
	   here specifically, desktop's higher-specificity-for-that-property
	   .is-start rule (left: 32px) would win over the shared mobile rule
	   above (which only has 1 class) regardless of source order, which is
	   exactly what was pushing the start icon off to the right. */
	.services-rail-icon.is-start {
		top: -40px;
		bottom: auto;
		left: -50px;
		/* translate3d, not translate — see the pulse keyframes below for why:
		   real mobile Chrome (confirmed by the user directly, not reproducible
		   in a desktop browser resized to the same width) was intermittently
		   failing to repaint this element while scrolling, making it vanish
		   until the next unrelated repaint. The earlier will-change fix for
		   this didn't hold up on their device — will-change is only a hint the
		   browser is free to handle inconsistently. translate3d forces actual
		   GPU compositing unconditionally, a much older and more reliably
		   supported technique for exactly this "vanishes mid-scroll" bug. */
		transform: translate3d(-50%, -50%, 0);
	}

	.services-rail-icon.is-end {
		bottom: -40px;
		top: auto;
		left: -50px;
		transform: translate3d(-50%, 50%, 0);
	}

	.step-badge {
		top: 50%;
		left: -20px;
		transform: translateY(-50%);
	}

	.step-badge::before {
		left: auto;
		right: 100%;
		bottom: auto;
		top: 50%;
		transform: translateY(-50%);
		width: 24px;
		height: 4px;
		background-size: 14px 4px;
		background-repeat: repeat-x;
	}

	/* Per explicit request: skip the scroll-triggered "pop in" grow/fade
	   transition on mobile (that's the part that was flickering/glitchy),
	   but keep the continuous animations running — the trunk/branch dots
	   keep flowing and the bookend icons keep pulsating regardless of
	   scroll position, starting as soon as the page loads rather than
	   waiting for .is-revealed/.is-visible.

	   Each rule below is duplicated against its desktop .is-revealed /
	   .is-visible gated equivalent (e.g. ".has-js .services-cards.is-revealed
	   .services-rail-icon.is-start") on purpose — those desktop selectors
	   chain 1-2 more classes than a plain ".has-js .services-rail-icon.is-start",
	   so they're MORE specific and win over this block regardless of source
	   order the moment JS actually adds .is-revealed/.is-visible (which it
	   still does — services-reveal.js wasn't touched). Without matching that
	   specificity here, the desktop animation/transform (wrong axis, wrong
	   keyframe) would silently take back over as soon as those classes land,
	   which is exactly the "flickering, icons not fixed" bug this caused. */
	.has-js .services-cards::before,
	.has-js .services-cards.is-revealed::before {
		transform: scaleY(1);
		transition: none;
		animation: services-dots-flow-y-forward 1.333s linear infinite;
	}

	.has-js .step-badge,
	.has-js .service-card.is-visible .step-badge {
		transform: translateY(-50%) scale(1);
		transition: none;
	}

	.has-js .step-badge::before,
	.has-js .service-card.is-visible .step-badge::before {
		transform: translateY(-50%) scaleX(1);
		transition: none;
		animation: services-dots-flow-x-reverse 1.333s linear infinite;
	}

	.has-js .services-rail-icon.is-start,
	.has-js .services-cards.is-revealed .services-rail-icon.is-start {
		opacity: 1;
		transition: none;
	}

	.has-js .services-rail-icon.is-end,
	.has-js .services-cards.is-revealed .services-rail-icon.is-end {
		opacity: 1;
		transition: none;
		animation: services-rail-pulse-end-mobile 4s ease-in-out 2s infinite;
	}
}

@keyframes services-rail-pulse-end-mobile {
	0%, 100% { transform: translate3d(-50%, 50%, 0) scale(1); }
	50% { transform: translate3d(-50%, 50%, 0) scale(1.15); }
}

@keyframes services-dots-flow-y-forward {
	from { background-position: 0 0; }
	to { background-position: 0 14px; }
}

@keyframes services-dots-flow-x-reverse {
	from { background-position: 14px 0; }
	to { background-position: 0 0; }
}

@media (max-width: 781px) and (prefers-reduced-motion: reduce) {
	.has-js .step-badge {
		transform: translateY(-50%);
	}

	.has-js .step-badge::before {
		transform: translateY(-50%);
	}
}

/* Boutique en ligne card: feature list replacing the old single paragraph.
   Dash bullets (not default discs) to match the same "—" accent used
   elsewhere on the site, in the brand red rather than plain text color. */
.boutique-features {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 10px;
}

.boutique-features li {
	position: relative;
	padding-left: 22px;
}

.boutique-features li::before {
	content: "—";
	position: absolute;
	left: 0;
	color: var(--wp--preset--color--primary);
}

/* Legal / agrément info rows */
.info-row {
	padding: 16px 0;
	border-bottom: 1px solid rgba(20, 22, 26, 0.07);
}

.info-row:last-child {
	border-bottom: none;
}

.info-label {
	display: block;
	font-size: 11px;
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
	margin-bottom: 6px;
}

/* Testimonials */
/* .gap-18 is only ever used on the row wrapping the two review cards (see
   homepage-content.php) — this row is a direct child of .testimonials-card
   flex-col, which otherwise leaves it at natural content height, shorter
   than the legal-card it sits next to (equal-height columns, see
   .confiance-columns) and leaving a visible empty gap below it.
   align-items:stretch (overriding .flex-row's own align-items:center, at
   higher specificity so it wins regardless of source order) makes the
   review cards actually grow taller to fill that space, not just get
   centered within it. */
.testimonials-card .flex-row.gap-18 {
	flex: 1;
	align-items: stretch;
}

.testimonial-card {
	border: 1px solid rgba(20, 22, 26, 0.1);
	border-radius: 8px;
	flex: 1 1 240px;
	justify-content: space-between;
}

.testimonial-card .stars {
	color: var(--wp--preset--color--primary);
	letter-spacing: 2px;
	font-size: 14px;
}

.tag-pill {
	display: inline-flex;
	align-self: flex-start;
	padding: 5px 10px;
	background-color: rgba(176, 16, 48, 0.16);
	color: var(--wp--preset--color--primary);
	border-radius: 8px;
	font-size: 11.5px;
	font-weight: 600;
}

/* Contact form (native core/html block, no form-builder plugin required) */
.electrone-form {
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.electrone-form button[type="submit"] {
	align-self: flex-start;
	margin-top: 6px;
	border: none;
	cursor: pointer;
}

.electrone-form .form-row {
	display: flex;
	gap: 14px;
	flex-wrap: wrap;
}

.electrone-form .form-field {
	flex: 1 1 160px;
	display: flex;
	flex-direction: column;
	gap: 2px;
	padding: 10px 16px;
	border: 1px solid rgba(255, 255, 255, 0.12);
	border-radius: 8px;
	background-color: #282828;
}

/* The Message field sits directly in .electrone-form's own column-direction
   flex flow (unlike the paired fields above it, which are flex-grow
   siblings inside a .form-row), so the flex-grow:1 it inherits from
   .form-field above makes it balloon to absorb the form's own leftover
   vertical space — 182px rendered vs ~113px of actual content. flex:none
   here makes it size to its content only. */
.electrone-form > .form-field {
	flex: none;
}

.electrone-form .form-field span {
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--wp--preset--color--text-on-dark-muted);
}

.electrone-form .form-field input,
.electrone-form .form-field textarea {
	border: none;
	background: transparent;
	outline: none;
	color: var(--wp--preset--color--text-on-dark);
	font-size: 15px;
	padding: 3px 0 0;
	font-family: inherit;
	resize: none;
}

.electrone-form .form-field input::placeholder,
.electrone-form .form-field textarea::placeholder {
	color: var(--wp--preset--color--muted);
}

/* Spam honeypot: invisible to sighted and keyboard users (tabindex="-1" in
   the markup keeps it out of tab order too), but a plain empty text field
   bots reliably fill in since it looks just like any other form field to
   automated scripts. See electrone_security_systems_handle_contact_form()
   in functions.php for the server-side check. */
.form-honeypot {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	left: -9999px;
}

.form-status {
	margin: 0;
	font-size: 13.5px;
	display: none;
}

.form-status.is-visible {
	display: block;
}

.form-status.is-success {
	color: var(--wp--preset--color--text-on-dark);
	font-weight: 600;
}

.form-status.is-error {
	color: var(--wp--preset--color--primary-hover);
	font-weight: 600;
}


/* Map placeholder card. min-height is tuned (not just a round number) so
   its bottom edge lands flush with the message textarea's bottom edge in
   the column next to it — verified against the live layout at desktop
   width. Re-check this if the form fields or heading above the map ever
   change enough to reflow either column's height. */
/* background-image is set inline in patterns/homepage-content.php instead
   of here — a plain url() in this static stylesheet has no cache-busting,
   unlike style.css itself (versioned via filemtime() on every enqueue), so
   swapping the map image file while keeping the same filename could get
   stuck behind a stale cached copy indefinitely. The inline version appends
   its own filemtime()-based query string, same mechanism, just applied to
   this one image instead of the whole stylesheet. */
.map-placeholder {
	position: relative;
	border-radius: 8px;
	overflow: hidden;
	border: 1px solid rgba(255, 255, 255, 0.1);
	min-height: 462px;
	background-color: var(--wp--preset--color--dark-surface);
	background-size: cover;
	background-position: center;
}

/* Makes the whole map card open Google Maps, not just the address text.
   Sits at the bottom of the stack (z-index:0) so it only catches clicks on
   the plain map background — .map-address and .map-overlay both need
   z-index:1 to stay above it, since they hold their own more specific
   links (this same Maps link, but also tel: and mailto:, which would
   otherwise get shadowed by this one covering the entire card). */
.map-click-overlay {
	position: absolute;
	inset: 0;
	z-index: 0;
}

.map-pin {
	position: absolute;
	top: 50%;
	left: 50%;
	width: 1px;
	height: 1px;
	/* Purely decorative — without this, the pin/rings (which paint above
	   .map-click-overlay, being later in the DOM at the same stacking
	   level) would swallow clicks aimed at it instead of letting them
	   fall through to that underlying map-wide link. */
	pointer-events: none;
}

/* Radar effect: both rings share the same base size and the same
   grow-and-fade keyframe, just offset half a cycle apart via animation-delay
   — so at any instant one ring is a small, bright wave just starting out
   while the other is a larger, faded one about to disappear, reading as a
   continuous stream of pings rather than two static circles. */
.map-pin .ring {
	position: absolute;
	top: 50%;
	left: 50%;
	width: 56px;
	height: 56px;
	margin: -28px 0 0 -28px;
	border-radius: 50%;
	border: 1px solid rgba(176, 16, 48, 0.85);
	animation: map-pin-radar 2.4s ease-out infinite;
}

.map-pin .ring.is-inner {
	animation-delay: 1.2s;
}

@keyframes map-pin-radar {
	0% {
		transform: scale(0.3);
		opacity: 0.95;
	}
	100% {
		transform: scale(3);
		opacity: 0;
	}
}

@media (prefers-reduced-motion: reduce) {
	.map-pin .ring {
		animation: none;
		transform: scale(1);
		opacity: 0.35;
	}

	.map-pin .dot {
		animation: none;
	}
}

.map-pin .dot {
	position: absolute;
	top: 50%;
	left: 50%;
	display: block;
	width: 16px;
	height: 16px;
	margin: -8px 0 0 -8px;
	border-radius: 50%;
	background-color: var(--wp--preset--color--primary);
	animation: map-pin-dot-pulse 1.8s ease-in-out infinite;
}

@keyframes map-pin-dot-pulse {
	0%, 100% {
		transform: scale(1);
	}
	50% {
		transform: scale(1.25);
	}
}

/* Same card treatment as .map-overlay below (border, radius, blur, padding)
   so the two read as one design language, just top-left and sized to its
   own content instead of spanning the full width — a single address line
   doesn't need three columns' worth of room. top (not bottom) keeps it
   clear of .map-overlay, which is also position:absolute and sits later in
   the DOM — anything anchored to the bottom here would render right
   underneath it and never actually be visible (confirmed: that's exactly
   where this sat before). */
.map-address {
	position: absolute;
	top: 16px;
	left: 16px;
	right: 16px;
	z-index: 1;
	background-color: #282828;
	backdrop-filter: blur(8px);
	-webkit-backdrop-filter: blur(8px);
	border: 1px solid rgba(255, 255, 255, 0.12);
	border-radius: 8px;
	padding: 14px 18px;
}

.map-overlay {
	position: absolute;
	left: 16px;
	right: 16px;
	bottom: 16px;
	z-index: 1;
	background-color: #282828;
	backdrop-filter: blur(8px);
	-webkit-backdrop-filter: blur(8px);
	border: 1px solid rgba(255, 255, 255, 0.12);
	border-radius: 8px;
	padding: 20px 22px;
	display: flex;
	gap: 20px;
	flex-wrap: wrap;
}

.map-overlay .overlay-col {
	flex: 1 1 160px;
}

/* Unscoped (not .map-overlay .overlay-label etc.) so .map-address — its own
   separate card, not a descendant of .map-overlay — can reuse the exact
   same label/value typography instead of duplicating it. */
.overlay-label {
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--wp--preset--color--text-on-dark-muted);
	margin-bottom: 5px;
}

.overlay-value {
	font-size: 13.5px;
	color: var(--wp--preset--color--text-on-dark);
	font-weight: 600;
	line-height: 1.5;
}

.overlay-value a {
	color: var(--wp--preset--color--text-on-dark);
}

.overlay-value a:hover {
	color: var(--wp--preset--color--primary);
}

.map-overlay .overlay-divider {
	flex: none;
	width: 1px;
	background-color: rgba(255, 255, 255, 0.12);
}

/* Below 600px, switch from flex-wrap (which stacked all 3 columns since
   none of them fit 2-3 to a row at this width) to an explicit 2-column
   grid: Horaires (the longest value, "Lun-Ven 8h30 - 17h00") spans both
   columns on its own row, while Téléphone and E-mail — each short enough to
   comfortably share a row — sit side by side underneath. This cuts the
   card down from 3 stacked rows to 2, which is what actually shrinks the
   gap over the map (a min-height reduction alone couldn't, without also
   pushing .map-pin's fixed dead-center underneath the card — see the
   earlier attempt this replaced).

   The dividers are hidden rather than kept between Téléphone/E-mail: with
   .overlay-divider's own display:none removing it from grid flow entirely,
   showing just the second one would need explicit grid-column placement to
   stop it displacing E-mail into a row of its own — not worth the
   fragility for a cosmetic line. */
@media (max-width: 599px) {
	.map-overlay {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: 16px 20px;
	}

	.map-overlay .overlay-col:first-child {
		grid-column: 1 / -1;
	}

	.map-overlay .overlay-divider {
		display: none;
	}
}

/* Footer row + partner badges */
.footer-row {
	display: flex;
	justify-content: space-between;
	align-items: center;
	flex-wrap: wrap;
	gap: 16px;
	max-width: 1440px;
	margin-left: auto;
	margin-right: auto;
	width: 100%;
}

.footer-badges {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 16px;
}

.footer-badges img {
	display: block;
	height: auto;
}

.footer-badges .is-inverted img {
	filter: brightness(0) invert(1);
	opacity: 0.8;
}

.footer-badges .footer-divider {
	width: 1px;
	height: 32px;
	background-color: rgba(255, 255, 255, 0.15);
}

.footer-legal-notice {
	margin: 0;
	line-height: 1.5;
}

.footer-legal-links {
	margin: 0;
}

/* Plain <button> (not <a href="#">) since these open a <dialog>, not a
   real page — styled to read as inline links. */
.footer-legal-link {
	background: none;
	border: none;
	padding: 0;
	font: inherit;
	color: inherit;
	cursor: pointer;
	text-decoration: none;
}

.footer-legal-link:hover,
.footer-legal-link:focus {
	color: var(--wp--preset--color--primary);
	text-decoration: underline;
}

/* Mentions légales / Politique de confidentialité popups, via the native
   <dialog> element — showModal()/close() (assets/js/legal-modals.js) give
   focus-trapping, a ::backdrop, and Escape-to-close for free, with no
   custom overlay/focus-trap JS to maintain. Sizing/display only; the
   dialog is centered by the browser's own UA stylesheet. */
.legal-dialog {
	max-width: 640px;
	width: calc(100% - 40px);
	max-height: calc(100vh - 80px);
	border: none;
	border-radius: 12px;
	padding: 0;
	background-color: var(--wp--preset--color--base);
	color: var(--wp--preset--color--body-text);
}

/* Gated behind [open]: unconditional display:flex here would override the
   UA stylesheet's dialog:not([open]){display:none} — author styles beat
   UA styles regardless of specificity, so this would leave the dialog
   permanently visible even when closed. */
.legal-dialog[open] {
	display: flex;
	flex-direction: column;
}

.legal-dialog::backdrop {
	background-color: rgba(0, 0, 0, 0.6);
}

.legal-dialog-header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding: 20px 24px;
	border-bottom: 1px solid rgba(20, 22, 26, 0.08);
}

.legal-dialog-title {
	font-size: 1.1875rem;
	margin: 0;
}

.legal-dialog-close {
	background: none;
	border: none;
	font-size: 24px;
	line-height: 1;
	cursor: pointer;
	color: var(--wp--preset--color--muted);
	padding: 4px 8px;
	flex: none;
}

.legal-dialog-close:hover,
.legal-dialog-close:focus {
	color: var(--wp--preset--color--primary);
}

.legal-dialog-body {
	padding: 20px 24px 28px;
	overflow-y: auto;
	font-size: 14.5px;
	line-height: 1.7;
}

.legal-dialog-body h3 {
	font-size: 1rem;
	margin-top: 20px;
	margin-bottom: 6px;
}

.legal-dialog-body h3:first-child {
	margin-top: 0;
}

.legal-dialog-body p {
	margin: 0 0 12px;
}
