/* ==========================================================================
   DFS Motion — base stylesheet
   ========================================================================== */

/* --- Universal Motion tab: pre-hide reveal/load elements before JS runs,
   so deferred script loading doesn't cause a flash of the final state.
   The JS applies the precise per-type transform on top of this. --------- */
.dfs-motion[data-dfs-motion-trigger="reveal"],
.dfs-motion[data-dfs-motion-trigger="load"] {
	opacity: 0;
}


/* ==========================================================================
   Scroll Accordion widget
   ========================================================================== */

.dfs-scrollaccordion__tick {
	width: 1px;
	height: 32px;
	margin: 0 auto;
	background-color: currentColor;
	opacity: 0.4;
}


.dfs-scrollaccordion__item {
	transition: opacity 0.3s ease;
}


.dfs-scrollaccordion__heading {
	display: block;
	width: 100%;
	background: none;
	border: 0;
	padding: 12px 0;
	margin: 0;
	text-align: left;
	cursor: pointer;
	transition: color 0.3s ease;
}


.dfs-scrollaccordion__body {
	height: 0;
	overflow: hidden;
}


.dfs-scrollaccordion__body-inner {
	padding: 24px 0 48px;
}


/* ==========================================================================
   VIDEO SERVICE SHOWCASE  —  reference: sharebien.com "Nos expertises"
   Measurements: widget-suite/MEASURED-SPEC-sharebien.md
   ==========================================================================

   ONE UNIT DRIVES EVERYTHING.

   The reference sizes the whole section in `rem` against a root font-size of
   0.69444vw, which is why its section height is exactly 1.549 x viewport width
   at 2560, 1920, 1536, 1440, 1366 and 1280 — six rungs, same ratio. A plugin
   cannot set the theme's root font-size, so the identical proportion is carried
   on a local custom property. `--vss-u` IS their rem: 13.3333px at the 1920
   control.

   The grid maths below is coupled to that on purpose, and the coupling is worth
   stating because it is what makes the media column exact:

     total          144u        (100vw)
     padding        2u each     -> 140u
     11 gaps x 2u   22u         -> 118u across 12 columns
     one column     9.8333u
     media column   4 cols + 3 gaps = 45.3333u  = 604.44px at 1920  [measured]
     frame height   45.3333u x 137%  = 62.107u  = 828.1px  at 1920  [measured]

   Change the column count or the gap and `--vss-media-w` has to be rederived.
   Nothing else in here needs to know. */

/* PP NEUE MONTREAL, SELF-HOSTED.
   Their `--font-body` is `"PP Neue Montreal", sans-serif`, and the section is
   weight 400 throughout — which is the BOOK file, not the one a "regular" guess
   would pick.

   This matters more than it looks. A substituted face changes glyph widths,
   which changes where each body wraps, which changes every item height, which
   changes the scroll distance between reveals. On mobile the substitute was
   wrapping bodies to six lines against their five and putting our section 8.2%
   short — a chain of consequences from one wrong typeface.

   The cufonfonts bundle declares each weight as a SEPARATE family with
   `font-weight: normal`, so `font-weight: 500` would silently fall back. These
   are one family with real weights, which is what the cascade expects.

   `font-display: swap` on purpose: a heading that arrives late is better than a
   heading that is invisible while the font loads. */
/* NO 400 FACE, DELIBERATELY — this mirrors the reference exactly.
   Their stylesheet declares only TWO faces for this family: Medium (500) and
   Bold (700). The section is written at `font-weight: 400`, and with no 400
   available CSS font matching resolves upward to 500 — so every heading and
   every paragraph on their site is rendering in MEDIUM.

   Declaring the Book file at 400, which is the obvious thing to do with the
   files to hand, therefore does NOT reproduce them: Book is narrower, our body
   copy wrapped to three lines against their four, and every item height and the
   whole section shortened by 1.6%. The bug was invisible from the outside —
   both sides reported `font-weight: 400` and the same family name — and only
   showed up on reading their @font-face rules.

   The book, thin and italic files ship in assets/fonts/ but are intentionally
   not declared here. Adding a 400 face silently changes what every existing
   weight-400 element renders as. */
