Legacy grids: published head stacks pre-hook card grids on phones (no re-save)

Stored pages (generated at editor-save time) from before release 2026.11.5 carry
FeaturesGrid / Testimonials / NumberCounter grids without data-wsb-grid, so a
republish kept repeat(N,1fr) on phones (OLS home: body 594px wide at 360).
The <=767px block now matches the exact inline styles those emitters stored
(whole-attribute signatures; no spaces, as in git history and every stored
OLS/TTM page). Gallery shares the prefix with a free gap and keeps its columns:
the two colliding signatures carry :not(:has(> * > Gallery thumbnail)) in their
own rule (a browser without :has drops only that rule). Customer grids never
match. Mirrored byte-for-byte by WHP generateCompiledHTML (fix/legacy-grid-backend).

Tests: vitest selector contract (legacy shapes match, == current emitters minus
the hook, Gallery and customer grids never match, pretty == minified); head
parity pins updated; new real-browser suite test:browser:legacy-grid (Chromium +
Firefox, 360/390/768/1280) on the stored OLS home page + synthetic legacy grids;
colors suite folds the new rules back like the mobile-guard ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-11-01 16:26:11 -08:00
1 parent 322742aa81
commit 96670b94f8
13 files changed
+460 -12

No files matched your search

+10 -1
View File
@@ -472,12 +472,21 @@ Every component has a static `toHtml(props, childrenHtml)` method. The `html-exp
Set `CHROMIUM_PATH` / `FIREFOX_PATH` if the Playwright-pinned browsers are not installed; record/compare a desktop
baseline across commits with `MOBILE_GUARD_RECORD=<file>` (on the base commit) / `MOBILE_GUARD_BASELINE=<file>`.
Run it whenever a component's `toHtml`, the exporter, or the head CSS changes.
- **Legacy grids (real browsers):** `npm run test:browser:legacy-grid` (Chromium + Firefox) renders STORED pre-hook
HTML (the OLS home page in `tests/fixtures/legacy-grid/`, plus every card-grid emitter with `data-wsb-grid`
stripped, colliding Galleries and an unwrapped customer grid) in the real head at 360/390/768/1280: no overflow,
legacy card grids stack on phones (and do NOT without the legacy rules), Galleries and customer grids keep their
columns, the legacy page is pixel-identical on phones to its re-saved form, and 768/1280 are box- and
pixel-identical with vs without the legacy rules.
- **Mobile hooks:** the head's `<=767px` block (RESPONSIVE_CSS in `utils/html-export.ts`, mirrored byte-for-byte by
WHP `generateCompiledHTML`) targets `[data-wsb-grid]` (component card grids -> one column), `body` (long words
wrap) and descendants of `[data-wsb-html]` (the inline `display:contents` wrapper the exporter puts around
HtmlBlock output: media shrink, `<pre>` scrolls on its own; tables are left alone). Never give that wrapper a box
or overflow: it clipped dropdowns, broke sticky and merged flex/grid items. A new component with an inline multi-column grid
should carry `data-wsb-grid`.
should carry `data-wsb-grid`. Pages stored before the hook are covered by whole-attribute `[style=...]` /
`[style^=...][style$=...]` signatures of the exact inline styles FeaturesGrid / Testimonials / NumberCounter stored
(Gallery excluded via `:not(:has(...))`), so republishing stacks them with no re-save. Those signatures are frozen: they
describe already-stored HTML, so keep them even if an emitter's inline style changes (new output carries the hook).
- **WHP integration:** Deploy to staging, verify save/load, verify PHP wrapper injection
## Development Notes
+1
View File
@@ -15,6 +15,7 @@
"test:browser:canvas": "node scripts/e2e-canvas-parity.mjs",
"test:browser:mobile": "node scripts/e2e-mobile-parity.mjs",
"test:browser:mobile-guard": "node scripts/e2e-mobile-guard.mjs",
"test:browser:legacy-grid": "node scripts/e2e-legacy-grid.mjs",
"test:browser:colorpick": "node scripts/e2e-color-pick.mjs",
"test:browser:colors": "node scripts/e2e-color-system.mjs",
"test:browser:sitesmith": "node scripts/e2e-sitesmith.mjs",
+7 -1
View File
@@ -34,6 +34,11 @@ const BASE_COMMIT = process.env.COLOR_BASE_COMMIT || '69fd34b';
// Deliberate head changes made AFTER the base, folded back before the comparison so it still proves the
// colour feature changed nothing else. Each `from` must occur exactly once (checked).
const HEAD_FOLDS = [
{
why: 'legacy-grid: stored (pre-hook) card-grid signatures right after [data-wsb-grid] in the <=767px block',
from: '[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat("][style$=",1fr);gap:32px;text-align:center"]{grid-template-columns:minmax(0,1fr)!important}[style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px"]:not(:has(>*>img[style^="width:100%;height:200px;object-fit:cover"])),[style^="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat("][style$=",1fr);gap:24px"]:not(:has(>*>img[style^="width:100%;height:200px;object-fit:cover"])){grid-template-columns:minmax(0,1fr)!important}',
to: '[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}',
},
{
why: 'mobile-guard: card-grid / long-word / HtmlBlock media rules in the <=767px block; Navbar wrap moved to <=991px',
from: '[class^="cols_"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}}',
@@ -41,7 +46,8 @@ const HEAD_FOLDS = [
},
];
const foldHead = (html) => HEAD_FOLDS.reduce((h, f) => (h.split(f.from).length === 2 ? h.replace(f.from, () => f.to) : h), html);
const foldsApplied = (html) => HEAD_FOLDS.map((f) => html.split(f.from).length - 1);
// Counted in order on the progressively folded document (a later fold's `from` may need an earlier fold applied).
const foldsApplied = (html) => HEAD_FOLDS.map((f) => { const n = html.split(f.from).length - 1; if (n === 1) html = html.replace(f.from, () => f.to); return n; });
const SHOTS = process.env.COLOR_SHOTS || '';
if (SHOTS) fs.mkdirSync(SHOTS, { recursive: true });
+51
View File
@@ -0,0 +1,51 @@
// Bundled (browser IIFE) by scripts/e2e-legacy-grid.mjs. Builds published documents from LEGACY stored HTML
// (pages saved before the data-wsb-grid hook existed) wrapped in the REAL Preview/publish head
// (exportComposedPreviewHtml: header + page + footer, as WHP handlePublish composes them; the head is byte-identical
// to WHP generateCompiledHTML, pinned on both sides by the head-parity tests).
import { exportBodyHtml, exportComposedPreviewHtml, RESPONSIVE_CSS_MINIFIED } from '../src/utils/html-export';
import { componentResolver } from '../src/components/resolver';
import olsHeader from '../tests/fixtures/legacy-grid/ols.header.html';
import olsIndex from '../tests/fixtures/legacy-grid/ols.index.html';
import olsFooter from '../tests/fixtures/legacy-grid/ols.footer.html';
import olsDesign from '../tests/fixtures/legacy-grid/ols.design.json';
type N = Record<string, any>;
const EMPTY = JSON.stringify({ ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: { tag: 'div', style: {} }, displayName: 'Container', custom: {}, hidden: false, nodes: [], linkedNodes: {} } });
const defaults = (type: string): Record<string, unknown> => {
const c = (componentResolver as any)[type];
return c?.craft?.props ? JSON.parse(JSON.stringify(c.craft.props)) : {};
};
const node = (type: string, props: Record<string, unknown>, extra: N = {}) => ({
type: { resolvedName: type }, isCanvas: type === 'Container', props: { ...defaults(type), ...props }, displayName: type,
custom: {}, hidden: false, nodes: [], linkedNodes: {}, ...extra,
});
const stripHook = (html: string) => html.replace(/ data-wsb-grid(="")?/g, '');
const doc = (body: string, title: string) => exportComposedPreviewHtml(EMPTY, body, { title, design: olsDesign as any });
/** Every card-grid emitter in its stored (pre-hook) form, Galleries whose gap collides with a card-grid
* signature, and an unwrapped customer grid (legacy HtmlBlock content has no data-wsb-html wrapper). */
function synthetic(): string {
const nodes: N = {};
const kids: string[] = [];
const add = (id: string, type: string, props: Record<string, unknown> = {}) => { nodes[id] = node(type, props, { parent: 'ROOT' }); kids.push(id); };
add('features', 'FeaturesGrid');
add('t2', 'Testimonials', { columns: 2 });
add('t3', 'Testimonials');
add('t4', 'Testimonials', { columns: 4 });
add('counter', 'NumberCounter', { columns: 4 });
add('gal', 'Gallery');
add('gal32', 'Gallery', { columns: 3, gap: '32px', lightbox: true });
add('gal24', 'Gallery', { columns: 4, gap: '24px' });
nodes.ROOT = node('Container', { tag: 'div', style: {} }, { nodes: kids });
const body = stripHook(exportBodyHtml(JSON.stringify(nodes)).html);
const customer = '<div id="customer" style="padding:16px"><div style="display:grid;grid-template-columns:repeat(3,1fr);gap:32px"><div>One</div><div>Two</div><div>Three</div></div></div>';
return olsHeader + '\n' + body + customer + '\n' + olsFooter;
}
(window as any).wsbLegacyGridDocs = (): Record<string, string> => ({
ols: doc(olsHeader + '\n' + olsIndex + '\n' + olsFooter, 'Home'),
// The same page as a re-save in 2026.11.5 would store it (hook added): phones must look identical.
olsResaved: doc(olsHeader + '\n' + olsIndex.replace('<div style="max-width:1100px;margin:0 auto;display:grid;', '<div data-wsb-grid style="max-width:1100px;margin:0 auto;display:grid;') + '\n' + olsFooter, 'Home'),
synthetic: doc(synthetic(), 'Legacy grids'),
});
(window as any).wsbResponsiveCss = RESPONSIVE_CSS_MINIFIED;
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env node
/**
* Legacy card grids: real-browser (Chromium AND Firefox, headless) proof that the published head stacks card grids
* stored BEFORE the data-wsb-grid hook existed (no re-save needed), and changes nothing else.
*
* npm run test:browser:legacy-grid
*
* Documents (scripts/e2e-legacy-grid-entry.ts, real exporter head):
* ols - the OLS Digital home page exactly as stored on the site (tests/fixtures/legacy-grid: header +
* page + footer, no data-wsb-* attributes; its FeaturesGrid is the legacy inline repeat(3,1fr))
* olsResaved - the same page with the hook added, i.e. what a re-save stores today
* synthetic - every card-grid emitter in its stored form (FeaturesGrid, Testimonials 2/3/4, NumberCounter 4),
* Galleries whose gap collides with a card-grid signature (3 cols gap 32px + lightbox, 4 cols gap
* 24px) plus a default one, and an unwrapped customer grid (legacy HtmlBlock content)
* measured at 360 / 390 / 768 / 1280.
*
* Asserted (every doc, every width unless noted):
* - no horizontal overflow: body.scrollWidth <= viewport and the right edge of every visible element box and text
* line <= viewport (html,body{overflow-x:clip} hides overflow from documentElement.scrollWidth), and the page
* cannot be scrolled sideways
* - <=767px: every legacy card grid is one card per row; every Gallery keeps more than one column; the customer
* grid keeps its 3 columns. Discriminating signal: with the legacy rules removed from the head, the same
* legacy card grids are back to several per row (so the rules, not something else, stack them)
* - <=767px: the legacy OLS page is pixel-identical to its re-saved (hooked) form
* - 768 and 1280: every element box AND the full-page screenshot are identical with vs without the legacy rules
*
* Env: CHROMIUM_PATH / FIREFOX_PATH (when the Playwright-pinned builds are not installed), LEGACY_ONLY=chromium|firefox.
*/
import { chromium, firefox } from '@playwright/test';
import { build as esbuild } from 'esbuild';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const out = await esbuild({
entryPoints: [path.join(root, 'scripts/e2e-legacy-grid-entry.ts')],
bundle: true, write: false, format: 'iife', platform: 'browser', jsx: 'automatic', logLevel: 'error',
loader: { '.html': 'text' },
define: { 'process.env.NODE_ENV': '"production"', __EDITOR_BUILD__: '"test"' },
});
const js = out.outputFiles[0].text;
const WIDTHS = [360, 390, 768, 1280];
const MOBILE_MAX = 767;
/** Runs in the page. */
function measure() {
const vw = window.innerWidth;
const skip = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE']);
const contained = (el) => {
for (let a = el.parentElement; a && a !== document.body && a !== document.documentElement; a = a.parentElement) {
if (getComputedStyle(a).overflowX !== 'visible' && a.getBoundingClientRect().right <= vw + 0.5) return true;
}
return false;
};
const label = (el) => `${el.tagName.toLowerCase()} "${(el.textContent || '').trim().slice(0, 24)}"`;
let maxRight = 0; let worst = '';
const bump = (right, el) => { if (right > maxRight) { maxRight = right; worst = label(el); } };
for (const el of document.body.querySelectorAll('*')) {
if (skip.has(el.tagName)) continue;
const r = el.getBoundingClientRect();
if (!(r.width || r.height) || getComputedStyle(el).visibility === 'hidden' || contained(el)) continue;
bump(r.right, el);
}
const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
for (let n = tw.nextNode(); n; n = tw.nextNode()) {
const el = n.parentElement;
if (!el || skip.has(el.tagName) || !n.textContent.trim() || contained(n)) continue;
const range = document.createRange(); range.selectNodeContents(n);
for (const r of range.getClientRects()) if (r.width) bump(r.right, el);
}
const grids = [...document.querySelectorAll('div')].filter((d) => getComputedStyle(d).display === 'grid').map((g) => {
const kids = [...g.children].filter((k) => !skip.has(k.tagName) && k.getBoundingClientRect().height > 0);
const firstTop = kids.length ? Math.round(kids[0].getBoundingClientRect().top) : 0;
const kind = g.closest('#customer') ? 'customer'
: g.querySelector(':scope > * > img[style^="width:100%;height:200px;object-fit:cover"]') ? 'gallery' : 'card';
return { kind, hook: g.hasAttribute('data-wsb-grid'), n: kids.length, perRow: kids.filter((k) => Math.round(k.getBoundingClientRect().top) === firstTop).length };
});
return { vw, docSW: document.documentElement.scrollWidth, bodySW: document.body.scrollWidth, maxRight: Math.round(maxRight * 10) / 10, worst, grids };
}
function sidewaysScroll() {
const y = window.scrollY;
window.scrollTo(5000, y);
const x = window.scrollX;
window.scrollTo(0, y);
return x;
}
function boxes() {
return [...document.body.querySelectorAll('*')]
.filter((e) => !['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(e.tagName))
.map((e) => { const r = e.getBoundingClientRect(); return [e.tagName, ...[r.x, r.y, r.width, r.height].map((v) => Math.round(v * 10) / 10)].join(','); });
}
let fail = 0;
const ok = (c, m) => { if (!c) { fail++; console.log(' FAIL', m); } };
const summary = [];
async function open(browser, html, w) {
const page = await browser.newPage({ viewport: { width: w, height: 900 } });
await page.route(/^https?:/, (r) => r.abort()); // deterministic: no fonts/CDN/images racing the layout
await page.setContent(html, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(50);
return page;
}
async function run(name, launcher, exe) {
const browser = await launcher.launch({ executablePath: exe || undefined, ...(name === 'chromium' ? { args: ['--no-sandbox'] } : {}) });
const gen = await browser.newPage();
await gen.setContent('<!doctype html><html><body></body></html>');
await gen.addScriptTag({ content: js });
const docs = await gen.evaluate(() => window.wsbLegacyGridDocs());
const css = await gen.evaluate(() => window.wsbResponsiveCss);
await gen.close();
// The legacy rules = everything between the [data-wsb-grid] rule and body{overflow-wrap...} in the head CSS.
const HOOK = '[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}';
const legacy = css.slice(css.indexOf(HOOK) + HOOK.length, css.indexOf('body{overflow-wrap'));
ok(legacy.includes('[style^=') && legacy.includes(':has('), `[${name}] could not isolate the legacy rules from the head CSS: ${legacy}`);
const bodyOf = (h) => h.slice(h.indexOf('<body'));
ok(bodyOf(docs.olsResaved).includes('data-wsb-grid') && !bodyOf(docs.ols).includes('data-wsb'), `[${name}] fixture: ols must be hook-free and olsResaved hooked`);
const without = Object.fromEntries(Object.entries(docs).map(([k, v]) => {
ok(v.split(legacy).length === 2, `[${name}] ${k}: the head does not carry the legacy rules exactly once`);
return [k, v.replace(legacy, '')];
}));
for (const [docName, html] of Object.entries(docs)) {
for (const w of WIDTHS) {
const tag = `[${name} ${w}] ${docName}`;
const mobile = w <= MOBILE_MAX;
const page = await open(browser, html, w);
const m = await page.evaluate(measure);
const sx = await page.evaluate(sidewaysScroll);
ok(sx === 0, `${tag}: the page scrolls sideways (scrollX=${sx})`);
ok(m.bodySW <= w, `${tag}: body.scrollWidth ${m.bodySW} > ${w}`);
ok(m.maxRight <= w + 0.5, `${tag}: content reaches x=${m.maxRight}: ${m.worst}`);
const base = await open(browser, without[docName], w);
const mb = await base.evaluate(measure);
const fmt = (g) => g.map((x) => `${x.kind[0]}${x.perRow}/${x.n}`).join(',');
summary.push(`${name.padEnd(8)} ${String(w).padEnd(4)} ${docName.padEnd(10)} bodySW=${m.bodySW} maxRight=${m.maxRight} grids=${fmt(m.grids)} | without rules: bodySW=${mb.bodySW} maxRight=${mb.maxRight}${mb.maxRight > w + 0.5 ? ' (' + mb.worst + ')' : ''} grids=${fmt(mb.grids)}`);
const cards = m.grids.filter((g) => g.kind === 'card' && g.n > 1);
if (docName !== 'olsResaved') ok(cards.length > 0, `${tag}: no legacy card grid found (fixture broken)`);
if (mobile) {
for (const g of cards) ok(g.perRow === 1, `${tag}: a legacy card grid keeps ${g.perRow} per row on a phone`);
for (const g of m.grids.filter((x) => x.kind === 'gallery')) ok(g.perRow > 1, `${tag}: a Gallery lost its columns (${g.perRow} per row)`);
for (const g of m.grids.filter((x) => x.kind === 'customer')) ok(g.perRow === 3, `${tag}: the customer grid was reflowed (${g.perRow} per row)`);
if (docName !== 'olsResaved') {
const legacyCards = mb.grids.filter((g) => g.kind === 'card' && g.n > 1 && !g.hook);
ok(legacyCards.length > 0 && legacyCards.every((g) => g.perRow > 1), `${tag}: without the legacy rules the card grids were expected to keep their columns (${fmt(legacyCards)})`);
}
} else {
for (const g of cards) ok(g.perRow > 1, `${tag}: a card grid lost its columns at ${w} (${g.perRow} per row)`);
const [b1, b2] = [await page.evaluate(boxes), await base.evaluate(boxes)];
const diffs = b1.length !== b2.length ? Infinity : b1.filter((b, i) => b !== b2[i]).length;
ok(diffs === 0, `${tag}: ${diffs} element boxes differ with vs without the legacy rules`);
const [s1, s2] = [await page.screenshot({ fullPage: true }), await base.screenshot({ fullPage: true })];
ok(s1.equals(s2), `${tag}: full-page pixels differ with vs without the legacy rules`);
}
if (mobile && docName === 'ols') {
const resaved = await open(browser, docs.olsResaved, w);
const [s1, s2] = [await page.screenshot({ fullPage: true }), await resaved.screenshot({ fullPage: true })];
ok(s1.equals(s2), `${tag}: the legacy page differs from its re-saved (hooked) form on a phone`);
await resaved.close();
}
await base.close();
await page.close();
}
}
await browser.close();
}
const only = process.env.LEGACY_ONLY;
if (!only || only === 'chromium') await run('chromium', chromium, process.env.CHROMIUM_PATH);
if (!only || only === 'firefox') await run('firefox', firefox, process.env.FIREFOX_PATH);
console.log('\n' + summary.join('\n'));
console.log(fail ? `\n${fail} FAILED` : '\nall passed');
process.exit(fail ? 1 : 0);
@@ -14,14 +14,14 @@ import { exportToHtml, exportBodyHtml, buildFontsLink, type ExportOptions } from
import { DEFAULT_SITE_DESIGN, type SiteDesign } from '../state/SiteDesignContext';
const PINNED_HEAD: Record<string, string> = {
A_defaults: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Joe's Pizza &amp; Grill</title>\n <meta property=\"og:title\" content=\"Joe&#39;s Pizza &amp; Grill\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
B_full: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta name=\"robots\" content=\"noindex, nofollow\">\n <title>About &quot;Us&quot;</title>\n <meta name=\"description\" content=\"We&#39;re &lt;the&gt; best &amp; &quot;only&quot; bakery\">\n <meta property=\"og:title\" content=\"About Joe&#39;s\">\n <meta property=\"og:description\" content=\"We&#39;re &lt;the&gt; best &amp; &quot;only&quot; bakery\">\n <meta property=\"og:image\" content=\"https://cdn.example.com/og.png\">\n <meta property=\"og:type\" content=\"website\">\n <meta name=\"twitter:card\" content=\"summary\">\n <link rel=\"icon\" href=\"https://cdn.example.com/favicon.ico\">\n <link rel=\"canonical\" href=\"https://example.com/about\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Roboto:wght@300;400;500;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation=\"fade-in\"].animated{animation-name:fadeIn}[data-animation=\"slide-up\"].animated{animation-name:slideUp}[data-animation=\"slide-left\"].animated{animation-name:slideLeft}[data-animation=\"slide-right\"].animated{animation-name:slideRight}[data-animation=\"zoom-in\"].animated{animation-name:zoomIn}[data-animation=\"bounce\"].animated{animation-name:bounce}:root{--wsb-primary:#ff0066;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:12px;--wsb-heading-font:Playfair Display, serif;--wsb-body-font:Roboto, sans-serif;--wsb-button-font:Roboto, sans-serif;--wsb-button-radius:999px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <noscript><style>[data-animation]{opacity:1}</style></noscript>\n <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-X\"></script>\n<meta name=\"google-site-verification\" content=\"abc123\">\n</head>",
C_unknown_font: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Contact</title>\n <meta name=\"description\" content=\"Get in touch\">\n <meta property=\"og:title\" content=\"Contact\">\n <meta property=\"og:description\" content=\"Get in touch\">\n <meta property=\"og:type\" content=\"website\">\n <meta name=\"twitter:card\" content=\"summary\">\n <link rel=\"canonical\" href=\"https://example.com/contact\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto:wght@300;400;500;700&family=Open+Sans:wght@300;400;600;700&family=Poppins:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@400;600;700&family=Merriweather:wght@300;400;700&family=Source+Code+Pro:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Comic Sans MS, cursive;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
D_body_font_and_blank_tokens: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Services</title>\n <meta property=\"og:title\" content=\"Services\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/services\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#111111;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
E_icons: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Icons</title>\n <meta property=\"og:title\" content=\"Icons\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/icons\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.min.css\" integrity=\"sha384-qrALq7+6jBOZIQsNnT6xGkMDru64qD6uTlDra39xrt2SoXl4pO3FX6Roz/RpR/BS\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/v4-shims.min.css\" integrity=\"sha384-BOBgwI2GEcZ4l/UA/+WOVnLChe2S4c7UhUCtdmX1m0v7Wdyk0S/tDoJOBci2ywvl\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
A_defaults: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Joe's Pizza &amp; Grill</title>\n <meta property=\"og:title\" content=\"Joe&#39;s Pizza &amp; Grill\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
B_full: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta name=\"robots\" content=\"noindex, nofollow\">\n <title>About &quot;Us&quot;</title>\n <meta name=\"description\" content=\"We&#39;re &lt;the&gt; best &amp; &quot;only&quot; bakery\">\n <meta property=\"og:title\" content=\"About Joe&#39;s\">\n <meta property=\"og:description\" content=\"We&#39;re &lt;the&gt; best &amp; &quot;only&quot; bakery\">\n <meta property=\"og:image\" content=\"https://cdn.example.com/og.png\">\n <meta property=\"og:type\" content=\"website\">\n <meta name=\"twitter:card\" content=\"summary\">\n <link rel=\"icon\" href=\"https://cdn.example.com/favicon.ico\">\n <link rel=\"canonical\" href=\"https://example.com/about\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Roboto:wght@300;400;500;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation=\"fade-in\"].animated{animation-name:fadeIn}[data-animation=\"slide-up\"].animated{animation-name:slideUp}[data-animation=\"slide-left\"].animated{animation-name:slideLeft}[data-animation=\"slide-right\"].animated{animation-name:slideRight}[data-animation=\"zoom-in\"].animated{animation-name:zoomIn}[data-animation=\"bounce\"].animated{animation-name:bounce}:root{--wsb-primary:#ff0066;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:12px;--wsb-heading-font:Playfair Display, serif;--wsb-body-font:Roboto, sans-serif;--wsb-button-font:Roboto, sans-serif;--wsb-button-radius:999px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <noscript><style>[data-animation]{opaLine truncated
C_unknown_font: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Contact</title>\n <meta name=\"description\" content=\"Get in touch\">\n <meta property=\"og:title\" content=\"Contact\">\n <meta property=\"og:description\" content=\"Get in touch\">\n <meta property=\"og:type\" content=\"website\">\n <meta name=\"twitter:card\" content=\"summary\">\n <link rel=\"canonical\" href=\"https://example.com/contact\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto:wght@300;400;500;700&family=Open+Sans:wght@300;400;600;700&family=Poppins:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@400;600;700&family=Merriweather:wght@300;400;700&family=Source+Code+Pro:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Comic Sans MS, cursive;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
D_body_font_and_blank_tokens: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Services</title>\n <meta property=\"og:title\" content=\"Services\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/services\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#111111;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
E_icons: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Icons</title>\n <meta property=\"og:title\" content=\"Icons\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/icons\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.min.css\" integrity=\"sha384-qrALq7+6jBOZIQsNnT6xGkMDru64qD6uTlDra39xrt2SoXl4pO3FX6Roz/RpR/BS\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/v4-shims.min.css\" integrity=\"sha384-BOBgwI2GEcZ4l/UA/+WOVnLChe2S4c7UhUCtdmX1m0v7Wdyk0S/tDoJOBci2ywvl\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
};
PINNED_HEAD.E_brand_swatches = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Brand</title>\n <meta property=\"og:title\" content=\"Brand\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px;--wsb-brand-1:#FFcc00;--wsb-brand-3:#0A7}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>";
PINNED_HEAD.E_brand_swatches = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Brand</title>\n <meta property=\"og:title\" content=\"Brand\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px;--wsb-brand-1:#FFcc00;--wsb-brand-3:#0A7}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>";
PINNED_HEAD.F_swatches_no_brand = PINNED_HEAD.A_defaults;
const PINNED_BODY: Record<string, string> = {
@@ -196,8 +196,8 @@ describe('composed preview: animation head gate covers header+body+footer', () =
* only in the header), default design, canonical https://example.com/
*/
const BACKEND_PINNED_HEAD: Record<string, string> = {
E_headcode_font: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Notes</title>\n <meta property=\"og:title\" content=\"Notes\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/notes\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <style>.x{font-family:Merriweather,serif}</style>\n</head>",
F_header_animation: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Home</title>\n <meta property=\"og:title\" content=\"Home\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation=\"fade-in\"].animated{animation-name:fadeIn}[data-animation=\"slide-up\"].animated{animation-name:slideUp}[data-animation=\"slide-left\"].animated{animation-name:slideLeft}[data-animation=\"slide-right\"].animated{animation-name:slideRight}[data-animation=\"zoom-in\"].animated{animation-name:zoomIn}[data-animation=\"bounce\"].animated{animation-name:bounce}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <noscript><style>[data-animation]{opacity:1}</style></noscript>\n</head>",
E_headcode_font: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Notes</title>\n <meta property=\"og:title\" content=\"Notes\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/notes\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <style>.x{font-family:Merriweather,serif}</style>\n</head>",
F_header_animation: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Home</title>\n <meta property=\"og:title\" content=\"Home\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation=\"fade-in\"].animated{animation-name:fadeIn}[data-animation=\"slide-up\"].animated{animation-name:slideUp}[data-animation=\"slide-left\"].animated{animation-name:slideLeft}[data-animation=\"slide-right\"].animated{animation-name:slideRight}[data-animation=\"zoom-in\"].animated{animation-name:zoomIn}[data-animation=\"bounce\"].animated{animation-name:bounce}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <noscript><style>[data-animation]{opacity:1}</style></noscript>\n</head>",
};
describe('Preview head === backend head for the head-code font scan and header/footer animation gate', () => {
@@ -275,7 +275,7 @@ describe('mobile stacking + navbar wrap head rules', () => {
test('mobile guard: card-grid hook, long-word wrap and the HtmlBlock net sit inside the 767px query only', async () => {
const { RESPONSIVE_CSS_MINIFIED } = await import('./html-export');
const mobile = /^@media\(max-width:767px\)\{(.*)\}@media\(max-width:991px\)/.exec(RESPONSIVE_CSS_MINIFIED)![1];
expect(mobile).toContain('[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}');
expect(mobile).toContain('[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:32px;text-align:center\"]{grid-template-columns:minmax(0,1fr)!important}[style=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])),[style^=\"max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(\"][style$=\",1fr);gap:24px\"]:not(:has(>*>img[style^=\"width:100%;height:200px;object-fit:cover\"])){grid-template-columns:minmax(0,1fr)!important}');
expect(mobile).toContain('body{overflow-wrap:break-word}');
expect(mobile).toContain('[data-wsb-html] pre{max-width:100%;overflow-x:auto}');
// The wrapper itself never gets a box or a scroll container (that clipped dropdowns and broke sticky).
@@ -0,0 +1,113 @@
/**
* Legacy card grids: pages stored before release 2026.11.5 carry FeaturesGrid / Testimonials / NumberCounter
* markup WITHOUT the data-wsb-grid hook (stored HTML is generated at editor-save time and the backend only wraps
* it in a head at publish). The head's <=767px block matches those stored inline styles byte-for-byte so a
* republish stacks them without the customer re-saving. jsdom has no layout: this pins WHICH elements the rules
* select; scripts/e2e-legacy-grid.mjs measures the layout in Chromium + Firefox.
*/
import { beforeAll, describe, expect, test } from 'vitest';
import { RESPONSIVE_CSS_MINIFIED, exportToHtml } from './html-export';
import { FeaturesGrid } from '../components/sections/FeaturesGrid';
import { Testimonials } from '../components/sections/Testimonials';
import { NumberCounter } from '../components/sections/NumberCounter';
import { Gallery } from '../components/sections/Gallery';
const toHtml = (c: unknown, props: Record<string, unknown>, id = 'n1'): string => (c as any).toHtml(props, '', id).html;
// The ONLY grid shapes these components ever stored before the hook (craft git history, and every stored page
// under .site-builder/pages on the OLS / TTM sites): no spaces, no trailing semicolon.
const LEGACY = {
features: '<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px"><div style="text-align:center;padding:32px 24px">x</div></div>',
testimonials: (n: number) => `<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(${n},1fr);gap:24px"><div style="border-radius:12px">x</div></div>`,
counter: (n: number) => `<div id="ctr_${n}" style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(${n},1fr);gap:32px;text-align:center"><div>1</div></div>`,
};
/** Selectors of the rules inside the <=767px block that set grid-template-columns. */
let gridSelectors: string[] = [];
let outsideMobile: string[] = [];
beforeAll(() => {
const style = document.createElement('style');
style.textContent = RESPONSIVE_CSS_MINIFIED;
document.head.appendChild(style);
for (const r of Array.from(style.sheet!.cssRules) as any[]) {
const inner = Array.from(r.cssRules || []) as CSSStyleRule[];
const sel = inner.filter((x) => /grid-template-columns/.test(x.cssText)).map((x) => x.selectorText);
if (r.media?.mediaText.replace(/\s/g, '') === '(max-width:767px)') gridSelectors = sel;
else outsideMobile.push(...sel);
}
});
/** Whether any <=767px grid rule selects the first element of `html`. */
function stacks(html: string): boolean {
const host = document.createElement('div');
host.innerHTML = html;
document.body.appendChild(host);
const el = host.firstElementChild!;
const hit = gridSelectors.some((s) => el.matches(s));
host.remove();
return hit;
}
const stripHook = (html: string) => html.replace(/ data-wsb-grid(="")?/g, '');
describe('legacy card grids stack on phones (head <=767px block)', () => {
test('the rules are parsed and live only in the <=767px block', () => {
expect(gridSelectors.length).toBeGreaterThanOrEqual(3);
expect(outsideMobile).toEqual([]);
});
test('the pretty (minifyCss:false) head carries the same grid rules', () => {
const html = exportToHtml(JSON.stringify({ ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: { tag: 'div', style: {} }, nodes: [], linkedNodes: {} } }), { minifyCss: false }).html;
const doc = new DOMParser().parseFromString(html, 'text/html');
const style = document.createElement('style');
style.textContent = doc.querySelector('style')!.textContent;
document.head.appendChild(style);
const sels = (Array.from(style.sheet!.cssRules) as any[])
.filter((r) => r.media?.mediaText.replace(/\s/g, '') === '(max-width:767px)')
.flatMap((r) => (Array.from(r.cssRules) as CSSStyleRule[]).filter((x) => /grid-template-columns/.test(x.cssText)).map((x) => x.selectorText));
style.remove();
const norm = (a: string[]) => a.map((x) => x.replace(/\s*([,>])\s*/g, '$1'));
expect(norm(sels)).toEqual(norm(gridSelectors));
});
test('stored FeaturesGrid / Testimonials / NumberCounter markup without the hook is selected', () => {
expect(stacks(LEGACY.features)).toBe(true);
for (const n of [1, 2, 3, 4, 5, 6]) {
expect(stacks(LEGACY.testimonials(n)), `testimonials ${n}`).toBe(true);
expect(stacks(LEGACY.counter(n)), `counter ${n}`).toBe(true);
}
});
test('the legacy shapes are exactly what the current emitters output minus the hook', () => {
const grid = (html: string) => /<div[^>]*display:grid[^>]*>/.exec(html)![0];
expect(stripHook(grid(toHtml(FeaturesGrid, {})))).toBe(grid(LEGACY.features));
for (const n of [2, 3, 4]) {
expect(stacks(stripHook(grid(toHtml(Testimonials, { columns: n }))) + '</div>')).toBe(true);
expect(stacks(stripHook(grid(toHtml(NumberCounter, { columns: n }))) + '</div>')).toBe(true);
expect(stripHook(grid(toHtml(Testimonials, { columns: n })))).toBe(grid(LEGACY.testimonials(n)));
}
// New saves keep the hook (and so are covered by [data-wsb-grid] as well).
expect(toHtml(FeaturesGrid, {})).toContain('data-wsb-grid');
});
test('Gallery keeps its columns: never selected, even when its gap equals a card-grid signature', () => {
for (const lightbox of [false, true]) {
for (const columns of [2, 3, 4]) {
for (const gap of ['16px', '24px', '32px']) {
const html = toHtml(Gallery, { columns, gap, lightbox }, `g${columns}${gap}`).replace(/^\s*<section[^>]*>\s*/, '');
expect(stacks(html), `gallery cols=${columns} gap=${gap} lightbox=${lightbox}`).toBe(false);
}
}
}
});
test('customer grids (HtmlBlock / hand-written inline styles) are left alone', () => {
for (const html of [
'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:32px"><div>x</div></div>',
'<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px"><div>x</div></div>',
'<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px;"><div>x</div></div>',
'<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(4,1fr);gap:40px;text-align:center;"><div>x</div></div>',
'<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr 1fr;gap:80px"><div>x</div></div>',
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:20px"><div>x</div></div>',
]) expect(stacks(html), html).toBe(false);
});
});
+4
View File
@@ -576,6 +576,10 @@ describe('export head unchanged by the TOKEN_VARS move (W0-tokens-core)', () =>
// 767 mobile boundary. Undo exactly that one change before hashing so these
// baselines still prove nothing ELSE in the head moved.
const undo767 = (html: string) => {
// legacy-grid: the stored-markup card-grid rules sit right after [data-wsb-grid]; drop them first.
const legacy = '[style^="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat("][style$=",1fr);gap:32px;text-align:center"]{grid-template-columns:minmax(0,1fr)!important}[style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px"]:not(:has(>*>img[style^="width:100%;height:200px;object-fit:cover"])),[style^="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat("][style$=",1fr);gap:24px"]:not(:has(>*>img[style^="width:100%;height:200px;object-fit:cover"])){grid-template-columns:minmax(0,1fr)!important}';
expect(html.split(legacy).length).toBe(2);
html = html.replace(legacy, '');
// mobile-guard: grid/word/HtmlBlock rules joined the 767 block and the Navbar wrap moved to its own
// <=991px block. Fold that back to the mobile-parity form first.
const guard = '[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}';
+18 -1
View File
@@ -406,6 +406,16 @@ const { mobileMax, tabletMin, tabletMax, desktopMin } = BREAKPOINTS;
// `[data-wsb-grid]`: card grids the components emit with inline `repeat(N,1fr)` (FeaturesGrid, Testimonials,
// NumberCounter) go to one column on a phone; tablet and desktop keep their columns. minmax(0,1fr), not 1fr,
// so a long word cannot push the single track wider than the screen.
// Legacy card grids: pages saved before the hook existed are stored WITHOUT data-wsb-grid (stored HTML is
// generated at editor-save time; publish only wraps it in this head), so the next two rules match the exact inline
// styles those three emitters stored (no spaces, no trailing ';', checked against craft git history and stored
// sites) and republishing stacks them with no re-save: NumberCounter (`...;gap:32px;text-align:center`, Gallery's
// gap cannot contain ';'), FeaturesGrid (always repeat(3,1fr);gap:32px) and Testimonials (`...;gap:24px`, any
// column count). Gallery emits the same prefix with a free `gap`, so the last two skip a grid whose items hold
// Gallery's thumbnail (`img[style^="width:100%;height:200px;object-fit:cover"]`): Gallery keeps its columns by
// design. :has() is unforgiving, so those two sit in their own rule: a browser without it (Safari <15.4, Firefox
// <121) drops only that rule and shows them as before. Customer grids (HtmlBlock, hand-written styles) never match
// these whole-attribute signatures and are left alone, like other customer layout (publish-checks warns).
// `body{overflow-wrap:break-word}`: an unbreakable word/URL wraps instead of running off a phone screen; text
// that already fits is untouched. break-word, not anywhere: anywhere would also shrink min-content sizes and so
// squeeze table columns and flex items. It inherits into HtmlBlock content through the display:contents wrapper.
@@ -430,6 +440,13 @@ const RESPONSIVE_CSS = `
[data-wsb-grid] {
grid-template-columns: minmax(0, 1fr) !important;
}
[style^="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat("][style$=",1fr);gap:32px;text-align:center"] {
grid-template-columns: minmax(0, 1fr) !important;
}
[style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px"]:not(:has(>*>img[style^="width:100%;height:200px;object-fit:cover"])),
[style^="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat("][style$=",1fr);gap:24px"]:not(:has(>*>img[style^="width:100%;height:200px;object-fit:cover"])) {
grid-template-columns: minmax(0, 1fr) !important;
}
body {
overflow-wrap: break-word;
}
@@ -450,7 +467,7 @@ const RESPONSIVE_CSS = `
}
}`;
export const RESPONSIVE_CSS_MINIFIED = `@media(max-width:${mobileMax}px){[style*="display: flex"][style*="flex-direction: row"],[style*="display:flex"][style*="flex-direction:row"]{flex-direction:column!important}[class^="cols_"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:${tabletMax}px){nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}}`;
export const RESPONSIVE_CSS_MINIFIED = `@media(max-width:${mobileMax}px){[style*="display: flex"][style*="flex-direction: row"],[style*="display:flex"][style*="flex-direction:row"]{flex-direction:column!important}[class^="cols_"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}[style^="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat("][style$=",1fr);gap:32px;text-align:center"]{grid-template-columns:minmax(0,1fr)!important}[style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px"]:not(:has(>*>img[style^="width:100%;height:200px;object-fit:cover"])),[style^="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat("][style$=",1fr);gap:24px"]:not(:has(>*>img[style^="width:100%;height:200px;object-fit:cover"])){grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] pre{max-width:100%;overflow-x:auto}}@media(max-width:${tabletMax}px){nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}}`;
const VISIBILITY_CSS = `
@media (min-width: ${desktopMin}px) { [data-hide-desktop] { display: none !important; } }
+21
View File
@@ -0,0 +1,21 @@
{
"accentColor": "#10b981",
"backgroundColor": "#ffffff",
"bodyFont": "Inter, sans-serif",
"borderColor": "#e5e7eb",
"borderRadius": "8px",
"buttonFont": "Inter, sans-serif",
"buttonRadius": "8px",
"errorColor": "#ef4444",
"favicon": "assets/ols-favicon.png",
"headCode": "",
"headingFont": "Inter, sans-serif",
"linkColor": "#3b82f6",
"mutedTextColor": "#6b7280",
"navStyle": "light",
"primaryColor": "#3b82f6",
"secondaryColor": "#8b5cf6",
"successColor": "#10b981",
"textColor": "#1f2937",
"warningColor": "#f59e0b"
}
+1
View File
@@ -0,0 +1 @@
<footer style="padding:24px 20px;text-align:center">© 2026 OLS Digital. All rights reserved. Questions? Email info@olsdigital.com</footer>
+14
View File
@@ -0,0 +1,14 @@
<style>
.nav_yk1fl5 .navbar-link:hover { color: #60a5fa !important; }
.nav_yk1fl5 .navbar-cta:hover { filter: brightness(1.1); }
</style>
<nav class="nav_yk1fl5" style="display:flex;align-items:center;justify-content:space-between;padding:16px 24px;background-color:#15202c;border-bottom:1px solid #15202c">
<a href="/" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><img src="/api/assets-proxy/?site_id=100&amp;filename=1789158005_2592c5cf_Minimalist_OLS_Digital_Logo_with_Rounded_Elements__400_x_135_px_.webp" alt="Logo" style="width:120px;height:auto;display:block" /></a>
<div class="navbar-links" id="nav_yk1fl5_links" style="display:flex;align-items:center;gap:24px">
<a href="/" class="navbar-link" style="text-decoration:none;font-size:14px;font-weight:400;color:#ffffff;background-color:transparent;padding:0;border-radius:0;transition:color 0.15s, background-color 0.15s">Home</a>
<a href="/brands" class="navbar-link" style="text-decoration:none;font-size:14px;font-weight:400;color:#ffffff;background-color:transparent;padding:0;border-radius:0;transition:color 0.15s, background-color 0.15s">Our Brands</a>
<a href="/about" class="navbar-link" style="text-decoration:none;font-size:14px;font-weight:400;color:#ffffff;background-color:transparent;padding:0;border-radius:0;transition:color 0.15s, background-color 0.15s">About</a>
<a href="/contact" class="navbar-link" style="text-decoration:none;font-size:14px;font-weight:400;color:#ffffff;background-color:transparent;padding:0;border-radius:0;transition:color 0.15s, background-color 0.15s">Contact</a>
<a href="/contact" class="navbar-cta" style="text-decoration:none;font-size:14px;font-weight:600;color:#ffffff;background-color:#2563eb;padding:8px 20px;border-radius:6px;transition:color 0.15s, background-color 0.15s">Contact</a>
</div>
</nav>
+32
View File
@@ -0,0 +1,32 @@
<div style="padding:0px;min-height:100px;border:none;box-shadow:none;opacity:1"><section id="home" style="background:linear-gradient(135deg, #15202c, #334155);min-height:60vh;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;padding:60px 20px">
<div style="max-width:800px;width:100%;position:relative;z-index:2;text-align:center">
<h1 style="font-size:48px;font-weight:700;color:#ffffff;margin-bottom:16px;line-height:1.2">OLS Digital</h1>
<p style="font-size:20px;color:#ffffff;opacity:0.85;margin-bottom:32px;line-height:1.6;white-space:pre-line">The parent company behind a growing family of trusted online brands, including AnHonestHost and StreamerTools.</p>
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap"><a href="/brands" style="display:inline-block;padding:14px 36px;background-color:#2563eb;color:#ffffff;text-decoration:none;border-radius:8px;font-weight:600;font-size:16px">Explore Our Brands</a><a href="/contact" style="display:inline-block;padding:14px 36px;background-color:transparent;color:#ffffff;text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;border:2px solid #ffffff">Contact Us</a></div>
</div>
</section><section id="what-we-do" style="padding:80px 20px;background-color:#ffffff">
<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px">
<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:var(--wsb-bg);border:1px solid var(--wsb-border)">
<div style="font-size:36px;margin-bottom:16px">🏢</div>
<h3 style="font-size:20px;font-weight:600;color:var(--wsb-text);margin-bottom:8px">Multi-Brand Portfolio</h3>
<p style="font-size:14px;color:var(--wsb-muted);line-height:1.6">We own and support a growing collection of independent, high-quality brands.</p>
</div>
<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:var(--wsb-bg);border:1px solid var(--wsb-border)">
<div style="font-size:36px;margin-bottom:16px">🌐</div>
<h3 style="font-size:20px;font-weight:600;color:var(--wsb-text);margin-bottom:8px">AnHonestHost</h3>
<p style="font-size:14px;color:var(--wsb-muted);line-height:1.6">Transparent, reliable web hosting built on trust.</p>
</div>
<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:var(--wsb-bg);border:1px solid var(--wsb-border)">
<div style="font-size:36px;margin-bottom:16px">🎮</div>
<h3 style="font-size:20px;font-weight:600;color:var(--wsb-text);margin-bottom:8px">StreamerTools</h3>
<p style="font-size:14px;color:var(--wsb-muted);line-height:1.6">Tools and resources built for content creators and streamers.</p>
</div>
</div>
</section><section id="about-preview" style="width:100%;padding-top:60px;padding-bottom:60px"><div style="max-width:1200px;margin:0 auto"><div style="padding:20px;min-height:100px;border:none;box-shadow:none;opacity:1"><h2 style="font-size:36px;font-weight:700;font-family:Inter, sans-serif;color:#1f2937;margin-bottom:16px;border:none;box-shadow:none;opacity:1">Building Brands That Matter</h2><p style="font-size:16px;line-height:1.6;color:#3f3f46;border:none;box-shadow:none;opacity:1">OLS Digital is a privately held parent company focused on acquiring and growing digital-first businesses. Each brand under our umbrella operates independently, guided by a shared commitment to honesty, quality, and great customer experiences.</p></div></div></section><section id="contact-cta" style="background:linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);padding:80px 20px;text-align:center">
<div style="max-width:700px;margin:0 auto">
<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px">Want to Learn More?</h2>
<p style="font-size:18px;color:rgba(255,255,255,0.85);margin-bottom:28px;line-height:1.6">For inquiries about OLS Digital or any of our brands, reach out to our team directly.</p>
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap"><a href="mailto:info@olsdigital.com" style="display:inline-block;padding:14px 36px;background-color:var(--wsb-bg);color:var(--wsb-text);text-decoration:none;border-radius:8px;font-weight:600;font-size:16px">Email info@olsdigital.com</a></div>
</div>
</section></div>