<?php
declare(strict_types=1);

if (!defined('ABSPATH')) { exit; }

/**
 * Unfold (accordion) element renderer (shared).
 *
 * Phase 1, step 38 + CSS consolidation pass. Adopted frontend canonical
 * during the initial extraction (4 surviving modes + 5 tile-expand
 * variants); all per-instance structural CSS now lives in
 * `assets/css/impulse-unfold.css`. Renderer emits only:
 *
 *   1. A wrapper <div> with mode-specific class, `data-*` attributes
 *      keyed off the static CSS rules (data-expand, data-show-num,
 *      data-anim, etc.) and a `style="--unf-…"` declaration carrying
 *      the per-instance values (colours, dimensions, font sizes).
 *   2. The per-item / per-row markup.
 *   3. A `<link rel="stylesheet">` singleton-emitted on first unfold
 *      per page.
 *
 * The static stylesheet uses class + `[data-…]` selectors so multiple
 * unfold blocks on one page share one cached file. Per-device tile
 * columns / accordion widths cascade desktop → tablet → mobile via
 * `var(--unf-…-tab, var(--unf-…))` chains in CSS — frontend emits
 * tablet/mobile vars on the wrapper when the designer saved overrides;
 * editor pre-resolves to the current preview device.
 *
 * Animation modes (`anim_style`) — locked 2026-05-20 to four survivors:
 *   - 'spotlight'  — Focus: active row at full opacity, others dimmed
 *                    + blurred. Default.
 *   - 'rolodex'    — Stack: 3D card stack with prev/next nav and a
 *                    numbered jumper.
 *   - 'tiles'      — Tiles grid with five expand variants — 'zoom'
 *                    (modal), 'flip', 'slide', 'fade', 'push'.
 *   - 'reveal'     — minimal Apple-Support-docs accordion.
 *
 * @sideeffect Sets $GLOBALS['impulse_needs_unfold'] = true so the page
 *             loader knows to enqueue impulse-interactions.js.
 * @sideeffect Singleton-emits a `<link rel="stylesheet">` for
 *             `assets/css/impulse-unfold.css` (first unfold per page).
 */