@font-face {
	font-family: 'PP Neue Montreal';
	src: url( '../fonts/ppneuemontreal-medium.woff' ) format( 'woff' );
	font-weight: 500;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: 'PP Neue Montreal';
	src: url( '../fonts/ppneuemontreal-bold.woff' ) format( 'woff' );
	font-weight: 700;
	font-style: normal;
	font-display: swap;
}


/* ==========================================================================
   HORIZONTAL SCROLL — @decision D98/D99/D100

   Any Elementor section or container, one toggle, its columns become a
   swipeable row. The mechanism is Video Showcase's mobile row, which contains
   no JavaScript at all: flex + overflow-x + scroll-snap, and the browser does
   the momentum, the rubber-banding and the touch handling for free.

   NOTHING HERE PINS OR HIJACKS SCROLL. That is the whole reason it needs no
   second section to "extend the runway" — the row moves sideways under the
   finger, independently of the page's vertical scroll.
   ========================================================================== */

/* The TRACK is not the same element in both models, and targeting only one is
   how this would silently work on new sections and do nothing on old ones.

   - flex container: children live in `.e-con-inner` (or the container itself
     when it is not boxed)
   - legacy section: children are `.elementor-column` inside `.elementor-container` */
.dfs-hscroll > .e-con-inner,
.dfs-hscroll > .elementor-container,
.dfs-hscroll.e-con > .e-con-inner {
	display: flex;
	flex-direction: row;
	flex-wrap: nowrap;
	align-items: stretch;

	gap: var( --dfs-hs-gap );

	overflow-x: auto;
	overflow-y: hidden;
	scroll-snap-type: var( --dfs-hs-snap, x mandatory );
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;

	/* A snapped row must not also try to be a grid. */
	justify-content: flex-start;
}


/*
 * @decision D155 — ELEMENTOR WRAPS EVERY SECTION CONTAINER AT <=1024, AND IT
 * BEATS US ON A TIE. This is why horizontal scroll worked on desktop and died
 * on mobile.
 *
 * Elementor `frontend.min.css` 4.2.2 ships:
 *
 *     @media ( max-width: 1024px ) {
 *         .elementor-section .elementor-container { flex-wrap: wrap; }
 *     }
 *
 * That selector is (0,2,0). Ours above is `.dfs-hscroll > .elementor-container`,
 * ALSO (0,2,0). A media query adds no specificity, so the two tie and the tie
 * goes to document order — and Elementor's stylesheet is enqueued after this
 * one. Below 1024 the track wrapped, every panel took its own line, and
 * `scrollWidth` equalled `clientWidth`, so there was nothing to scroll.
 *
 * MEASURED, because the symptom pointed the wrong way. The panels were the
 * correct 312px (390 / 1.25 visible) the whole time, so the widths looked
 * healthy and it read as a broken scroll. The tell was in the boxes: all five
 * at `left: 0` with tops 4046 / 4650 / 5254 / 5858 / 6462 — a wrapped column,
 * not a clipped row.
 *
 * `!important` is deliberately NOT the fix. Raising specificity to (0,3,0) by
 * naming the section class we are already inside beats Elementor without
 * arming a rule that would then outrank anything a user control emits — the
 * A1/A2/A4 trap, and the same call made in D153.
 *
 * The container leg (`.e-con-inner`) is listed for symmetry only. Elementor's
 * wrap rule is scoped to `.elementor-section`, so containers were never
 * affected — but a rule that silently covers one of two supported layouts is
 * how this class of defect hides.
 */
@media ( max-width: 1024px ) {
	.elementor-section.dfs-hscroll > .elementor-container,
	.dfs-hscroll.e-con > .e-con-inner,
	.dfs-hscroll > .e-con-inner {
		flex-wrap: nowrap;
	}
}


