return function () {
    $self = url('/@object/@action/' . urlencode('themes/bs_theme'));
    $is_bs5 = conf('css_framework') === 'bs5';
    $theme = clone _class('bs_theme');
    // the preview is laid over the theme of this page; pick another one with the theme switcher
    $base = $theme->base_of(common()->bs_current_theme()) ?: 'bootstrap';
    $defaults = ['primary' => '#0d6efd', 'body-bg' => '#ffffff', 'body-color' => '#212529', 'border-radius' => '0.375rem'];
    $radii = ['0' => 'square', '0.375rem' => 'default', '0.75rem' => 'round', '1.5rem' => 'pill-like'];
    $input = [];
    foreach ($defaults as $k => $v) {
        $input[$k] = is_string($_GET[$k] ?? null) ? $_GET[$k] : $v;
    }
    // Validation is yf_bs_theme's own: only #rgb / #rrggbb colours and rem / em / px lengths survive, anything else is dropped
    $theme->THEMES = ['preview' => ['base' => $base] + $input];
    $theme->_noticed = [];
    $vars = @$theme->variables('preview');
    $dropped = array_diff_key($input, $vars);
    $css = $theme->css($vars, $base);

    // The generated sheet targets the whole page (:root and .btn-*); scoped under .docs-theme-preview it styles the preview box only
    $scoped = preg_replace_callback('~([^{}]+)\{([^{}]*)\}~', function ($m) {
        $selectors = [];
        foreach (explode(',', $m[1]) as $sel) {
            $sel = trim($sel);
            $selectors[] = in_array($sel, [':root', '[data-bs-theme=light]'], true) ? '.docs-theme-preview' : '.docs-theme-preview ' . $sel;
        }
        return implode(',', array_unique($selectors)) . '{' . $m[2] . '}';
    }, preg_replace('~/\*.*?\*/~s', '', $css));
    css($scoped . PHP_EOL . '.docs-theme-preview { background-color:var(--bs-body-bg); color:var(--bs-body-color); border:1px solid var(--bs-border-color, #ddd); border-radius:var(--bs-border-radius, 4px); padding:15px; margin-bottom:15px; }'
        . PHP_EOL . '.docs-demo-bs-theme-form input[type=color] { width:4em; height:2.2em; padding:2px; }'
        . PHP_EOL . '.docs-demo-css code { white-space:pre-wrap; word-break:break-all; }');

    $intro = '<p class="docs-demo-intro">A site colour scheme with <code>yf_bs_theme</code>, the Bootstrap 5 runtime theme layer: pick colours,
        and the framework computes the hover, active and focus shades and the contrasting text colour the way Bootstrap\'s Sass does, as a
        small stylesheet of <code>--bs-*</code> variables over the base theme (here <b>' . $base . '</b>, the current theme). Every value
        passes <code>yf_bs_theme::variables()</code>, which keeps only <code>#rgb</code> / <code>#rrggbb</code> colours and
        <code>rem</code> / <code>em</code> / <code>px</code> lengths, so a request cannot inject CSS. Nothing is saved; the preview is
        scoped to the box below. Classes: <code>yf_bs_theme</code>, <code>yf_common</code>.</p>';
    $notes = '';
    if ( ! $is_bs5) {
        $notes .= '<div class="alert alert-warning">This page runs Bootstrap 3, which has no CSS variables: a site theme renders its base
            there. Switch to <a href="' . _prepare_html(_class('docs')->_bs_switch_url('5'), 0) . '">Bootstrap 5</a> to see the preview.</div>';
    }
    if ($dropped) {
        $notes .= '<div class="alert alert-danger">Dropped as invalid: ' . _prepare_html(implode(', ', array_keys($dropped)), 0) . '.</div>';
    }
    $fields = '';
    foreach (['primary' => 'Primary', 'body-bg' => 'Body background', 'body-color' => 'Body text'] as $k => $label) {
        $value = $vars[$k] ?? $defaults[$k];
        $fields .= '<label class="docs-demo-color">' . $label . ' <input type="color" name="' . $k . '" value="' . _prepare_html($value, 0) . '"></label> ';
    }
    $options = '';
    foreach ($radii as $value => $label) {
        $options .= '<option value="' . $value . '"' . (($vars['border-radius'] ?? $defaults['border-radius']) === $value ? ' selected' : '') . '>' . $label . '</option>';
    }
    $form = '<form method="get" action="' . $self . '" class="form-inline docs-demo-bs-theme-form">' . $fields
        . '<label>Corners <select name="border-radius" class="form-control form-select d-inline-block w-auto">' . $options . '</select></label> '
        . '<button type="submit" class="btn btn-primary">Preview</button> <a class="btn btn-default btn-outline-secondary" href="' . $self . '">Reset</a></form>';
    $preview = '<div class="docs-theme-preview">'
        . '<h4>Preview</h4><p>Body text with <a href="#">a link</a>.</p>'
        . '<p><button type="button" class="btn btn-primary">Primary</button> <button type="button" class="btn btn-outline-primary">Outline</button> '
        . '<button type="button" class="btn btn-secondary">Secondary</button> <span class="badge text-bg-primary">badge</span></p>'
        . '<ul class="nav nav-pills mb-2"><li class="nav-item"><a class="nav-link active" href="#">Active pill</a></li><li class="nav-item"><a class="nav-link" href="#">Pill</a></li></ul>'
        . '<div class="progress mb-2"><div class="progress-bar" style="width:40%">40%</div></div>'
        . '<input class="form-control" placeholder="Focus me"></div>';
    $config = '$PROJECT_CONF[\'bs_theme\'][\'THEMES\'][\'brand\'] = ' . var_export(['base' => $base] + $vars, true) . ';' . PHP_EOL
        . '$CONF[\'DEF_BOOTSTRAP_THEME_USER\'] = \'brand\';';
    return $intro . $notes . $form . $preview
        . '<h4>Project config for this theme</h4><pre><code class="php">' . _prepare_html($config, 0) . '</code></pre>'
        . '<h4>Generated stylesheet</h4><pre class="docs-demo-css"><code class="css">' . _prepare_html($css, 0) . '</code></pre>';
};

