Plugin Directory

Changeset 3707052


Ignore:
Timestamp:
09/22/2026 10:26:57 AM (4 weeks ago)
Author:
zehirlioglu
Message:

1.0.2: code comments in English, RTL back arrow fixed

Location:
funnel-elite-lite
Files:
58 edited

Legend:

Unmodified
Added
Removed
  • funnel-elite-lite/tags/1.0.2/admin/css/admin-bundle.css

    r3706934 r3707052  
    4343select:focus,
    4444textarea:focus {
    45   /* Farbe aus dem Admin-Farbschema des Users, damit der Ring auch in
    46      "Modern", "Midnight" usw. passt. outline bleibt vorhanden (transparent),
    47      damit im Windows-Kontrastmodus ueberhaupt ein Ring gezeichnet wird. */
     45  /* Transparent outline keeps a visible ring in forced-colors mode. */
    4846  border-color: var(--wp-admin-theme-color, #2271b1);
    4947  box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1);
    … …  
    241239}
    242240
    243 /* Die Zentrierung rechnet mit der eingestellten Spaltenzahl. Ab 1400px
    244    abwaerts schaltet das Raster auf auto-fill um, dort wuerde sie die
    245    letzten Karten schmal quetschen oder in eine eigene Zeile schieben. */
     241/* Below 1401px the grid uses auto-fill, where this centering breaks. */
    246242@media (min-width: 1401px) {
    247243  /* Center last card(s) if alone in incomplete row — only in cards view */
    … …  
    14921488}
    14931489
    1494 /* Parent muss position: relative haben */
     1490/* Parent needs position: relative */
    14951491.funnel-group-box {
    14961492  position: relative;
    … …  
    15241520}
    15251521
    1526 /* Pulsieren effizienter: nur scale statt Opacity + optional */
     1522/* Pulse with scale only (cheaper than opacity) */
    15271523@keyframes pulse-badge {
    15281524  0%,
    … …  
    15371533}
    15381534
    1539 /* Flash Animation beim Scroll */
     1535/* Flash animation on scroll */
    15401536.scroll-highlight {
    15411537  animation: scroll-flash-blue 0.8s ease-in-out 5;
    … …  
    16741670}
    16751671
    1676 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on Neuer Funnel) */
     1672/* Normalize every dashicon inside toolbar buttons (incl. the "+" on New Funnel) */
    16771673.actions-group .button .dashicons {
    16781674  font-size: 18px;
    … …  
    20302026    font-weight: 600;
    20312027    display: flex;
    2032     /* Titel linksbündig neben dem Icon halten.
    2033        Vorher stand hier justify-content: space-between, was mit den erwarteten
    2034        zwei Flex-Items (Text + Pfeil) auch links ausrichtete. WordPress ersetzt
    2035        im Admin aber Emoji durch <img class="emoji"> (wp-emoji-release.min.js) —
    2036        damit werden aus dem einen Textknoten ZWEI Items, in Summe drei, und
    2037        space-between schob das Icon nach links, den Titel in die Mitte und den
    2038        Pfeil nach rechts. flex-start + margin-left:auto auf dem Pfeil ist
    2039        unabhängig von der Anzahl der Items. */
     2028    /* Not space-between: WordPress turns emoji into <img>, adding flex items. */
    20402029    justify-content: flex-start;
    20412030    gap: 8px;
    … …  
    20442033}
    20452034
    2046 /* Der aufklapp-Pfeil (▼) ist immer das letzte Kind und bleibt rechts.
    2047    Das inline style="float:right" im Markup ist auf einem Flex-Item wirkungslos. */
     2035/* The arrow is always the last child. */
    20482036.section-toggle > span:last-child {
    20492037    margin-left: auto;
    … …  
    20882076    border-top: 1px solid #dcdcde;
    20892077}
    2090 /* Funnel Design Tab: Spalten-Layout für Typography, Dimensions & Images */
     2078/* Funnel Design Tab: column layout for Typography, Dimensions & Images */
    20912079.funnel-dt-typo-grid--columns {
    20922080    display: grid;
    … …  
    21082096.funnel-design-sticky-save .submit {
    21092097    margin: 0;
    2110     /* Ohne padding:0 behaelt der Kern seine 10px/1.5em und die Leiste wird
    2111        121px hoch - auf einem Handy im Querformat fast die halbe Ansicht. */
    21122098    padding: 0;
    21132099    text-align: center;
    … …  
    21152101
    21162102.funnel-design-sticky-save .button-hero {
    2117     /* schlaegt das globale .button-hero{margin-top:20px!important} */
     2103    /* Overrides core .button-hero { margin-top: 20px !important; } */
    21182104    margin-top: 0 !important;
    21192105}
  • funnel-elite-lite/tags/1.0.2/admin/css/admin-style.css

    r3706934 r3707052  
    4242select:focus,
    4343textarea:focus {
    44   /* Farbe aus dem Admin-Farbschema des Users, damit der Ring auch in
    45      "Modern", "Midnight" usw. passt. outline bleibt vorhanden (transparent),
    46      damit im Windows-Kontrastmodus ueberhaupt ein Ring gezeichnet wird. */
     44  /* Transparent outline keeps a visible ring in forced-colors mode. */
    4745  border-color: var(--wp-admin-theme-color, #2271b1);
    4846  box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1);
    … …  
    240238}
    241239
    242 /* Die Zentrierung rechnet mit der eingestellten Spaltenzahl. Ab 1400px
    243    abwaerts schaltet das Raster auf auto-fill um, dort wuerde sie die
    244    letzten Karten schmal quetschen oder in eine eigene Zeile schieben. */
     240/* Below 1401px the grid uses auto-fill, where this centering breaks. */
    245241@media (min-width: 1401px) {
    246242  /* Center last card(s) if alone in incomplete row — only in cards view */
  • funnel-elite-lite/tags/1.0.2/admin/css/admin-tab-dashboard.css

    r3589214 r3707052  
    443443}
    444444
    445 /* Parent muss position: relative haben */
     445/* Parent needs position: relative */
    446446.funnel-group-box {
    447447  position: relative;
    … …  
    475475}
    476476
    477 /* Pulsieren effizienter: nur scale statt Opacity + optional */
     477/* Pulse with scale only (cheaper than opacity) */
    478478@keyframes pulse-badge {
    479479  0%,
    … …  
    488488}
    489489
    490 /* Flash Animation beim Scroll */
     490/* Flash animation on scroll */
    491491.scroll-highlight {
    492492  animation: scroll-flash-blue 0.8s ease-in-out 5;
    … …  
    625625}
    626626
    627 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on Neuer Funnel) */
     627/* Normalize every dashicon inside toolbar buttons (incl. the "+" on New Funnel) */
    628628.actions-group .button .dashicons {
    629629  font-size: 18px;
  • funnel-elite-lite/tags/1.0.2/admin/css/admin-tab-design.css

    r3706934 r3707052  
    1717    font-weight: 600;
    1818    display: flex;
    19     /* Titel linksbündig neben dem Icon halten.
    20        Vorher stand hier justify-content: space-between, was mit den erwarteten
    21        zwei Flex-Items (Text + Pfeil) auch links ausrichtete. WordPress ersetzt
    22        im Admin aber Emoji durch <img class="emoji"> (wp-emoji-release.min.js) —
    23        damit werden aus dem einen Textknoten ZWEI Items, in Summe drei, und
    24        space-between schob das Icon nach links, den Titel in die Mitte und den
    25        Pfeil nach rechts. flex-start + margin-left:auto auf dem Pfeil ist
    26        unabhängig von der Anzahl der Items. */
     19    /* Not space-between: WordPress turns emoji into <img>, adding flex items. */
    2720    justify-content: flex-start;
    2821    gap: 8px;
    … …  
    3124}
    3225
    33 /* Der aufklapp-Pfeil (▼) ist immer das letzte Kind und bleibt rechts.
    34    Das inline style="float:right" im Markup ist auf einem Flex-Item wirkungslos. */
     26/* The arrow is always the last child. */
    3527.section-toggle > span:last-child {
    3628    margin-left: auto;
    … …  
    7567    border-top: 1px solid #dcdcde;
    7668}
    77 /* Funnel Design Tab: Spalten-Layout für Typography, Dimensions & Images */
     69/* Funnel Design Tab: column layout for Typography, Dimensions & Images */
    7870.funnel-dt-typo-grid--columns {
    7971    display: grid;
    … …  
    9587.funnel-design-sticky-save .submit {
    9688    margin: 0;
    97     /* Ohne padding:0 behaelt der Kern seine 10px/1.5em und die Leiste wird
    98        121px hoch - auf einem Handy im Querformat fast die halbe Ansicht. */
    9989    padding: 0;
    10090    text-align: center;
    … …  
    10292
    10393.funnel-design-sticky-save .button-hero {
    104     /* schlaegt das globale .button-hero{margin-top:20px!important} */
     94    /* Overrides core .button-hero { margin-top: 20px !important; } */
    10595    margin-top: 0 !important;
    10696}
  • funnel-elite-lite/tags/1.0.2/admin/includes/class-funnel-builder-manager.php

    r3706934 r3707052  
    309309        ];
    310310       
    311         // Unbekannte Meta-Keys des Bestands erhalten.
    312         //
    313         // Das Array oben wird bei jedem Speichern vollstaendig aus $_POST neu
    314         // gebaut. Alles, was diese Version nicht kennt, waere damit still weg,
    315         // sobald jemand die Karte einmal oeffnet und speichert — in Lite z. B. die
    316         // komplette Konfiguration der Pro-Kartentypen (Slider, Mengen-Items,
    317         // Distance Map, Consent-Texte, Pricing-Table-Spalten), die ueber einen
    318         // Import aus Pro in der Datenbank liegt.
    319         //
    320         // Bekannte Keys gewinnen, weil sie oben immer gesetzt werden; unbekannte
    321         // bleiben unveraendert stehen.
     311        // Keep stored meta keys this version does not know (e.g. imported from Pro).
    322312        if ( ! empty( $_POST['id'] ) ) {
    323313            $existing_raw  = $this->db->get_var( $this->db->prepare( "SELECT meta_data FROM {$this->table_cards} WHERE id = %d", intval( wp_unslash( $_POST['id'] ) ) ) );
    … …  
    368358
    369359    /**
    370      * Kartenpreis aus dem Karten-Modal lesen.
     360     * Parse a typed price. With both separators the last one is the decimal
     361     * mark; several of the same kind are thousands separators.
    371362     *
    372      * Das Feld ist ein Textfeld (ohne die Pfeile eines Zahlenfelds), es kommt
    373      * also an, was getippt wurde. Frueher genuegte str_replace(',', '.') - das
    374      * Zahlenfeld liess "1.200,00" gar nicht zu. Jetzt wuerde daraus 1.2.
    375      *
    376      * - Komma UND Punkt: das hintere Zeichen trennt die Dezimalen, das andere
    377      *   die Tausender ("1.200,00", "1,200.00").
    378      * - Nur Kommas: eines ist das Dezimalzeichen ("1200,00", "1,2" fuer ×1,2),
    379      *   mehrere trennen Tausender.
    380      * - Nur Punkte: einer ist das Dezimalzeichen ("1200.50"), mehrere trennen
    381      *   Tausender ("1.200.000"). "1.200" bleibt 1.2 wie bisher - nicht eindeutig.
    382      * - Leerzeichen und Waehrungszeichen fallen weg.
    383      *
    384      * @param  string $raw Eingabe aus dem Preisfeld.
     363     * @param  string $raw Raw price field value.
    385364     * @return float
    386365     */
  • funnel-elite-lite/tags/1.0.2/admin/includes/class-funnel-settings-manager.php

    r3706934 r3707052  
    3131
    3232    /**
    33      * Keep an email template only if it was actually edited.
    34      *
    35      * The editor in the Email tab is pre-filled with the default template, so
    36      * ANY save on that tab — a new SMTP password, a changed colour — writes the
    37      * whole default into the project settings, even when nobody touched the
    38      * HTML. From that moment the email is that frozen HTML: it keeps the
    39      * language it happened to have at the time and never follows the site
    40      * again. That is how a German site ends up sending a half-English email,
    41      * and no plugin update can repair it, because the database wins.
    42      *
    43      * An empty field means "use the default", and the default is built fresh on
    44      * every send, so it speaks the language of the site.
    45      *
    46      * Both sides are compared the way they are stored: the POST value has gone
    47      * through wp_kses_post(), which strips <html>/<body> out of the template,
    48      * and a textarea submits CRLF where the source has LF. Without normalising
    49      * both, nothing would ever match and the guard would be dead code.
     33     * Store the template only if it differs from the default, so an untouched
     34     * one keeps following the site language.
    5035     *
    5136     * @param  string $posted   The already sanitized POST value.
    … …  
    6247
    6348    /**
    64      * The default admin template, or an empty string if the mail class is not
    65      * loaded — then nothing can match and the value is kept as posted.
     49     * Default admin template, or '' if the mail class is not loaded.
    6650     */
    6751    private static function default_admin_template() {
  • funnel-elite-lite/tags/1.0.2/admin/js/admin-builder.js

    r3706934 r3707052  
    380380    form.find('input[name="answer"]').val(answer);
    381381    form.find('input[name="image_url"]').val(img);
    382     // Ohne Preis leer lassen, damit der Platzhalter ("z. B. 1200,00") zu sehen ist.
     382    // Empty instead of 0, so the placeholder shows.
    383383    form.find('input[name="price"]').val(parseFloat(price) ? price : "");
    384384    form.find('select[name="price_operator"]').val(operator);
  • funnel-elite-lite/tags/1.0.2/admin/js/admin-visual-nodes-trees.js

    r3706934 r3707052  
    2626  let lastRenderedSource = ""; // Performance cache
    2727
    28   // Welche Karte gerade steht (Routen-Filter) und ob der Admin den Zoom selbst
    29   // gewaehlt hat oder die Karte nur eingepasst ist. Siehe "ANSICHT MERKEN".
     28  // zoomChosen: zoom set by the user (false = fitted to the container).
    3029  let currentFilter = "all";
    3130  let zoomChosen = false;
    … …  
    9291
    9392  // ===================================
    94   // ANSICHT MERKEN (Zoom + Scrollposition)
    95   // ===================================
    96 
    97   // Nach dem Speichern laedt die Seite neu, und die Karte wurde dabei jedes Mal
    98   // wieder eingepasst - wer hineingezoomt hatte, musste die Stelle neu suchen.
    99   // Gemerkt wird im Browser, je Funnel und je Routen-Filter: jeder Filter
    100   // zeichnet eine andere Karte, deren Stelle nicht auf die naechste passt.
     93  // REMEMBER VIEW (per funnel and route filter)
     94  // ===================================
    10195
    10296  function mapProjectId() {
    … …  
    125119      localStorage.setItem(mapViewKey(), JSON.stringify(views));
    126120    } catch (e) {
    127       // Privates Fenster oder voller Speicher: dann eben ohne Gedaechtnis.
     121      // Storage unavailable (private window, quota).
    128122    }
    129123  }
    … …  
    131125  function saveMapView() {
    132126    const scroller = document.getElementById("visual-map-scroll-container");
    133     // Solange die gemerkte Ansicht nicht angewendet ist, wuerde der Zwischenstand
    134     // sie ueberschreiben. Und versteckt (anderer Tab, eingeklappte Karte) liest
    135     // sich jede Scrollposition als 0.
     127    // Hidden containers report 0; never overwrite a view not yet applied.
    136128    if (!viewApplied || !scroller || !$(scroller).is(":visible")) return;
    137129    if (!DOM.$graphContainer.find("svg").length) return;
    … …  
    139131    const views = readMapViews();
    140132    views[currentFilter] = {
    141       // null = nur eingepasst. Waechst die Karte, passt sie sich dann neu ein,
    142       // statt auf einem veralteten Einpass-Wert stehen zu bleiben.
     133      // null = fitted, so a grown map is fitted again.
    143134      zoom: zoomChosen ? currentZoom : null,
    144135      left: scroller.scrollLeft,
    … …  
    154145  }
    155146
    156   // Der Routen-Filter selbst sprang nach dem Neuladen auf "Alle" zurueck - die
    157   // fuer einen Filter gemerkte Stelle kam so nie zum Zug. Ebenfalls je Funnel.
    158   // "Alle" ist der Normalfall und wird nicht gespeichert.
     147  // "all" is the default and is not stored.
    159148
    160149  function mapFilterKey() {
    … …  
    170159      }
    171160    } catch (e) {
    172       // Privates Fenster oder voller Speicher: dann eben ohne Gedaechtnis.
     161      // Storage unavailable (private window, quota).
    173162    }
    174163  }
    175164
    176165  /**
    177    * Den gemerkten Filter zurueck in die Auswahl schreiben, bevor zum ersten
    178    * Mal gezeichnet wird. Gibt es die Route nicht mehr (Karte umbenannt oder
    179    * geloescht), bleibt "Alle" stehen und der alte Wert wird vergessen.
     166   * Put the stored filter back into the dropdown; forget it if the route is gone.
    180167   */
    181168  function restoreMapFilter() {
    … …  
    201188
    202189  /**
    203    * Karte einpassen und die gemerkte Ansicht wiederherstellen, falls es eine
    204    * gibt. Nur bei sichtbarem Container aufrufen - versteckt nimmt der Browser
    205    * keine Scrollposition an.
     190   * Fit the map and restore the stored view. Call only on a visible container.
    206191   */
    207192  function showMapView() {
    … …  
    350335    if (!sourceCode) return;
    351336
    352     // Ohne Angabe gilt, was in der Auswahl steht. Vorher zeichnete z. B. der
    353     // Wechsel zurueck auf den Builder-Tab "Alle", waehrend die Auswahl noch
    354     // einen Filter zeigte.
    355337    if (filterRoute === undefined) {
    356338      filterRoute = $("#pages-cards-route-filter").val() || "all";
    … …  
    363345    // PERFORMANCE: Skip expensive render if source is unchanged
    364346    if (sourceCode === lastRenderedSource) {
    365       // Same map shown again (e.g. back on the Builder tab): skip DOM
    366       // generation, but put the view back where it was.
     347      // Unchanged source: skip rendering, restore the view.
    367348      showMapView();
    368349      return;
    … …  
    421402      });
    422403
    423       // Fit map after render. Shown first: the stored scroll position only
    424       // sticks on a visible container.
     404      // Show first: scroll positions only stick on a visible container.
    425405      setTimeout(() => {
    426406        const $collapsible = $("#visual-pages-cards-collapsible");
    … …  
    475455  });
    476456
    477   // Zuruecksetzen heisst auch vergessen: nach dem Neuladen wird wieder
    478   // eingepasst.
    479457  $(".zoom-map-reset").on("click", (e) => {
    480458    e.preventDefault();
    … …  
    483461  });
    484462
    485   // Die Scrollposition laufend mitschreiben. "pagehide" faengt den letzten
    486   // Stand ab, wenn direkt nach dem Scrollen gespeichert und neu geladen wird.
     463  // pagehide catches a scroll right before a reload.
    487464  $("#visual-map-scroll-container").on("scroll", debounce(saveMapView, 150));
    488465  window.addEventListener("pagehide", saveMapView);
    … …  
    697674
    698675  /**
    699    * Apply the stored collapsed state to the map panel.
    700    *
    701    * Runs without Mermaid so the panel is in its final state even when the
    702    * library is never loaded.
     676   * Apply the stored collapsed state (works without Mermaid).
    703677   *
    704678   * @return {boolean} true when the map is collapsed.
    … …  
    720694
    721695  function initMermaidAndRender() {
    722     // loadMermaidIfNeeded() resolves even when the local file fails to load
    723     // (fail-silently, WP.org Guideline #8 — no CDN fallback). Guard so a missing
    724     // library cannot throw here.
     696    // loadMermaidIfNeeded() also resolves when the file failed to load.
    725697    if (typeof mermaid === "undefined") {
    726698      applyCollapsedState();
    … …  
    733705      securityLevel: "loose",
    734706      htmlLabels: true,
    735       // NO size limit on the map.
    736       //
    737       // Mermaid stops at 50 000 characters by default and REPLACES the diagram
    738       // with "Maximum text size in diagram exceeded" (it does not throw:
    739       //   text.length > (config.maxTextSize ?? 5e4) && (text = errorGraph)
    740       // ). That guard is meant for UNTRUSTED input; here the definition is
    741       // generated from the admin's own funnel, so the default only hides the
    742       // map once a funnel grows - after roughly ten steps, because the length
    743       // comes from the labels rather than from the structure.
    744       //
    745       // MAX_SAFE_INTEGER rather than Infinity: the comparison is numeric either
    746       // way, but a plain number survives any clone of the config object, while
    747       // Infinity can come back as null - and `?? 5e4` would then quietly put
    748       // the default back.
     707      // No size limit (the source is built by the plugin). Not Infinity: it can
     708      // become null when the config is cloned, which restores the default.
    749709      maxTextSize: Number.MAX_SAFE_INTEGER,
    750710      maxEdges: Number.MAX_SAFE_INTEGER,
    … …  
    791751  // Global Initialization Boundary
    792752  try {
    793     // The stored route filter goes back into the dropdown before anything is
    794     // drawn, so the first render already uses it.
    795753    restoreMapFilter();
    796754
    797     // Put the panel into its stored state first — this must happen whether or
    798     // not Mermaid ever loads.
    799755    const startsCollapsed = applyCollapsedState();
    800756
    … …  
    806762      loadMermaidIfNeeded().then(initMermaidAndRender);
    807763    } else {
    808       // Nothing to draw yet. Mermaid is ~650 KB, so it stays unloaded until the
    809       // map is actually going to be shown: either the Builder tab is opened
    810       // (with the map expanded) or the user expands the collapsed map.
     764      // Mermaid (~650 KB) loads only once the map is shown.
    811765      $(document).on("click.funnelMermaidLazy", '.nav-tab[href="#tab-builder"]', function () {
    812766        if (localStorage.getItem("funnel_pages_cards_collapsed") === "true") return;
  • funnel-elite-lite/tags/1.0.2/admin/traits/trait-admin-enqueue.php

    r3706934 r3707052  
    3030
    3131    /**
    32      * Is the current admin screen one of the plugin's own?
    33      *
    34      * WordPress passes the screen's hook suffix to admin_enqueue_scripts
    35      * ('toplevel_page_funnel-elite-lite'); $_GET['page'] is kept as a fallback
    36      * for the AJAX-rendered tabs, which reach this code without a suffix.
     32     * Is the current admin screen the plugin's own page?
    3733     *
    3834     * @param string $hook_suffix Hook suffix handed over by WordPress.
    … …  
    5147
    5248        public function enqueue_styles( $hook_suffix = '' ) {
    53         // Nur auf der eigenen Plugin-Seite laden. Das Bundle gestaltet Labels,
    54         // Eingabefelder und Buttons global um; auf fremden Seiten hat es
    55         // Kern-Formulare, Listentabellen und den Block-Editor veraendert.
    56         // Das Plugin zeichnet ausserhalb seiner Seite nichts (kein
    57         // admin_notices, kein admin_bar_menu, kein Dashboard-Widget).
    5849        if ( ! $this->is_plugin_admin_page( $hook_suffix ) ) {
    5950            return;
    … …  
    8374            . '.slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background-color:#fff;-webkit-transition:.4s;transition:.4s}'
    8475            . 'input:checked+.slider{background-color:#d63384}'
    85             // Tastatur-Fokus: der echte Haken ist 0x0 und unsichtbar, ohne
    86             // diesen Ring sieht man beim Tabben nicht, welcher Schalter dran ist.
    8776            . 'input:focus-visible+.slider{outline:2px solid var(--wp-admin-theme-color,#2271b1);outline-offset:2px}'
    8877            . 'input:checked+.slider:before{-webkit-transform:translateX(26px);-ms-transform:translateX(26px);transform:translateX(26px)}'
    … …  
    163152        $mermaid_rel = 'js/vendor/mermaid.min.js';
    164153        $mermaid_abs = FUNNEL_ELITE_PATH . 'admin/' . $mermaid_rel;
    165         // Mit Dateiversion: Mermaid wird per JS nachgeladen und bekommt deshalb
    166         // keine ?ver von wp_enqueue_script. Ohne sie nimmt der Browser nach einem
    167         // Update die alte Datei aus dem Cache - z. B. mit der alten Kartenreihenfolge.
     154        // Lazy-loaded by JS, so it needs its own cache-busting version.
    168155        $mermaid_url = file_exists( $mermaid_abs )
    169156            ? FUNNEL_ELITE_URL . 'admin/' . $mermaid_rel . '?ver=' . rawurlencode( $this->file_version( $mermaid_abs ) )
  • funnel-elite-lite/tags/1.0.2/admin/views/tabs/design/section-summary.php

    r3706934 r3707052  
    8282                    <div class="funnel-dt-section-title"><?php echo esc_html( Funnel_System_I18n::get_admin('dt_typo_spacing_title') ); ?></div>
    8383                    <div class="funnel-dt-details-body">
    84                         <!-- 💻 DESKTOP -->
    8584                        <h4 style="margin:0 0 10px; color:#475569; font-size:12px; text-transform:uppercase; letter-spacing:0.5px;">💻 <?php echo esc_html( Funnel_System_I18n::get_admin('dt_desktop') ); ?></h4>
    8685                        <div class="funnel-dt-typo-grid">
    … …  
    129128                        </div>
    130129                        <div class="funnel-dt-typo-divider"></div>
    131                         <!-- 📱 MOBILE -->
    132130                        <?php
    133                         // Ein leeres Mobil-Feld erbt den Desktop-Wert. Damit der
    134                         // Platzhalter nicht etwas anderes verspricht, zeigt er
    135                         // genau diesen Wert an.
    136                         $dt_wert = static function ( $key, $fallback ) use ( $project_settings ) {
     131                        // Placeholder shows the desktop value an empty mobile field inherits.
     132                        $dt_value = static function ( $key, $fallback ) use ( $project_settings ) {
    137133                            return ( isset( $project_settings[ $key ] ) && '' !== $project_settings[ $key ] )
    138134                                ? $project_settings[ $key ]
    … …  
    145141                                <label for="mobile_summary_title_size"><?php echo esc_html( Funnel_System_I18n::get_admin('title_font_size_label') ); ?></label>
    146142                                <div class="funnel-dt-input-wrap">
    147                                     <input name="mobile_summary_title_size" type="number" id="mobile_summary_title_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_title_size']) ? $project_settings['mobile_summary_title_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'summary_title_size', 32 ) ); ?>" class="small-text" min="10" max="60">
     143                                    <input name="mobile_summary_title_size" type="number" id="mobile_summary_title_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_title_size']) ? $project_settings['mobile_summary_title_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_title_size', 32 ) ); ?>" class="small-text" min="10" max="60">
    148144                                    <span class="funnel-dt-unit">px</span>
    149145                                </div>
    … …  
    152148                                <label for="mobile_summary_text_size"><?php echo esc_html( Funnel_System_I18n::get_admin('text_font_size_label') ); ?></label>
    153149                                <div class="funnel-dt-input-wrap">
    154                                     <input name="mobile_summary_text_size" type="number" id="mobile_summary_text_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_text_size']) ? $project_settings['mobile_summary_text_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'summary_text_size', 16 ) ); ?>" class="small-text" min="10" max="60">
     150                                    <input name="mobile_summary_text_size" type="number" id="mobile_summary_text_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_text_size']) ? $project_settings['mobile_summary_text_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_text_size', 16 ) ); ?>" class="small-text" min="10" max="60">
    155151                                    <span class="funnel-dt-unit">px</span>
    156152                                </div>
    … …  
    159155                                <label for="mobile_summary_label_font_size"><?php echo esc_html( Funnel_System_I18n::get_admin('price_label_font_size_label') ); ?></label>
    160156                                <div class="funnel-dt-input-wrap">
    161                                     <input name="mobile_summary_label_font_size" type="number" id="mobile_summary_label_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_label_font_size']) ? $project_settings['mobile_summary_label_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'summary_label_font_size', 14 ) ); ?>" class="small-text" min="10" max="60">
     157                                    <input name="mobile_summary_label_font_size" type="number" id="mobile_summary_label_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_label_font_size']) ? $project_settings['mobile_summary_label_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_label_font_size', 14 ) ); ?>" class="small-text" min="10" max="60">
    162158                                    <span class="funnel-dt-unit">px</span>
    163159                                </div>
    … …  
    166162                                <label for="mobile_price_font_size"><?php echo esc_html( Funnel_System_I18n::get_admin('price_font_size_label') ); ?></label>
    167163                                <div class="funnel-dt-input-wrap">
    168                                     <input name="mobile_price_font_size" type="number" id="mobile_price_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_price_font_size']) ? $project_settings['mobile_price_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'price_font_size', 32 ) ); ?>" class="small-text" min="10" max="80">
     164                                    <input name="mobile_price_font_size" type="number" id="mobile_price_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_price_font_size']) ? $project_settings['mobile_price_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'price_font_size', 32 ) ); ?>" class="small-text" min="10" max="80">
    169165                                    <span class="funnel-dt-unit">px</span>
    170166                                </div>
    … …  
    173169                                <label for="mobile_summary_hint_size"><?php echo esc_html( Funnel_System_I18n::get_admin('hint_font_size_label') ); ?></label>
    174170                                <div class="funnel-dt-input-wrap">
    175                                     <input name="mobile_summary_hint_size" type="number" id="mobile_summary_hint_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_hint_size']) ? $project_settings['mobile_summary_hint_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'summary_hint_size', 14 ) ); ?>" class="small-text" min="10" max="60">
     171                                    <input name="mobile_summary_hint_size" type="number" id="mobile_summary_hint_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_hint_size']) ? $project_settings['mobile_summary_hint_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_hint_size', 14 ) ); ?>" class="small-text" min="10" max="60">
    176172                                    <span class="funnel-dt-unit">px</span>
    177173                                </div>
    … …  
    228224                </table>
    229225
    230                 <!-- ── Ergebnisseite: Weiter-Button (optional, leer = global) ── -->
     226                <!-- ── Continue Button ─────────────────────────────── -->
    231227                <div class="funnel-dt-details funnel-dt-details--static" style="margin-top:12px;">
    232228                    <div class="funnel-dt-section-title"><?php echo esc_html( Funnel_System_I18n::get_admin('dt_summary_btn_sizes_title') ); ?></div>
  • funnel-elite-lite/tags/1.0.2/includes/class-funnel-css-generator.php

    r3706934 r3707052  
    248248        $summary_hint_size  = ! empty( $settings['summary_hint_size'] )  ? $settings['summary_hint_size']  : 14;
    249249
    250         // Ergebnisseite auf dem Telefon. Ein leeres Feld erbt bewusst den
    251         // Desktop-Wert - so aendert sich an bestehenden Funnels nichts, solange
    252         // niemand ein Mobil-Feld ausfuellt. Der Platzhalter im Design-Tab zeigt
    253         // deshalb genau diesen Desktop-Wert an.
     250        // Empty mobile fields inherit the desktop value.
    254251        $mobile_summary_title_size = ! empty( $settings['mobile_summary_title_size'] ) ? $settings['mobile_summary_title_size'] : $summary_title_size;
    255252        $mobile_summary_text_size  = ! empty( $settings['mobile_summary_text_size'] )  ? $settings['mobile_summary_text_size']  : $summary_text_size;
    … …  
    270267        $back_btn_color = $settings['back_btn_color'];
    271268        $modal_bg_color = $settings['modal_bg_color'];
    272         // Als rgba ausgeben: das Video-Overlay braucht die 0.96 Deckkraft,
    273         // ein reiner Hex-Wert wuerde es undurchsichtig machen.
    274269        $modal_overlay_rgba = self::hexToRgba( $modal_bg_color, 0.96 );
    275270        $max_width      = $settings['max_content_width'];
    … …  
    365360        $card_bg_3     = self::hexToRgba($settings['card_bg_color_3'], $card_bg_intensity);
    366361       
    367         /*
    368          * Summary button overrides (Ergebnisseite).
    369          *
    370          * Every field is optional and means "inherit the global button style" when
    371          * empty, so only the properties the admin actually filled in are emitted.
    372          * An untouched funnel therefore produces no rule at all and looks exactly
    373          * as it did before these settings existed.
    374          *
    375          * The selector is #funnelbody #btn-summary-next: two IDs beat the global
    376          * "#funnelbody .btn-primary" rule, and !important is repeated because that
    377          * rule carries it too (see the card hover note — an important declaration
    378          * only loses to another important one).
    379          */
     362        // Summary button overrides: only filled fields are emitted, empty ones
     363        // inherit the global button style (which is !important, hence here too).
    380364        $summary_btn_decls = array();
    381365
    … …  
    545529            }
    546530           
    547             /* Hier standen die beiden Regeln zur Einstellung Layout Format
    548                (Views). Kein PHP und kein JS hat die zugehoerigen Klassen je an
    549                #funnelbody gehaengt, die Auswahl war also wirkungslos;
    550                ausserdem hat der Inline-Minifier die Leerzeichen um das Plus in
    551                ihrer calc()-Rechnung entfernt und sie damit ungueltig gemacht.
    552                Einstellung und Regeln sind entfernt.
    553                Fuer eine echte Vollbreite gibt es die Einstellung Vollbreite erzwingen. */
    554 
    555531            #funnelbody h1, #funnelbody h2 {
    556532                color: var(--funnel-headline);
    … …  
    590566            }
    591567
    592             /* Hover/focus background MUST repeat the !important.
    593                funnel-cards.css sets --funnel-card-hover-bg on :hover without it,
    594                and an !important declaration always wins over a normal one —
    595                regardless of specificity or order. The Hover Color setting
    596                therefore had no visible effect at all until this rule existed.
    597                Do not drop the !important here without also dropping it above. */
     568            /* Needs !important to beat the card background rule. */
    598569            .st-funnel-card .card:hover,
    599570            .st-funnel-card:focus-visible .card {
    … …  
    706677                #funnelbody .st-funnel-card img { height: {$mobile_img_height}px !important; }
    707678
    708                 /* Ergebnisseite. Die Variablen oben decken auch die Inline-Styles
    709                    ab, die der Renderer setzt; diese Regeln sind fuer alles, was
    710                    ueber das Stylesheet kommt. */
    711679                #funnelbody .summary-title { font-size: {$mobile_summary_title_size}px !important; }
    712680                #funnelbody .summary-text { font-size: {$mobile_summary_text_size}px !important; }
    … …  
    752720                margin-right: 0 !important;
    753721            }
    754             /* Offsetbasiert statt 100vw/-50vw: der alte Trick stimmt nur, wenn
    755                die Inhaltsspalte mittig im Fenster steht. In einem Layout mit
    756                Seitenleiste schob er den Funnel aus dem Bild (gemessen bei 1440px:
    757                240px links abgeschnitten). Die beiden Werte setzt das JS aus der
    758                tatsaechlichen Position; ohne JS bleibt alles in der Spalte. */
     722            /* --funnel-bleed-l/-r are measured by JS. */
    759723            body:not(.funnel-active-page) #funnelbody {
    760724                width: auto !important;
  • funnel-elite-lite/tags/1.0.2/includes/class-funnel-data.php

    r3706934 r3707052  
    55
    66    /**
    7      * Card meta keys that must never reach the frontend.
    8      *
    9      * funnelDataRaw is printed into the page source and the whole meta_data array
    10      * is merged into it, so every key in there is public by default.
    11      *
    12      * Lite has no UI for these — they can only arrive through an import of a funnel
    13      * that was built in Pro, where the export carries meta_data 1:1. The data is
    14      * kept (so a Pro -> Lite -> Pro round trip stays lossless) but never published.
     7     * Card meta keys (imported from Pro) that are kept but never printed to the page.
    158     */
    169    const ADMIN_ONLY_META_KEYS = array( 'admin_note' );
    … …  
    121114            ], $meta);
    122115
    123             // Internal Pro-only fields must not be published — see the constant.
    124116            foreach ( self::ADMIN_ONLY_META_KEYS as $admin_only_key ) {
    125117                unset( $relation_entry[ $admin_only_key ] );
  • funnel-elite-lite/tags/1.0.2/includes/class-funnel-i18n.php

    r3706934 r3707052  
    4141
    4242        /**
    43          * Cache key for the string tables.
    44          *
    45          * Keyed by locale so switch_to_locale() still returns the right table.
    46          * Calls that happen before `init` can still be untranslated (WordPress loads
    47          * plugin translations just-in-time), so those get their own slot and never
    48          * poison the translated table.
     43         * Cache key per locale; calls before `init` get their own untranslated slot.
    4944         */
    5045        private static function string_cache_key() {
    … …  
    6459        /**
    6560         * Get strings for the Admin Backend (built once per locale and request).
    66          *
    67          * PERFORMANCE: the table below holds ~515 __() lookups. It used to be rebuilt
    68          * on every single get_admin() call — with ~530 call sites in the admin views
    69          * that added up to ~275,000 gettext lookups per admin page render (measured:
    70          * 107ms of pure PHP, 0.2ms with this cache). Never drop this cache.
    7161         */
    7262        public static function get_admin_strings() {
    … …  
    232222
    233223            'card_icon_label' => __( 'Icon (Lucide)', 'funnel-elite-lite' ),
    234                                 // UI Labels aus Design-Vorlage
     224                                // UI labels from the design template
    235225                                'icon_size_label' => __( 'Icon Size', 'funnel-elite-lite' ),
    236226                        'icon_color_label' => __( 'Icon Color', 'funnel-elite-lite' ),
    … …  
    238228                       
    239229                        'card_price_label' => __( 'Card Price', 'funnel-elite-lite' ),
    240                         // Platzhalter im Preisfeld - je Sprache mit deren Dezimalzeichen uebersetzen.
     230                        // translators: example price, use the decimal separator of your language.
    241231                        'card_price_placeholder' => __( 'e.g. 1200.00', 'funnel-elite-lite' ),
    242232                        'card_price_help_text' => __( 'Operator + price set how this card changes the running total: + adds and − subtracts the price; × and ÷ multiply or divide the total (e.g. ×1.2 = +20%).', 'funnel-elite-lite' ),
    … …  
    584574                        'sort_order'         => __( 'Sort Order', 'funnel-elite-lite' ),
    585575                        'video_url_optional' => __( 'Video URL (Optional)', 'funnel-elite-lite' ),
    586                         // Knopf im Seiten-Modal - vorher stand dort "Save Card". Muss zum Titel
    587                         // edit_step ("Edit Page") passen.
    588576                        'save_step'          => __( 'Save Page', 'funnel-elite-lite' ),
    589577                        'edit_card'          => __( 'Edit Card', 'funnel-elite-lite' ),
    … …  
    675663                        'dt_price_box_content_title' => __( 'Price Box Content', 'funnel-elite-lite' ),
    676664                        'dt_summary_colors_title'    => __( 'Summary Colors', 'funnel-elite-lite' ),
    677                         // Ergebnisseite: Weiter-Button (optionale Overrides)
    678665                        'dt_summary_btn_colors_title' => __( 'Continue Button (Result Page)', 'funnel-elite-lite' ),
    679666                        'dt_summary_btn_sizes_title'  => __( 'Continue Button Dimensions (Result Page)', 'funnel-elite-lite' ),
    … …  
    693680        /**
    694681         * Get strings for the Public Frontend (built once per locale and request).
    695          *
    696          * Same caching rationale as get_admin_strings().
    697682         */
    698683        public static function get_public_strings() {
  • funnel-elite-lite/tags/1.0.2/includes/class-funnel-main.php

    r3706934 r3707052  
    6060        $this->loader->add_action( 'wp_ajax_funnel_partial_lead', $plugin_public, 'handle_partial_lead' );
    6161        $this->loader->add_action( 'wp_ajax_nopriv_funnel_partial_lead', $plugin_public, 'handle_partial_lead' );
    62         // Frische Sicherheitsmarke, wenn ein Seiten-Cache eine abgelaufene ausliefert.
    6362        $this->loader->add_action( 'wp_ajax_funnel_fresh_nonce', $plugin_public, 'handle_fresh_nonce' );
    6463        $this->loader->add_action( 'wp_ajax_nopriv_funnel_fresh_nonce', $plugin_public, 'handle_fresh_nonce' );
  • funnel-elite-lite/tags/1.0.2/includes/class-funnel-settings-registry.php

    r3706934 r3707052  
    204204
    205205    /**
    206      * Sanitize an optional pixel value for the summary button overrides.
    207      *
    208      * These fields mean *inherit the global button style* when left empty, so the
    209      * empty string has to survive sanitizing. intval('') would be 0 — and 0 is a
    210      * legitimate value here (sharp corners, no border, no padding), so the two
    211      * cases must stay distinguishable.
     206     * Optional pixel value: '' means inherit, 0 is a valid value.
    212207     *
    213208     * @param mixed $value Raw value.
    … …  
    228223
    229224    /**
    230      * Sanitize an optional colour for the summary button overrides.
    231      *
    232      * Same idea as sanitize_optional_px(): empty means inherit.
     225     * Optional colour: '' means inherit.
    233226     *
    234227     * @param mixed $value Raw value.
    … …  
    325318            [ 'id' => 'card_bg_intensity',  'sanitize' => 'intval',              'default' => 100 ],
    326319            [ 'id' => 'card_text_color',    'sanitize' => 'sanitize_hex_color',  'default' => '#212529', 'css_var' => '--funnel-card-text' ],
    327             // #007bff liess die Kartenbeschreibung beim Ueberfahren und bei
    328             // Tastatur-Auswahl auf 3.10:1 fallen; #e9ecef bringt sie auf 7.2:1.
    329320            [ 'id' => 'card_hover_color',   'sanitize' => 'sanitize_hex_color',  'default' => '#e9ecef', 'css_var' => '--funnel-card-hover-bg' ],
    330321            [ 'id' => 'card_width',         'sanitize' => 'intval',  'default' => 300,   'unit' => 'px' ],
    … …  
    400391
    401392            // ── Buttons ──────────────────────────────────────────
    402             // Weiss auf dem Standard-Sandton #c3b696 sind 2.0:1 - unlesbar.
    403             // #1d2327 (WordPress-Schwarz) ergibt 7.9:1.
    404393            [ 'id' => 'btn_text_color',   'sanitize' => 'sanitize_hex_color', 'default' => '#1d2327', 'css_var' => '--funnel-btn-text' ],
    405394            [ 'id' => 'btn_border_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#c3b696', 'css_var' => '--funnel-btn-border-color' ],
    … …  
    423412
    424413            // ── Summary & Price ──────────────────────────────────
    425             // Die fuenf mobile_*-Schriftgroessen der Ergebnisseite sind optional:
    426             // leer bedeutet, dass der Desktop-Wert auch auf dem Telefon gilt.
    427             // Ein fester Standard wuerde bestehende Funnels beim Update
    428             // ungefragt umstellen.
     414            // mobile_* sizes are optional: empty = desktop value.
    429415            [ 'id' => 'price_font_size',        'sanitize' => 'intval',              'default' => 32,      'css_var' => '--funnel-price-font-size', 'unit' => 'px' ],
    430416            [ 'id' => 'mobile_price_font_size', 'sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ],
    … …  
    433419            [ 'id' => 'summary_label_font_size','sanitize' => 'intval',              'default' => 14,      'css_var' => '--funnel-label-font-size', 'unit' => 'px' ],
    434420            [ 'id' => 'mobile_summary_label_font_size','sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ],
    435             // #8c857b plus der Deckkraft 0.8 ergaben 2.68:1 auf Weiss.
    436421            [ 'id' => 'summary_label_color',    'sanitize' => 'sanitize_hex_color',  'default' => '#6b645a','css_var' => '--funnel-label-color' ],
    437422            [ 'id' => 'summary_label_text',     'sanitize' => 'sanitize_text_field', 'default' => '' ],
    … …  
    451436            [ 'id' => 'summary_hint_size',      'sanitize' => 'intval', 'default' => 14, 'css_var' => '--funnel-summary-hint-size',  'unit' => 'px' ],
    452437            [ 'id' => 'mobile_summary_hint_size','sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ],
    453             // ── Ergebnisseite: Weiter-Button (alle optional, leer = global erben) ──
    454             // Die globalen Button-Einstellungen gelten weiter, solange hier nichts
    455             // gesetzt ist. Der CSS-Generator gibt nur die Eigenschaften aus, die
    456             // wirklich gefüllt sind — bestehende Funnels sehen unverändert aus.
     438            // ── Summary Button (optional, empty = global button style) ──
    457439            [ 'id' => 'summary_btn_bg',           'sanitize' => array( __CLASS__, 'sanitize_optional_hex_color' ), 'default' => '' ],
    458440            [ 'id' => 'summary_btn_text_color',   'sanitize' => array( __CLASS__, 'sanitize_optional_hex_color' ), 'default' => '' ],
  • funnel-elite-lite/tags/1.0.2/includes/emails/trait-email-formatter.php

    r3706934 r3707052  
    55
    66    /**
    7      * Text einer Mail-Vorlage in der Sprache der Website.
     7     * Template text in the site language, English fallback.
    88     *
    9      * Die Vorlage hatte ihre Ueberschrift fest auf Englisch im Code. Den
    10      * passenden Schluessel gibt es laengst und er ist in allen mitgelieferten
    11      * Sprachen uebersetzt - er wurde hier nur nie benutzt.
    12      *
    13      * Faellt auf den englischen Text zurueck, falls die i18n-Klasse (noch) nicht
    14      * geladen ist: eine Mail ohne Ueberschrift waere schlimmer als eine
    15      * englische.
    16      *
    17      * @param  string $key       Schluessel aus der oeffentlichen i18n-Tabelle.
    18      * @param  string $fallback  Englischer Text, falls nichts gefunden wird.
     9     * @param  string $key      Key of the public i18n table.
     10     * @param  string $fallback Text used when the key is not available.
    1911     * @return string
    2012     */
  • funnel-elite-lite/tags/1.0.2/includes/emails/trait-email-preview.php

    r3706934 r3707052  
    1313     */
    1414    public static function get_default_admin_template( $settings = array() ) {
    15         // No <html>/<body>: wp_kses_post() removes both when the template is
    16         // saved - together with the font and background set on them. They sit
    17         // on the outer table instead, so a saved template looks like the editor.
    18         // No fixed 600px container: phones shrink a fixed width, text included,
    19         // to about two thirds. A div with max-width grows with the screen
    20         // (Chrome and iOS Mail ignore max-width on a table).
     15        // No <html>/<body>: wp_kses_post() strips them on save. max-width sits on
     16        // a div because some mail clients ignore it on tables.
    2117        $t  = '<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7f9; padding: 32px 0; font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2d3748;">' . "\n";
    2218        $t .= '<tr><td align="center" style="padding: 0 12px;">' . "\n";
  • funnel-elite-lite/tags/1.0.2/includes/emails/trait-email-template.php

    r3706934 r3707052  
    7575        $message .= '<table width="100%" cellpadding="0" cellspacing="0" style="background-color: ' . $bg_color . '; padding: 40px 0;"><tr><td align="center" style="padding: 0 12px;">';
    7676
    77         // Main Container. Grows with the screen instead of a fixed 600px:
    78         // phones shrink a fixed width, text included, to about two thirds, and
    79         // Chrome and iOS Mail ignore max-width on a table - hence the div.
     77        // Main Container (max-width on a div, some mail clients ignore it on tables)
    8078        $message .= '<div style="max-width: 600px; margin: 0 auto;"><table width="100%" cellpadding="0" cellspacing="0" style="width: 100%; background-color: ' . $container_bg . '; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); overflow: hidden;">';
    8179
    … …  
    153151
    154152        // Header
    155         //
    156         // This is the mail the RESULT PAGE sends whenever the template editor is
    157         // empty - and it never had a logo, not even a place for one, although
    158         // the admin template does. Same markup as build_default_template(), so
    159         // both look alike at the top.
    160153        $message .= '<tr><td style="padding: 30px 40px; text-align: center; border-bottom: 3px solid ' . $primary_color . ';">';
    161154        if ( $logo_url ) {
  • funnel-elite-lite/tags/1.0.2/public/class-funnel-public.php

    r3706934 r3707052  
    111111    }
    112112
    113 
    114     /**
    115      * Soll auf dieser Seite ueberhaupt etwas vom Funnel geladen werden?
    116      *
    117      * Ohne is_singular() greift die Pruefung auch auf Such- und Archivseiten:
    118      * dort zeigt das globale $post auf den ersten Treffer. Steht darin ein
    119      * Funnel, luden alle Stylesheets und das komplette JS mit, obwohl die
    120      * Auszuege den Shortcode entfernen und gar kein #funnelbody entsteht.
    121      *
    122      * Wer den Funnel bewusst ausserhalb einer Einzelansicht rendert (Page
    123      * Builder, Widget, Archiv mit vollem Inhalt), schaltet die Dateien wie
    124      * bisher ueber den Filter frei:
    125      *
    126      *   add_filter( 'funnel_system_load_assets', '__return_true' );
     113    /**
     114     * Load assets only on singular views that contain the shortcode.
    127115     *
    128116     * @return bool
    … …  
    250238        wp_enqueue_script( $this->plugin_name . '-main', $main['url'], array( 'jquery', $this->plugin_name . '-engine' ), $main['version'], true );
    251239
    252         // Drei der angebotenen Knopf-Effekte brauchen JS: "Scroll Trigger"
    253         // setzt .is-inview, "Click Feedback" setzt .is-pressed, "Ripple Click"
    254         // legt das .play-ripple-Element an. In Lite fehlte diese Datei, die
    255         // Effekte standen also im Admin zur Wahl und taten nichts - "Scroll
    256         // Trigger" liess den Knopf sogar dauerhaft blass.
    257240        $effect_settings = $project_id ? $this->get_project_settings( $project_id ) : null;
    258241        if ( $effect_settings ) {
    … …  
    287270                wp_add_inline_script(
    288271                    $this->plugin_name . '-model',
    289                     // Nur wenn der Funnel wirklich gerendert wurde: auf Such-
    290                     // und Archivseiten strippen Auszuege den Shortcode, die
    291                     // Klasse haette dort das Scrollen der ganzen Seite gesperrt.
     272                    // Excerpts strip the shortcode, so check that the funnel was rendered.
    292273                    'if(document.getElementById("funnelbody")){document.documentElement.classList.add("funnel-active-page");document.body.classList.add("funnel-active-page");}',
    293274                    'before'
    … …  
    405386        if ($bg_type === 'image') $layout_class = 'has-bg-image';
    406387        if ($bg_type === 'pattern') $layout_class = 'has-bg-pattern';
    407         // "Vollbreite erzwingen": Marker fuer das JS, das die beiden Abstaende
    408         // zum Fensterrand misst (--funnel-bleed-l / -r). Ohne JS bleibt der
    409         // Funnel einfach in seiner Spalte.
    410388        if ( ! empty( $settings['enable_fullscreen_override'] ) ) {
    411389            $layout_class = trim( $layout_class . ' funnel-bleed' );
    … …  
    644622
    645623    /**
    646      * Returns a fresh public nonce for the funnel's own AJAX request.
    647      *
    648      * The nonce (funnel_public_nonce) is printed into the page. A page cache can
    649      * serve a copy that is older than the nonce lifetime (12-24 hours); then
    650      * handle_partial_lead() rejects the request with 403 and the selection
    651      * e-mail is silently lost. After such a rejection the frontend calls this
    652      * endpoint once and repeats the request with the new nonce.
    653      *
    654      * Intentionally no nonce check here: a visitor holding an expired nonce must
    655      * be able to obtain a new one. The endpoint reads no input, writes nothing
    656      * and returns exactly what every page load already prints; it sends
    657      * no-cache headers. admin-ajax only lets the site's own origins read the
    658      * response (get_allowed_http_origins()). The nonce it returns only unlocks
    659      * funnel_partial_lead, which verifies it and is open to visitors anyway.
     624     * Returns a fresh public nonce, so a cached page with an expired one can retry.
     625     *
     626     * No nonce check by design: it reads no input, writes nothing and returns
     627     * what every page load prints anyway.
    660628     *
    661629     * @return void
    … …  
    742710        }
    743711
    744         // The logo has to travel in the meta: build_partial_lead_email() only
    745         // sees what it is handed, and without this the result mail never had
    746         // one - neither the logo nor anywhere to put it.
    747712        $email_meta = array(
    748713            'subject'    => $subject,
    … …  
    760725        }
    761726
    762         // Sender name from the Email tab - resolved before SMTP is configured,
    763         // because configure_phpmailer() sets it last and would otherwise win.
    764727        $from_name = self::resolve_from_name( $project_settings );
    765728
    … …  
    877840
    878841    /**
    879      * The sender name as the admin typed it into "Sender Name".
    880      *
    881      * The result mail used to send as get_bloginfo('name') and never read this
    882      * field. Only the "Send test email" button used it - so the test mail
    883      * showed the configured name and the real mails did not, which is exactly
    884      * why it went unnoticed.
    885      *
    886      * Order follows the rest of the mail code (see the logo in
    887      * prepare_placeholder_data): project value, then the global option the Email
    888      * tab writes, then the site name.
     842     * Sender name: project setting, then the global option, then the site name.
    889843     *
    890844     * @param  array $project_settings Decoded project settings.
    … …  
    900854        }
    901855
    902         // The site name is stored HTML-escaped ("A &amp; B"); a mail header
    903         // wants the plain text. Line breaks would allow header injection, and
    904         // angle brackets break wp_mail's "Name <address>" parsing.
     856        // Strip line breaks (header injection) and angle brackets ("Name <address>").
    905857        $name = wp_specialchars_decode( $name, ENT_QUOTES );
    906858
    … …  
    909861
    910862    /**
    911      * The brand logo from the Email tab, same order as resolve_from_name().
     863     * Brand logo: project setting, then the global option.
    912864     *
    913865     * @param  array $project_settings Decoded project settings.
    … …  
    925877     * Apply the plugin's custom SMTP configuration to PHPMailer (if set).
    926878     *
    927      * @param string $from_name Display name for the From header. With SMTP
    928      *                          authentication the address has to be the SMTP
    929      *                          user, but the NAME is free - and this is the
    930      *                          last place it is set before sending, so a
    931      *                          correct header alone would be overwritten here.
     879     * @param string $from_name Display name for the From header.
    932880     */
    933881    private function configure_phpmailer( $from_name = '' ) {
    … …  
    971919                $phpmailer->Username = $user;
    972920                $phpmailer->Password = $pass;
    973                 // The ADDRESS has to be the SMTP user (servers reject a From
    974                 // they do not own). The NAME stays the configured one - forcing
    975                 // it to the site name here silently undid the From header.
    976921                $phpmailer->From     = $user;
    977922                $phpmailer->FromName = $from_name;
  • funnel-elite-lite/tags/1.0.2/public/css/funnel-button-attention.css

    r3706934 r3707052  
    6969        content: "";
    7070        position: absolute;
    71         /* Langform statt `inset`: aeltere mobile Browser (Samsung Internet < 14,
    72            Chrome < 87, iOS < 14.5) kennen die Kurzform nicht und verwerfen die
    73            Deklaration komplett - der Ring faellt dann auf wenige Pixel zusammen. */
     71        /* Longhand instead of inset for older mobile browsers. */
    7472        top: -6px;
    7573        right: -6px;
    … …  
    187185                transition: none !important;
    188186                transform: none !important;
    189                 /* Kein `opacity: 1 !important` mehr: es hat auch den ausgegrauten
    190                    "Weiter"-Knopf wieder voll sichtbar gemacht, obwohl er nicht
    191                    klickbar ist - und liess den Ring am Video-Knopf dauerhaft stehen. */
    192187                box-shadow: none !important;
    193188        }
  • funnel-elite-lite/tags/1.0.2/public/css/funnel-button-effects.css

    r3706934 r3707052  
    103103
    104104/* ---------- Play Button Effects ---------- */
    105 /* `:active` ist hier kein Beiwerk: Ein Telefon kennt kein Hover, also waeren
    106    diese vier Effekte auf jedem Mobilgeraet wirkungslos - genau das war die
    107    Beobachtung "ich merke da nichts". Mit `:active` reagieren sie auf die
    108    Beruehrung, solange der Finger liegt. Am Rechner aendert sich nichts: wer
    109    klickt, fuhr vorher schon mit dem Zeiger darueber. */
     105/* :active makes these effects work on touch screens. */
    110106.funnel-container .play-effect--soft-pulse:hover,
    111107.funnel-container .play-effect--soft-pulse:focus-visible,
    … …  
    117113  content: "";
    118114  position: absolute;
    119   /* Langform statt `inset` - siehe funnel-button-attention.css. */
     115  /* Longhand instead of inset for older mobile browsers. */
    120116  top: -4px;
    121117  right: -4px;
    … …  
    189185}
    190186
    191 /* Mobil/Desktop (17.09.2026): Der Ring des Effekts "Glow Ring" lag ausserhalb
    192    des Knopfes und wurde von dessen overflow: hidden abgeschnitten - der Effekt
    193    war unsichtbar. !important, weil funnel-button-attention.css spaeter geladen
    194    wird und dieselbe Spezifitaet hat. */
     187/* The ring sits outside the button. !important: funnel-button-attention.css loads later. */
    195188.funnel-container .play-btn.play-effect--glow-ring {
    196189  overflow: visible !important;
  • funnel-elite-lite/tags/1.0.2/public/css/funnel-cards.css

    r3706934 r3707052  
    4242  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
    4343  border-color: rgba(0, 0, 0, 0.1);
    44   /* Eigener Rahmen statt nur der Hover-Farbe: bei Verlauf-, Glanz- oder
    45      Mesh-Hintergruenden deckt das Hintergrundbild die Hover-Farbe zu, dann
    46      war die Tastatur-Auswahl nur noch am 8px-Anheben zu erkennen. */
     44  /* Background images can cover the hover colour, so add an outline. */
    4745  outline: 2px solid var(--funnel-headline, #1d2327);
    4846  outline-offset: 3px;
    … …  
    111109  font-family: var(--funnel-font-family, sans-serif);
    112110  font-size: var(--funnel-card-title-size, 1.4rem);
    113   /* War fest auf #2c2825. Auf dunklen Karten (Karte #1d2327, Text weiss)
    114      blieb der Titel fast schwarz - gemessen 1.09:1. */
    115111  color: var(--funnel-card-text, #2c2825);
    116112  margin: 0;
    … …  
    118114  word-wrap: break-word;
    119115  overflow-wrap: break-word;
    120   /* Keine Silbentrennung: `hyphens: auto` zerlegte Antworten mitten im Wort
    121      ("Fotoshooting & Brautabho-lung & Saalbegleitung"). Ein Umbruch ZWISCHEN
    122      Woertern ist in Ordnung, einer INNERHALB nicht.
    123      `manual` statt weglassen, weil hyphens vererbt wird - ein Theme mit
    124      `hyphens: auto` auf body wuerde es sonst wieder einschleppen.
    125      `overflow-wrap: break-word` bleibt als Notnagel: es greift nur, wenn ein
    126      einzelnes Wort allein nicht auf eine Zeile passt. */
     116  /* manual, not unset: hyphens is inherited and themes may set auto. */
    127117  hyphens: manual;
    128118  -webkit-hyphens: manual;
    … …  
    142132  word-wrap: break-word;
    143133  overflow-wrap: break-word;
    144   /* Gleiche Regel wie beim Titel - siehe dort. */
    145134  hyphens: manual;
    146135  -webkit-hyphens: manual;
    … …  
    341330
    342331/* ---- WEITER / SUMMARY BUTTON: globale Layout- & Stil-Basis ---- */
    343 /* Stil (Farbe, Padding, Font) wird über .btn-primary im CSS-Generator aus den Admin-Einstellungen gesetzt */
     332/* Colour, padding and font come from .btn-primary in the CSS generator */
    344333.funnel-container .funnel-card-next {
    345334  display: block;
    … …  
    372361}
    373362
    374 /* Der Haken schiebt das Wort aus der Mitte: die Gruppe aus Haken und Wort wird
    375    zentriert, also sitzt das Wort um die halbe Hakenbreite plus den halben
    376    Abstand daneben - gemessen 10,9 px bei 320 px Knopfbreite. Auffallen tut aber
    377    das Wort, nicht die Gruppe.
    378    Deshalb steht auf der Gegenseite ein unsichtbarer Zwilling desselben
    379    Zeichens. Er ist exakt so breit wie der Haken - auch wenn eine andere
    380    Schrift einspringt, was bei U+2713 in einer Serifenschrift die Regel ist -
    381    und traegt denselben `gap`. Damit liegt das Wort auf der Knopfmitte.
    382    `visibility: hidden` statt `display: none`: der Platz muss erhalten bleiben. */
     363/* Invisible twin of the tick keeps the label centred. */
    383364#btn-summary-next .summary-next-balance {
    384365  visibility: hidden;
    … …  
    435416}
    436417
    437 /* ── Mobil (17.09.2026) ────────────────────────────────────────────────────
    438    Streifen-Layout: Auf schmalen Telefonen ragten Preis-Abzeichen und Bild aus
    439    der Karte heraus, sobald ein Titel ein langes Wort hatte (gemessen bei 320
    440    und 375px: Abzeichen endete 37px hinter der Karte, Bild links angeschnitten).
    441    Die flexiblen Spalten geben ohne min-width: 0 nicht nach. Unter 480px wird
    442    das Bild kleiner, sonst bleibt fuer den Titel nur ein schmaler Streifen. */
     418/* Stripe layout: flex columns need min-width: 0 to shrink. */
    443419#funnelbody .funnel-layout-stripe .card-body,
    444420#funnelbody .funnel-layout-stripe .card-text-wrapper {
    … …  
    463439  }
    464440
    465   /* Das Abzeichen nimmt sonst die halbe Kartenbreite, und der Titel bricht
    466      mitten im Wort. Groesser als 13px wird es hier nicht, kleiner eingestellte
    467      Werte bleiben erhalten. */
    468441  #funnelbody .funnel-layout-stripe .card-price-badge,
    469442  #funnelbody .funnel-layout-stripe .card-adjust-badge,
    … …  
    474447}
    475448
    476 /* Wer im Betriebssystem "Bewegung reduzieren" eingestellt hat, bekam trotzdem
    477    das volle Programm: 8px-Anheben bei Hover und Tastatur-Auswahl, dazu die
    478    0.4s-Ueberblendung auf jeder Karte. */
    479449@media (prefers-reduced-motion: reduce) {
    480450  .st-funnel-card .card {
  • funnel-elite-lite/tags/1.0.2/public/css/funnel-core.css

    r3706934 r3707052  
    33
    44/* GLOBAL PAGE OVERRIDE - FULLSCREEN MODE
    5    Die Hoehe kommt aus top/bottom, NICHT aus 100vh: iOS Safari rechnet vh mit
    6    eingeklappten Leisten. Der Kasten ragte dadurch 80-135px unter die untere
    7    Safari-Leiste, und weil html/body gesperrt sind, klappt Safari sie nie ein -
    8    das Ende der Liste war nie erreichbar (iPhone 13, 17.09.2026). Mit
    9    bottom: 0 sitzt die Unterkante immer auf der sichtbaren Kante.
    10    overscroll-behavior-y haelt die Wischgeste im Funnel, statt sie an die
    11    gesperrte Seite zu geben (nur y: x wuerde das Zurueckwischen in Chrome
    12    abschalten). */
     5   Height from top/bottom, not 100vh: iOS Safari measures vh without its bars. */
    136body.funnel-active-page #funnelbody {
    147  position: fixed !important;
    … …  
    9891}
    9992
    100 /* Gescopt: .active-step ist ein gaengiger Klassenname in Steppern und
    101    Kassen-Strecken; mit !important hob die Regel fremde Elemente an. */
    10293#funnelbody .active-step {
    10394  position: relative !important;
    … …  
    142133  gap: 8px;
    143134  background: transparent !important;
    144   /* Liest jetzt die Einstellung "Zurueck-Knopf-Farbe"; 0.85 statt 0.7 hebt
    145      den Kontrast auf weissem Grund von 3.57:1 auf 5.11:1 (AA). */
    146135  color: var(--funnel-back-btn-color, #4a5568) !important;
    147136  border: none !important;
    … …  
    159148
    160149.funnel-back-btn:hover {
    161   /* Die feste Hover-Farbe ist raus: sie hat jede eingestellte Farbe beim
    162      Ueberfahren wieder verworfen. Die volle Deckkraft reicht als Rueckmeldung. */
    163150  opacity: 1;
    164151  transform: translateX(-3px);
    … …  
    175162  body.funnel-active-page #funnelbody {
    176163    font-size: var(--funnel-mobile-font-size, 14px);
    177     /* Im Vollbild steht ueber der Ueberschrift nichts mehr - die 60px vom
    178        Desktop sind auf einem Telefon verschenkte Hoehe, also 15px weniger.
    179        !important, weil die Grundregel padding als Kurzform mit !important
    180        setzt und sonst gewinnt. */
    181164    padding-top: 45px !important;
    182165    padding-bottom: 60px !important;
    183     /* Zweite Angabe fuer Geraete mit Hausbalken/Notch (nur mit
    184        viewport-fit=cover im Theme wirksam, sonst 0). */
    185166    padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px)) !important;
    186167  }
    … …  
    276257}
    277258
    278 /* ── Mobil (17.09.2026) ────────────────────────────────────────────────────
    279    Ueberschriften: lange deutsche Woerter liefen aus dem Bild und wurden
    280    abgeschnitten (gemessen bei 320px). Umgebrochen wird nur, was sonst nicht
    281    passt; hyphens: manual haelt die bewusste Entscheidung gegen automatische
    282    Silbentrennung und ueberstimmt ein Theme mit hyphens: auto. */
    283259#funnelbody h1,
    284260#funnelbody h2 {
    … …  
    288264}
    289265
    290 /* Zurueck-Knopf: nur 15-18px hoch, im Querformat 12px - zu klein zum
    291    Antippen. 44px ist die gaengige Mindestgroesse fuer Finger. Die zweite
    292    font-size gilt nur, wo max() bekannt ist; aeltere Engines behalten die
    293    erste Zeile, statt die Groesse an das Theme zu verlieren. */
     266/* 44px touch target. The first font-size is the fallback without max(). */
    294267.funnel-back-btn {
    295268  min-height: 44px;
    … …  
    300273}
    301274
    302 /* ── Drucken (17.09.2026) ─────────────────────────────────────────────────
    303    Im Vollbild blieb der Funnel beim Drucken ein fester, scrollender Kasten:
    304    das Blatt zeigte nur den ersten Bildschirm, der Rest des Schritts oder der
    305    Ergebnisseite fehlte. Beim Druck wird der Funnel wieder normaler Inhalt. */
    306275@media print {
    307276  html.funnel-active-page,
    … …  
    326295}
    327296
    328 /* Eigener Stapelkontext im Section-Modus: die Schritte (z-index: 100) bleiben
    329    im Funnel, statt sich mit festen Kopfzeilen des Themes zu messen. */
     297/* Keeps the steps' z-index inside the funnel. */
    330298body:not(.funnel-active-page) #funnelbody {
    331299  isolation: isolate;
    332300}
    333301
    334 /* Ladeanzeige der Ergebnisseite bei "Bewegung reduzieren" anhalten. Die
    335    Effekte stehen inline am Element, deshalb braucht es hier !important.
    336    Der Pulsring wuchs sonst dauerhaft auf das 2,1-fache. */
     302/* Loader effects are inline styles, hence !important. */
    337303@media (prefers-reduced-motion: reduce) {
    338304  .funnel-summary-loader,
    … …  
    346312}
    347313
    348 /* Sprachen von rechts nach links: der Pfeil des Zurueck-Knopfes muss in die
    349    andere Richtung zeigen und sich auch dorthin bewegen. */
    350314[dir="rtl"] .funnel-back-btn::before {
    351   content: "92";
     315  content: "\2192";
    352316}
    353317
  • funnel-elite-lite/tags/1.0.2/public/css/funnel-core.min.css

    r3706934 r3707052  
    22    --funnel-font-family,
    33    sans-serif
    4   )!important;font-size:2.8em;font-weight:400;margin-bottom:1.5rem;color:var(--funnel-headline,#2c2825);line-height:1.2}#funnelbody .funnel-description,.funnel-description{font-family:var(--funnel-font-family, sans-serif)!important;font-size:var(--funnel-group-desc-size, 1.1rem);color:var(--funnel-text,#6d665e);font-weight:300;max-width:600px;margin:0 auto 30px}.funnel-back-btn{display:inline-flex!important;align-items:center;gap:8px;background:0 0!important;color:var(--funnel-back-btn-color,#4a5568)!important;border:none!important;font-family:var(--funnel-font-family, sans-serif);font-weight:400;text-transform:uppercase;letter-spacing:1.5px;cursor:pointer;transition:opacity .3s,color .3s,transform .3s;opacity:.85}.funnel-back-btn:hover{opacity:1;transform:translateX(-3px)}.funnel-back-btn::before{content:"\2190";font-size:1.1rem;line-height:1}#funnel-progress-wrapper{width:100%;max-width:var(--funnel-progress-width,600px);margin:0 auto;position:relative;height:var(--funnel-progress-size,6px);background:rgba(0,0,0,.05);border-radius:calc(var(--funnel-progress-size,6px)/ 2);overflow:hidden;z-index:10}#funnel-progress-fill{height:100%;background-color:var(--funnel-progress-color,#0ba360);transition:width .5s cubic-bezier(.22, 1, .36, 1);border-radius:calc(var(--funnel-progress-size,6px)/ 2)}.funnel-pb-shape-segmented{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05),rgba(0,0,0,.05) 10px,transparent 10px,transparent 15px)!important;border-radius:0!important}.funnel-pb-shape-segmented #funnel-progress-fill{background-image:repeating-linear-gradient(90deg,transparent,transparent 10px,rgba(255,255,255,.3) 10px,rgba(255,255,255,.3) 15px)!important;border-radius:0!important}.funnel-pb-shape-dots{background:rgba(0,0,0,.1)!important;border-radius:0!important;mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));-webkit-mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%;-webkit-mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%}.funnel-pb-shape-dots #funnel-progress-fill{border-radius:0!important}@media (max-width:768px){body.funnel-active-page #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-top:45px!important;padding-bottom:60px!important;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))!important}body:not(.funnel-active-page) #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-bottom:60px!important}.st-funnel-step h2{font-size:2.2em}.funnel-description{font-size:var(--funnel-mobile-group-desc-size, 14px)}#funnel-progress-wrapper{margin:0 auto 20px;width:calc(100% - 30px)}}#funnelbody h1,#funnelbody h2{overflow-wrap:break-word;hyphens:manual;-webkit-hyphens:manual}.funnel-back-btn{min-height:44px;padding:0 12px 0 0!important;margin-bottom:8px;font-size:.85rem!important;font-size:max(13px, .85rem)!important}@media print{body.funnel-active-page,html.funnel-active-page{overflow:visible!important;height:auto!important}body.funnel-active-page #funnelbody,body:not(.funnel-active-page) #funnelbody{position:static!important;height:auto!important;max-height:none!important;overflow:visible!important}.funnel-loader,.funnel-notice-overlay,.funnel-video-modal{display:none!important}}@media (prefers-reduced-motion:reduce){.funnel-loader .spinner,.funnel-summary-loader,.funnel-summary-loader *{animation:none!important}}[dir=rtl] .funnel-back-btn::before{content:"92"}[dir=rtl] .funnel-back-btn:hover{transform:translateX(3px)}
     4  )!important;font-size:2.8em;font-weight:400;margin-bottom:1.5rem;color:var(--funnel-headline,#2c2825);line-height:1.2}#funnelbody .funnel-description,.funnel-description{font-family:var(--funnel-font-family, sans-serif)!important;font-size:var(--funnel-group-desc-size, 1.1rem);color:var(--funnel-text,#6d665e);font-weight:300;max-width:600px;margin:0 auto 30px}.funnel-back-btn{display:inline-flex!important;align-items:center;gap:8px;background:0 0!important;color:var(--funnel-back-btn-color,#4a5568)!important;border:none!important;font-family:var(--funnel-font-family, sans-serif);font-weight:400;text-transform:uppercase;letter-spacing:1.5px;cursor:pointer;transition:opacity .3s,color .3s,transform .3s;opacity:.85}.funnel-back-btn:hover{opacity:1;transform:translateX(-3px)}.funnel-back-btn::before{content:"\2190";font-size:1.1rem;line-height:1}#funnel-progress-wrapper{width:100%;max-width:var(--funnel-progress-width,600px);margin:0 auto;position:relative;height:var(--funnel-progress-size,6px);background:rgba(0,0,0,.05);border-radius:calc(var(--funnel-progress-size,6px)/ 2);overflow:hidden;z-index:10}#funnel-progress-fill{height:100%;background-color:var(--funnel-progress-color,#0ba360);transition:width .5s cubic-bezier(.22, 1, .36, 1);border-radius:calc(var(--funnel-progress-size,6px)/ 2)}.funnel-pb-shape-segmented{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05),rgba(0,0,0,.05) 10px,transparent 10px,transparent 15px)!important;border-radius:0!important}.funnel-pb-shape-segmented #funnel-progress-fill{background-image:repeating-linear-gradient(90deg,transparent,transparent 10px,rgba(255,255,255,.3) 10px,rgba(255,255,255,.3) 15px)!important;border-radius:0!important}.funnel-pb-shape-dots{background:rgba(0,0,0,.1)!important;border-radius:0!important;mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));-webkit-mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%;-webkit-mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%}.funnel-pb-shape-dots #funnel-progress-fill{border-radius:0!important}@media (max-width:768px){body.funnel-active-page #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-top:45px!important;padding-bottom:60px!important;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))!important}body:not(.funnel-active-page) #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-bottom:60px!important}.st-funnel-step h2{font-size:2.2em}.funnel-description{font-size:var(--funnel-mobile-group-desc-size, 14px)}#funnel-progress-wrapper{margin:0 auto 20px;width:calc(100% - 30px)}}#funnelbody h1,#funnelbody h2{overflow-wrap:break-word;hyphens:manual;-webkit-hyphens:manual}.funnel-back-btn{min-height:44px;padding:0 12px 0 0!important;margin-bottom:8px;font-size:.85rem!important;font-size:max(13px, .85rem)!important}@media print{body.funnel-active-page,html.funnel-active-page{overflow:visible!important;height:auto!important}body.funnel-active-page #funnelbody,body:not(.funnel-active-page) #funnelbody{position:static!important;height:auto!important;max-height:none!important;overflow:visible!important}.funnel-loader,.funnel-notice-overlay,.funnel-video-modal{display:none!important}}@media (prefers-reduced-motion:reduce){.funnel-loader .spinner,.funnel-summary-loader,.funnel-summary-loader *{animation:none!important}}[dir=rtl] .funnel-back-btn::before{content:"\2192"}[dir=rtl] .funnel-back-btn:hover{transform:translateX(3px)}
  • funnel-elite-lite/tags/1.0.2/public/css/funnel-modal.css

    r3706934 r3707052  
    88    width: 100%;
    99    height: 100%;
    10     /* Liest die Einstellung "Modal Overlay Color"; der Rueckfallwert ist
    11        genau der bisherige Ton, bestehende Seiten sehen unveraendert aus. */
    1210    background-color: var(--funnel-modal-overlay, rgba(44, 40, 37, 0.96));
    1311    backdrop-filter: blur(10px);
    … …  
    3129    color: #fff !important;
    3230    font-family: var(--funnel-font-family, sans-serif);
    33     /* 0.3rem waren 4.8px - ein unlesbarer Fleck neben dem Kreuz. */
    3431    font-size: 0.75rem;
    3532    text-transform: uppercase;
    … …  
    5249}
    5350
    54 /* An das Popup gebunden: Themes nutzen .video-container fuer ihre eigenen
    55    responsiven Einbettungen, die Regel hat sie auf 16:9 gezwungen. */
    5651.funnel-video-modal .video-container {
    5752    position: relative;
    … …  
    8782}
    8883
    89 /* ── Mobil (17.09.2026) ────────────────────────────────────────────────────
    90    Video-Popup im Querformat: Unterkante des Videos (Steuerleiste) lag
    91    ausserhalb des Bildschirms, der Schliessen-Knopf teilweise daneben. Die
    92    Breite richtet sich jetzt nach der verfuegbaren Hoehe (16:9), oben bleiben
    93    mindestens 60px fuer den Knopf. Am Desktop aendert sich nichts: dort
    94    begrenzt weiter max-width: 1000px. */
     84/* 16:9 width from the available height, at least 60px above for the close button. */
    9585.funnel-video-modal .modal-content {
    9686  width: min(90%, calc((100vh - 140px) * 16 / 9));
    … …  
    10494}
    10595
    106 /* Feste Groesse statt rem: im Querformat schrumpfte die Themeschrift, und
    107    "Schliessen" stand mit ~4px da. */
    10896.funnel-video-modal .close {
    10997  font-size: 12px;
    … …  
    112100}
    113101
    114 /* Bei starker Zoomstufe (200 %) ragte der Inhalt des Video-Popups unten aus
    115    dem Bild und war nicht erreichbar - das Overlay konnte nicht scrollen. */
    116102.funnel-video-modal {
    117103  overflow-y: auto;
    118104}
    119105
    120 /* Der Schliesser ist ein <span> mit role="button"; ohne diese Regel sieht
    121    man beim Tabben nicht, dass er ausgewaehlt ist. */
    122106.funnel-video-modal .close:focus-visible {
    123107  outline: 2px solid #fff;
  • funnel-elite-lite/tags/1.0.2/public/css/funnel-utilities.css

    r3706934 r3707052  
    1 /* Alle Klassennamen hier sind an .funnel-container gebunden (die Klasse
    2    sitzt auf #funnelbody). Ungescopt faerbten .row, .btn-primary und Co.
    3    auf Theme-Markup derselben Seite ab. 17.09.2026 */
     1/* Scoped to .funnel-container (on #funnelbody) so theme markup is untouched. */
    42/* ---- UTILITY CLASSES (Lightweight Bootstrap Replacement) ---- */
    53/* Only the exact utility classes used by the funnel frontend */
    … …  
    2826
    2927:where(.funnel-container) .text-left {
    30   /* start statt left: in Arabisch/Hebraeisch soll der Text rechts beginnen.
    31      In Sprachen von links nach rechts ist das identisch. */
    3228  text-align: start;
    3329}
  • funnel-elite-lite/tags/1.0.2/public/js/funnel-motion.js

    r3706934 r3707052  
    2828
    2929  /**
    30    * Hat der Besucher im Betriebssystem "Bewegung reduzieren" eingeschaltet?
    31    *
    32    * Wird bei jedem Aufruf frisch gelesen, damit eine Umstellung waehrend des
    33    * Besuchs sofort greift.
     30   * Is "reduce motion" enabled? Read on every call.
    3431   */
    3532  function prefersReducedMotion() {
  • funnel-elite-lite/tags/1.0.2/public/js/modules/funnel-engine.js

    r3706934 r3707052  
    1212    let isUIBackClick = false;
    1313
    14     // Oeffentliche Anfragen mit Sicherheitsmarke (Nonce). Die Marke steht in der
    15     // Seite; liefert ein Seiten-Cache eine Kopie aus, die aelter als 12-24
    16     // Stunden ist, antwortet WordPress mit 403 "-1" - die Auswahl-Mail blieb
    17     // dann still aus. Jetzt: einmal eine frische Marke holen
    18     // (funnel_fresh_nonce) und dieselbe Anfrage wiederholen.
     14    // A cached page can carry an expired nonce: fetch a fresh one and retry once.
    1915    let nonceRefresh = null;
    2016    const freshNonce = () => {
    21       // Gleichzeitige Anfragen teilen sich EINE Erneuerung.
     17      // Concurrent requests share one refresh.
    2218      if (nonceRefresh) return nonceRefresh;
    2319      const d = $.Deferred();
    2420      const p = d.promise();
    2521      nonceRefresh = p;
    26       const fertig = () => {
     22      const done = () => {
    2723        if (nonceRefresh === p) nonceRefresh = null;
    2824      };
    … …  
    3127        { action: "funnel_fresh_nonce" },
    3228        (res) => {
    33           fertig();
     29          done();
    3430          if (res && res.success && res.data && res.data.nonce) {
    3531            funnelDataRaw.nonce = res.data.nonce;
    … …  
    4238      if (req && typeof req.fail === "function") {
    4339        req.fail(() => {
    44           fertig();
     40          done();
    4541          d.reject();
    4642        });
    … …  
    5652      );
    5753    };
    58     // Wie $.post(url, daten, erfolg) - liefert ein Promise mit .fail/.always.
     54    // Like $.post(url, data, success); returns a promise.
    5955    const publicPost = (data, success) => {
    60       // Die Daten beim Aufruf einfrieren. Die Aufrufer uebergeben historyStack
    61       // selbst; eine Wiederholung nach der Erneuerung haette sonst mitgenommen,
    62       // was spaetere Klicks darin geaendert haben (neue Auswahl, alter Preis).
     56      // Snapshot, so a retry does not pick up later changes.
    6357      const payload = $.extend(true, {}, data);
    6458      const result = $.Deferred();
    6559      const send = (retried) => {
    6660        const sentNonce = funnelDataRaw.nonce;
    67         // Abgelehnt: einmal mit frischer Marke wiederholen. Hat eine andere
    68         // Anfrage inzwischen schon eine geholt, gleich mit der.
     61        // Reuse a nonce another request already refreshed.
    6962        const retry = (fail) => {
    7063          if (retried) return false;
    … …  
    200193      }
    201194
    202       // Kein Inline-z-index mehr: die 50 schlugen die 100000 aus dem
    203       // Stylesheet, dadurch lagen feste Theme-Kopfzeilen (Kadence, Astra,
    204       // Divi, Chat-Widgets) ueber dem Vollbild-Funnel, und im Section-Modus
    205       // lag der Funnel umgekehrt ueber Menues des Themes. 17.09.2026
    206 
    207195      // Consolidated Video Modal [v5 - Responsive]
    208196      if ($("#funnel-video-modal").length === 0) {
    … …  
    273261
    274262          $modal.fadeIn(300, function () {
    275             // Fokus ins Popup, sonst steht die Tastatur weiter hinter dem
    276             // Overlay im Schritt und Escape traefe niemanden.
    277263            $modal.find(".close").trigger("focus");
    278264          });
    … …  
    303289      );
    304290
    305       // Mit der Tastatur: Enter/Leertaste auf dem Schliesser, Escape ueberall.
    306       // Ohne das kam man aus dem bildschirmfuellenden Video nicht mehr heraus.
    307291      $(document).on("keydown", "#funnel-video-modal .close", function (e) {
    308292        if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
    … …  
    707691        const safeText = (text || "").toString().replace(/\n/g, "<br>");
    708692
    709         // Die Klasse ist der Anker fuer "Bewegung reduzieren" (funnel-core.css);
    710         // die Effekte selbst haengen an inline gesetzten animation-Werten.
     693        // The class is the reduced-motion hook in funnel-core.css.
    711694        const commonWrapperStart = `<div class="funnel-summary-loader" style="display:flex; align-items:center; justify-content:center; min-height:60vh; width:100%; padding:20px; box-sizing:border-box; font-family:var(--funnel-font-family, sans-serif);">
    712695              <div style="width:100%; max-width:540px; text-align:center; padding:30px 20px; color:var(--funnel-headline, #2c2825);">`;
    … …  
    721704        }
    722705
    723         // Abstaende und Positionen bewusst ohne `gap` und ohne `inset`: beide
    724         // kennen aeltere mobile Browser (Samsung Internet < 14, Chrome < 87,
    725         // iOS < 14.5) nicht, und eine unbekannte Deklaration wird komplett
    726         // verworfen. Beim Pulsring fiel der Ring dadurch von 27px auf 4px
    727         // zusammen - der Effekt war unsichtbar, waehrend die anderen drei
    728         // liefen. Nachgemessen am 15.09.2026.
     706        // No gap or inset, older mobile browsers drop them.
    729707        if (effect === "dots") {
    730708          return `${commonWrapperStart}
  • funnel-elite-lite/tags/1.0.2/public/js/modules/funnel-renderer.js

    r3706934 r3707052  
    5959        funnelDataRaw.section_display_mode === "dynamic"
    6060      ) {
    61         // Messen statt schaetzen: der Kasten ist border-box, outerHeight()
    62         // enthaelt also Innenabstaende und die Fortschrittsleiste. Die alte
    63         // Rechnung "Schritt + 80" lag daneben, und weil der Section-Kasten
    64         // overflow: hidden hat, schnitt die zu kleine Hoehe Karten ab.
    6561        const $body = $(parent);
    6662        $body.css("height", "auto");
    … …  
    7066
    7167    /**
    72      * "Vollbreite erzwingen": misst, wie weit die Inhaltsspalte vom linken und
    73      * rechten Fensterrand entfernt ist, und gibt beides als CSS-Variablen
    74      * weiter. Der frueher benutzte Trick width:100vw; left:50%; margin-left:-50vw
    75      * stimmt nur, wenn die Spalte mittig im Fenster sitzt - in einem Layout mit
    76      * Seitenleiste schnitt er den Funnel links ab (gemessen bei 1440px: 240px).
    77      *
    78      * clientWidth statt innerWidth: sonst zaehlt auf Windows die klassische
    79      * Scrollleiste (~17px) mit und der Funnel wird zu breit.
     68     * "Force Full Screen Width": distance of the column to both viewport edges.
     69     * clientWidth excludes the scrollbar.
    8070     */
    8171    this.updateBleed = () => {
    … …  
    9282    };
    9383
    94     // Beim Drehen, Zoomen oder Fenster-Aendern neu messen: vorher blieb die
    95     // feste Hoehe des letzten Schritts stehen (gemessen: 199px fehlten nach
    96     // dem Drehen auf 1024x768). 17.09.2026
    9784    let heightTimer = null;
    9885    if (typeof window !== "undefined" && window.addEventListener) {
    … …  
    248235
    249236        // --- BREATHING ROOM DELAY (skip if old step was interrupted) ---
    250         // Bei "Bewegung reduzieren" faellt die Pause weg: sonst bleibt der
    251         // Bildschirm bei der Standardgeschwindigkeit 0.75s lang leer, obwohl
    252         // gar nichts mehr animiert wird.
    253237        let reducedMotion = false;
    254238        try {
    … …  
    436420        });
    437421
    438         // iOS wendet `:active` auf ein <button> nur an, wenn das Element selbst
    439         // einen Touch-Listener traegt - ein bekannter Eigensinn von Safari.
    440         // Ohne diese leere Zeile bleiben die Beruehrungs-Effekte auf iPhones
    441         // aus, waehrend sie auf Android laufen. `passive`, damit das Scrollen
    442         // nicht ausgebremst wird.
     422        // iOS applies :active only to elements with a touch listener.
    443423        $btn[0].addEventListener("touchstart", function () {}, { passive: true });
    444424
    … …  
    473453      $row.append($headline);
    474454
    475       // Das Feld im Backend ist ein <textarea>: wer dort Enter drueckt, meint
    476       // einen Umbruch. HTML frisst ihn, also wird er zu <br>. Ergebnistext,
    477       // Hinweistext, Formularbeschreibungen und die Limit-Meldung machen das
    478       // laengst - die Seitenbeschreibung war die Ausnahme.
    479455      const $desc = $("<p />", {
    480456        class: "funnel-description text-center col-12",
  • funnel-elite-lite/trunk/admin/css/admin-bundle.css

    r3706931 r3707052  
    4343select:focus,
    4444textarea:focus {
    45   /* Farbe aus dem Admin-Farbschema des Users, damit der Ring auch in
    46      "Modern", "Midnight" usw. passt. outline bleibt vorhanden (transparent),
    47      damit im Windows-Kontrastmodus ueberhaupt ein Ring gezeichnet wird. */
     45  /* Transparent outline keeps a visible ring in forced-colors mode. */
    4846  border-color: var(--wp-admin-theme-color, #2271b1);
    4947  box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1);
    … …  
    241239}
    242240
    243 /* Die Zentrierung rechnet mit der eingestellten Spaltenzahl. Ab 1400px
    244    abwaerts schaltet das Raster auf auto-fill um, dort wuerde sie die
    245    letzten Karten schmal quetschen oder in eine eigene Zeile schieben. */
     241/* Below 1401px the grid uses auto-fill, where this centering breaks. */
    246242@media (min-width: 1401px) {
    247243  /* Center last card(s) if alone in incomplete row — only in cards view */
    … …  
    14921488}
    14931489
    1494 /* Parent muss position: relative haben */
     1490/* Parent needs position: relative */
    14951491.funnel-group-box {
    14961492  position: relative;
    … …  
    15241520}
    15251521
    1526 /* Pulsieren effizienter: nur scale statt Opacity + optional */
     1522/* Pulse with scale only (cheaper than opacity) */
    15271523@keyframes pulse-badge {
    15281524  0%,
    … …  
    15371533}
    15381534
    1539 /* Flash Animation beim Scroll */
     1535/* Flash animation on scroll */
    15401536.scroll-highlight {
    15411537  animation: scroll-flash-blue 0.8s ease-in-out 5;
    … …  
    16741670}
    16751671
    1676 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on Neuer Funnel) */
     1672/* Normalize every dashicon inside toolbar buttons (incl. the "+" on New Funnel) */
    16771673.actions-group .button .dashicons {
    16781674  font-size: 18px;
    … …  
    20302026    font-weight: 600;
    20312027    display: flex;
    2032     /* Titel linksbündig neben dem Icon halten.
    2033        Vorher stand hier justify-content: space-between, was mit den erwarteten
    2034        zwei Flex-Items (Text + Pfeil) auch links ausrichtete. WordPress ersetzt
    2035        im Admin aber Emoji durch <img class="emoji"> (wp-emoji-release.min.js) —
    2036        damit werden aus dem einen Textknoten ZWEI Items, in Summe drei, und
    2037        space-between schob das Icon nach links, den Titel in die Mitte und den
    2038        Pfeil nach rechts. flex-start + margin-left:auto auf dem Pfeil ist
    2039        unabhängig von der Anzahl der Items. */
     2028    /* Not space-between: WordPress turns emoji into <img>, adding flex items. */
    20402029    justify-content: flex-start;
    20412030    gap: 8px;
    … …  
    20442033}
    20452034
    2046 /* Der aufklapp-Pfeil (▼) ist immer das letzte Kind und bleibt rechts.
    2047    Das inline style="float:right" im Markup ist auf einem Flex-Item wirkungslos. */
     2035/* The arrow is always the last child. */
    20482036.section-toggle > span:last-child {
    20492037    margin-left: auto;
    … …  
    20882076    border-top: 1px solid #dcdcde;
    20892077}
    2090 /* Funnel Design Tab: Spalten-Layout für Typography, Dimensions & Images */
     2078/* Funnel Design Tab: column layout for Typography, Dimensions & Images */
    20912079.funnel-dt-typo-grid--columns {
    20922080    display: grid;
    … …  
    21082096.funnel-design-sticky-save .submit {
    21092097    margin: 0;
    2110     /* Ohne padding:0 behaelt der Kern seine 10px/1.5em und die Leiste wird
    2111        121px hoch - auf einem Handy im Querformat fast die halbe Ansicht. */
    21122098    padding: 0;
    21132099    text-align: center;
    … …  
    21152101
    21162102.funnel-design-sticky-save .button-hero {
    2117     /* schlaegt das globale .button-hero{margin-top:20px!important} */
     2103    /* Overrides core .button-hero { margin-top: 20px !important; } */
    21182104    margin-top: 0 !important;
    21192105}
  • funnel-elite-lite/trunk/admin/css/admin-style.css

    r3706931 r3707052  
    4242select:focus,
    4343textarea:focus {
    44   /* Farbe aus dem Admin-Farbschema des Users, damit der Ring auch in
    45      "Modern", "Midnight" usw. passt. outline bleibt vorhanden (transparent),
    46      damit im Windows-Kontrastmodus ueberhaupt ein Ring gezeichnet wird. */
     44  /* Transparent outline keeps a visible ring in forced-colors mode. */
    4745  border-color: var(--wp-admin-theme-color, #2271b1);
    4846  box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1);
    … …  
    240238}
    241239
    242 /* Die Zentrierung rechnet mit der eingestellten Spaltenzahl. Ab 1400px
    243    abwaerts schaltet das Raster auf auto-fill um, dort wuerde sie die
    244    letzten Karten schmal quetschen oder in eine eigene Zeile schieben. */
     240/* Below 1401px the grid uses auto-fill, where this centering breaks. */
    245241@media (min-width: 1401px) {
    246242  /* Center last card(s) if alone in incomplete row — only in cards view */
  • funnel-elite-lite/trunk/admin/css/admin-tab-dashboard.css

    r3589214 r3707052  
    443443}
    444444
    445 /* Parent muss position: relative haben */
     445/* Parent needs position: relative */
    446446.funnel-group-box {
    447447  position: relative;
    … …  
    475475}
    476476
    477 /* Pulsieren effizienter: nur scale statt Opacity + optional */
     477/* Pulse with scale only (cheaper than opacity) */
    478478@keyframes pulse-badge {
    479479  0%,
    … …  
    488488}
    489489
    490 /* Flash Animation beim Scroll */
     490/* Flash animation on scroll */
    491491.scroll-highlight {
    492492  animation: scroll-flash-blue 0.8s ease-in-out 5;
    … …  
    625625}
    626626
    627 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on Neuer Funnel) */
     627/* Normalize every dashicon inside toolbar buttons (incl. the "+" on New Funnel) */
    628628.actions-group .button .dashicons {
    629629  font-size: 18px;
  • funnel-elite-lite/trunk/admin/css/admin-tab-design.css

    r3706931 r3707052  
    1717    font-weight: 600;
    1818    display: flex;
    19     /* Titel linksbündig neben dem Icon halten.
    20        Vorher stand hier justify-content: space-between, was mit den erwarteten
    21        zwei Flex-Items (Text + Pfeil) auch links ausrichtete. WordPress ersetzt
    22        im Admin aber Emoji durch <img class="emoji"> (wp-emoji-release.min.js) —
    23        damit werden aus dem einen Textknoten ZWEI Items, in Summe drei, und
    24        space-between schob das Icon nach links, den Titel in die Mitte und den
    25        Pfeil nach rechts. flex-start + margin-left:auto auf dem Pfeil ist
    26        unabhängig von der Anzahl der Items. */
     19    /* Not space-between: WordPress turns emoji into <img>, adding flex items. */
    2720    justify-content: flex-start;
    2821    gap: 8px;
    … …  
    3124}
    3225
    33 /* Der aufklapp-Pfeil (▼) ist immer das letzte Kind und bleibt rechts.
    34    Das inline style="float:right" im Markup ist auf einem Flex-Item wirkungslos. */
     26/* The arrow is always the last child. */
    3527.section-toggle > span:last-child {
    3628    margin-left: auto;
    … …  
    7567    border-top: 1px solid #dcdcde;
    7668}
    77 /* Funnel Design Tab: Spalten-Layout für Typography, Dimensions & Images */
     69/* Funnel Design Tab: column layout for Typography, Dimensions & Images */
    7870.funnel-dt-typo-grid--columns {
    7971    display: grid;
    … …  
    9587.funnel-design-sticky-save .submit {
    9688    margin: 0;
    97     /* Ohne padding:0 behaelt der Kern seine 10px/1.5em und die Leiste wird
    98        121px hoch - auf einem Handy im Querformat fast die halbe Ansicht. */
    9989    padding: 0;
    10090    text-align: center;
    … …  
    10292
    10393.funnel-design-sticky-save .button-hero {
    104     /* schlaegt das globale .button-hero{margin-top:20px!important} */
     94    /* Overrides core .button-hero { margin-top: 20px !important; } */
    10595    margin-top: 0 !important;
    10696}
  • funnel-elite-lite/trunk/admin/includes/class-funnel-builder-manager.php

    r3706931 r3707052  
    309309        ];
    310310       
    311         // Unbekannte Meta-Keys des Bestands erhalten.
    312         //
    313         // Das Array oben wird bei jedem Speichern vollstaendig aus $_POST neu
    314         // gebaut. Alles, was diese Version nicht kennt, waere damit still weg,
    315         // sobald jemand die Karte einmal oeffnet und speichert — in Lite z. B. die
    316         // komplette Konfiguration der Pro-Kartentypen (Slider, Mengen-Items,
    317         // Distance Map, Consent-Texte, Pricing-Table-Spalten), die ueber einen
    318         // Import aus Pro in der Datenbank liegt.
    319         //
    320         // Bekannte Keys gewinnen, weil sie oben immer gesetzt werden; unbekannte
    321         // bleiben unveraendert stehen.
     311        // Keep stored meta keys this version does not know (e.g. imported from Pro).
    322312        if ( ! empty( $_POST['id'] ) ) {
    323313            $existing_raw  = $this->db->get_var( $this->db->prepare( "SELECT meta_data FROM {$this->table_cards} WHERE id = %d", intval( wp_unslash( $_POST['id'] ) ) ) );
    … …  
    368358
    369359    /**
    370      * Kartenpreis aus dem Karten-Modal lesen.
     360     * Parse a typed price. With both separators the last one is the decimal
     361     * mark; several of the same kind are thousands separators.
    371362     *
    372      * Das Feld ist ein Textfeld (ohne die Pfeile eines Zahlenfelds), es kommt
    373      * also an, was getippt wurde. Frueher genuegte str_replace(',', '.') - das
    374      * Zahlenfeld liess "1.200,00" gar nicht zu. Jetzt wuerde daraus 1.2.
    375      *
    376      * - Komma UND Punkt: das hintere Zeichen trennt die Dezimalen, das andere
    377      *   die Tausender ("1.200,00", "1,200.00").
    378      * - Nur Kommas: eines ist das Dezimalzeichen ("1200,00", "1,2" fuer ×1,2),
    379      *   mehrere trennen Tausender.
    380      * - Nur Punkte: einer ist das Dezimalzeichen ("1200.50"), mehrere trennen
    381      *   Tausender ("1.200.000"). "1.200" bleibt 1.2 wie bisher - nicht eindeutig.
    382      * - Leerzeichen und Waehrungszeichen fallen weg.
    383      *
    384      * @param  string $raw Eingabe aus dem Preisfeld.
     363     * @param  string $raw Raw price field value.
    385364     * @return float
    386365     */
  • funnel-elite-lite/trunk/admin/includes/class-funnel-settings-manager.php

    r3706931 r3707052  
    3131
    3232    /**
    33      * Keep an email template only if it was actually edited.
    34      *
    35      * The editor in the Email tab is pre-filled with the default template, so
    36      * ANY save on that tab — a new SMTP password, a changed colour — writes the
    37      * whole default into the project settings, even when nobody touched the
    38      * HTML. From that moment the email is that frozen HTML: it keeps the
    39      * language it happened to have at the time and never follows the site
    40      * again. That is how a German site ends up sending a half-English email,
    41      * and no plugin update can repair it, because the database wins.
    42      *
    43      * An empty field means "use the default", and the default is built fresh on
    44      * every send, so it speaks the language of the site.
    45      *
    46      * Both sides are compared the way they are stored: the POST value has gone
    47      * through wp_kses_post(), which strips <html>/<body> out of the template,
    48      * and a textarea submits CRLF where the source has LF. Without normalising
    49      * both, nothing would ever match and the guard would be dead code.
     33     * Store the template only if it differs from the default, so an untouched
     34     * one keeps following the site language.
    5035     *
    5136     * @param  string $posted   The already sanitized POST value.
    … …  
    6247
    6348    /**
    64      * The default admin template, or an empty string if the mail class is not
    65      * loaded — then nothing can match and the value is kept as posted.
     49     * Default admin template, or '' if the mail class is not loaded.
    6650     */
    6751    private static function default_admin_template() {
  • funnel-elite-lite/trunk/admin/js/admin-builder.js

    r3706931 r3707052  
    380380    form.find('input[name="answer"]').val(answer);
    381381    form.find('input[name="image_url"]').val(img);
    382     // Ohne Preis leer lassen, damit der Platzhalter ("z. B. 1200,00") zu sehen ist.
     382    // Empty instead of 0, so the placeholder shows.
    383383    form.find('input[name="price"]').val(parseFloat(price) ? price : "");
    384384    form.find('select[name="price_operator"]').val(operator);
  • funnel-elite-lite/trunk/admin/js/admin-visual-nodes-trees.js

    r3706931 r3707052  
    2626  let lastRenderedSource = ""; // Performance cache
    2727
    28   // Welche Karte gerade steht (Routen-Filter) und ob der Admin den Zoom selbst
    29   // gewaehlt hat oder die Karte nur eingepasst ist. Siehe "ANSICHT MERKEN".
     28  // zoomChosen: zoom set by the user (false = fitted to the container).
    3029  let currentFilter = "all";
    3130  let zoomChosen = false;
    … …  
    9291
    9392  // ===================================
    94   // ANSICHT MERKEN (Zoom + Scrollposition)
    95   // ===================================
    96 
    97   // Nach dem Speichern laedt die Seite neu, und die Karte wurde dabei jedes Mal
    98   // wieder eingepasst - wer hineingezoomt hatte, musste die Stelle neu suchen.
    99   // Gemerkt wird im Browser, je Funnel und je Routen-Filter: jeder Filter
    100   // zeichnet eine andere Karte, deren Stelle nicht auf die naechste passt.
     93  // REMEMBER VIEW (per funnel and route filter)
     94  // ===================================
    10195
    10296  function mapProjectId() {
    … …  
    125119      localStorage.setItem(mapViewKey(), JSON.stringify(views));
    126120    } catch (e) {
    127       // Privates Fenster oder voller Speicher: dann eben ohne Gedaechtnis.
     121      // Storage unavailable (private window, quota).
    128122    }
    129123  }
    … …  
    131125  function saveMapView() {
    132126    const scroller = document.getElementById("visual-map-scroll-container");
    133     // Solange die gemerkte Ansicht nicht angewendet ist, wuerde der Zwischenstand
    134     // sie ueberschreiben. Und versteckt (anderer Tab, eingeklappte Karte) liest
    135     // sich jede Scrollposition als 0.
     127    // Hidden containers report 0; never overwrite a view not yet applied.
    136128    if (!viewApplied || !scroller || !$(scroller).is(":visible")) return;
    137129    if (!DOM.$graphContainer.find("svg").length) return;
    … …  
    139131    const views = readMapViews();
    140132    views[currentFilter] = {
    141       // null = nur eingepasst. Waechst die Karte, passt sie sich dann neu ein,
    142       // statt auf einem veralteten Einpass-Wert stehen zu bleiben.
     133      // null = fitted, so a grown map is fitted again.
    143134      zoom: zoomChosen ? currentZoom : null,
    144135      left: scroller.scrollLeft,
    … …  
    154145  }
    155146
    156   // Der Routen-Filter selbst sprang nach dem Neuladen auf "Alle" zurueck - die
    157   // fuer einen Filter gemerkte Stelle kam so nie zum Zug. Ebenfalls je Funnel.
    158   // "Alle" ist der Normalfall und wird nicht gespeichert.
     147  // "all" is the default and is not stored.
    159148
    160149  function mapFilterKey() {
    … …  
    170159      }
    171160    } catch (e) {
    172       // Privates Fenster oder voller Speicher: dann eben ohne Gedaechtnis.
     161      // Storage unavailable (private window, quota).
    173162    }
    174163  }
    175164
    176165  /**
    177    * Den gemerkten Filter zurueck in die Auswahl schreiben, bevor zum ersten
    178    * Mal gezeichnet wird. Gibt es die Route nicht mehr (Karte umbenannt oder
    179    * geloescht), bleibt "Alle" stehen und der alte Wert wird vergessen.
     166   * Put the stored filter back into the dropdown; forget it if the route is gone.
    180167   */
    181168  function restoreMapFilter() {
    … …  
    201188
    202189  /**
    203    * Karte einpassen und die gemerkte Ansicht wiederherstellen, falls es eine
    204    * gibt. Nur bei sichtbarem Container aufrufen - versteckt nimmt der Browser
    205    * keine Scrollposition an.
     190   * Fit the map and restore the stored view. Call only on a visible container.
    206191   */
    207192  function showMapView() {
    … …  
    350335    if (!sourceCode) return;
    351336
    352     // Ohne Angabe gilt, was in der Auswahl steht. Vorher zeichnete z. B. der
    353     // Wechsel zurueck auf den Builder-Tab "Alle", waehrend die Auswahl noch
    354     // einen Filter zeigte.
    355337    if (filterRoute === undefined) {
    356338      filterRoute = $("#pages-cards-route-filter").val() || "all";
    … …  
    363345    // PERFORMANCE: Skip expensive render if source is unchanged
    364346    if (sourceCode === lastRenderedSource) {
    365       // Same map shown again (e.g. back on the Builder tab): skip DOM
    366       // generation, but put the view back where it was.
     347      // Unchanged source: skip rendering, restore the view.
    367348      showMapView();
    368349      return;
    … …  
    421402      });
    422403
    423       // Fit map after render. Shown first: the stored scroll position only
    424       // sticks on a visible container.
     404      // Show first: scroll positions only stick on a visible container.
    425405      setTimeout(() => {
    426406        const $collapsible = $("#visual-pages-cards-collapsible");
    … …  
    475455  });
    476456
    477   // Zuruecksetzen heisst auch vergessen: nach dem Neuladen wird wieder
    478   // eingepasst.
    479457  $(".zoom-map-reset").on("click", (e) => {
    480458    e.preventDefault();
    … …  
    483461  });
    484462
    485   // Die Scrollposition laufend mitschreiben. "pagehide" faengt den letzten
    486   // Stand ab, wenn direkt nach dem Scrollen gespeichert und neu geladen wird.
     463  // pagehide catches a scroll right before a reload.
    487464  $("#visual-map-scroll-container").on("scroll", debounce(saveMapView, 150));
    488465  window.addEventListener("pagehide", saveMapView);
    … …  
    697674
    698675  /**
    699    * Apply the stored collapsed state to the map panel.
    700    *
    701    * Runs without Mermaid so the panel is in its final state even when the
    702    * library is never loaded.
     676   * Apply the stored collapsed state (works without Mermaid).
    703677   *
    704678   * @return {boolean} true when the map is collapsed.
    … …  
    720694
    721695  function initMermaidAndRender() {
    722     // loadMermaidIfNeeded() resolves even when the local file fails to load
    723     // (fail-silently, WP.org Guideline #8 — no CDN fallback). Guard so a missing
    724     // library cannot throw here.
     696    // loadMermaidIfNeeded() also resolves when the file failed to load.
    725697    if (typeof mermaid === "undefined") {
    726698      applyCollapsedState();
    … …  
    733705      securityLevel: "loose",
    734706      htmlLabels: true,
    735       // NO size limit on the map.
    736       //
    737       // Mermaid stops at 50 000 characters by default and REPLACES the diagram
    738       // with "Maximum text size in diagram exceeded" (it does not throw:
    739       //   text.length > (config.maxTextSize ?? 5e4) && (text = errorGraph)
    740       // ). That guard is meant for UNTRUSTED input; here the definition is
    741       // generated from the admin's own funnel, so the default only hides the
    742       // map once a funnel grows - after roughly ten steps, because the length
    743       // comes from the labels rather than from the structure.
    744       //
    745       // MAX_SAFE_INTEGER rather than Infinity: the comparison is numeric either
    746       // way, but a plain number survives any clone of the config object, while
    747       // Infinity can come back as null - and `?? 5e4` would then quietly put
    748       // the default back.
     707      // No size limit (the source is built by the plugin). Not Infinity: it can
     708      // become null when the config is cloned, which restores the default.
    749709      maxTextSize: Number.MAX_SAFE_INTEGER,
    750710      maxEdges: Number.MAX_SAFE_INTEGER,
    … …  
    791751  // Global Initialization Boundary
    792752  try {
    793     // The stored route filter goes back into the dropdown before anything is
    794     // drawn, so the first render already uses it.
    795753    restoreMapFilter();
    796754
    797     // Put the panel into its stored state first — this must happen whether or
    798     // not Mermaid ever loads.
    799755    const startsCollapsed = applyCollapsedState();
    800756
    … …  
    806762      loadMermaidIfNeeded().then(initMermaidAndRender);
    807763    } else {
    808       // Nothing to draw yet. Mermaid is ~650 KB, so it stays unloaded until the
    809       // map is actually going to be shown: either the Builder tab is opened
    810       // (with the map expanded) or the user expands the collapsed map.
     764      // Mermaid (~650 KB) loads only once the map is shown.
    811765      $(document).on("click.funnelMermaidLazy", '.nav-tab[href="#tab-builder"]', function () {
    812766        if (localStorage.getItem("funnel_pages_cards_collapsed") === "true") return;
  • funnel-elite-lite/trunk/admin/traits/trait-admin-enqueue.php

    r3706931 r3707052  
    3030
    3131    /**
    32      * Is the current admin screen one of the plugin's own?
    33      *
    34      * WordPress passes the screen's hook suffix to admin_enqueue_scripts
    35      * ('toplevel_page_funnel-elite-lite'); $_GET['page'] is kept as a fallback
    36      * for the AJAX-rendered tabs, which reach this code without a suffix.
     32     * Is the current admin screen the plugin's own page?
    3733     *
    3834     * @param string $hook_suffix Hook suffix handed over by WordPress.
    … …  
    5147
    5248        public function enqueue_styles( $hook_suffix = '' ) {
    53         // Nur auf der eigenen Plugin-Seite laden. Das Bundle gestaltet Labels,
    54         // Eingabefelder und Buttons global um; auf fremden Seiten hat es
    55         // Kern-Formulare, Listentabellen und den Block-Editor veraendert.
    56         // Das Plugin zeichnet ausserhalb seiner Seite nichts (kein
    57         // admin_notices, kein admin_bar_menu, kein Dashboard-Widget).
    5849        if ( ! $this->is_plugin_admin_page( $hook_suffix ) ) {
    5950            return;
    … …  
    8374            . '.slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background-color:#fff;-webkit-transition:.4s;transition:.4s}'
    8475            . 'input:checked+.slider{background-color:#d63384}'
    85             // Tastatur-Fokus: der echte Haken ist 0x0 und unsichtbar, ohne
    86             // diesen Ring sieht man beim Tabben nicht, welcher Schalter dran ist.
    8776            . 'input:focus-visible+.slider{outline:2px solid var(--wp-admin-theme-color,#2271b1);outline-offset:2px}'
    8877            . 'input:checked+.slider:before{-webkit-transform:translateX(26px);-ms-transform:translateX(26px);transform:translateX(26px)}'
    … …  
    163152        $mermaid_rel = 'js/vendor/mermaid.min.js';
    164153        $mermaid_abs = FUNNEL_ELITE_PATH . 'admin/' . $mermaid_rel;
    165         // Mit Dateiversion: Mermaid wird per JS nachgeladen und bekommt deshalb
    166         // keine ?ver von wp_enqueue_script. Ohne sie nimmt der Browser nach einem
    167         // Update die alte Datei aus dem Cache - z. B. mit der alten Kartenreihenfolge.
     154        // Lazy-loaded by JS, so it needs its own cache-busting version.
    168155        $mermaid_url = file_exists( $mermaid_abs )
    169156            ? FUNNEL_ELITE_URL . 'admin/' . $mermaid_rel . '?ver=' . rawurlencode( $this->file_version( $mermaid_abs ) )
  • funnel-elite-lite/trunk/admin/views/tabs/design/section-summary.php

    r3706931 r3707052  
    8282                    <div class="funnel-dt-section-title"><?php echo esc_html( Funnel_System_I18n::get_admin('dt_typo_spacing_title') ); ?></div>
    8383                    <div class="funnel-dt-details-body">
    84                         <!-- 💻 DESKTOP -->
    8584                        <h4 style="margin:0 0 10px; color:#475569; font-size:12px; text-transform:uppercase; letter-spacing:0.5px;">💻 <?php echo esc_html( Funnel_System_I18n::get_admin('dt_desktop') ); ?></h4>
    8685                        <div class="funnel-dt-typo-grid">
    … …  
    129128                        </div>
    130129                        <div class="funnel-dt-typo-divider"></div>
    131                         <!-- 📱 MOBILE -->
    132130                        <?php
    133                         // Ein leeres Mobil-Feld erbt den Desktop-Wert. Damit der
    134                         // Platzhalter nicht etwas anderes verspricht, zeigt er
    135                         // genau diesen Wert an.
    136                         $dt_wert = static function ( $key, $fallback ) use ( $project_settings ) {
     131                        // Placeholder shows the desktop value an empty mobile field inherits.
     132                        $dt_value = static function ( $key, $fallback ) use ( $project_settings ) {
    137133                            return ( isset( $project_settings[ $key ] ) && '' !== $project_settings[ $key ] )
    138134                                ? $project_settings[ $key ]
    … …  
    145141                                <label for="mobile_summary_title_size"><?php echo esc_html( Funnel_System_I18n::get_admin('title_font_size_label') ); ?></label>
    146142                                <div class="funnel-dt-input-wrap">
    147                                     <input name="mobile_summary_title_size" type="number" id="mobile_summary_title_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_title_size']) ? $project_settings['mobile_summary_title_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'summary_title_size', 32 ) ); ?>" class="small-text" min="10" max="60">
     143                                    <input name="mobile_summary_title_size" type="number" id="mobile_summary_title_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_title_size']) ? $project_settings['mobile_summary_title_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_title_size', 32 ) ); ?>" class="small-text" min="10" max="60">
    148144                                    <span class="funnel-dt-unit">px</span>
    149145                                </div>
    … …  
    152148                                <label for="mobile_summary_text_size"><?php echo esc_html( Funnel_System_I18n::get_admin('text_font_size_label') ); ?></label>
    153149                                <div class="funnel-dt-input-wrap">
    154                                     <input name="mobile_summary_text_size" type="number" id="mobile_summary_text_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_text_size']) ? $project_settings['mobile_summary_text_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'summary_text_size', 16 ) ); ?>" class="small-text" min="10" max="60">
     150                                    <input name="mobile_summary_text_size" type="number" id="mobile_summary_text_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_text_size']) ? $project_settings['mobile_summary_text_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_text_size', 16 ) ); ?>" class="small-text" min="10" max="60">
    155151                                    <span class="funnel-dt-unit">px</span>
    156152                                </div>
    … …  
    159155                                <label for="mobile_summary_label_font_size"><?php echo esc_html( Funnel_System_I18n::get_admin('price_label_font_size_label') ); ?></label>
    160156                                <div class="funnel-dt-input-wrap">
    161                                     <input name="mobile_summary_label_font_size" type="number" id="mobile_summary_label_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_label_font_size']) ? $project_settings['mobile_summary_label_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'summary_label_font_size', 14 ) ); ?>" class="small-text" min="10" max="60">
     157                                    <input name="mobile_summary_label_font_size" type="number" id="mobile_summary_label_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_label_font_size']) ? $project_settings['mobile_summary_label_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_label_font_size', 14 ) ); ?>" class="small-text" min="10" max="60">
    162158                                    <span class="funnel-dt-unit">px</span>
    163159                                </div>
    … …  
    166162                                <label for="mobile_price_font_size"><?php echo esc_html( Funnel_System_I18n::get_admin('price_font_size_label') ); ?></label>
    167163                                <div class="funnel-dt-input-wrap">
    168                                     <input name="mobile_price_font_size" type="number" id="mobile_price_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_price_font_size']) ? $project_settings['mobile_price_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'price_font_size', 32 ) ); ?>" class="small-text" min="10" max="80">
     164                                    <input name="mobile_price_font_size" type="number" id="mobile_price_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_price_font_size']) ? $project_settings['mobile_price_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'price_font_size', 32 ) ); ?>" class="small-text" min="10" max="80">
    169165                                    <span class="funnel-dt-unit">px</span>
    170166                                </div>
    … …  
    173169                                <label for="mobile_summary_hint_size"><?php echo esc_html( Funnel_System_I18n::get_admin('hint_font_size_label') ); ?></label>
    174170                                <div class="funnel-dt-input-wrap">
    175                                     <input name="mobile_summary_hint_size" type="number" id="mobile_summary_hint_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_hint_size']) ? $project_settings['mobile_summary_hint_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_wert( 'summary_hint_size', 14 ) ); ?>" class="small-text" min="10" max="60">
     171                                    <input name="mobile_summary_hint_size" type="number" id="mobile_summary_hint_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_hint_size']) ? $project_settings['mobile_summary_hint_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_hint_size', 14 ) ); ?>" class="small-text" min="10" max="60">
    176172                                    <span class="funnel-dt-unit">px</span>
    177173                                </div>
    … …  
    228224                </table>
    229225
    230                 <!-- ── Ergebnisseite: Weiter-Button (optional, leer = global) ── -->
     226                <!-- ── Continue Button ─────────────────────────────── -->
    231227                <div class="funnel-dt-details funnel-dt-details--static" style="margin-top:12px;">
    232228                    <div class="funnel-dt-section-title"><?php echo esc_html( Funnel_System_I18n::get_admin('dt_summary_btn_sizes_title') ); ?></div>
  • funnel-elite-lite/trunk/includes/class-funnel-css-generator.php

    r3706931 r3707052  
    248248        $summary_hint_size  = ! empty( $settings['summary_hint_size'] )  ? $settings['summary_hint_size']  : 14;
    249249
    250         // Ergebnisseite auf dem Telefon. Ein leeres Feld erbt bewusst den
    251         // Desktop-Wert - so aendert sich an bestehenden Funnels nichts, solange
    252         // niemand ein Mobil-Feld ausfuellt. Der Platzhalter im Design-Tab zeigt
    253         // deshalb genau diesen Desktop-Wert an.
     250        // Empty mobile fields inherit the desktop value.
    254251        $mobile_summary_title_size = ! empty( $settings['mobile_summary_title_size'] ) ? $settings['mobile_summary_title_size'] : $summary_title_size;
    255252        $mobile_summary_text_size  = ! empty( $settings['mobile_summary_text_size'] )  ? $settings['mobile_summary_text_size']  : $summary_text_size;
    … …  
    270267        $back_btn_color = $settings['back_btn_color'];
    271268        $modal_bg_color = $settings['modal_bg_color'];
    272         // Als rgba ausgeben: das Video-Overlay braucht die 0.96 Deckkraft,
    273         // ein reiner Hex-Wert wuerde es undurchsichtig machen.
    274269        $modal_overlay_rgba = self::hexToRgba( $modal_bg_color, 0.96 );
    275270        $max_width      = $settings['max_content_width'];
    … …  
    365360        $card_bg_3     = self::hexToRgba($settings['card_bg_color_3'], $card_bg_intensity);
    366361       
    367         /*
    368          * Summary button overrides (Ergebnisseite).
    369          *
    370          * Every field is optional and means "inherit the global button style" when
    371          * empty, so only the properties the admin actually filled in are emitted.
    372          * An untouched funnel therefore produces no rule at all and looks exactly
    373          * as it did before these settings existed.
    374          *
    375          * The selector is #funnelbody #btn-summary-next: two IDs beat the global
    376          * "#funnelbody .btn-primary" rule, and !important is repeated because that
    377          * rule carries it too (see the card hover note — an important declaration
    378          * only loses to another important one).
    379          */
     362        // Summary button overrides: only filled fields are emitted, empty ones
     363        // inherit the global button style (which is !important, hence here too).
    380364        $summary_btn_decls = array();
    381365
    … …  
    545529            }
    546530           
    547             /* Hier standen die beiden Regeln zur Einstellung Layout Format
    548                (Views). Kein PHP und kein JS hat die zugehoerigen Klassen je an
    549                #funnelbody gehaengt, die Auswahl war also wirkungslos;
    550                ausserdem hat der Inline-Minifier die Leerzeichen um das Plus in
    551                ihrer calc()-Rechnung entfernt und sie damit ungueltig gemacht.
    552                Einstellung und Regeln sind entfernt.
    553                Fuer eine echte Vollbreite gibt es die Einstellung Vollbreite erzwingen. */
    554 
    555531            #funnelbody h1, #funnelbody h2 {
    556532                color: var(--funnel-headline);
    … …  
    590566            }
    591567
    592             /* Hover/focus background MUST repeat the !important.
    593                funnel-cards.css sets --funnel-card-hover-bg on :hover without it,
    594                and an !important declaration always wins over a normal one —
    595                regardless of specificity or order. The Hover Color setting
    596                therefore had no visible effect at all until this rule existed.
    597                Do not drop the !important here without also dropping it above. */
     568            /* Needs !important to beat the card background rule. */
    598569            .st-funnel-card .card:hover,
    599570            .st-funnel-card:focus-visible .card {
    … …  
    706677                #funnelbody .st-funnel-card img { height: {$mobile_img_height}px !important; }
    707678
    708                 /* Ergebnisseite. Die Variablen oben decken auch die Inline-Styles
    709                    ab, die der Renderer setzt; diese Regeln sind fuer alles, was
    710                    ueber das Stylesheet kommt. */
    711679                #funnelbody .summary-title { font-size: {$mobile_summary_title_size}px !important; }
    712680                #funnelbody .summary-text { font-size: {$mobile_summary_text_size}px !important; }
    … …  
    752720                margin-right: 0 !important;
    753721            }
    754             /* Offsetbasiert statt 100vw/-50vw: der alte Trick stimmt nur, wenn
    755                die Inhaltsspalte mittig im Fenster steht. In einem Layout mit
    756                Seitenleiste schob er den Funnel aus dem Bild (gemessen bei 1440px:
    757                240px links abgeschnitten). Die beiden Werte setzt das JS aus der
    758                tatsaechlichen Position; ohne JS bleibt alles in der Spalte. */
     722            /* --funnel-bleed-l/-r are measured by JS. */
    759723            body:not(.funnel-active-page) #funnelbody {
    760724                width: auto !important;
  • funnel-elite-lite/trunk/includes/class-funnel-data.php

    r3706931 r3707052  
    55
    66    /**
    7      * Card meta keys that must never reach the frontend.
    8      *
    9      * funnelDataRaw is printed into the page source and the whole meta_data array
    10      * is merged into it, so every key in there is public by default.
    11      *
    12      * Lite has no UI for these — they can only arrive through an import of a funnel
    13      * that was built in Pro, where the export carries meta_data 1:1. The data is
    14      * kept (so a Pro -> Lite -> Pro round trip stays lossless) but never published.
     7     * Card meta keys (imported from Pro) that are kept but never printed to the page.
    158     */
    169    const ADMIN_ONLY_META_KEYS = array( 'admin_note' );
    … …  
    121114            ], $meta);
    122115
    123             // Internal Pro-only fields must not be published — see the constant.
    124116            foreach ( self::ADMIN_ONLY_META_KEYS as $admin_only_key ) {
    125117                unset( $relation_entry[ $admin_only_key ] );
  • funnel-elite-lite/trunk/includes/class-funnel-i18n.php

    r3706931 r3707052  
    4141
    4242        /**
    43          * Cache key for the string tables.
    44          *
    45          * Keyed by locale so switch_to_locale() still returns the right table.
    46          * Calls that happen before `init` can still be untranslated (WordPress loads
    47          * plugin translations just-in-time), so those get their own slot and never
    48          * poison the translated table.
     43         * Cache key per locale; calls before `init` get their own untranslated slot.
    4944         */
    5045        private static function string_cache_key() {
    … …  
    6459        /**
    6560         * Get strings for the Admin Backend (built once per locale and request).
    66          *
    67          * PERFORMANCE: the table below holds ~515 __() lookups. It used to be rebuilt
    68          * on every single get_admin() call — with ~530 call sites in the admin views
    69          * that added up to ~275,000 gettext lookups per admin page render (measured:
    70          * 107ms of pure PHP, 0.2ms with this cache). Never drop this cache.
    7161         */
    7262        public static function get_admin_strings() {
    … …  
    232222
    233223            'card_icon_label' => __( 'Icon (Lucide)', 'funnel-elite-lite' ),
    234                                 // UI Labels aus Design-Vorlage
     224                                // UI labels from the design template
    235225                                'icon_size_label' => __( 'Icon Size', 'funnel-elite-lite' ),
    236226                        'icon_color_label' => __( 'Icon Color', 'funnel-elite-lite' ),
    … …  
    238228                       
    239229                        'card_price_label' => __( 'Card Price', 'funnel-elite-lite' ),
    240                         // Platzhalter im Preisfeld - je Sprache mit deren Dezimalzeichen uebersetzen.
     230                        // translators: example price, use the decimal separator of your language.
    241231                        'card_price_placeholder' => __( 'e.g. 1200.00', 'funnel-elite-lite' ),
    242232                        'card_price_help_text' => __( 'Operator + price set how this card changes the running total: + adds and − subtracts the price; × and ÷ multiply or divide the total (e.g. ×1.2 = +20%).', 'funnel-elite-lite' ),
    … …  
    584574                        'sort_order'         => __( 'Sort Order', 'funnel-elite-lite' ),
    585575                        'video_url_optional' => __( 'Video URL (Optional)', 'funnel-elite-lite' ),
    586                         // Knopf im Seiten-Modal - vorher stand dort "Save Card". Muss zum Titel
    587                         // edit_step ("Edit Page") passen.
    588576                        'save_step'          => __( 'Save Page', 'funnel-elite-lite' ),
    589577                        'edit_card'          => __( 'Edit Card', 'funnel-elite-lite' ),
    … …  
    675663                        'dt_price_box_content_title' => __( 'Price Box Content', 'funnel-elite-lite' ),
    676664                        'dt_summary_colors_title'    => __( 'Summary Colors', 'funnel-elite-lite' ),
    677                         // Ergebnisseite: Weiter-Button (optionale Overrides)
    678665                        'dt_summary_btn_colors_title' => __( 'Continue Button (Result Page)', 'funnel-elite-lite' ),
    679666                        'dt_summary_btn_sizes_title'  => __( 'Continue Button Dimensions (Result Page)', 'funnel-elite-lite' ),
    … …  
    693680        /**
    694681         * Get strings for the Public Frontend (built once per locale and request).
    695          *
    696          * Same caching rationale as get_admin_strings().
    697682         */
    698683        public static function get_public_strings() {
  • funnel-elite-lite/trunk/includes/class-funnel-main.php

    r3706931 r3707052  
    6060        $this->loader->add_action( 'wp_ajax_funnel_partial_lead', $plugin_public, 'handle_partial_lead' );
    6161        $this->loader->add_action( 'wp_ajax_nopriv_funnel_partial_lead', $plugin_public, 'handle_partial_lead' );
    62         // Frische Sicherheitsmarke, wenn ein Seiten-Cache eine abgelaufene ausliefert.
    6362        $this->loader->add_action( 'wp_ajax_funnel_fresh_nonce', $plugin_public, 'handle_fresh_nonce' );
    6463        $this->loader->add_action( 'wp_ajax_nopriv_funnel_fresh_nonce', $plugin_public, 'handle_fresh_nonce' );
  • funnel-elite-lite/trunk/includes/class-funnel-settings-registry.php

    r3706931 r3707052  
    204204
    205205    /**
    206      * Sanitize an optional pixel value for the summary button overrides.
    207      *
    208      * These fields mean *inherit the global button style* when left empty, so the
    209      * empty string has to survive sanitizing. intval('') would be 0 — and 0 is a
    210      * legitimate value here (sharp corners, no border, no padding), so the two
    211      * cases must stay distinguishable.
     206     * Optional pixel value: '' means inherit, 0 is a valid value.
    212207     *
    213208     * @param mixed $value Raw value.
    … …  
    228223
    229224    /**
    230      * Sanitize an optional colour for the summary button overrides.
    231      *
    232      * Same idea as sanitize_optional_px(): empty means inherit.
     225     * Optional colour: '' means inherit.
    233226     *
    234227     * @param mixed $value Raw value.
    … …  
    325318            [ 'id' => 'card_bg_intensity',  'sanitize' => 'intval',              'default' => 100 ],
    326319            [ 'id' => 'card_text_color',    'sanitize' => 'sanitize_hex_color',  'default' => '#212529', 'css_var' => '--funnel-card-text' ],
    327             // #007bff liess die Kartenbeschreibung beim Ueberfahren und bei
    328             // Tastatur-Auswahl auf 3.10:1 fallen; #e9ecef bringt sie auf 7.2:1.
    329320            [ 'id' => 'card_hover_color',   'sanitize' => 'sanitize_hex_color',  'default' => '#e9ecef', 'css_var' => '--funnel-card-hover-bg' ],
    330321            [ 'id' => 'card_width',         'sanitize' => 'intval',  'default' => 300,   'unit' => 'px' ],
    … …  
    400391
    401392            // ── Buttons ──────────────────────────────────────────
    402             // Weiss auf dem Standard-Sandton #c3b696 sind 2.0:1 - unlesbar.
    403             // #1d2327 (WordPress-Schwarz) ergibt 7.9:1.
    404393            [ 'id' => 'btn_text_color',   'sanitize' => 'sanitize_hex_color', 'default' => '#1d2327', 'css_var' => '--funnel-btn-text' ],
    405394            [ 'id' => 'btn_border_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#c3b696', 'css_var' => '--funnel-btn-border-color' ],
    … …  
    423412
    424413            // ── Summary & Price ──────────────────────────────────
    425             // Die fuenf mobile_*-Schriftgroessen der Ergebnisseite sind optional:
    426             // leer bedeutet, dass der Desktop-Wert auch auf dem Telefon gilt.
    427             // Ein fester Standard wuerde bestehende Funnels beim Update
    428             // ungefragt umstellen.
     414            // mobile_* sizes are optional: empty = desktop value.
    429415            [ 'id' => 'price_font_size',        'sanitize' => 'intval',              'default' => 32,      'css_var' => '--funnel-price-font-size', 'unit' => 'px' ],
    430416            [ 'id' => 'mobile_price_font_size', 'sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ],
    … …  
    433419            [ 'id' => 'summary_label_font_size','sanitize' => 'intval',              'default' => 14,      'css_var' => '--funnel-label-font-size', 'unit' => 'px' ],
    434420            [ 'id' => 'mobile_summary_label_font_size','sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ],
    435             // #8c857b plus der Deckkraft 0.8 ergaben 2.68:1 auf Weiss.
    436421            [ 'id' => 'summary_label_color',    'sanitize' => 'sanitize_hex_color',  'default' => '#6b645a','css_var' => '--funnel-label-color' ],
    437422            [ 'id' => 'summary_label_text',     'sanitize' => 'sanitize_text_field', 'default' => '' ],
    … …  
    451436            [ 'id' => 'summary_hint_size',      'sanitize' => 'intval', 'default' => 14, 'css_var' => '--funnel-summary-hint-size',  'unit' => 'px' ],
    452437            [ 'id' => 'mobile_summary_hint_size','sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ],
    453             // ── Ergebnisseite: Weiter-Button (alle optional, leer = global erben) ──
    454             // Die globalen Button-Einstellungen gelten weiter, solange hier nichts
    455             // gesetzt ist. Der CSS-Generator gibt nur die Eigenschaften aus, die
    456             // wirklich gefüllt sind — bestehende Funnels sehen unverändert aus.
     438            // ── Summary Button (optional, empty = global button style) ──
    457439            [ 'id' => 'summary_btn_bg',           'sanitize' => array( __CLASS__, 'sanitize_optional_hex_color' ), 'default' => '' ],
    458440            [ 'id' => 'summary_btn_text_color',   'sanitize' => array( __CLASS__, 'sanitize_optional_hex_color' ), 'default' => '' ],
  • funnel-elite-lite/trunk/includes/emails/trait-email-formatter.php

    r3706931 r3707052  
    55
    66    /**
    7      * Text einer Mail-Vorlage in der Sprache der Website.
     7     * Template text in the site language, English fallback.
    88     *
    9      * Die Vorlage hatte ihre Ueberschrift fest auf Englisch im Code. Den
    10      * passenden Schluessel gibt es laengst und er ist in allen mitgelieferten
    11      * Sprachen uebersetzt - er wurde hier nur nie benutzt.
    12      *
    13      * Faellt auf den englischen Text zurueck, falls die i18n-Klasse (noch) nicht
    14      * geladen ist: eine Mail ohne Ueberschrift waere schlimmer als eine
    15      * englische.
    16      *
    17      * @param  string $key       Schluessel aus der oeffentlichen i18n-Tabelle.
    18      * @param  string $fallback  Englischer Text, falls nichts gefunden wird.
     9     * @param  string $key      Key of the public i18n table.
     10     * @param  string $fallback Text used when the key is not available.
    1911     * @return string
    2012     */
  • funnel-elite-lite/trunk/includes/emails/trait-email-preview.php

    r3706931 r3707052  
    1313     */
    1414    public static function get_default_admin_template( $settings = array() ) {
    15         // No <html>/<body>: wp_kses_post() removes both when the template is
    16         // saved - together with the font and background set on them. They sit
    17         // on the outer table instead, so a saved template looks like the editor.
    18         // No fixed 600px container: phones shrink a fixed width, text included,
    19         // to about two thirds. A div with max-width grows with the screen
    20         // (Chrome and iOS Mail ignore max-width on a table).
     15        // No <html>/<body>: wp_kses_post() strips them on save. max-width sits on
     16        // a div because some mail clients ignore it on tables.
    2117        $t  = '<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7f9; padding: 32px 0; font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2d3748;">' . "\n";
    2218        $t .= '<tr><td align="center" style="padding: 0 12px;">' . "\n";
  • funnel-elite-lite/trunk/includes/emails/trait-email-template.php

    r3706931 r3707052  
    7575        $message .= '<table width="100%" cellpadding="0" cellspacing="0" style="background-color: ' . $bg_color . '; padding: 40px 0;"><tr><td align="center" style="padding: 0 12px;">';
    7676
    77         // Main Container. Grows with the screen instead of a fixed 600px:
    78         // phones shrink a fixed width, text included, to about two thirds, and
    79         // Chrome and iOS Mail ignore max-width on a table - hence the div.
     77        // Main Container (max-width on a div, some mail clients ignore it on tables)
    8078        $message .= '<div style="max-width: 600px; margin: 0 auto;"><table width="100%" cellpadding="0" cellspacing="0" style="width: 100%; background-color: ' . $container_bg . '; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); overflow: hidden;">';
    8179
    … …  
    153151
    154152        // Header
    155         //
    156         // This is the mail the RESULT PAGE sends whenever the template editor is
    157         // empty - and it never had a logo, not even a place for one, although
    158         // the admin template does. Same markup as build_default_template(), so
    159         // both look alike at the top.
    160153        $message .= '<tr><td style="padding: 30px 40px; text-align: center; border-bottom: 3px solid ' . $primary_color . ';">';
    161154        if ( $logo_url ) {
  • funnel-elite-lite/trunk/public/class-funnel-public.php

    r3706931 r3707052  
    111111    }
    112112
    113 
    114     /**
    115      * Soll auf dieser Seite ueberhaupt etwas vom Funnel geladen werden?
    116      *
    117      * Ohne is_singular() greift die Pruefung auch auf Such- und Archivseiten:
    118      * dort zeigt das globale $post auf den ersten Treffer. Steht darin ein
    119      * Funnel, luden alle Stylesheets und das komplette JS mit, obwohl die
    120      * Auszuege den Shortcode entfernen und gar kein #funnelbody entsteht.
    121      *
    122      * Wer den Funnel bewusst ausserhalb einer Einzelansicht rendert (Page
    123      * Builder, Widget, Archiv mit vollem Inhalt), schaltet die Dateien wie
    124      * bisher ueber den Filter frei:
    125      *
    126      *   add_filter( 'funnel_system_load_assets', '__return_true' );
     113    /**
     114     * Load assets only on singular views that contain the shortcode.
    127115     *
    128116     * @return bool
    … …  
    250238        wp_enqueue_script( $this->plugin_name . '-main', $main['url'], array( 'jquery', $this->plugin_name . '-engine' ), $main['version'], true );
    251239
    252         // Drei der angebotenen Knopf-Effekte brauchen JS: "Scroll Trigger"
    253         // setzt .is-inview, "Click Feedback" setzt .is-pressed, "Ripple Click"
    254         // legt das .play-ripple-Element an. In Lite fehlte diese Datei, die
    255         // Effekte standen also im Admin zur Wahl und taten nichts - "Scroll
    256         // Trigger" liess den Knopf sogar dauerhaft blass.
    257240        $effect_settings = $project_id ? $this->get_project_settings( $project_id ) : null;
    258241        if ( $effect_settings ) {
    … …  
    287270                wp_add_inline_script(
    288271                    $this->plugin_name . '-model',
    289                     // Nur wenn der Funnel wirklich gerendert wurde: auf Such-
    290                     // und Archivseiten strippen Auszuege den Shortcode, die
    291                     // Klasse haette dort das Scrollen der ganzen Seite gesperrt.
     272                    // Excerpts strip the shortcode, so check that the funnel was rendered.
    292273                    'if(document.getElementById("funnelbody")){document.documentElement.classList.add("funnel-active-page");document.body.classList.add("funnel-active-page");}',
    293274                    'before'
    … …  
    405386        if ($bg_type === 'image') $layout_class = 'has-bg-image';
    406387        if ($bg_type === 'pattern') $layout_class = 'has-bg-pattern';
    407         // "Vollbreite erzwingen": Marker fuer das JS, das die beiden Abstaende
    408         // zum Fensterrand misst (--funnel-bleed-l / -r). Ohne JS bleibt der
    409         // Funnel einfach in seiner Spalte.
    410388        if ( ! empty( $settings['enable_fullscreen_override'] ) ) {
    411389            $layout_class = trim( $layout_class . ' funnel-bleed' );
    … …  
    644622
    645623    /**
    646      * Returns a fresh public nonce for the funnel's own AJAX request.
    647      *
    648      * The nonce (funnel_public_nonce) is printed into the page. A page cache can
    649      * serve a copy that is older than the nonce lifetime (12-24 hours); then
    650      * handle_partial_lead() rejects the request with 403 and the selection
    651      * e-mail is silently lost. After such a rejection the frontend calls this
    652      * endpoint once and repeats the request with the new nonce.
    653      *
    654      * Intentionally no nonce check here: a visitor holding an expired nonce must
    655      * be able to obtain a new one. The endpoint reads no input, writes nothing
    656      * and returns exactly what every page load already prints; it sends
    657      * no-cache headers. admin-ajax only lets the site's own origins read the
    658      * response (get_allowed_http_origins()). The nonce it returns only unlocks
    659      * funnel_partial_lead, which verifies it and is open to visitors anyway.
     624     * Returns a fresh public nonce, so a cached page with an expired one can retry.
     625     *
     626     * No nonce check by design: it reads no input, writes nothing and returns
     627     * what every page load prints anyway.
    660628     *
    661629     * @return void
    … …  
    742710        }
    743711
    744         // The logo has to travel in the meta: build_partial_lead_email() only
    745         // sees what it is handed, and without this the result mail never had
    746         // one - neither the logo nor anywhere to put it.
    747712        $email_meta = array(
    748713            'subject'    => $subject,
    … …  
    760725        }
    761726
    762         // Sender name from the Email tab - resolved before SMTP is configured,
    763         // because configure_phpmailer() sets it last and would otherwise win.
    764727        $from_name = self::resolve_from_name( $project_settings );
    765728
    … …  
    877840
    878841    /**
    879      * The sender name as the admin typed it into "Sender Name".
    880      *
    881      * The result mail used to send as get_bloginfo('name') and never read this
    882      * field. Only the "Send test email" button used it - so the test mail
    883      * showed the configured name and the real mails did not, which is exactly
    884      * why it went unnoticed.
    885      *
    886      * Order follows the rest of the mail code (see the logo in
    887      * prepare_placeholder_data): project value, then the global option the Email
    888      * tab writes, then the site name.
     842     * Sender name: project setting, then the global option, then the site name.
    889843     *
    890844     * @param  array $project_settings Decoded project settings.
    … …  
    900854        }
    901855
    902         // The site name is stored HTML-escaped ("A &amp; B"); a mail header
    903         // wants the plain text. Line breaks would allow header injection, and
    904         // angle brackets break wp_mail's "Name <address>" parsing.
     856        // Strip line breaks (header injection) and angle brackets ("Name <address>").
    905857        $name = wp_specialchars_decode( $name, ENT_QUOTES );
    906858
    … …  
    909861
    910862    /**
    911      * The brand logo from the Email tab, same order as resolve_from_name().
     863     * Brand logo: project setting, then the global option.
    912864     *
    913865     * @param  array $project_settings Decoded project settings.
    … …  
    925877     * Apply the plugin's custom SMTP configuration to PHPMailer (if set).
    926878     *
    927      * @param string $from_name Display name for the From header. With SMTP
    928      *                          authentication the address has to be the SMTP
    929      *                          user, but the NAME is free - and this is the
    930      *                          last place it is set before sending, so a
    931      *                          correct header alone would be overwritten here.
     879     * @param string $from_name Display name for the From header.
    932880     */
    933881    private function configure_phpmailer( $from_name = '' ) {
    … …  
    971919                $phpmailer->Username = $user;
    972920                $phpmailer->Password = $pass;
    973                 // The ADDRESS has to be the SMTP user (servers reject a From
    974                 // they do not own). The NAME stays the configured one - forcing
    975                 // it to the site name here silently undid the From header.
    976921                $phpmailer->From     = $user;
    977922                $phpmailer->FromName = $from_name;
  • funnel-elite-lite/trunk/public/css/funnel-button-attention.css

    r3706931 r3707052  
    6969        content: "";
    7070        position: absolute;
    71         /* Langform statt `inset`: aeltere mobile Browser (Samsung Internet < 14,
    72            Chrome < 87, iOS < 14.5) kennen die Kurzform nicht und verwerfen die
    73            Deklaration komplett - der Ring faellt dann auf wenige Pixel zusammen. */
     71        /* Longhand instead of inset for older mobile browsers. */
    7472        top: -6px;
    7573        right: -6px;
    … …  
    187185                transition: none !important;
    188186                transform: none !important;
    189                 /* Kein `opacity: 1 !important` mehr: es hat auch den ausgegrauten
    190                    "Weiter"-Knopf wieder voll sichtbar gemacht, obwohl er nicht
    191                    klickbar ist - und liess den Ring am Video-Knopf dauerhaft stehen. */
    192187                box-shadow: none !important;
    193188        }
  • funnel-elite-lite/trunk/public/css/funnel-button-effects.css

    r3706931 r3707052  
    103103
    104104/* ---------- Play Button Effects ---------- */
    105 /* `:active` ist hier kein Beiwerk: Ein Telefon kennt kein Hover, also waeren
    106    diese vier Effekte auf jedem Mobilgeraet wirkungslos - genau das war die
    107    Beobachtung "ich merke da nichts". Mit `:active` reagieren sie auf die
    108    Beruehrung, solange der Finger liegt. Am Rechner aendert sich nichts: wer
    109    klickt, fuhr vorher schon mit dem Zeiger darueber. */
     105/* :active makes these effects work on touch screens. */
    110106.funnel-container .play-effect--soft-pulse:hover,
    111107.funnel-container .play-effect--soft-pulse:focus-visible,
    … …  
    117113  content: "";
    118114  position: absolute;
    119   /* Langform statt `inset` - siehe funnel-button-attention.css. */
     115  /* Longhand instead of inset for older mobile browsers. */
    120116  top: -4px;
    121117  right: -4px;
    … …  
    189185}
    190186
    191 /* Mobil/Desktop (17.09.2026): Der Ring des Effekts "Glow Ring" lag ausserhalb
    192    des Knopfes und wurde von dessen overflow: hidden abgeschnitten - der Effekt
    193    war unsichtbar. !important, weil funnel-button-attention.css spaeter geladen
    194    wird und dieselbe Spezifitaet hat. */
     187/* The ring sits outside the button. !important: funnel-button-attention.css loads later. */
    195188.funnel-container .play-btn.play-effect--glow-ring {
    196189  overflow: visible !important;
  • funnel-elite-lite/trunk/public/css/funnel-cards.css

    r3706931 r3707052  
    4242  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
    4343  border-color: rgba(0, 0, 0, 0.1);
    44   /* Eigener Rahmen statt nur der Hover-Farbe: bei Verlauf-, Glanz- oder
    45      Mesh-Hintergruenden deckt das Hintergrundbild die Hover-Farbe zu, dann
    46      war die Tastatur-Auswahl nur noch am 8px-Anheben zu erkennen. */
     44  /* Background images can cover the hover colour, so add an outline. */
    4745  outline: 2px solid var(--funnel-headline, #1d2327);
    4846  outline-offset: 3px;
    … …  
    111109  font-family: var(--funnel-font-family, sans-serif);
    112110  font-size: var(--funnel-card-title-size, 1.4rem);
    113   /* War fest auf #2c2825. Auf dunklen Karten (Karte #1d2327, Text weiss)
    114      blieb der Titel fast schwarz - gemessen 1.09:1. */
    115111  color: var(--funnel-card-text, #2c2825);
    116112  margin: 0;
    … …  
    118114  word-wrap: break-word;
    119115  overflow-wrap: break-word;
    120   /* Keine Silbentrennung: `hyphens: auto` zerlegte Antworten mitten im Wort
    121      ("Fotoshooting & Brautabho-lung & Saalbegleitung"). Ein Umbruch ZWISCHEN
    122      Woertern ist in Ordnung, einer INNERHALB nicht.
    123      `manual` statt weglassen, weil hyphens vererbt wird - ein Theme mit
    124      `hyphens: auto` auf body wuerde es sonst wieder einschleppen.
    125      `overflow-wrap: break-word` bleibt als Notnagel: es greift nur, wenn ein
    126      einzelnes Wort allein nicht auf eine Zeile passt. */
     116  /* manual, not unset: hyphens is inherited and themes may set auto. */
    127117  hyphens: manual;
    128118  -webkit-hyphens: manual;
    … …  
    142132  word-wrap: break-word;
    143133  overflow-wrap: break-word;
    144   /* Gleiche Regel wie beim Titel - siehe dort. */
    145134  hyphens: manual;
    146135  -webkit-hyphens: manual;
    … …  
    341330
    342331/* ---- WEITER / SUMMARY BUTTON: globale Layout- & Stil-Basis ---- */
    343 /* Stil (Farbe, Padding, Font) wird über .btn-primary im CSS-Generator aus den Admin-Einstellungen gesetzt */
     332/* Colour, padding and font come from .btn-primary in the CSS generator */
    344333.funnel-container .funnel-card-next {
    345334  display: block;
    … …  
    372361}
    373362
    374 /* Der Haken schiebt das Wort aus der Mitte: die Gruppe aus Haken und Wort wird
    375    zentriert, also sitzt das Wort um die halbe Hakenbreite plus den halben
    376    Abstand daneben - gemessen 10,9 px bei 320 px Knopfbreite. Auffallen tut aber
    377    das Wort, nicht die Gruppe.
    378    Deshalb steht auf der Gegenseite ein unsichtbarer Zwilling desselben
    379    Zeichens. Er ist exakt so breit wie der Haken - auch wenn eine andere
    380    Schrift einspringt, was bei U+2713 in einer Serifenschrift die Regel ist -
    381    und traegt denselben `gap`. Damit liegt das Wort auf der Knopfmitte.
    382    `visibility: hidden` statt `display: none`: der Platz muss erhalten bleiben. */
     363/* Invisible twin of the tick keeps the label centred. */
    383364#btn-summary-next .summary-next-balance {
    384365  visibility: hidden;
    … …  
    435416}
    436417
    437 /* ── Mobil (17.09.2026) ────────────────────────────────────────────────────
    438    Streifen-Layout: Auf schmalen Telefonen ragten Preis-Abzeichen und Bild aus
    439    der Karte heraus, sobald ein Titel ein langes Wort hatte (gemessen bei 320
    440    und 375px: Abzeichen endete 37px hinter der Karte, Bild links angeschnitten).
    441    Die flexiblen Spalten geben ohne min-width: 0 nicht nach. Unter 480px wird
    442    das Bild kleiner, sonst bleibt fuer den Titel nur ein schmaler Streifen. */
     418/* Stripe layout: flex columns need min-width: 0 to shrink. */
    443419#funnelbody .funnel-layout-stripe .card-body,
    444420#funnelbody .funnel-layout-stripe .card-text-wrapper {
    … …  
    463439  }
    464440
    465   /* Das Abzeichen nimmt sonst die halbe Kartenbreite, und der Titel bricht
    466      mitten im Wort. Groesser als 13px wird es hier nicht, kleiner eingestellte
    467      Werte bleiben erhalten. */
    468441  #funnelbody .funnel-layout-stripe .card-price-badge,
    469442  #funnelbody .funnel-layout-stripe .card-adjust-badge,
    … …  
    474447}
    475448
    476 /* Wer im Betriebssystem "Bewegung reduzieren" eingestellt hat, bekam trotzdem
    477    das volle Programm: 8px-Anheben bei Hover und Tastatur-Auswahl, dazu die
    478    0.4s-Ueberblendung auf jeder Karte. */
    479449@media (prefers-reduced-motion: reduce) {
    480450  .st-funnel-card .card {
  • funnel-elite-lite/trunk/public/css/funnel-core.css

    r3706931 r3707052  
    33
    44/* GLOBAL PAGE OVERRIDE - FULLSCREEN MODE
    5    Die Hoehe kommt aus top/bottom, NICHT aus 100vh: iOS Safari rechnet vh mit
    6    eingeklappten Leisten. Der Kasten ragte dadurch 80-135px unter die untere
    7    Safari-Leiste, und weil html/body gesperrt sind, klappt Safari sie nie ein -
    8    das Ende der Liste war nie erreichbar (iPhone 13, 17.09.2026). Mit
    9    bottom: 0 sitzt die Unterkante immer auf der sichtbaren Kante.
    10    overscroll-behavior-y haelt die Wischgeste im Funnel, statt sie an die
    11    gesperrte Seite zu geben (nur y: x wuerde das Zurueckwischen in Chrome
    12    abschalten). */
     5   Height from top/bottom, not 100vh: iOS Safari measures vh without its bars. */
    136body.funnel-active-page #funnelbody {
    147  position: fixed !important;
    … …  
    9891}
    9992
    100 /* Gescopt: .active-step ist ein gaengiger Klassenname in Steppern und
    101    Kassen-Strecken; mit !important hob die Regel fremde Elemente an. */
    10293#funnelbody .active-step {
    10394  position: relative !important;
    … …  
    142133  gap: 8px;
    143134  background: transparent !important;
    144   /* Liest jetzt die Einstellung "Zurueck-Knopf-Farbe"; 0.85 statt 0.7 hebt
    145      den Kontrast auf weissem Grund von 3.57:1 auf 5.11:1 (AA). */
    146135  color: var(--funnel-back-btn-color, #4a5568) !important;
    147136  border: none !important;
    … …  
    159148
    160149.funnel-back-btn:hover {
    161   /* Die feste Hover-Farbe ist raus: sie hat jede eingestellte Farbe beim
    162      Ueberfahren wieder verworfen. Die volle Deckkraft reicht als Rueckmeldung. */
    163150  opacity: 1;
    164151  transform: translateX(-3px);
    … …  
    175162  body.funnel-active-page #funnelbody {
    176163    font-size: var(--funnel-mobile-font-size, 14px);
    177     /* Im Vollbild steht ueber der Ueberschrift nichts mehr - die 60px vom
    178        Desktop sind auf einem Telefon verschenkte Hoehe, also 15px weniger.
    179        !important, weil die Grundregel padding als Kurzform mit !important
    180        setzt und sonst gewinnt. */
    181164    padding-top: 45px !important;
    182165    padding-bottom: 60px !important;
    183     /* Zweite Angabe fuer Geraete mit Hausbalken/Notch (nur mit
    184        viewport-fit=cover im Theme wirksam, sonst 0). */
    185166    padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px)) !important;
    186167  }
    … …  
    276257}
    277258
    278 /* ── Mobil (17.09.2026) ────────────────────────────────────────────────────
    279    Ueberschriften: lange deutsche Woerter liefen aus dem Bild und wurden
    280    abgeschnitten (gemessen bei 320px). Umgebrochen wird nur, was sonst nicht
    281    passt; hyphens: manual haelt die bewusste Entscheidung gegen automatische
    282    Silbentrennung und ueberstimmt ein Theme mit hyphens: auto. */
    283259#funnelbody h1,
    284260#funnelbody h2 {
    … …  
    288264}
    289265
    290 /* Zurueck-Knopf: nur 15-18px hoch, im Querformat 12px - zu klein zum
    291    Antippen. 44px ist die gaengige Mindestgroesse fuer Finger. Die zweite
    292    font-size gilt nur, wo max() bekannt ist; aeltere Engines behalten die
    293    erste Zeile, statt die Groesse an das Theme zu verlieren. */
     266/* 44px touch target. The first font-size is the fallback without max(). */
    294267.funnel-back-btn {
    295268  min-height: 44px;
    … …  
    300273}
    301274
    302 /* ── Drucken (17.09.2026) ─────────────────────────────────────────────────
    303    Im Vollbild blieb der Funnel beim Drucken ein fester, scrollender Kasten:
    304    das Blatt zeigte nur den ersten Bildschirm, der Rest des Schritts oder der
    305    Ergebnisseite fehlte. Beim Druck wird der Funnel wieder normaler Inhalt. */
    306275@media print {
    307276  html.funnel-active-page,
    … …  
    326295}
    327296
    328 /* Eigener Stapelkontext im Section-Modus: die Schritte (z-index: 100) bleiben
    329    im Funnel, statt sich mit festen Kopfzeilen des Themes zu messen. */
     297/* Keeps the steps' z-index inside the funnel. */
    330298body:not(.funnel-active-page) #funnelbody {
    331299  isolation: isolate;
    332300}
    333301
    334 /* Ladeanzeige der Ergebnisseite bei "Bewegung reduzieren" anhalten. Die
    335    Effekte stehen inline am Element, deshalb braucht es hier !important.
    336    Der Pulsring wuchs sonst dauerhaft auf das 2,1-fache. */
     302/* Loader effects are inline styles, hence !important. */
    337303@media (prefers-reduced-motion: reduce) {
    338304  .funnel-summary-loader,
    … …  
    346312}
    347313
    348 /* Sprachen von rechts nach links: der Pfeil des Zurueck-Knopfes muss in die
    349    andere Richtung zeigen und sich auch dorthin bewegen. */
    350314[dir="rtl"] .funnel-back-btn::before {
    351   content: "92";
     315  content: "\2192";
    352316}
    353317
  • funnel-elite-lite/trunk/public/css/funnel-core.min.css

    r3706931 r3707052  
    22    --funnel-font-family,
    33    sans-serif
    4   )!important;font-size:2.8em;font-weight:400;margin-bottom:1.5rem;color:var(--funnel-headline,#2c2825);line-height:1.2}#funnelbody .funnel-description,.funnel-description{font-family:var(--funnel-font-family, sans-serif)!important;font-size:var(--funnel-group-desc-size, 1.1rem);color:var(--funnel-text,#6d665e);font-weight:300;max-width:600px;margin:0 auto 30px}.funnel-back-btn{display:inline-flex!important;align-items:center;gap:8px;background:0 0!important;color:var(--funnel-back-btn-color,#4a5568)!important;border:none!important;font-family:var(--funnel-font-family, sans-serif);font-weight:400;text-transform:uppercase;letter-spacing:1.5px;cursor:pointer;transition:opacity .3s,color .3s,transform .3s;opacity:.85}.funnel-back-btn:hover{opacity:1;transform:translateX(-3px)}.funnel-back-btn::before{content:"\2190";font-size:1.1rem;line-height:1}#funnel-progress-wrapper{width:100%;max-width:var(--funnel-progress-width,600px);margin:0 auto;position:relative;height:var(--funnel-progress-size,6px);background:rgba(0,0,0,.05);border-radius:calc(var(--funnel-progress-size,6px)/ 2);overflow:hidden;z-index:10}#funnel-progress-fill{height:100%;background-color:var(--funnel-progress-color,#0ba360);transition:width .5s cubic-bezier(.22, 1, .36, 1);border-radius:calc(var(--funnel-progress-size,6px)/ 2)}.funnel-pb-shape-segmented{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05),rgba(0,0,0,.05) 10px,transparent 10px,transparent 15px)!important;border-radius:0!important}.funnel-pb-shape-segmented #funnel-progress-fill{background-image:repeating-linear-gradient(90deg,transparent,transparent 10px,rgba(255,255,255,.3) 10px,rgba(255,255,255,.3) 15px)!important;border-radius:0!important}.funnel-pb-shape-dots{background:rgba(0,0,0,.1)!important;border-radius:0!important;mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));-webkit-mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%;-webkit-mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%}.funnel-pb-shape-dots #funnel-progress-fill{border-radius:0!important}@media (max-width:768px){body.funnel-active-page #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-top:45px!important;padding-bottom:60px!important;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))!important}body:not(.funnel-active-page) #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-bottom:60px!important}.st-funnel-step h2{font-size:2.2em}.funnel-description{font-size:var(--funnel-mobile-group-desc-size, 14px)}#funnel-progress-wrapper{margin:0 auto 20px;width:calc(100% - 30px)}}#funnelbody h1,#funnelbody h2{overflow-wrap:break-word;hyphens:manual;-webkit-hyphens:manual}.funnel-back-btn{min-height:44px;padding:0 12px 0 0!important;margin-bottom:8px;font-size:.85rem!important;font-size:max(13px, .85rem)!important}@media print{body.funnel-active-page,html.funnel-active-page{overflow:visible!important;height:auto!important}body.funnel-active-page #funnelbody,body:not(.funnel-active-page) #funnelbody{position:static!important;height:auto!important;max-height:none!important;overflow:visible!important}.funnel-loader,.funnel-notice-overlay,.funnel-video-modal{display:none!important}}@media (prefers-reduced-motion:reduce){.funnel-loader .spinner,.funnel-summary-loader,.funnel-summary-loader *{animation:none!important}}[dir=rtl] .funnel-back-btn::before{content:"92"}[dir=rtl] .funnel-back-btn:hover{transform:translateX(3px)}
     4  )!important;font-size:2.8em;font-weight:400;margin-bottom:1.5rem;color:var(--funnel-headline,#2c2825);line-height:1.2}#funnelbody .funnel-description,.funnel-description{font-family:var(--funnel-font-family, sans-serif)!important;font-size:var(--funnel-group-desc-size, 1.1rem);color:var(--funnel-text,#6d665e);font-weight:300;max-width:600px;margin:0 auto 30px}.funnel-back-btn{display:inline-flex!important;align-items:center;gap:8px;background:0 0!important;color:var(--funnel-back-btn-color,#4a5568)!important;border:none!important;font-family:var(--funnel-font-family, sans-serif);font-weight:400;text-transform:uppercase;letter-spacing:1.5px;cursor:pointer;transition:opacity .3s,color .3s,transform .3s;opacity:.85}.funnel-back-btn:hover{opacity:1;transform:translateX(-3px)}.funnel-back-btn::before{content:"\2190";font-size:1.1rem;line-height:1}#funnel-progress-wrapper{width:100%;max-width:var(--funnel-progress-width,600px);margin:0 auto;position:relative;height:var(--funnel-progress-size,6px);background:rgba(0,0,0,.05);border-radius:calc(var(--funnel-progress-size,6px)/ 2);overflow:hidden;z-index:10}#funnel-progress-fill{height:100%;background-color:var(--funnel-progress-color,#0ba360);transition:width .5s cubic-bezier(.22, 1, .36, 1);border-radius:calc(var(--funnel-progress-size,6px)/ 2)}.funnel-pb-shape-segmented{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05),rgba(0,0,0,.05) 10px,transparent 10px,transparent 15px)!important;border-radius:0!important}.funnel-pb-shape-segmented #funnel-progress-fill{background-image:repeating-linear-gradient(90deg,transparent,transparent 10px,rgba(255,255,255,.3) 10px,rgba(255,255,255,.3) 15px)!important;border-radius:0!important}.funnel-pb-shape-dots{background:rgba(0,0,0,.1)!important;border-radius:0!important;mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));-webkit-mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%;-webkit-mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%}.funnel-pb-shape-dots #funnel-progress-fill{border-radius:0!important}@media (max-width:768px){body.funnel-active-page #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-top:45px!important;padding-bottom:60px!important;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))!important}body:not(.funnel-active-page) #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-bottom:60px!important}.st-funnel-step h2{font-size:2.2em}.funnel-description{font-size:var(--funnel-mobile-group-desc-size, 14px)}#funnel-progress-wrapper{margin:0 auto 20px;width:calc(100% - 30px)}}#funnelbody h1,#funnelbody h2{overflow-wrap:break-word;hyphens:manual;-webkit-hyphens:manual}.funnel-back-btn{min-height:44px;padding:0 12px 0 0!important;margin-bottom:8px;font-size:.85rem!important;font-size:max(13px, .85rem)!important}@media print{body.funnel-active-page,html.funnel-active-page{overflow:visible!important;height:auto!important}body.funnel-active-page #funnelbody,body:not(.funnel-active-page) #funnelbody{position:static!important;height:auto!important;max-height:none!important;overflow:visible!important}.funnel-loader,.funnel-notice-overlay,.funnel-video-modal{display:none!important}}@media (prefers-reduced-motion:reduce){.funnel-loader .spinner,.funnel-summary-loader,.funnel-summary-loader *{animation:none!important}}[dir=rtl] .funnel-back-btn::before{content:"\2192"}[dir=rtl] .funnel-back-btn:hover{transform:translateX(3px)}
  • funnel-elite-lite/trunk/public/css/funnel-modal.css

    r3706931 r3707052  
    88    width: 100%;
    99    height: 100%;
    10     /* Liest die Einstellung "Modal Overlay Color"; der Rueckfallwert ist
    11        genau der bisherige Ton, bestehende Seiten sehen unveraendert aus. */
    1210    background-color: var(--funnel-modal-overlay, rgba(44, 40, 37, 0.96));
    1311    backdrop-filter: blur(10px);
    … …  
    3129    color: #fff !important;
    3230    font-family: var(--funnel-font-family, sans-serif);
    33     /* 0.3rem waren 4.8px - ein unlesbarer Fleck neben dem Kreuz. */
    3431    font-size: 0.75rem;
    3532    text-transform: uppercase;
    … …  
    5249}
    5350
    54 /* An das Popup gebunden: Themes nutzen .video-container fuer ihre eigenen
    55    responsiven Einbettungen, die Regel hat sie auf 16:9 gezwungen. */
    5651.funnel-video-modal .video-container {
    5752    position: relative;
    … …  
    8782}
    8883
    89 /* ── Mobil (17.09.2026) ────────────────────────────────────────────────────
    90    Video-Popup im Querformat: Unterkante des Videos (Steuerleiste) lag
    91    ausserhalb des Bildschirms, der Schliessen-Knopf teilweise daneben. Die
    92    Breite richtet sich jetzt nach der verfuegbaren Hoehe (16:9), oben bleiben
    93    mindestens 60px fuer den Knopf. Am Desktop aendert sich nichts: dort
    94    begrenzt weiter max-width: 1000px. */
     84/* 16:9 width from the available height, at least 60px above for the close button. */
    9585.funnel-video-modal .modal-content {
    9686  width: min(90%, calc((100vh - 140px) * 16 / 9));
    … …  
    10494}
    10595
    106 /* Feste Groesse statt rem: im Querformat schrumpfte die Themeschrift, und
    107    "Schliessen" stand mit ~4px da. */
    10896.funnel-video-modal .close {
    10997  font-size: 12px;
    … …  
    112100}
    113101
    114 /* Bei starker Zoomstufe (200 %) ragte der Inhalt des Video-Popups unten aus
    115    dem Bild und war nicht erreichbar - das Overlay konnte nicht scrollen. */
    116102.funnel-video-modal {
    117103  overflow-y: auto;
    118104}
    119105
    120 /* Der Schliesser ist ein <span> mit role="button"; ohne diese Regel sieht
    121    man beim Tabben nicht, dass er ausgewaehlt ist. */
    122106.funnel-video-modal .close:focus-visible {
    123107  outline: 2px solid #fff;
  • funnel-elite-lite/trunk/public/css/funnel-utilities.css

    r3706931 r3707052  
    1 /* Alle Klassennamen hier sind an .funnel-container gebunden (die Klasse
    2    sitzt auf #funnelbody). Ungescopt faerbten .row, .btn-primary und Co.
    3    auf Theme-Markup derselben Seite ab. 17.09.2026 */
     1/* Scoped to .funnel-container (on #funnelbody) so theme markup is untouched. */
    42/* ---- UTILITY CLASSES (Lightweight Bootstrap Replacement) ---- */
    53/* Only the exact utility classes used by the funnel frontend */
    … …  
    2826
    2927:where(.funnel-container) .text-left {
    30   /* start statt left: in Arabisch/Hebraeisch soll der Text rechts beginnen.
    31      In Sprachen von links nach rechts ist das identisch. */
    3228  text-align: start;
    3329}
  • funnel-elite-lite/trunk/public/js/funnel-motion.js

    r3706931 r3707052  
    2828
    2929  /**
    30    * Hat der Besucher im Betriebssystem "Bewegung reduzieren" eingeschaltet?
    31    *
    32    * Wird bei jedem Aufruf frisch gelesen, damit eine Umstellung waehrend des
    33    * Besuchs sofort greift.
     30   * Is "reduce motion" enabled? Read on every call.
    3431   */
    3532  function prefersReducedMotion() {
  • funnel-elite-lite/trunk/public/js/modules/funnel-engine.js

    r3706931 r3707052  
    1212    let isUIBackClick = false;
    1313
    14     // Oeffentliche Anfragen mit Sicherheitsmarke (Nonce). Die Marke steht in der
    15     // Seite; liefert ein Seiten-Cache eine Kopie aus, die aelter als 12-24
    16     // Stunden ist, antwortet WordPress mit 403 "-1" - die Auswahl-Mail blieb
    17     // dann still aus. Jetzt: einmal eine frische Marke holen
    18     // (funnel_fresh_nonce) und dieselbe Anfrage wiederholen.
     14    // A cached page can carry an expired nonce: fetch a fresh one and retry once.
    1915    let nonceRefresh = null;
    2016    const freshNonce = () => {
    21       // Gleichzeitige Anfragen teilen sich EINE Erneuerung.
     17      // Concurrent requests share one refresh.
    2218      if (nonceRefresh) return nonceRefresh;
    2319      const d = $.Deferred();
    2420      const p = d.promise();
    2521      nonceRefresh = p;
    26       const fertig = () => {
     22      const done = () => {
    2723        if (nonceRefresh === p) nonceRefresh = null;
    2824      };
    … …  
    3127        { action: "funnel_fresh_nonce" },
    3228        (res) => {
    33           fertig();
     29          done();
    3430          if (res && res.success && res.data && res.data.nonce) {
    3531            funnelDataRaw.nonce = res.data.nonce;
    … …  
    4238      if (req && typeof req.fail === "function") {
    4339        req.fail(() => {
    44           fertig();
     40          done();
    4541          d.reject();
    4642        });
    … …  
    5652      );
    5753    };
    58     // Wie $.post(url, daten, erfolg) - liefert ein Promise mit .fail/.always.
     54    // Like $.post(url, data, success); returns a promise.
    5955    const publicPost = (data, success) => {
    60       // Die Daten beim Aufruf einfrieren. Die Aufrufer uebergeben historyStack
    61       // selbst; eine Wiederholung nach der Erneuerung haette sonst mitgenommen,
    62       // was spaetere Klicks darin geaendert haben (neue Auswahl, alter Preis).
     56      // Snapshot, so a retry does not pick up later changes.
    6357      const payload = $.extend(true, {}, data);
    6458      const result = $.Deferred();
    6559      const send = (retried) => {
    6660        const sentNonce = funnelDataRaw.nonce;
    67         // Abgelehnt: einmal mit frischer Marke wiederholen. Hat eine andere
    68         // Anfrage inzwischen schon eine geholt, gleich mit der.
     61        // Reuse a nonce another request already refreshed.
    6962        const retry = (fail) => {
    7063          if (retried) return false;
    … …  
    200193      }
    201194
    202       // Kein Inline-z-index mehr: die 50 schlugen die 100000 aus dem
    203       // Stylesheet, dadurch lagen feste Theme-Kopfzeilen (Kadence, Astra,
    204       // Divi, Chat-Widgets) ueber dem Vollbild-Funnel, und im Section-Modus
    205       // lag der Funnel umgekehrt ueber Menues des Themes. 17.09.2026
    206 
    207195      // Consolidated Video Modal [v5 - Responsive]
    208196      if ($("#funnel-video-modal").length === 0) {
    … …  
    273261
    274262          $modal.fadeIn(300, function () {
    275             // Fokus ins Popup, sonst steht die Tastatur weiter hinter dem
    276             // Overlay im Schritt und Escape traefe niemanden.
    277263            $modal.find(".close").trigger("focus");
    278264          });
    … …  
    303289      );
    304290
    305       // Mit der Tastatur: Enter/Leertaste auf dem Schliesser, Escape ueberall.
    306       // Ohne das kam man aus dem bildschirmfuellenden Video nicht mehr heraus.
    307291      $(document).on("keydown", "#funnel-video-modal .close", function (e) {
    308292        if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
    … …  
    707691        const safeText = (text || "").toString().replace(/\n/g, "<br>");
    708692
    709         // Die Klasse ist der Anker fuer "Bewegung reduzieren" (funnel-core.css);
    710         // die Effekte selbst haengen an inline gesetzten animation-Werten.
     693        // The class is the reduced-motion hook in funnel-core.css.
    711694        const commonWrapperStart = `<div class="funnel-summary-loader" style="display:flex; align-items:center; justify-content:center; min-height:60vh; width:100%; padding:20px; box-sizing:border-box; font-family:var(--funnel-font-family, sans-serif);">
    712695              <div style="width:100%; max-width:540px; text-align:center; padding:30px 20px; color:var(--funnel-headline, #2c2825);">`;
    … …  
    721704        }
    722705
    723         // Abstaende und Positionen bewusst ohne `gap` und ohne `inset`: beide
    724         // kennen aeltere mobile Browser (Samsung Internet < 14, Chrome < 87,
    725         // iOS < 14.5) nicht, und eine unbekannte Deklaration wird komplett
    726         // verworfen. Beim Pulsring fiel der Ring dadurch von 27px auf 4px
    727         // zusammen - der Effekt war unsichtbar, waehrend die anderen drei
    728         // liefen. Nachgemessen am 15.09.2026.
     706        // No gap or inset, older mobile browsers drop them.
    729707        if (effect === "dots") {
    730708          return `${commonWrapperStart}
  • funnel-elite-lite/trunk/public/js/modules/funnel-renderer.js

    r3706931 r3707052  
    5959        funnelDataRaw.section_display_mode === "dynamic"
    6060      ) {
    61         // Messen statt schaetzen: der Kasten ist border-box, outerHeight()
    62         // enthaelt also Innenabstaende und die Fortschrittsleiste. Die alte
    63         // Rechnung "Schritt + 80" lag daneben, und weil der Section-Kasten
    64         // overflow: hidden hat, schnitt die zu kleine Hoehe Karten ab.
    6561        const $body = $(parent);
    6662        $body.css("height", "auto");
    … …  
    7066
    7167    /**
    72      * "Vollbreite erzwingen": misst, wie weit die Inhaltsspalte vom linken und
    73      * rechten Fensterrand entfernt ist, und gibt beides als CSS-Variablen
    74      * weiter. Der frueher benutzte Trick width:100vw; left:50%; margin-left:-50vw
    75      * stimmt nur, wenn die Spalte mittig im Fenster sitzt - in einem Layout mit
    76      * Seitenleiste schnitt er den Funnel links ab (gemessen bei 1440px: 240px).
    77      *
    78      * clientWidth statt innerWidth: sonst zaehlt auf Windows die klassische
    79      * Scrollleiste (~17px) mit und der Funnel wird zu breit.
     68     * "Force Full Screen Width": distance of the column to both viewport edges.
     69     * clientWidth excludes the scrollbar.
    8070     */
    8171    this.updateBleed = () => {
    … …  
    9282    };
    9383
    94     // Beim Drehen, Zoomen oder Fenster-Aendern neu messen: vorher blieb die
    95     // feste Hoehe des letzten Schritts stehen (gemessen: 199px fehlten nach
    96     // dem Drehen auf 1024x768). 17.09.2026
    9784    let heightTimer = null;
    9885    if (typeof window !== "undefined" && window.addEventListener) {
    … …  
    248235
    249236        // --- BREATHING ROOM DELAY (skip if old step was interrupted) ---
    250         // Bei "Bewegung reduzieren" faellt die Pause weg: sonst bleibt der
    251         // Bildschirm bei der Standardgeschwindigkeit 0.75s lang leer, obwohl
    252         // gar nichts mehr animiert wird.
    253237        let reducedMotion = false;
    254238        try {
    … …  
    436420        });
    437421
    438         // iOS wendet `:active` auf ein <button> nur an, wenn das Element selbst
    439         // einen Touch-Listener traegt - ein bekannter Eigensinn von Safari.
    440         // Ohne diese leere Zeile bleiben die Beruehrungs-Effekte auf iPhones
    441         // aus, waehrend sie auf Android laufen. `passive`, damit das Scrollen
    442         // nicht ausgebremst wird.
     422        // iOS applies :active only to elements with a touch listener.
    443423        $btn[0].addEventListener("touchstart", function () {}, { passive: true });
    444424
    … …  
    473453      $row.append($headline);
    474454
    475       // Das Feld im Backend ist ein <textarea>: wer dort Enter drueckt, meint
    476       // einen Umbruch. HTML frisst ihn, also wird er zu <br>. Ergebnistext,
    477       // Hinweistext, Formularbeschreibungen und die Limit-Meldung machen das
    478       // laengst - die Seitenbeschreibung war die Ausnahme.
    479455      const $desc = $("<p />", {
    480456        class: "funnel-description text-center col-12",
Note: See TracChangeset for help on using the changeset viewer.