.dfs-hscroll:not( .dfs-hscroll--bar ) > .e-con-inner::-webkit-scrollbar,
.dfs-hscroll:not( .dfs-hscroll--bar ) > .elementor-container::-webkit-scrollbar {
	display: none;
}


.dfs-hscroll--bar > .e-con-inner,
.dfs-hscroll--bar > .elementor-container {
	scrollbar-width: thin;
}


/* THE PANEL WIDTH IS THE WHOLE FEATURE.

   width = ( 100% - (visible - 1) * gap ) / visible

   A fractional `visible` is what leaves the next panel peeking past the edge,
   and that peek is load-bearing rather than decorative: a row of whole panels
   with a clean edge reads as a static grid and nobody swipes it. The
   reference's own card is 81% for exactly this reason.

   `flex: 0 0 auto` with an explicit width, never `flex-basis` alone — Elementor
   columns carry their own percentage widths and a basis would lose to them. */
.dfs-hscroll > .e-con-inner > .e-con,
.dfs-hscroll > .e-con-inner > .elementor-element,
.dfs-hscroll > .elementor-container > .elementor-column {
	flex: 0 0 auto;
	width: calc(
		( 100% - ( var( --dfs-hs-visible ) - 1 ) * var( --dfs-hs-gap ) )
		/ var( --dfs-hs-visible )
	);
	max-width: none;
	scroll-snap-align: var( --dfs-hs-align, center );
	/*
	 * @decision D260 — NO scroll-snap-stop. It shipped here as `always`,
	 * which forbids a flick from travelling more than one snap point: on a
	 * four-column row every swipe halted a quarter-step later, and Brad felt
	 * it — "the widget's version feels way smoother to the touch than the
	 * section's". The Video Showcase row — the REFERENCE this mechanism was
	 * copied from — has no snap-stop at all: momentum glides across cards
	 * and settles on one. The property was an unmeasured addition; removed
	 * 2026-08-15 to restore parity with the reference implementation.
	 */
}


/* Edge-to-edge: pull out by the section's own inline padding, grow by twice it,
   then pad it back inside the track. Copied in mechanism from the Video
   Showcase row, which is where it was measured. */
.dfs-hscroll--bleed > .e-con-inner,
.dfs-hscroll--bleed > .elementor-container {
	margin-left: calc( var( --dfs-hs-bleed-pad, 0px ) * -1 );
	width: calc( 100% + 2 * var( --dfs-hs-bleed-pad, 0px ) );
	padding-inline: var( --dfs-hs-bleed-pad, 0px );
	scroll-padding-inline: var( --dfs-hs-bleed-pad, 0px );
}


/* PARTS RESOLVED HERE, NEVER IN PHP — @decision D79.

   An inline custom property outranks every media query, so emitting a single
   resolved `--dfs-hs-visible` would freeze the desktop count onto the phone.
   That exact defect recurred four times in one session on the Site Menu: the
   side margin, the marker size, the bar padding and the nav indent. PHP emits
   `-desktop`/`-tablet`/`-mobile`; only this file chooses between them.

   Mobile-first, matching how `--dfs-sm-margin` is done. */
.dfs-hscroll {
	--dfs-hs-visible: var( --dfs-hs-visible-mobile, 1.15 );
	--dfs-hs-gap: var( --dfs-hs-gap-mobile, 10px );
}


@media ( min-width: 768px ) {
	.dfs-hscroll {
		--dfs-hs-visible: var( --dfs-hs-visible-tablet, 2.2 );
		--dfs-hs-gap: var( --dfs-hs-gap-tablet, 16px );
	}
}


@media ( min-width: 1025px ) {
	.dfs-hscroll {
		--dfs-hs-visible: var( --dfs-hs-visible-desktop, 3 );
		--dfs-hs-gap: var( --dfs-hs-gap-desktop, 16px );
	}
}