A site colour scheme with yf_bs_theme, the Bootstrap 5 runtime theme layer: pick colours, and the framework computes the hover, active and focus shades and the contrasting text colour the way Bootstrap's Sass does, as a small stylesheet of --bs-* variables over the base theme (here slate, the current theme). Every value passes yf_bs_theme::variables(), which keeps only #rgb / #rrggbb colours and rem / em / px lengths, so a request cannot inject CSS. Nothing is saved; the preview is scoped to the box below. Classes: yf_bs_theme, yf_common.

This page runs Bootstrap 3, which has no CSS variables: a site theme renders its base there. Switch to Bootstrap 5 to see the preview.
Reset

Preview

Body text with a link.

badge

40%

Project config for this theme

$PROJECT_CONF['bs_theme']['THEMES']['brand'] = array (
  'base' => 'slate',
  'primary' => '#0d6efd',
  'body-bg' => '#ffffff',
  'body-color' => '#212529',
  'border-radius' => '0.375rem',
);
$CONF['DEF_BOOTSTRAP_THEME_USER'] = 'brand';

Generated stylesheet

/* yf bs_theme v1 over slate */
:root,[data-bs-theme=light]{
--bs-primary:#0d6efd;
--bs-primary-rgb:13,110,253;
--bs-primary-text-emphasis:#052c65;
--bs-primary-bg-subtle:#cfe2ff;
--bs-primary-border-subtle:#9ec5fe;
--bs-focus-ring-color:rgba(13,110,253,.25);
--bs-body-bg:#ffffff;
--bs-body-bg-rgb:255,255,255;
--bs-body-color:#212529;
--bs-body-color-rgb:33,37,41;
--bs-border-radius:0.375rem
}
.btn-primary{--bs-btn-color:#ffffff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-disabled-color:#ffffff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}
.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}
.text-bg-primary{color:#ffffff!important}
.nav-pills{--bs-nav-pills-link-active-bg:#0d6efd}
.pagination{--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-focus-box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}
.dropdown-menu{--bs-dropdown-link-active-bg:#0d6efd}
.list-group{--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd}
.progress,.progress-stacked{--bs-progress-bar-bg:#0d6efd}
.form-control:focus,.form-select:focus,.form-check-input:focus{border-color:#86b7fe;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}
.form-check-input:checked,.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd}
.form-range::-webkit-slider-thumb{background-color:#0d6efd}.form-range::-moz-range-thumb{background-color:#0d6efd}