/**
 * Fischer Hotspot Explainer — frontend styles.
 *
 * Ported from the hotspot-v12.html reference build (design-export-code-
 * impeccable project) — same .hs9-* component styling, unchanged, minus
 * the page-shell rules (.hs-page/.hs-header/.hs-eyebrow/.hs-title/.hs-lead)
 * that belong to V12's own standalone page, not this widget, and minus the
 * .hs9-legend nav (permanently display:none in V12, never exposed via a
 * control here — dropped rather than shipped inert).
 *
 * Custom properties moved from :root to .fhe-hotspot (this widget's own
 * root class) so they don't leak into/collide with the surrounding theme's
 * global tokens on an arbitrary Elementor page. Same reasoning for
 * box-sizing/reduced-motion: scoped to .fhe-hotspot instead of the
 * universal `*` selector V12 could safely use on its own dedicated page.
 *
 * Every selector below is prefixed with `.fhe-hotspot ` (bumping
 * specificity from one class to two) — added after the widget shipped to
 * a real Elementor site and .hs9-dot/.hs9-close/.hs9-sim-close/.hs9-sim-btn
 * rendered as the theme's default solid-orange <button> styling instead of
 * this file's own look. V12 never had this problem (nothing else on its
 * page styled a bare `button`), but a real theme/Elementor global-button
 * rule can carry enough specificity to beat a single-class selector like
 * `.hs9-dot { background: none; }`. `.hs9-callout-nav button` (class+tag,
 * already higher specificity) was never affected — only the plain
 * single-class rules were — which is what gave this away.
 */

    .fhe-hotspot {
      --hs-orange: #ff6800;
      --hs-orange-a11y: #a0491a;
      --hs-orange-soft: #ffe7d6;
      --hs-ink: #1a1a1a;
      --hs-body: #6b625a;
      --hs-body-2: #6f685f;
      --hs-card: #faf7f2;
      --hs-card-line: #eee7de;
      --hs-line: #f1eae0;
      --hs-shadow-lg: 0 24px 60px rgba(26, 26, 26, 0.16);
      --hs-shadow-md: 0 12px 40px rgba(26, 26, 26, 0.10);
      --hs-shadow-sm: 0 3px 12px rgba(160, 73, 26, 0.10);
      --hs-radius-lg: 24px;
      --hs-radius-md: 16px;
      --hs-font: Manrope, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
      --hs-mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
      --hs-ease: cubic-bezier(.22, 1, .36, 1);
    }

    .fhe-hotspot, .fhe-hotspot *, .fhe-hotspot *::before, .fhe-hotspot *::after { box-sizing: border-box; }

    /* Baseline reset for every <button> in this widget, ahead of each
       component's own rules further down. The .fhe-hotspot-prefixed
       specificity bump (see each component rule) already wins fights
       over properties both sides set — background, width, height,
       etc. — but it can't fix a property the THEME sets and this file
       never mentions at all: nothing to "win", so the theme's value
       just stands. min-width/margin are the classic culprits (most
       themes add one or both to every button for a comfortable tap
       target), and they're exactly what turned the circular close
       button into an oval and pushed the callout's Zurück/Weiter pair
       wider than the card that's supposed to contain them — a fixed
       width/height doesn't override a min-width the theme set on top
       of it, since they're different properties. Reset broadly here so
       the next theme-button-default property that turns up doesn't
       need its own bug report; each component rule below still sets
       whatever it actually needs on top of this. */
    .fhe-hotspot button {
      margin: 0;
      padding: 0;
      min-width: 0;
      min-height: 0;
      max-width: none;
      line-height: normal;
      font-family: inherit;
      font-weight: normal;
      text-transform: none;
      letter-spacing: normal;
      text-align: center;
      box-shadow: none;
      -webkit-appearance: none;
      appearance: none;
    }

    @media (prefers-reduced-motion: reduce) {
      .fhe-hotspot *, .fhe-hotspot *::before, .fhe-hotspot *::after {
        animation-duration: .001ms !important;
        transition-duration: .001ms !important;
      }
    }

    /* ---------------------------------------------------------------
       Stage — the zoomable product shot
       --------------------------------------------------------------- */
    .fhe-hotspot .hs9-stage {
      position: relative;
      background: #fff;
      border: 1px solid var(--hs-card-line);
      border-radius: var(--hs-radius-lg);
      box-shadow: var(--hs-shadow-lg);
      padding: clamp(14px, 2.4vw, 28px);
      transition: background-color .7s var(--hs-ease);
    }

    /* Cinema-style dim on play: the whole card (this padding/border
       area, plus .hs9-clip below for the space around the product
       silhouette) fades to black instead of staying white while the
       simulation runs, so it reads like a darkened room/projector
       screen rather than "a white card with some effects on it".
       :has() is what lets a descendant state (.hs9-frame.is-sim) drive
       an ANCESTOR's background — .hs9-stage isn't a descendant of
       .hs9-frame, it's the parent, so a plain descendant selector
       can't reach it the way .hs9-frame.is-sim .hs9-clip below can. */
    .fhe-hotspot .hs9-stage:has(.hs9-frame.is-sim) {
      background: #05050a;
    }

    .fhe-hotspot .hs9-frame {
      position: relative;
      aspect-ratio: 5504 / 3072;
      /* Lets the dots below size themselves off the FRAME's own
         rendered width (cqw) instead of the viewport's (vw). On a
         card-style module like this the frame's width already tracks
         the viewport indirectly, but not 1:1 — page gutters and the
         stage's own padding eat into it by a different amount at
         every breakpoint, so a vw-based dot size drifts out of
         proportion with the actual image. cqw stays correct at any
         width without needing a breakpoint-specific override. */
      container-type: inline-size;
    }

    /* Clips only the image itself. Dots, the close button and the
       callout live OUTSIDE this element (as direct children of
       .hs9-frame instead) so that on short/narrow frames (mobile)
       they're free to extend beyond the image's box instead of being
       silently cut off by overflow:hidden.

       No background of its own anymore (used to be a separate white/
       black fill covering the product PNG's transparent margin — see
       git history). Now transparent on purpose: the Three.js effect
       canvases (.hs9-sim-cold/.hs9-warm-shimmer-canvas/.hs9-side-glow-canvas)
       moved OUT to be full-stage-sized siblings of .hs9-frame (see the
       markup comment above them), sitting behind it — with .hs9-clip
       transparent, .hs9-stage's own background (white idle / black
       during sim, via the :has() rule above) shows through consistently
       whether you're looking at the space around the radiator's
       silhouette INSIDE the frame or the padding margin OUTSIDE it,
       instead of two separately-colored layers that happened to match. */
    .fhe-hotspot .hs9-clip {
      position: absolute;
      inset: 0;
      overflow: hidden;
      border-radius: var(--hs-radius-md);
    }

    .fhe-hotspot .hs9-image {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: contain;
      user-select: none;
      -webkit-user-drag: none;
      transform-origin: 50% 50%;
      will-change: transform;
      filter: drop-shadow(0 20px 30px rgba(90, 52, 16, 0.14));
    }

    /* Darkens the frame around the zoomed detail so the magnified area
       (and the callout sitting over it) read clearly against it. */
    .fhe-hotspot .hs9-dim {
      position: absolute;
      inset: 0;
      background: rgba(20, 14, 8, 0.28);
      opacity: 0;
      pointer-events: none;
    }

    /* Simulation step 2/3 — the Schamotte core "lighting up". Sits
       over the diamond/rhombus core shape visible in the photo (same
       area the "Fischer 40mm Schamotte-Kern" hotspot points at) and
       uses the same warm amber the core already glows in the static
       photo, so it reads as that specific part of the product
       intensifying rather than a generic overlay.

       Single clip-path polygon tracing the rhombus's exact 4 corners
       (extracted via color-analysis of the source PNG). A three-blob
       decomposition was tried instead (to avoid any straight
       clip-path edge showing at all) but was judged uglier overall —
       a recognizable diamond silhouette reads better than three
       overlapping circles that no longer read as "the rhombus" at
       all. So: single shape.

       The gradient's own transparent stop now lands at 58% (was 78%)
       — well inside the polygon — so alpha is already close to zero
       by the time it reaches the clip-path boundary; blur is then
       pushed up to 26px. Together these mean the polygon's straight
       edges are cutting through an area that's already faded rather
       than through the gradient's still-opaque middle, so there's no
       hard "stacco" between the glow and the photo underneath it on
       any of the 4 sides — the previous 14px blur on a gradient that
       was still ~50% opaque at 78% wasn't enough softening to hide
       that the cut was happening somewhere still visible.

       Pulse is stronger now (opacity .55-1, scale .94-1.06) — the
       .82-1 version read as "barely pulsing". This is still shy of
       the old .35-1 "blink" swing, but noticeably more alive.

       mix-blend-mode: screen (not a flat painted color) is what makes
       it read as light building up on top of the existing photo
       detail instead of a colored shape sitting over it. Plain CSS
       transition + class toggle — same pattern every other opacity
       toggle on this page uses (.hs9-sim, .hs9-sim-cold,
       .hs9-sim-caption) — NOT GSAP: GSAP's own rAF-driven ticker can
       silently never advance a single frame in some sandboxed/
       background-tab conditions (confirmed via gsap.ticker.frame
       staying 0 for 30+ real seconds while this page's plain CSS
       transitions and raw-rAF Three.js layers kept animating fine),
       so anything that must reliably show shouldn't depend on it. */
    .fhe-hotspot .hs9-sim-glow {
      position: absolute;
      left: 38.9%;
      top: 23.47%;
      width: 15.5%;
      height: 49.54%;
      clip-path: polygon(54.51% 0%, 100% 56.11%, 54.75% 100%, 0% 39.42%);
      background: radial-gradient(circle, rgba(255, 158, 64, 1) 0%, rgba(255, 138, 48, 0.62) 32%, rgba(255, 138, 48, 0) 58%);
      filter: blur(26px);
      mix-blend-mode: screen;
      opacity: 0;
      pointer-events: none;
      /* Fade-in slowed from .6s to 2.2s — the core visibly building up
         to full strength ("piano piano") rather than snapping on. */
      transition: opacity 2.2s var(--hs-ease);
    }

    .fhe-hotspot .hs9-sim-glow.is-active {
      opacity: 1;
      /* Pulse doesn't start until the slow fade-in above has actually
         finished (delay matches the 2.2s transition), so the two
         motions don't overlap and blur into each other. */
      animation: hs9-glow-pulse 2.3s ease-in-out 2.2s infinite;
    }

    /* Stronger pulse: opacity swings .62-1 (was .55-1 — a shallower
       dip reads as a slightly more consistently intense glow), scale
       .94-1.06 — clearly breathing, not just a flicker, but still
       short of a hard blink. Combined with the richer gradient alpha
       stops above, the fully-appeared state is a step brighter overall
       than before, not just a stronger swing. */
    @keyframes hs9-glow-pulse {
      0%, 100% { opacity: 1; transform: scale(1); }
      50% { opacity: .62; transform: scale(.94); }
    }

    .fhe-hotspot .hs9-dot {
      position: absolute;
      left: var(--x);
      top: var(--y);
      /* Visible by default (scale 1) — JS (see fhe-frontend.js) sets this
         to scale(0) itself right before running the GSAP entrance
         stagger, mirroring how .hs9-stage/.hs9-callout/.hs9-close/
         .hs9-sim-btn all default to visible in CSS and are hidden by JS,
         not the other way round. If a dot defaulted to invisible here and
         something upstream in initInstance() ever failed to reach the
         entrance block (e.g. in an editor/preview context with different
         timing than the public frontend), it would stay permanently
         invisible with no fallback — exactly the "can't see or position
         the hotspots in the Elementor editor" symptom. Defaulting to
         visible means a stalled/failed entrance just skips the pop-in
         animation instead of hiding the marker forever. */
      transform: translate(-50%, -50%) scale(1);
      /* Sized off the frame's own width (cqw, see .hs9-frame's
         container-type) so the marker stays in proportion to the
         product photo at any screen size — a fixed px size (the old
         34px/28px desktop/mobile pair) reads fine on a ~900px desktop
         frame but towers over the same product shrunk to ~300px on a
         phone. 34px was ~4% of a typical desktop frame's width, so
         4cqw reproduces that ratio continuously instead of jumping
         between two fixed sizes at the 720px breakpoint. Floor of
         22px keeps it a legible tap target on very narrow frames. */
      width: clamp(22px, 4cqw, 34px);
      height: clamp(22px, 4cqw, 34px);
      display: grid;
      place-items: center;
      border: 0;
      background: none;
      padding: 0;
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
      z-index: 3;
    }

    .fhe-hotspot .hs9-dot-ring {
      position: absolute;
      inset: 0;
      border-radius: 50%;
      border: 1.5px solid var(--hs-orange);
      opacity: 0;
    }

    /* Plain marker — no number badge. A solid core with a white halo
       reads clearly against both the light housing and the darker
       metal parts of the product shot. */
    .fhe-hotspot .hs9-dot-core {
      position: relative;
      /* Same cqw-proportional logic as .hs9-dot above, scaled down to
         match its ~41% share of the outer marker's diameter. */
      width: clamp(9px, 1.65cqw, 14px);
      height: clamp(9px, 1.65cqw, 14px);
      border-radius: 50%;
      background: var(--hs-orange);
      box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.92), 0 6px 14px rgba(160, 73, 26, 0.32);
      transition: background .25s var(--hs-ease), transform .25s var(--hs-ease), opacity .3s var(--hs-ease);
    }

    /* The .hs9-dot button itself is deliberately invisible (background:
       none, border:0, padding:0 above) — only its .hs9-dot-core/-ring
       children are drawn. That invisibility is just as vulnerable to a
       theme's generic button:hover/:active/:focus rule as any of this
       widget's visible buttons: if the theme adds a background/border/
       padding on interaction, a hotspot dot would suddenly sprout an
       ugly box around itself the moment someone hovers, clicks, or tabs
       to it, even though .hs9-dot-core's own hover/focus rules (below)
       already work correctly. Same defensive reinforcement as every
       other button in this file. */
    .fhe-hotspot .hs9-dot:hover,
    .fhe-hotspot .hs9-dot:active,
    .fhe-hotspot .hs9-dot:focus,
    .fhe-hotspot .hs9-dot:focus-visible {
      background: none;
      border: 0;
      padding: 0;
      margin: 0;
      min-width: 0;
      min-height: 0;
      box-shadow: none;
    }

    .fhe-hotspot .hs9-dot:hover .hs9-dot-core { transform: scale(1.15); }

    .fhe-hotspot .hs9-dot.is-active .hs9-dot-core {
      background: var(--hs-orange-a11y);
      transform: scale(1.2);
    }

    .fhe-hotspot .hs9-dot.is-dimmed .hs9-dot-core, .fhe-hotspot .hs9-dot.is-dimmed .hs9-dot-ring {
      opacity: 0;
      pointer-events: none;
    }

    .fhe-hotspot .hs9-dot:focus-visible .hs9-dot-core {
      outline: 2px solid var(--hs-orange-a11y);
      outline-offset: 3px;
    }

    /* Close button — only meaningful, and only shown, while zoomed. */
    .fhe-hotspot .hs9-close {
      position: absolute;
      top: 14px;
      right: 14px;
      width: 38px;
      height: 38px;
      border-radius: 50%;
      border: 1px solid rgba(255, 255, 255, 0.5);
      background: rgba(26, 20, 14, 0.55);
      backdrop-filter: blur(6px);
      color: #fff;
      display: grid;
      place-items: center;
      cursor: pointer;
      opacity: 0;
      transform: scale(.7);
      pointer-events: none;
      /* Above .hs9-callout (z-index 5) — on narrow frames where the
         card's anchored position could land near the top corner it
         must never be allowed to paint over, or steal clicks from,
         the close button. */
      z-index: 6;
    }

    /* Explicit :hover/:active/:focus-visible restating width/height/
       border-radius (not just outline) — V12 never needed this
       (nothing else on its page styled a bare <button>), but on a real
       host page a theme's own generic button:hover/:active rule only
       "activates" during that exact pseudo-class, so it gets its own
       chance to out-cascade this circle's shape even though the resting
       state (plain .hs9-close, no pseudo-class) already won that fight.
       :active is easy to miss testing with a mouse (it's the instant
       between mousedown and mouseup, gone before a screenshot usually
       catches it) but it's exactly as real a state as :hover — a
       generic theme button reset just as often targets it, and a
       button that's circular while merely hovered but square for the
       one frame it's actually being clicked reads as a visible glitch
       on every single click. Pinning the shape again in every
       pseudo-class the theme might target closes that gap. */
    .fhe-hotspot .hs9-close:hover,
    .fhe-hotspot .hs9-close:active,
    .fhe-hotspot .hs9-close:focus,
    .fhe-hotspot .hs9-close:focus-visible {
      width: 38px;
      height: 38px;
      border-radius: 50%;
      padding: 0;
      margin: 0;
      min-width: 0;
      min-height: 0;
    }

    .fhe-hotspot .hs9-close:focus-visible {
      outline: 2px solid #fff;
      outline-offset: 2px;
    }

    /* ---------------------------------------------------------------
       Simulation — a floating CTA over the media's bottom-right
       corner (the frame's own natural "play" spot) that swaps the
       static hotspot view for a short ambient animation: the same
       product photo, with wavy cold-air ribbons drawn in from
       outside the card at the base and warm-air ones leaving through
       the vented top, auto-narrated by three short captions and a
       non-interactive progress bar. There's no real video asset in
       this project yet — a real file will replace this later — so
       for now this *is* the "video": motion built entirely from the
       existing photo plus CSS/timers.
       --------------------------------------------------------------- */
    .fhe-hotspot .hs9-sim-btn {
      position: absolute;
      right: clamp(14px, 2.4vw, 28px);
      bottom: clamp(14px, 2.4vw, 28px);
      display: inline-flex;
      align-items: center;
      gap: 8px;
      height: 44px;
      padding: 0 20px 0 6px;
      border-radius: 999px;
      border: 1px solid rgba(255, 255, 255, 0.5);
      background: rgba(26, 20, 14, 0.72);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      color: #fff;
      font-family: var(--hs-font);
      font-weight: 700;
      font-size: 13px;
      letter-spacing: -0.005em;
      cursor: pointer;
      box-shadow: var(--hs-shadow-md);
      z-index: 4;
      transition: transform .2s var(--hs-ease), background .2s var(--hs-ease), opacity .25s var(--hs-ease);
    }

    /* border-radius/margin restated on every interactive pseudo-class,
       same reasoning as .hs9-close above — a theme's own button:hover/
       :active rule only competes with this shape while that exact
       pseudo-class is active, so the resting-state win isn't enough on
       its own. Padding/width/height deliberately NOT restated here —
       unlike border-radius (999px in both the desktop pill and the
       mobile icon-only circle) those differ by breakpoint, so hardcoding
       one value here would fight the mobile override below instead of
       the theme. The mobile block gets its own matching hover rule. */
    .fhe-hotspot .hs9-sim-btn:hover,
    .fhe-hotspot .hs9-sim-btn:active,
    .fhe-hotspot .hs9-sim-btn:focus,
    .fhe-hotspot .hs9-sim-btn:focus-visible {
      border-radius: 999px;
      margin: 0;
      min-width: 0;
      min-height: 0;
    }
    .fhe-hotspot .hs9-sim-btn:hover { transform: translateY(-1px); background: rgba(26, 20, 14, 0.85); }
    .fhe-hotspot .hs9-sim-btn:active { transform: translateY(0) scale(.97); }
    .fhe-hotspot .hs9-sim-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

    .fhe-hotspot .hs9-sim-btn-icon {
      display: grid;
      place-items: center;
      width: 32px;
      height: 32px;
      border-radius: 50%;
      background: var(--hs-orange);
      color: #fff;
      flex: none;
    }

    /* Dots can't be clicked open mid-simulation. */
    .fhe-hotspot .hs9-frame.is-sim .hs9-dot {
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s var(--hs-ease);
    }

    .fhe-hotspot .hs9-sim {
      position: absolute;
      inset: 0;
      overflow: hidden;
      border-radius: var(--hs-radius-md);
      z-index: 4;
      opacity: 0;
      pointer-events: none;
      transition: opacity .35s var(--hs-ease);
    }

    .fhe-hotspot .hs9-frame.is-sim .hs9-sim {
      opacity: 1;
      pointer-events: auto;
    }

    /* Mount point for warm-shimmer-v12.js — thin undulating light
       strands leaving the vented top, same Three.js tapering-ribbon
       technique cold-airflow.js uses for the entry half, mirrored.
       Sized to the FULL .hs9-stage, not just the frame (see the markup
       comment above these mount points) — its ribbon geometry
       deliberately rises off-canvas above the card, and clipping it to
       the frame's own box would throw that part away instead of
       letting it continue into the surrounding cinema band. It's
       positioned BEFORE #hs9Frame in DOM order (a sibling, not a
       descendant) so the opaque product photo still draws on top of
       it and occludes it wherever their paths cross. Since it's not
       inside .hs9-frame, its opacity toggle below uses :has() instead
       of a plain descendant selector — same technique the cinema-black
       stage background uses, for the same reason. */
    .fhe-hotspot .hs9-warm-shimmer-canvas {
      position: absolute;
      inset: 0;
      z-index: 0;
      opacity: 0;
      pointer-events: none;
      transition: opacity .35s var(--hs-ease);
    }

    .fhe-hotspot .hs9-stage:has(.hs9-frame.is-sim) .hs9-warm-shimmer-canvas {
      opacity: 1;
      pointer-events: auto;
    }

    .fhe-hotspot .hs9-warm-shimmer-canvas canvas {
      position: absolute;
      inset: 0;
      width: 100% !important;
      height: 100% !important;
    }

    /* Mount point for side-glow-v11.js (V11-only — a new fourth
       effect layer, added alongside the top warm effect, not
       replacing it: strands emanate from the product's own left/right
       edges and bow toward the camera as they travel outward). Same
       rules as .hs9-warm-shimmer-canvas, EXCEPT it also clips to the
       card's own rounded shape (border-radius + overflow: hidden):
       this canvas is a plain rectangle the same size as .hs9-stage,
       but .hs9-stage itself has rounded corners — without the clip the
       WebGL strands, which travel sideways toward each rounded corner,
       render right up to the canvas's own square edge and visibly
       poke out past the card's curve there. The top warm-shimmer
       layer doesn't get this treatment: its ribbons deliberately rise
       above the card into the surrounding cinema band (see its own
       comment above), so clipping it would cut off a wanted effect,
       not a bug — the side glow's horizontal strands have no such
       intentional overflow, so clipping them is a pure fix. */
    .fhe-hotspot .hs9-side-glow-canvas {
      position: absolute;
      inset: 0;
      z-index: 0;
      opacity: 0;
      pointer-events: none;
      transition: opacity .35s var(--hs-ease);
      overflow: hidden;
      border-radius: var(--hs-radius-lg);
    }

    .fhe-hotspot .hs9-stage:has(.hs9-frame.is-sim) .hs9-side-glow-canvas {
      opacity: 1;
      pointer-events: auto;
    }

    .fhe-hotspot .hs9-side-glow-canvas canvas {
      position: absolute;
      inset: 0;
      width: 100% !important;
      height: 100% !important;
    }

    /* Cold air entering at the base ("Kalte Luft wird vom Boden
       angezogen") is a separate layer (cold-airflow.js — see
       window.createColdAirflow()): genuine 3D Three.js ribbon geometry
       lofted along a path from off-canvas left into the radiator's
       base. Full-stage-sized and positioned before #hs9Frame, same
       reasoning as .hs9-warm-shimmer-canvas above — its path also dips below
       the frame's own bottom edge (see the file's own "Containment"
       note), which used to get clipped away by .hs9-clip the same
       way. Painting it behind the product photo means wherever the
       ribbon's path crosses the radiator's own (transparent-PNG)
       silhouette, the photo's opaque pixels naturally occlude it — the
       end of the ribbon reads as disappearing behind the unit instead
       of drawn on top of it. */
    .fhe-hotspot .hs9-sim-cold {
      position: absolute;
      inset: 0;
      z-index: 0;
      opacity: 0;
      pointer-events: none;
      transition: opacity .35s var(--hs-ease);
    }

    .fhe-hotspot .hs9-stage:has(.hs9-frame.is-sim) .hs9-sim-cold {
      opacity: 1;
      pointer-events: auto;
    }

    .fhe-hotspot .hs9-sim-cold canvas {
      position: absolute;
      inset: 0;
      width: 100% !important;
      height: 100% !important;
    }

    /* Reuses the zoom close button's exact look — same affordance,
       same corner, so it never has to be re-learned between modes. */
    .fhe-hotspot .hs9-sim-close {
      position: absolute;
      top: 14px;
      right: 14px;
      width: 38px;
      height: 38px;
      border-radius: 50%;
      border: 1px solid rgba(255, 255, 255, 0.5);
      background: rgba(26, 20, 14, 0.55);
      backdrop-filter: blur(6px);
      color: #fff;
      display: grid;
      place-items: center;
      cursor: pointer;
    }

    .fhe-hotspot .hs9-sim-close:hover,
    .fhe-hotspot .hs9-sim-close:active,
    .fhe-hotspot .hs9-sim-close:focus,
    .fhe-hotspot .hs9-sim-close:focus-visible {
      width: 38px;
      height: 38px;
      border-radius: 50%;
      padding: 0;
      margin: 0;
      min-width: 0;
      min-height: 0;
    }

    .fhe-hotspot .hs9-sim-close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

    /* Moved out from over the product image into the idle hint's own
       slot below the card (see .hs9-status-slot) — the two never
       show at once, so they share one box instead of the caption
       floating as a pill on top of whatever airflow/glow effect is
       currently playing underneath it.

       The dark translucent pill (rgba(26,20,14,.62)) was designed for
       sitting on the product photo — a near-black chip read clearly
       there. This slot sits on .hs9-stage's OWN near-black cinema
       background during the simulation, so that same dark-on-dark
       fill nearly disappeared — text still technically legible, but
       the caption lost its "chip" definition entirely, reading as
       text floating loose rather than a distinct caption. Fixed with
       a proper frosted-glass treatment instead of a darker fill:
       a light, low-opacity white base + a visible 1px light border
       (the edge is what separates it from the black background, not
       fill contrast) + a warm-tinted shadow so it still feels stitched
       into this orange-accented UI rather than a generic gray chip. */
    .fhe-hotspot .hs9-sim-caption {
      /* No position:absolute/inset needed — grid-area:1/1 on
         .hs9-status-slot's children (see above) handles stacking it
         over the hint's cell now, and unlike absolute positioning
         this keeps the caption contributing to the slot's own height
         whenever it's rendered. */
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 0;
      padding: 11px 22px;
      border-radius: 999px;
      background: rgba(255, 255, 255, 0.1);
      border: 1px solid rgba(255, 255, 255, 0.22);
      box-shadow: 0 6px 20px rgba(10, 6, 2, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.12);
      backdrop-filter: blur(10px) saturate(140%);
      color: #fff;
      font-family: var(--hs-font);
      font-weight: 700;
      /* Was 12.5px — read as "quasi illeggibile" against the busy
         product photo/effects behind it. Bumped a full size step up;
         padding grew with it so the pill stays proportioned instead
         of the text crowding its edges. */
      font-size: 15px;
      letter-spacing: -0.005em;
      white-space: nowrap;
      width: fit-content;
      margin-inline: auto;
      opacity: 0;
      pointer-events: none;
      transform: translateY(4px);
      transition: opacity .3s var(--hs-ease), transform .3s var(--hs-ease);
    }

    .fhe-hotspot .hs9-sim-caption.is-visible {
      opacity: 1;
      transform: translateY(0);
    }

    /* Shared box for the idle hint and the sim caption: a 1x1 CSS
       grid with both children stacked on the same cell (grid-area:
       1/1 below), so the slot's height always tracks whichever child
       actually has content — not just whichever one happens to be in
       normal flow.
       This used to be position:absolute + inset:0 on the caption with
       only the hint contributing flow height, on the theory that the
       two never show at once so one "host" box was enough. It broke
       on mobile: .hs9-hint.is-hidden switches to display:none there
       (see that rule below) to avoid leaving an empty gap, which
       removes the hint from flow entirely — collapsing the slot to
       0 height at the exact moment the (absolutely-positioned, so
       layout-invisible) caption needed that space, and the caption
       pill spilled out below the card's own rounded bottom edge.
       Grid stacking fixes this at the root: every child (hint or
       caption) still participates in the grid's auto-height sizing
       whenever it's actually rendered (opacity toggles don't remove
       it from flow the way display:none does), so the slot sizes
       itself off whichever one is currently relevant. */
    .fhe-hotspot .hs9-status-slot {
      position: relative;
      margin-top: 16px;
      display: grid;
      /* Without this, the slot's height is exactly the caption pill's
         own natural height (padding + line-height, ~45px on desktop)
         — the pill fills its box edge to edge with zero breathing
         room around it. min-height stretches the grid row taller than
         that; since the pill is a grid item with the default stretch
         alignment plus its own align-items:center, the extra space
         shows up as visual padding above/below it instead of changing
         the pill's own size. Same fixed value the mobile-only rule
         used to set on its own (see the media query below, now just
         inheriting this) — the desktop pill deserves the same cushion
         mobile already had. */
      min-height: 58px;
    }

    .fhe-hotspot .hs9-status-slot > * {
      grid-area: 1 / 1;
    }

    /* Auto-advancing only — there's nothing to drag or click, just a
       cue that the simulation ends on its own so it never feels like
       it's trapping the view.

       "bottom" is a % of .hs9-sim's own height (it fills .hs9-frame,
       inset:0), not a fixed px offset — on purpose. The product photo
       itself has a transparent margin baked into the source PNG below
       its own silhouette (~5.2% of the image's height — the same
       measured bbox the airflow effect files calibrate against), and
       a fixed 14px offset only reliably lands inside that margin on a
       tall desktop frame (14px is a small fraction of ~600px+ there).
       On mobile the frame's absolute height shrinks a lot more than
       its width (fixed aspect-ratio), so that same 14px stopped being
       a small fraction and started landing well inside the product's
       own visible housing instead of below it. A %-based offset scales
       with the frame the same way the margin itself does, so the bar
       stays clear of the product at any frame size instead of just
       the sizes it happened to be tuned against. */
    .fhe-hotspot .hs9-sim-progress {
      position: absolute;
      left: 14px;
      right: 14px;
      bottom: 3%;
      height: 3px;
      border-radius: 999px;
      background: rgba(255, 255, 255, 0.28);
      overflow: hidden;
    }

    .fhe-hotspot .hs9-sim-progress-bar {
      display: block;
      height: 100%;
      width: 0%;
      border-radius: inherit;
      background: var(--hs-orange);
    }

    /* ---------------------------------------------------------------
       Callout — anchored directly beside the active dot (not docked
       at a fixed frame edge) with a small triangular tail pointing
       back at it. positionCallout() in JS measures the dot and the
       card's real size each time and places the card on whichever
       side has room, vertically centered on the dot; the tail then
       tracks the dot's exact Y even if the card had to be nudged to
       stay inside the frame. Close enough that the tail alone reads
       as "this points at that" — no separate connector line needed.
       --------------------------------------------------------------- */
    .fhe-hotspot .hs9-callout {
      position: absolute;
      width: min(240px, calc(50% - 48px));
      background: rgba(255, 255, 255, 0.94);
      backdrop-filter: blur(16px);
      -webkit-backdrop-filter: blur(16px);
      border: 1px solid rgba(255, 255, 255, 0.7);
      border-radius: 18px;
      box-shadow: var(--hs-shadow-lg);
      padding: 16px 18px;
      opacity: 0;
      pointer-events: none;
      z-index: 5;
    }

    .fhe-hotspot .hs9-callout-tail {
      position: absolute;
      width: 14px;
      height: 14px;
      background: rgba(255, 255, 255, 0.96);
      filter: drop-shadow(1px 2px 3px rgba(90, 52, 16, 0.16));
      display: none;
    }

    /* Card sits to the right of the dot: tail on the card's left
       edge, pointing left (back at the dot). */
    .fhe-hotspot .hs9-callout.tail-left .hs9-callout-tail {
      display: block;
      left: -13px;
      clip-path: polygon(100% 0, 0 50%, 100% 100%);
    }

    /* Card sits to the left of the dot: tail on the card's right
       edge, pointing right. */
    .fhe-hotspot .hs9-callout.tail-right .hs9-callout-tail {
      display: block;
      right: -13px;
      clip-path: polygon(0 0, 100% 50%, 0 100%);
    }

    .fhe-hotspot .hs9-callout-text {
      font-family: var(--hs-font);
      font-weight: 700;
      font-size: 14.5px;
      line-height: 1.45;
      letter-spacing: -0.005em;
      color: var(--hs-ink);
      margin: 0;
    }

    .fhe-hotspot .hs9-callout-nav {
      display: flex;
      gap: 8px;
      margin-top: 14px;
      padding-top: 12px;
      border-top: 1px solid var(--hs-line);
    }

    .fhe-hotspot .hs9-callout-nav button {
      flex: 1;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 5px;
      height: 36px;
      border-radius: 999px;
      border: 1px solid var(--hs-card-line);
      background: #fff;
      color: var(--hs-ink);
      font-family: var(--hs-font);
      font-weight: 700;
      font-size: 12px;
      cursor: pointer;
      transition: transform .18s var(--hs-ease), border-color .18s var(--hs-ease);
    }

    /* Same shape-reinforcement pattern as every other button in this
       file (see .hs9-close's own comment for the full reasoning) —
       V12 never needed a :active rule here at all (no theme button
       style competing with it there), but this widget does. Height
       isn't restated here since it differs by breakpoint (36px desktop
       / 38px mobile, see the media query) same as .hs9-sim-btn's
       padding — hardcoding one would fight the other breakpoint's own
       value instead of the theme. */
    .fhe-hotspot .hs9-callout-nav button:hover,
    .fhe-hotspot .hs9-callout-nav button:active,
    .fhe-hotspot .hs9-callout-nav button:focus,
    .fhe-hotspot .hs9-callout-nav button:focus-visible {
      border-radius: 999px;
      background: #fff;
      padding: 0;
      margin: 0;
      min-width: 0;
      min-height: 0;
    }
    .fhe-hotspot .hs9-callout-nav button:hover { transform: translateY(-1px); border-color: rgba(160, 73, 26, 0.25); }
    .fhe-hotspot .hs9-callout-nav button:active { transform: translateY(0) scale(.97); }
    .fhe-hotspot .hs9-callout-nav button:focus-visible { outline: 2px solid var(--hs-orange-a11y); outline-offset: 2px; }

    /* Defensive icon sizing: the inline SVGs throughout this widget
       rely on their own width/height HTML attributes (16, 14, 12...)
       for size — presentational attributes, which ANY css rule beats
       regardless of specificity, including a theme's generic
       `svg { width: ...; height: ... }` reset (common on icon-heavy
       themes that standardize icon sizing sitewide). Restating each
       one's actual size in CSS, scoped to this widget, closes that
       gap the same way the button reset above closes the button one.

       !important here (unlike everywhere else in this file, which
       relies on the .fhe-hotspot specificity bump instead) because a
       plain specificity fight isn't enough if the theme's own reset
       is ALSO !important — tested against `svg { width/height: 100%
       !important }` specifically, since some sitewide "make icons
       responsive" resets do exactly that. When it wins, .hs9-hint's
       icon (the one case here without a fixed-size ancestor box to
       otherwise contain it) blows up to fill the entire stage instead
       of staying a 14px glyph. There's no legitimate reason a site
       owner would want THIS widget's own bundled icons at a different
       size than the ones drawn for it, so !important is a safe,
       narrowly-scoped trade here — not a general escape hatch. */
    .fhe-hotspot .hs9-hint svg { width: 14px !important; height: 14px !important; flex: none; }
    .fhe-hotspot .hs9-close svg { width: 16px !important; height: 16px !important; }
    .fhe-hotspot .hs9-sim-close svg { width: 16px !important; height: 16px !important; }
    .fhe-hotspot .hs9-sim-btn-icon svg { width: 12px !important; height: 12px !important; }
    .fhe-hotspot .hs9-callout-nav svg { width: 12px !important; height: 12px !important; flex: none; }

    /* ---------------------------------------------------------------
       Idle hint + legend
       --------------------------------------------------------------- */
    .fhe-hotspot .hs9-hint {
      display: flex;
      align-items: center;
      gap: 8px;
      justify-content: center;
      /* margin-top moved to the .hs9-status-slot wrapper, which now
         also holds .hs9-sim-caption. */
      font-family: var(--hs-mono);
      font-size: 12px;
      color: var(--hs-body-2);
      opacity: 1;
      transition: opacity .3s var(--hs-ease);
    }

    .fhe-hotspot .hs9-hint.is-hidden { opacity: 0; }


    @media (max-width: 720px) {
      /* On short/narrow frames the callout can no longer float beside
         the zoomed detail, and docking it inside the frame forces a
         capped height with internal scrolling — the Zurück/Weiter
         buttons could end up scrolled out of view. Instead, JS
         relocates the actual callout element (see placeCallout()) to
         sit in normal document flow right below the frame, in the
         space the idle hint normally occupies. As a static block it
         can take whatever height its content needs, so nothing ever
         has to scroll and the nav buttons are always visible. */
      .fhe-hotspot .hs9-callout {
        position: static;
        width: 100%;
        max-width: none;
        margin-top: 16px;
        padding: 16px 18px;
        border-radius: 16px;
      }
      .fhe-hotspot .hs9-callout-tail { display: none !important; }
      .fhe-hotspot .hs9-callout-text { font-size: 14px; }
      .fhe-hotspot .hs9-callout-nav { margin-top: 12px; padding-top: 10px; }
      .fhe-hotspot .hs9-callout-nav button { height: 38px; font-size: 12.5px; }
      .fhe-hotspot .hs9-close { top: 10px; right: 10px; width: 34px; height: 34px; }
      /* Dot sizing itself now lives on .hs9-dot/.hs9-dot-core directly
         (cqw-based, proportional to the frame at any width) — no fixed
         mobile override needed here anymore. */
      /* The hint is replaced by the callout in this same slot while
         zoomed — collapse it fully instead of leaving an empty gap
         from the desktop opacity-only fade. */
      .fhe-hotspot .hs9-hint.is-hidden { display: none; }

      /* Bigger product on mobile: drop the card's own border/radius/
         shadow/padding so the photo uses the widget's full available
         width instead of being inset inside a bordered card — thanks
         to the frame's fixed aspect-ratio that grows its height too.
         In the standalone V12 build this went further and broke the
         card out to the viewport's own edges (a negative-margin
         escape past the page's own centered container). That trick
         assumes a specific, simple centered-container ancestor chain
         which V12 controlled directly — inside Elementor this widget
         can land in arbitrary nested columns/containers, so escaping
         to 100vw there is unreliable (it can overflow a non-centered
         parent instead of the viewport). Using 100% of the widget's
         own container instead is the safe equivalent: still noticeably
         bigger than the padded desktop card, without assuming
         anything about what wraps it. */
      .fhe-hotspot .hs9-stage {
        width: 100%;
        border: none;
        border-radius: 0;
        box-shadow: none;
        padding: 10px 14px;
      }

      /* Icon-only on ALL mobile widths (was previously a separate
         <400px breakpoint) — with the frame now full-bleed the label
         pill had more room in theory, but a floating text button over
         the product photo reads cluttered at any phone width, and the
         user asked for just the play control here. The accessible name
         still lives on the button's own aria-label (set in the markup)
         so screen readers announce "Simulation abspielen" even with
         the label visually hidden, not removed. */
      .fhe-hotspot .hs9-sim-btn {
        width: 40px;
        height: 40px;
        padding: 0;
        justify-content: center;
        gap: 0;
      }
      /* Same shape-pinning idea as the desktop :hover rule above, but
         with THIS breakpoint's actual width/height/padding (40px
         square, no padding) instead of the desktop pill's — a theme
         :hover rule fighting the base mobile size needs to be beaten
         with the mobile size, not the desktop one. */
      .fhe-hotspot .hs9-sim-btn:hover,
      .fhe-hotspot .hs9-sim-btn:active,
      .fhe-hotspot .hs9-sim-btn:focus,
      .fhe-hotspot .hs9-sim-btn:focus-visible {
        width: 40px;
        height: 40px;
        padding: 0;
      }
      .fhe-hotspot .hs9-sim-close:hover,
      .fhe-hotspot .hs9-sim-close:active,
      .fhe-hotspot .hs9-sim-close:focus,
      .fhe-hotspot .hs9-sim-close:focus-visible {
        width: 34px;
        height: 34px;
      }
      .fhe-hotspot .hs9-close:hover,
      .fhe-hotspot .hs9-close:active,
      .fhe-hotspot .hs9-close:focus,
      .fhe-hotspot .hs9-close:focus-visible {
        width: 34px;
        height: 34px;
      }
      .fhe-hotspot .hs9-sim-btn-label {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
      }
      .fhe-hotspot .hs9-sim-close { top: 10px; right: 10px; width: 34px; height: 34px; }
      .fhe-hotspot .hs9-sim-caption {
        max-width: calc(100% - 28px);
        white-space: normal;
        text-align: center;
        font-size: 13.5px;
        padding: 9px 18px;
      }

      /* .hs9-status-slot's min-height (58px, see the base rule above)
         already covers mobile too — no override needed here. Worth
         noting why that specific value matters more on mobile than
         desktop, though: the caption can wrap to two lines on a narrow
         phone ("Der Schamotte-Kern speichert und strahlt Wärme ab" is
         the longest of the three) while the other two stay on one
         line. If the slot's height ever tracked actual content again
         (the old bug this min-height also fixes), that 1-line-to-
         2-line transition would change .hs9-stage's own total height
         mid-simulation — and nothing re-runs the effect canvases'
         resize()/recomputeWorldSize() on an internal reflow (only a
         real window resize does), so their Three.js cameras would stay
         calibrated to the pre-reflow size and the airflow ribbons would
         visibly jump out of alignment with the product photo. 58px is
         tall enough to contain the 2-line case without growing past it
         — verified live, .hs9-stage's height stays identical across
         all three captions. */
    }