/* THE EDITOR WRAPS INSTEAD OF SCROLLING — @decision D100.

   Same rule as @decision D70 and @decision D01. A scrolled row in the canvas
   hides every panel past the first screenful INCLUDING Elementor's own add
   button, so adding a fourth panel would mean scrolling a nested container
   sideways to find it. Wrapping shows all of them and keeps the section
   editable, which is the entire point of building this onto the section rather
   than shipping another widget.

   The panels keep their real width so the proportions being edited are the
   proportions that ship. */
.dfs-hscroll--editor > .e-con-inner,
.dfs-hscroll--editor > .elementor-container {
	flex-wrap: wrap;
	overflow-x: visible;
	overflow-y: visible;
	scroll-snap-type: none;
}


/* Reduced motion: snapping is fine (it is not motion the user did not ask for),
   but smooth-scrolling behaviour is removed so a jump is a jump. */
@media ( prefers-reduced-motion: reduce ) {
	.dfs-hscroll > .e-con-inner,
	.dfs-hscroll > .elementor-container { scroll-behavior: auto; }
}


/* ==========================================================================
   GSAP EFFECTS ON NATIVE ELEMENTOR WIDGETS — @decision D104-D107
   ========================================================================== */

/* Split spans. `inline-block` is required for transforms to apply at all —
   an inline box ignores translate. */
/* `vertical-align: top` is MEASURED, not taste. An inline-block's baseline is
   its bottom margin edge, so the descender space below it is added to the line
   box, and every split heading on the live home page grew the moment the
   engine ran: 150 -> 162 and 30 -> 34. Aligning to top takes both deltas to 0.

   The padding/margin pair was the obvious suspect and IS NOT THE CAUSE —
   removing it left the growth at exactly 12/4. It stays because it does a
   different job: keeping g, y and p off the edge of the `overflow: hidden`
   mask cascade_reveal writes onto each word. Four candidates were tested and
   two cured it; `overflow: visible` was the other, and it is not available,
   because that mask is what makes cascade_reveal a reveal instead of a slide. */
.dfs-fx-word { display: inline-block; vertical-align: top; padding-bottom: 0.06em; margin-bottom: -0.06em; }

.dfs-fx-char { display: inline-block; vertical-align: top; }

.dfs-fx-line { display: block; }

/* 2026-08-27 — Typewriter Loop caret (framer: protexttypeeffect) */
.dfs-type-txt { white-space: pre-wrap; }
.dfs-type-caret { display: inline-block; font-weight: 400; }
.dfs-type-caret--block { width: .58em; margin-left: .06em; background: currentColor; color: transparent; line-height: 1; }
.dfs-type-caret--bar { border-left: 2px solid currentColor; margin-left: .06em; }
.dfs-type-caret--underscore { margin-left: .02em; }
.dfs-type-caret--block, .dfs-type-caret--bar, .dfs-type-caret--underscore { animation: dfs-type-caret-blink 1.05s steps(1) infinite; }
@keyframes dfs-type-caret-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
	.dfs-type-caret--block, .dfs-type-caret--bar, .dfs-type-caret--underscore { animation: none; }
}


/* THE BACKGROUND LAYER SITS BEHIND CONTENT, ALWAYS.

   Elementor content inside a container is not automatically above an inserted
   first child, because both are in the same stacking context at auto z-index. */
.dfs-gsap-bg { position: relative; }

.dfs-gsap-bg > .dfs-gsap-bg-layer { z-index: 0; }

.dfs-gsap-bg > .e-con-inner,
.dfs-gsap-bg > .elementor-container { position: relative; z-index: 1; }


/* Canvas backgrounds must never intercept the pointer — the section still has
   to be clickable and editable through them. */
.dfs-gsap-bg-layer,
.dfs-gsap-bg-layer * { pointer-events: none; }