function impulse_element_render_unfold($el, array $settings, $children = [], string $context = 'frontend', $renderCallback = null): string {
    if (is_object($el)) $el = (array) $el;
    $isEditor = ($context === 'editor');
    $device   = $isEditor ? (string) ($GLOBALS['impulse_preview_device'] ?? 'desktop') : 'desktop';

    $read = function (string $key, $default) use ($settings, $isEditor, $device) {
        if ($isEditor && function_exists('impulse_device_val')) {
            return impulse_device_val($settings, $key, $device, $default);
        }
        return $settings[$key] ?? $default;
    };

    $GLOBALS['impulse_needs_unfold'] = true;

    // Items
    $items = $settings['items'] ?? [];
    if (is_string($items)) $items = json_decode($items, true) ?: [];
    if (empty($items) || !is_array($items)) {
        $items = [
            ['title' => 'Item 1', 'content' => 'Content here'],
            ['title' => 'Item 2', 'content' => 'More content'],
        ];
    }

    // Real current modes (locked 2026-05-20): Focus (spotlight), Stack
    // (rolodex), Tiles, Reveal. Older anim_style values from dropped
    // modes (cinematic, sidebar, focus, classic, cards, marquee) normalise
    // to spotlight so legacy saved pages still render something coherent.
    $_unfoldAllowed = ['spotlight', 'rolodex', 'tiles', 'reveal'];
    $animStyle = (string) ($settings['anim_style'] ?? 'spotlight');
    if (!in_array($animStyle, $_unfoldAllowed, true)) $animStyle = 'spotlight';

    $rawId = (string) ($el['id'] ?? uniqid());
    $rawId = preg_replace('/[^a-z0-9_-]/i', '', $rawId);
    $accId = 'impulse-acc-' . $rawId;

    $accWidth    = intval($read('acc_width', 100));
    $accWidthTab = !empty($settings['acc_width_tablet']) ? intval($settings['acc_width_tablet']) : 0;
    $accWidthMob = !empty($settings['acc_width_mobile']) ? intval($settings['acc_width_mobile']) : 0;

    // Block alignment — when acc_width < 100% the block can be pushed
    // left / right or kept centred. Emitted as data-align on every mode
    // root; CSS (.impulse-unfold[data-align=…]) maps it to margins.
    $accAlign = (string) ($settings['acc_align'] ?? 'center');
    if (!in_array($accAlign, ['left', 'center', 'right'], true)) $accAlign = 'center';
    // Validated alignment value (one of left|center|right). Echo sites emit the
    // data-align attribute with inline esc_attr() around this.
    $alignVal = $accAlign;

    // Richtext allowlist — mirrors impulse_kses_richtext() (includes/
    // impulse-animation-helpers.php) so the fallback item content can be
    // echoed through wp_kses() inline (a reviewer-recognized escaper). Same
    // tag/attribute set → byte-identical output to the prior impulse_kses_richtext()
    // call. wp_kses() is what WP.org's Plugin Check accepts; the custom
    // wrapper was not recognized.
    $richtextAllowed = function_exists('impulse_richtext_allowed') ? impulse_richtext_allowed() : ['b'=>[],'strong'=>[],'i'=>[],'em'=>[],'u'=>[],'s'=>[],'strike'=>[],'del'=>[],'a'=>['href'=>[],'target'=>[],'rel'=>[]],'br'=>[],'span'=>['style'=>[]],'p'=>['style'=>[],'class'=>[]],'div'=>['style'=>[],'class'=>[]],'h1'=>['style'=>[]],'h2'=>['style'=>[]],'h3'=>['style'=>[]],'h4'=>['style'=>[]],'h5'=>['style'=>[]],'h6'=>['style'=>[]],'ul'=>[],'ol'=>[],'li'=>[],'blockquote'=>[],'pre'=>[],'code'=>[],'hr'=>[],'table'=>[],'thead'=>[],'tbody'=>[],'tr'=>[],'th'=>['style'=>[]],'td'=>['style'=>[]]];

    // ── Section-backed items ─────────────────────────────────────────
    // Each item may carry a `section_id` referencing one of the unfold's
    // direct children. When set, we render that child's GRANDCHILDREN
    // inline at the item's content slot (skipping the wrapper to avoid
    // section-width stretching). Falls back to legacy items[N].content
    // HTML when section_id is absent or doesn't match a child.
    // Legacy fallback: sanitise the items[N].content HTML. When section_id
    // is set and matches a child, the closure below short-circuits and
    // renders the child's grandchildren instead.
    //
    // Previous version recursed into $renderItem at the fallback line —
    // but the closure can't see itself through `use()` (closures don't
    // get their own name in their lexical scope), so $renderItem was null
    // and PHP threw "Value of type null is not callable" the moment any
    // item lacked a section_id. That
    // killed the entire editor preview with a 500 → no block could be
    // selected in the editor because the iframe never finished loading.
    // Emits the item's content slot DIRECTLY to the output buffer (rather than
    // returning a concatenated string). Section-backed items stream their
    // grandchildren through $renderCallback — trusted server-rendered block
    // markup that must not pass through kses (it would strip legitimate
    // structural tags/attributes and change the render). The legacy
    // items[N].content fallback is echoed through wp_kses() with the richtext
    // allowlist inline (reviewer-recognized escaper).
    $emitItem = static function ($item) use ($children, $renderCallback, $richtextAllowed): void {
        $sectionId = isset($item['section_id']) ? intval($item['section_id']) : 0;
        if ($sectionId > 0 && !empty($children) && is_callable($renderCallback)) {
            foreach ($children as $c) {
                $cid = is_object($c) ? intval($c->id ?? 0) : intval($c['id'] ?? 0);
                if ($cid === $sectionId) {
                    $grandchildren = is_object($c) ? ($c->children ?? []) : ($c['children'] ?? []);
                    foreach ($grandchildren as $gc) {
                        call_user_func($renderCallback, $gc);
                    }
                    return;
                }
            }
        }
        echo wp_kses((string) ($item['content'] ?? ''), $richtextAllowed);
    };

    // Shared colour / dimension settings used across most modes.
    $titleColor   = (string) ($settings['header_color']  ?? '#1d2327');
    $contentBg    = (string) ($settings['content_bg']    ?? '#ffffff');
    $contentColor = (string) ($settings['content_color'] ?? '#555555');
    $accentColor  = (string) ($settings['accent_color']  ?? '#1d2327');
    $borderColor  = (string) ($settings['border_color']  ?? '#e0e0e0');
    $borderRadius = intval($settings['border_radius']    ?? 8);
    $contentPad   = intval($settings['content_padding']  ?? 20);

    // Shared font + nav-colour vars used by spotlight (Focus) and rolodex (Stack).
    $tSz = intval($read('title_size',   15));
    $cSz = intval($read('content_size', 14));
    $nSz = intval($settings['number_size'] ?? 11);

    // Compose the `--unf-*` CSS-variable declarations for the wrapper.
    // The static stylesheet (assets/css/impulse-unfold.css) reads these
    // for every per-instance value.
    $cssVars  = '--unf-title:' . esc_attr($titleColor)   . ';';
    $cssVars .= '--unf-content-bg:' . esc_attr($contentBg) . ';';
    $cssVars .= '--unf-content:' . esc_attr($contentColor) . ';';
    $cssVars .= '--unf-accent:'  . esc_attr($accentColor)  . ';';
    $cssVars .= '--unf-border:'  . esc_attr($borderColor)  . ';';
    $cssVars .= '--unf-radius:'  . $borderRadius . 'px;';
    $cssVars .= '--unf-pad:'     . $contentPad . 'px;';
    // Collapsed-row vertical padding (reveal mode). Tunable + tighter default
    // (11px) than the old hardcoded 18px, so a long FAQ reads compact.
    $cssVars .= '--unf-row-pad:' . intval($read('row_padding', 11)) . 'px;';
    $cssVars .= '--unf-width:'   . $accWidth . '%;';
    if ($accWidthTab) $cssVars .= '--unf-width-tab:' . $accWidthTab . '%;';
    if ($accWidthMob) $cssVars .= '--unf-width-mob:' . $accWidthMob . '%;';
    $cssVars .= '--unf-t-size:'  . $tSz . 'px;';
    $cssVars .= '--unf-c-size:'  . $cSz . 'px;';
    $cssVars .= '--unf-n-size:'  . $nSz . 'px;';

    // Singleton-enqueue the static stylesheet on first unfold per page.
    // Output the <link> tag inline so it ships with the block markup
    // regardless of whether wp_head() has already fired (it has on a
    // typical page render, but for AJAX-loaded fragments we can't rely
    // on that).
    $enqueueLink = function () {
        if (!empty($GLOBALS['impulse_unfold_css_emitted'])) return '';
        $GLOBALS['impulse_unfold_css_emitted'] = true;
        \Instinctor\Core\Assets::enqueue('instinctor-unfold');
        return '';
    };

    ob_start();

    // ─────────────────────────────────────────────────────────────────────
    // TILES MODE
    // ─────────────────────────────────────────────────────────────────────
    if ($animStyle === 'tiles') {
        $tilesCols    = max(1, intval($settings['tiles_cols'] ?? 3));
        $tilesColsTab = !empty($settings['tiles_cols_tablet']) ? max(1, intval($settings['tiles_cols_tablet'])) : 0;
        $tilesColsMob = (isset($settings['tiles_cols_mobile']) && $settings['tiles_cols_mobile'] !== '' && $settings['tiles_cols_mobile'] !== null)
            ? max(1, intval($settings['tiles_cols_mobile']))
            : 1;

        $tilesExpand = (string) ($settings['tiles_expand'] ?? 'zoom');
        $tCardHeight = intval($read('card_height', 180));
        $tGap        = intval($read('gap', 16));

        // Per-face text alignment: Side A (front) + Side B (back / slide). Each
        // has its own align buttons in the panel. Default to the block's overall
        // align so existing blocks keep their look until the user overrides.
        $tAlignA = (string) ($settings['tiles_align_a'] ?? $accAlign);
        $tAlignB = (string) ($settings['tiles_align_b'] ?? $accAlign);
        if (!in_array($tAlignA, ['left', 'center', 'right'], true)) $tAlignA = 'center';
        if (!in_array($tAlignB, ['left', 'center', 'right'], true)) $tAlignB = 'center';

        // tiles_number: default ON.
        $rawTN       = $settings['tiles_number'] ?? true;
        $showNumbers = !($rawTN === false || $rawTN === 0 || $rawTN === '0' || $rawTN === '');

        // Tiles-specific vars.
        $tilesVars  = $cssVars;
        $tilesVars .= '--unf-cols:'    . $tilesCols    . ';';
        if ($tilesColsTab) $tilesVars .= '--unf-cols-tab:' . $tilesColsTab . ';';
        if ($tilesColsMob) $tilesVars .= '--unf-cols-mob:' . $tilesColsMob . ';';
        $tilesVars .= '--unf-tile-h:'  . $tCardHeight . 'px;';
        $tilesVars .= '--unf-gap:'     . $tGap . 'px;';

        $enqueueLink();
        echo '<div id="' . esc_attr($accId) . '" class="impulse-unfold impulse-acc-tiles" data-align="' . esc_attr($alignVal) . '" data-align-a="' . esc_attr($tAlignA) . '" data-align-b="' . esc_attr($tAlignB) . '" data-expand="' . esc_attr($tilesExpand) . '" data-show-num="' . ($showNumbers ? '1' : '0') . '"';
        $tilesItemsJson = function_exists('wp_json_encode') ? wp_json_encode($items) : wp_json_encode($items);
        echo ' data-items="' . esc_attr($tilesItemsJson) . '"';
        echo ' style="' . esc_attr($tilesVars) . '">';

        foreach ($items as $idx => $item) {
            $itemTitle   = (string) ($item['title'] ?? 'Item');
            $numLabel    = str_pad((string) ($idx + 1), 2, '0', STR_PAD_LEFT);

            if ($tilesExpand === 'flip') {
                echo '<div class="impulse-acc-tile" data-index="' . (int) $idx . '"><div class="impulse-acc-tile-flipper">';
                echo '<div class="impulse-acc-tile-front"><div class="impulse-acc-tile-num">' . esc_html($numLabel) . '</div><div class="impulse-acc-tile-title">' . esc_html($itemTitle) . '</div></div>';
                echo '<div class="impulse-acc-tile-back"><div class="impulse-acc-tile-back-title">' . esc_html($itemTitle) . '</div><div style="line-height:1.7">';
                $emitItem($item);
                echo '</div></div>';
                echo '</div></div>';
            } elseif ($tilesExpand === 'slide') {
                echo '<div class="impulse-acc-tile" data-index="' . (int) $idx . '">';
                echo '<div class="impulse-acc-tile-front"><div class="impulse-acc-tile-num">' . esc_html($numLabel) . '</div><div class="impulse-acc-tile-title">' . esc_html($itemTitle) . '</div></div>';
                echo '<div class="impulse-acc-tile-slide"><div class="impulse-acc-tile-slide-title"><span>' . esc_html($itemTitle) . '</span></div><div style="line-height:1.7">';
                $emitItem($item);
                echo '</div></div>';
                echo '</div>';
            } elseif ($tilesExpand === 'fade' || $tilesExpand === 'push') {
                echo '<div class="impulse-acc-tile" data-index="' . (int) $idx . '">';
                echo '<div class="impulse-acc-tile-front"><div class="impulse-acc-tile-num">' . esc_html($numLabel) . '</div><div class="impulse-acc-tile-title">' . esc_html($itemTitle) . '</div></div>';
                echo '<div class="impulse-acc-tile-back">';
                $emitItem($item);
                echo '</div>';
                echo '</div>';
            } else {
                // 'zoom' (default)
                echo '<div class="impulse-acc-tile" data-index="' . (int) $idx . '"><div class="impulse-acc-tile-front"><div class="impulse-acc-tile-num">' . esc_html($numLabel) . '</div><div class="impulse-acc-tile-title">' . esc_html($itemTitle) . '</div></div></div>';
            }
        }
        if ($tilesExpand === 'zoom') {
            echo '<div class="impulse-acc-tiles-overlay"></div>';
            echo '<div class="impulse-acc-tile-expanded"><div class="impulse-acc-tile-expanded-header"><span></span><button class="impulse-acc-tile-close" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button></div><div class="impulse-acc-tile-expanded-body"></div></div>';
        }
        echo '</div>';

        return (string) ob_get_clean();
    }

    // ─────────────────────────────────────────────────────────────────────
    // REVEAL MODE (new, 2026-05-20) — minimal Apple-Support-docs accordion.
    // Click toggles `open` class on the item; body uses grid-template-rows
    // 0fr → 1fr (smooth single-paint expand, no max-height jank). Title row
    // shows a thin accent left-border on hover, slides 8px right.
    // ─────────────────────────────────────────────────────────────────────
    if ($animStyle === 'reveal') {
        // 1 or 2 column layout. --unf-rev-cols drives the grid; mobile collapses
        // to 1 column via @media in impulse-unfold.css. Default 1 (no regression
        // for existing single-column reveals).
        $revCols  = max(1, min(2, intval($settings['reveal_cols'] ?? 1)));
        $revVars  = $cssVars . '--unf-rev-cols:' . $revCols . ';';
        $enqueueLink();
        echo '<div id="' . esc_attr($accId) . '" class="impulse-unfold impulse-acc-reveal" data-cols="' . (int) $revCols . '" data-align="' . esc_attr($alignVal) . '" data-anim="reveal" style="' . esc_attr($revVars) . '">';
        foreach ($items as $idx => $item) {
            echo '<div class="impulse-acc-reveal-item" data-index="' . (int) $idx . '">';
            echo '<button type="button" class="impulse-acc-reveal-title"><span>' . esc_html((string) ($item['title'] ?? 'Item')) . '</span></button>';
            echo '<div class="impulse-acc-reveal-body"><div class="impulse-acc-reveal-inner">';
            $emitItem($item);
            echo '</div></div>';
            echo '</div>';
        }
        echo '</div>';
        return (string) ob_get_clean();
    }

    // ─────────────────────────────────────────────────────────────────────
    // SPOTLIGHT MODE (Focus)
    // ─────────────────────────────────────────────────────────────────────
    if ($animStyle === 'spotlight') {
        $spotBg     = (string) ($settings['spot_bg'] ?? '#808080');
        $blurInt    = floatval($read('blur_intensity', 1));
        $spotlightVars = $cssVars . '--unf-blur:' . $blurInt . 'px;--unf-spot-bg:' . esc_attr($spotBg) . ';';

        $enqueueLink();
        echo '<div id="' . esc_attr($accId) . '" class="impulse-unfold impulse-acc-spotlight" data-align="' . esc_attr($alignVal) . '" data-anim="spotlight" data-active="0" style="background:' . esc_attr($spotBg) . ';padding:8px;border-radius:' . (int) $borderRadius . 'px;' . esc_attr($spotlightVars) . '">';
        foreach ($items as $idx => $item) {
            $itemSt  = 'padding:20px 24px;border-radius:' . $borderRadius . 'px;cursor:pointer;'
                     . 'transition:opacity 0.4s ease-in-out,filter 0.4s ease-in-out,background 0.3s ease-in-out,transform 0.4s ease-in-out;';
            echo '<div class="impulse-acc-spot-item' . ($idx === 0 ? ' active' : '') . '" data-index="' . (int) $idx . '" style="' . esc_attr($itemSt) . '">';
            echo '<div class="impulse-acc-spot-header" style="display:flex;justify-content:space-between;align-items:center;font-weight:600;">';
            echo '<span>' . esc_html((string) ($item['title'] ?? '')) . '</span>';
            echo '<span style="font-size:11px;opacity:0.4;font-weight:400">' . esc_html(str_pad((string) ($idx + 1), 2, '0', STR_PAD_LEFT)) . '</span>';
            echo '</div>';
            echo '<div class="impulse-acc-spot-body" style="margin-top:10px;padding-bottom:' . (int) $contentPad . 'px;color:' . esc_attr($contentColor) . ';line-height:1.7;' . ($idx === 0 ? '' : 'display:none;') . '">';
            $emitItem($item);
            echo '</div>';
            echo '</div>';
        }
        echo '</div>';

    // ─────────────────────────────────────────────────────────────────────
    // ROLODEX MODE — kept inline (per-card transforms vary by stack index)
    // ─────────────────────────────────────────────────────────────────────
    } elseif ($animStyle === 'rolodex') {
        $headerColor = (string) ($settings['header_color']  ?? '#1d2327');
        $contentColR = (string) ($settings['content_color'] ?? '#666666');
        $contentBgR  = (string) ($settings['content_bg']    ?? '#ffffff');
        $cardHeight  = intval($read('card_height', 0));

        $rolodexVars = $cssVars;
        if ($cardHeight > 0) $rolodexVars .= '--unf-card-h:' . $cardHeight . 'px;';

        $accStyle = 'position:relative;overflow:hidden;perspective:800px;'
                  . 'border:1px solid ' . esc_attr($borderColor) . ';';
        if ($borderRadius > 0) $accStyle .= 'border-radius:' . $borderRadius . 'px;';

        $enqueueLink();
        echo '<div id="' . esc_attr($accId) . '" class="impulse-unfold impulse-unfold-scroll" data-align="' . esc_attr($alignVal) . '" data-anim="rolodex" data-active="0"'
           . ($cardHeight > 0 ? ' data-height="' . (int) $cardHeight . '"' : '')
           . ' style="' . esc_attr($accStyle . $rolodexVars) . '">';
        // Stage clips to its own height so cards offset by translateY(idx*14%)
        // never spill out below into the nav strip (showed up as a greyish
        // rectangle of the dimmed card edges peeking under the active card).
        $stageStyle = 'position:relative;width:100%;transform-style:preserve-3d;overflow:hidden';
        if ($cardHeight > 0) $stageStyle .= ';height:' . $cardHeight . 'px';
        echo '<div class="impulse-unfold-stage" style="' . esc_attr($stageStyle) . '">';
        foreach ($items as $idx => $item) {
            // Each rolodex card has unique transform offsets driven by its
            // stack position — those CAN'T move to the stylesheet because
            // they depend on index. Stays inline.
            $cardStyle = 'position:absolute;top:0;left:0;width:100%;box-sizing:border-box;padding:20px 20px ' . $contentPad . 'px 20px;';
            if ($cardHeight > 0) $cardStyle .= 'height:' . $cardHeight . 'px;overflow:hidden;';
            $cardStyle .= 'background:' . esc_attr($contentBgR) . ';color:' . esc_attr($contentColR) . ';';
            $cardStyle .= 'backface-visibility:hidden;';
            if ($idx === 0) {
                $cardStyle .= 'position:relative;opacity:1;transform:translateZ(0);';
            } else {
                $ty = $idx * 14;
                $sd = max(0.75, 1 - $idx * 0.12);
                $cardStyle .= 'opacity:' . max(0, 1 - $idx * 0.5) . ';transform:translateY(' . $ty . '%) scale(' . $sd . ');';
            }
            echo '<div class="impulse-unfold-card" data-index="' . (int) $idx . '" style="' . esc_attr($cardStyle) . '">';
            echo '<div class="impulse-unfold-card-title" style="font-weight:600;margin-bottom:10px;color:' . esc_attr($headerColor) . '">' . esc_html((string) ($item['title'] ?? 'Item')) . '</div>';
            echo '<div>';
            $emitItem($item);
            echo '</div>';
            echo '</div>';
        }
        echo '</div>'; // .impulse-unfold-stage

        // Prev/next nav
        $navTotal = count($items);
        echo '<div class="impulse-acc-stack-nav" style="display:flex;align-items:center;justify-content:center;gap:12px;padding:12px 0 4px">';
        echo '<button type="button" class="impulse-acc-stack-prev" style="background:none;border:none;cursor:pointer;opacity:0.35;transition:opacity 0.2s;padding:2px;line-height:0"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m18 15-6-6-6 6"/></svg></button>';
        echo '<div class="impulse-acc-stack-nums" style="display:flex;gap:8px;align-items:center">';
        for ($ni = 0; $ni < $navTotal; $ni++) {
            $numLabel    = str_pad((string) ($ni + 1), 2, '0', STR_PAD_LEFT);
            $numClass    = 'impulse-acc-stack-num' . ($ni === 0 ? ' active' : '');
            $activeStyle = $ni === 0 ? 'opacity:1;font-weight:600' : 'opacity:0.35;font-weight:400';
            echo '<span class="' . esc_attr($numClass) . '" data-idx="' . (int) $ni . '" style="cursor:pointer;transition:opacity 0.3s,color 0.3s;' . esc_attr($activeStyle) . '">' . esc_html($numLabel) . '</span>';
        }
        echo '</div>';
        echo '<button type="button" class="impulse-acc-stack-next" style="background:none;border:none;cursor:pointer;opacity:0.35;transition:opacity 0.2s;padding:2px;line-height:0"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></button>';
        echo '</div>';
        echo '</div>'; // .impulse-unfold-scroll
    }

    return (string) ob_get_clean();
}