/* ==========================================================================
   CSS BACKGROUND FAMILY — @decision D373

   Five section backgrounds that cost NOTHING per frame on the main thread.
   The JS half (cssLayer in dfs-motion-gsap.js) inserts the layer, names it,
   and writes four custom properties; everything below is the whole effect.

   WHY THAT SPLIT MATTERS RATHER THAN BEING TIDINESS: the other ten
   backgrounds hold a live GSAP tween, so a section carrying one AND a scrub
   is asking the same thread to do both. These hold nothing. That is the only
   reason to add a background to a section that already pins or scrubs.

   EVERY EFFECT USES BOTH COLOUR CONTROLS. A control that is visible and does
   nothing is a defect, not a simplification — so no effect here ignores
   Colour One or Colour Two, even where a single-colour version would have
   been easier to write.

   Reduced motion is already handled: `.dfs-gsap-bg-layer { display: none }`
   in the block below removes the layer outright, which is the correct floor
   for something purely decorative.
   ========================================================================== */

.dfs-bgcss {
	/* Fallbacks for the values cssLayer writes — a layer that somehow arrives
	   without them renders as a designed default, never as black. */
	--dfs-bg-a: #141314;
	--dfs-bg-b: #FB460D;
	--dfs-bg-speed: 18s;
	--dfs-bg-op: 0.35;
}


/* --- Film Grain ----------------------------------------------------------
   TWO LAYERS, ONE CONTROL, AND THE CONTROL ONLY MOVES ONE OF THEM. Intensity
   dials the colour wash; the grain itself holds a fixed strength. Applied to
   both — which is what a plain opacity on the parent does — turning the wash
   down to a tasteful 0.2 took the grain to 0.2 as well, and an effect called
   Film Grain rendered as a gradient with no visible grain. The name is the
   spec; the texture is not the thing to fade.

   `steps(1)` with discrete keyframe stops, NOT a smooth translate. Grain that
   slides is fog; grain that jumps is film. The speed control is divided down
   hard because a full-length grain cycle is not grain at any tempo a user
   would pick for a gradient. */
.dfs-bgcss--grain {
	background: linear-gradient(155deg, var(--dfs-bg-a), var(--dfs-bg-b));
	opacity: var(--dfs-bg-op);
}

.dfs-bgcss--grain::before {
	content: "";
	position: absolute;
	inset: 0;
	opacity: 0.55;
	mix-blend-mode: overlay;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
	background-repeat: repeat;
	background-size: 180px 180px;
	animation: dfs-bg-grain calc(var(--dfs-bg-speed) / 22) steps(1) infinite;
}

@keyframes dfs-bg-grain {
	0%   { background-position:   0px   0px; }
	12%  { background-position: -42px  18px; }
	25%  { background-position:  31px -37px; }
	37%  { background-position: -19px -44px; }
	50%  { background-position:  47px  25px; }
	62%  { background-position: -35px  41px; }
	75%  { background-position:  22px -21px; }
	87%  { background-position: -48px  -9px; }
	100% { background-position:   0px   0px; }
}


/* --- Drafting Grid -------------------------------------------------------
   Two pitches, not one: a 64px hairline grid in Colour Two with a 256px
   major grid in Colour One over it, which is what a drawing sheet actually
   looks like and is also what stops the pattern reading as a table.

   The elliptical mask is load-bearing. A grid that meets the section edge at
   full strength announces itself as a repeating background image; fading it
   out well before the edge is the entire difference between "texture" and
   "wallpaper". */
.dfs-bgcss--grid {
	background-image:
		repeating-linear-gradient(0deg,  var(--dfs-bg-b) 0 1px, transparent 1px 64px),
		repeating-linear-gradient(90deg, var(--dfs-bg-b) 0 1px, transparent 1px 64px),
		repeating-linear-gradient(0deg,  var(--dfs-bg-a) 0 1px, transparent 1px 256px),
		repeating-linear-gradient(90deg, var(--dfs-bg-a) 0 1px, transparent 1px 256px);
	opacity: var(--dfs-bg-op);
	-webkit-mask-image: radial-gradient(ellipse at 50% 50%, #000 22%, transparent 78%);
	mask-image: radial-gradient(ellipse at 50% 50%, #000 22%, transparent 78%);
	animation: dfs-bg-grid var(--dfs-bg-speed) linear infinite;
}

@keyframes dfs-bg-grid {
	from { background-position: 0 0, 0 0, 0 0, 0 0; }
	to   { background-position: 64px 64px, 64px 64px, 256px 256px, 256px 256px; }
}


/* --- Conic Halo ----------------------------------------------------------
   The blur is on the LAYER and the rotation on the pseudo-element, which is
   not interchangeable with the obvious arrangement: filtering a rotating box
   re-rasterises the blur every frame, while rotating inside an already
   filtered box is a composited transform. Same picture, different bill.

   THE MASK IS NOT DECORATION — it is what keeps the blur inside the section.
   `overflow: hidden` on the layer clips its CHILDREN, and the filter is then
   applied to that clipped result, so a 70px blur spreads colour 70px OUTSIDE
   the section and paints over whatever sits next to it. The rendering order
   is filter, then clip-path, then mask, so a full-coverage mask on the same
   element is the one thing that trims the filter's own output back to the
   box. The softened edge it leaves is the intended look.

   Sized at 170% with a 1:1 aspect ratio so the square sweep still covers a
   wide section's corners at every angle of the rotation. */
.dfs-bgcss--halo {
	opacity: var(--dfs-bg-op);
	filter: blur(70px);
	-webkit-mask-image: linear-gradient(#000, #000);
	mask-image: linear-gradient(#000, #000);
}

.dfs-bgcss--halo::before {
	content: "";
	position: absolute;
	top: 50%;
	left: 50%;
	width: 170%;
	aspect-ratio: 1;
	margin: -85% 0 0 -85%;
	background: conic-gradient(
		from 0turn,
		transparent 0%,
		var(--dfs-bg-b) 14%,
		transparent 34%,
		transparent 56%,
		var(--dfs-bg-a) 76%,
		transparent 96%
	);
	animation: dfs-bg-spin calc(var(--dfs-bg-speed) * 2) linear infinite;
}

@keyframes dfs-bg-spin {
	to { transform: rotate(360deg); }
}


/* --- Ripple Rings --------------------------------------------------------
   Two ring sets half a cycle apart. One set alone leaves a dead beat every
   time it fades out and restarts at the centre; the negative delay on the
   second set means there is always one expanding.

   THE MASK IS WHAT KEEPS THIS OUT OF RADAR TERRITORY. Hard 1px circles at
   even strength across the whole box read as an instrument readout, not as a
   luxury surface — the same failure the Drafting Grid's mask exists to avoid,
   and the reason both carry one. Fading the rings out well before the section
   edge leaves a disturbance in the middle of the composition instead of a
   diagram drawn over it. */
.dfs-bgcss--rings {
	opacity: var(--dfs-bg-op);
	-webkit-mask-image: radial-gradient(ellipse at 50% 50%, #000 8%, transparent 68%);
	mask-image: radial-gradient(ellipse at 50% 50%, #000 8%, transparent 68%);
}

.dfs-bgcss--rings::before,
.dfs-bgcss--rings::after {
	content: "";
	position: absolute;
	top: 50%;
	left: 50%;
	width: 200%;
	aspect-ratio: 1;
	margin: -100% 0 0 -100%;
	background: repeating-radial-gradient(circle at 50% 50%, var(--dfs-bg-b) 0 1px, transparent 1px 72px);
	animation: dfs-bg-rings var(--dfs-bg-speed) linear infinite;
}

.dfs-bgcss--rings::after {
	background: repeating-radial-gradient(circle at 50% 50%, var(--dfs-bg-a) 0 1px, transparent 1px 72px);
	animation-delay: calc(var(--dfs-bg-speed) / -2);
}

@keyframes dfs-bg-rings {
	0%   { transform: scale(0.3); opacity: 0; }
	20%  { opacity: 1; }
	75%  { opacity: 1; }
	100% { transform: scale(1.55); opacity: 0; }
}


/* --- Prism Sheen ---------------------------------------------------------
   Satin, not specular. bg_sweep fires a hard band ONCE on entry because a
   repeating specular highlight looks cheap; this is the other material — a
   wide soft-focus band that never stops and never announces itself. They are
   two different surfaces, which is why both exist.

   Blur on the LAYER, translate on the pseudo-element, mask to trim the
   filter output — the same three-part arrangement as Conic Halo and for the
   same two reasons: an animated blurred box re-rasterises every frame, and
   an unmasked filter bleeds outside the section. */
.dfs-bgcss--sheen {
	opacity: var(--dfs-bg-op);
	filter: blur(45px);
	-webkit-mask-image: linear-gradient(#000, #000);
	mask-image: linear-gradient(#000, #000);
}

.dfs-bgcss--sheen::before {
	content: "";
	position: absolute;
	top: -25%;
	bottom: -25%;
	left: -60%;
	width: 220%;
	background: linear-gradient(
		100deg,
		transparent 0%,
		var(--dfs-bg-a) 18%,
		transparent 32%,
		var(--dfs-bg-b) 50%,
		transparent 68%,
		var(--dfs-bg-a) 84%,
		transparent 100%
	);
	animation: dfs-bg-sheen var(--dfs-bg-speed) ease-in-out infinite alternate;
}

@keyframes dfs-bg-sheen {
	from { transform: translate3d(-13%, 0, 0); }
	to   { transform: translate3d(13%, 0, 0); }
}


/* REDUCED MOTION — a hard floor, matching @decision D24. The JS bails, but a
   bail leaves whatever inline state a tween had already written. These rules
   guarantee the element is VISIBLE rather than merely un-animated. */
@media ( prefers-reduced-motion: reduce ) {
	.dfs-gsap [class^="dfs-fx-"],
	.dfs-gsap {
		opacity: 1 !important;
		transform: none !important;
		filter: none !important;
		clip-path: none !important;
	}
	.dfs-gsap-bg-layer { display: none; }
}


/* ==========================================================================
   HORIZONTAL SCROLL — THE EDITOR CANVAS
   ==========================================================================

   Everything above is delivered by add_render_attribute, which is a PHP RENDER
   path. Elementor builds sections and columns in the editor canvas from its own
   JS templates, so none of it — not the .dfs-hscroll class, not the data
   attribute, not one CSS variable — exists there. That is why the canvas showed
   the raw Elementor row while the front end scrolled correctly, and why
   swapping is_edit_mode() for is_preview_mode() could not have helped: the code
   carrying the answer never ran.

   This block keys off two things Elementor DOES put in the canvas:
     body.elementor-editor-active   the canvas body class (D205)
     .dfs-hscroll-yes               written by the control's prefix_class

   It WRAPS rather than scrolls (@decision D100), so every panel including
   Elementor's own add affordance stays reachable, and it uses the SAME width
   formula as the front end so the proportions being edited are the proportions
   that ship. Brad: "if its 25% on frontend i need it 25% in editor".
   ========================================================================== */

body.elementor-editor-active .dfs-hscroll-yes > .elementor-container,
body.elementor-editor-active .dfs-hscroll-yes > .e-con-inner {
	display: flex;
	flex-wrap: wrap;
	overflow: visible;
	scroll-snap-type: none;
	gap: var( --dfs-hs-gap, 16px );
}

body.elementor-editor-active .dfs-hscroll-yes > .elementor-container > .elementor-column,
body.elementor-editor-active .dfs-hscroll-yes > .e-con-inner > .e-con,
body.elementor-editor-active .dfs-hscroll-yes > .e-con-inner > .elementor-element {
	flex: 0 0 auto;
	width: calc(
		( 100% - ( var( --dfs-hs-visible, 3 ) - 1 ) * var( --dfs-hs-gap, 16px ) )
		/ var( --dfs-hs-visible, 3 )
	);
	max-width: none;
}
