fix(scifi-clean): reveal ghosting, bundled fonts, lexer trap, blog length

- Reveal: in-viewport [data-sc-reveal]/schematic elements now reveal on the
  first animation frame (getBoundingClientRect pass) and the safety-net timer
  drops 1600ms -> 400ms (under the 1500ms capture settle). Fixes the ~10%
  opacity above-the-fold KPI row + benchmark table and the washed-out About
  "Recent releases" console, in both modes and for no-JS/no-scroll.
- Fonts: @font-face for Space Grotesk / Inter / JetBrains Mono against the
  served /templates/scifi-clean/fonts/*.woff2 paths + typography refs in all
  four presets (bundled_fonts is CMS dead code). --font-heading now resolves
  to Space Grotesk live.
- Lexer trap: contact-form CSS `){#` -> `){ #` (was failing whole-theme
  registration: "Newline not permitted in a single-line comment").
- Seed: set `title` so the navbar/site title shows "Halcyon Robotics" (was
  leaking the gallery instance name); expanded the control-loop article to
  ~10 paragraphs to kill the footer dead space.
- SVG guard: explicit width/height on the <use> icon SVGs.
- Wave B gallery screenshots (home/about/article x light+dark), preview
  = light home (light-canonical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-06 20:23:05 +08:00
parent 43c876b6be
commit 8238bcce0e
19 changed files with 92 additions and 10 deletions

View File

@ -131,6 +131,17 @@ css:
--font-body: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif; --font-body: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
} }
/* Bundled display faces. The host does not emit @font-face for bundled_fonts
* (LoadedPlugin.BundledFonts is unconsumed), so the theme declares its own
* faces against the served artifact paths (/templates/scifi-clean/fonts/...).
* Presets set typography.fontHeading/body/mono to template:scifi-clean:<Family>
* so --font-heading/--font-body/--font-mono resolve to these families. */
@font-face { font-family: 'Space Grotesk'; src: url('/templates/scifi-clean/fonts/space-grotesk-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: 'Space Grotesk'; src: url('/templates/scifi-clean/fonts/space-grotesk-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Inter'; src: url('/templates/scifi-clean/fonts/inter-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Inter'; src: url('/templates/scifi-clean/fonts/inter-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: 'JetBrains Mono'; src: url('/templates/scifi-clean/fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'JetBrains Mono'; src: url('/templates/scifi-clean/fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
/* ============================================================ /* ============================================================
Corporate Modernist — appended component CSS. Corporate Modernist — appended component CSS.
Semantic tokens only: hsl(var(--token)). No literal colors. Semantic tokens only: hsl(var(--token)). No literal colors.

View File

@ -5,6 +5,13 @@
"description": "The default Sci-Fi Clean look. Light is a white-room lab with technical-blue HUD lines and a signal-orange accent; dark is deep mission-control navy with lifted blue readouts.", "description": "The default Sci-Fi Clean look. Light is a white-room lab with technical-blue HUD lines and a signal-orange accent; dark is deep mission-control navy with lifted blue readouts.",
"theme": { "theme": {
"mode": "both", "mode": "both",
"typography": {
"fontHeading": "template:scifi-clean:Space Grotesk",
"fontBody": "template:scifi-clean:Inter",
"fontMono": "template:scifi-clean:JetBrains Mono",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": { "lightColors": {
"background": "210 14% 97%", "background": "210 14% 97%",
"foreground": "215 30% 12%", "foreground": "215 30% 12%",
@ -55,6 +62,13 @@
"description": "Highest-contrast surgical white with deep-navy ink in light, and near-black space navy in dark. For high-precision spec and dashboard work.", "description": "Highest-contrast surgical white with deep-navy ink in light, and near-black space navy in dark. For high-precision spec and dashboard work.",
"theme": { "theme": {
"mode": "both", "mode": "both",
"typography": {
"fontHeading": "template:scifi-clean:Space Grotesk",
"fontBody": "template:scifi-clean:Inter",
"fontMono": "template:scifi-clean:JetBrains Mono",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": { "lightColors": {
"background": "0 0% 100%", "background": "0 0% 100%",
"foreground": "220 40% 10%", "foreground": "220 40% 10%",
@ -105,6 +119,13 @@
"description": "Overt HUD styling. Cyan primary guide lines with an amber signal accent, on a cool white lab ground in light and a teal-tinted mission ground in dark.", "description": "Overt HUD styling. Cyan primary guide lines with an amber signal accent, on a cool white lab ground in light and a teal-tinted mission ground in dark.",
"theme": { "theme": {
"mode": "both", "mode": "both",
"typography": {
"fontHeading": "template:scifi-clean:Space Grotesk",
"fontBody": "template:scifi-clean:Inter",
"fontMono": "template:scifi-clean:JetBrains Mono",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": { "lightColors": {
"background": "195 20% 97%", "background": "195 20% 97%",
"foreground": "200 40% 11%", "foreground": "200 40% 11%",
@ -155,6 +176,13 @@
"description": "Restrained neutral. Slate-blue primary and a muted signal accent on graphite surfaces. Quietest of the four for text-dense product docs.", "description": "Restrained neutral. Slate-blue primary and a muted signal accent on graphite surfaces. Quietest of the four for text-dense product docs.",
"theme": { "theme": {
"mode": "both", "mode": "both",
"typography": {
"fontHeading": "template:scifi-clean:Space Grotesk",
"fontBody": "template:scifi-clean:Inter",
"fontMono": "template:scifi-clean:JetBrains Mono",
"fontSizeBase": "17",
"lineHeightBase": "1.6"
},
"lightColors": { "lightColors": {
"background": "220 12% 96%", "background": "220 12% 96%",
"foreground": "220 20% 14%", "foreground": "220 20% 14%",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 387 KiB

View File

@ -2,6 +2,7 @@
"demo": true, "demo": true,
"settings": { "settings": {
"merge": { "merge": {
"title": "Halcyon Robotics",
"site_name": "Halcyon Robotics", "site_name": "Halcyon Robotics",
"tagline": "Autonomy systems for the field." "tagline": "Autonomy systems for the field."
} }
@ -66,7 +67,7 @@
"reconcile_blocks": true, "reconcile_blocks": true,
"blocks": [ "blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Why we run the control loop at 1000 Hz", "level": 1 } }, { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Why we run the control loop at 1000 Hz", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>On a paved test track a 200 Hz loop tracks a path cleanly. On loose gravel the wheels slip between updates and the estimate lags the truth. The machine corrects late and starts to hunt.</p><p>At 1000 Hz the loop sees the slip inside a single wheel rotation. The correction is small and the path stays smooth. The cost is a tighter compute budget, which is why the runtime is deterministic rather than best effort.</p>" } } { "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>On a paved test track a 200 Hz loop tracks a path cleanly. On loose gravel the wheels slip between updates and the estimate lags the truth. The machine corrects late and starts to hunt.</p><p>At 1000 Hz the loop sees the slip inside a single wheel rotation. The correction is small and the path stays smooth. The cost is a tighter compute budget, which is why the runtime is deterministic rather than best effort.</p><p>The first version of the platform ran the controller at 250 Hz. It looked fine in the lab and on the paved apron behind the workshop. The trouble started the first time we drove it across a dry creek bed.</p><p>On that surface the tyres broke traction for tens of milliseconds at a time. Between control updates the vehicle rotated a few degrees more than the model expected, and by the time the next update landed the correction it applied was already stale. The machine oscillated around the path instead of settling on it.</p><p>We spent a week convinced the problem was the estimator. It was not. The state estimate was accurate to within the sensor noise floor. The gap was time: the world was changing faster than we were reading it back.</p><p>Raising the loop rate is not free. Every doubling halves the time budget for the estimator, the planner, and the control solve. At 1000 Hz the whole chain has to finish in under a millisecond, every millisecond, with no exceptions and no pauses to collect memory.</p><p>That constraint is why the runtime is written the way it is. Memory is allocated once at start-up. The control thread is pinned to an isolated core. The scheduler is deterministic rather than best effort, so a slow frame is a bug we can catch instead of a statistic we learn to tolerate.</p><p>The payoff shows up on exactly the surfaces that used to break it. On gravel and loose dirt the corrections are now small and continuous. The path traces stay inside a few centimetres of the reference even when the tyres are slipping the whole way through a turn.</p><p>There is a ceiling to this. Past a certain rate the actuators cannot respond any faster and the extra updates buy nothing but heat. For our drivetrain that ceiling sits a little above 1000 Hz, which is why we stopped there rather than chase a larger number for its own sake.</p><p>If you are specifying a system like this, do not ask a vendor for their loop rate in isolation. Ask what surface they measured on, what the worst-case frame time is, and what happens on the frame that runs long. Those three answers tell you far more than the headline figure.</p>" } }
] ]
}, },
{ {

View File

@ -1 +1 @@
{% if context.currentAuthor %}<section class="sc-section {{ class }}" data-block="scifi-clean:author-bio-hero"><div class="sc-content-well"><div class="sc-measure sc-stack" style="text-align: center; margin-left: auto; margin-right: auto;">{% if showAvatar and context.currentAuthor.avatarUrl %}<img class="sc-avatar" src="{{ context.currentAuthor.avatarUrl }}" alt="{{ context.currentAuthor.displayName }}" style="width: 6rem; height: 6rem; border-radius: 9999px; object-fit: cover; margin-left: auto; margin-right: auto;">{% else %}<span class="sc-mark sc-mark-ring" aria-hidden="true" style="margin-left: auto; margin-right: auto;"></span>{% endif %}<h1 class="sc-display" style="margin: 0;">{{ context.currentAuthor.displayName }}</h1>{% if context.currentAuthor.bio %}<p class="sc-lede" style="margin: 0;">{{ context.currentAuthor.bio }}</p>{% endif %}{% if showSocial %}<div class="sc-btn-row" style="justify-content: center;">{% if context.currentAuthor.websiteUrl %}<a class="sc-btn-ghost sc-btn-sm" href="{{ context.currentAuthor.websiteUrl }}" target="_blank" rel="noopener noreferrer" title="Website"><svg class="sc-icon" aria-hidden="true"><use href="/icons/lucide.svg#globe"/></svg></a>{% endif %}{% if context.currentAuthor.twitterHandle %}<a class="sc-btn-ghost sc-btn-sm" href="https://twitter.com/{{ context.currentAuthor.twitterHandle|cut:"@" }}" target="_blank" rel="noopener noreferrer" title="X"><svg class="sc-icon" aria-hidden="true"><use href="/icons/simple-icons.svg#x"/></svg></a>{% endif %}{% if context.currentAuthor.linkedinUrl %}<a class="sc-btn-ghost sc-btn-sm" href="{{ context.currentAuthor.linkedinUrl }}" target="_blank" rel="noopener noreferrer" title="LinkedIn"><svg class="sc-icon" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"/></svg></a>{% endif %}</div>{% endif %}</div></div></section>{% endif %} {% if context.currentAuthor %}<section class="sc-section {{ class }}" data-block="scifi-clean:author-bio-hero"><div class="sc-content-well"><div class="sc-measure sc-stack" style="text-align: center; margin-left: auto; margin-right: auto;">{% if showAvatar and context.currentAuthor.avatarUrl %}<img class="sc-avatar" src="{{ context.currentAuthor.avatarUrl }}" alt="{{ context.currentAuthor.displayName }}" style="width: 6rem; height: 6rem; border-radius: 9999px; object-fit: cover; margin-left: auto; margin-right: auto;">{% else %}<span class="sc-mark sc-mark-ring" aria-hidden="true" style="margin-left: auto; margin-right: auto;"></span>{% endif %}<h1 class="sc-display" style="margin: 0;">{{ context.currentAuthor.displayName }}</h1>{% if context.currentAuthor.bio %}<p class="sc-lede" style="margin: 0;">{{ context.currentAuthor.bio }}</p>{% endif %}{% if showSocial %}<div class="sc-btn-row" style="justify-content: center;">{% if context.currentAuthor.websiteUrl %}<a class="sc-btn-ghost sc-btn-sm" href="{{ context.currentAuthor.websiteUrl }}" target="_blank" rel="noopener noreferrer" title="Website"><svg class="sc-icon" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#globe"/></svg></a>{% endif %}{% if context.currentAuthor.twitterHandle %}<a class="sc-btn-ghost sc-btn-sm" href="https://twitter.com/{{ context.currentAuthor.twitterHandle|cut:"@" }}" target="_blank" rel="noopener noreferrer" title="X"><svg class="sc-icon" width="16" height="16" aria-hidden="true"><use href="/icons/simple-icons.svg#x"/></svg></a>{% endif %}{% if context.currentAuthor.linkedinUrl %}<a class="sc-btn-ghost sc-btn-sm" href="{{ context.currentAuthor.linkedinUrl }}" target="_blank" rel="noopener noreferrer" title="LinkedIn"><svg class="sc-icon" width="16" height="16" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"/></svg></a>{% endif %}</div>{% endif %}</div></div></section>{% endif %}

View File

@ -1,3 +1,3 @@
<nav class="{{ class }}" data-block="scifi-clean:sidebar-nav" style="{% if showBorder %}border-right: 1px solid hsl(var(--border)); padding-right: 1rem;{% endif %}">{% if title %}<h3 class="sc-kicker" style="margin-top: 0;">{{ title }}</h3>{% endif %}<ul class="sc-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for item in menu.items %}<li><a class="sc-sidebar-link" href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link>{% if item.icon %}<svg class="sc-icon" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"/></svg>{% endif %}{{ item.label }}</a></li>{% endfor %}</ul>{% if highlightCurrent %}<script> <nav class="{{ class }}" data-block="scifi-clean:sidebar-nav" style="{% if showBorder %}border-right: 1px solid hsl(var(--border)); padding-right: 1rem;{% endif %}">{% if title %}<h3 class="sc-kicker" style="margin-top: 0;">{{ title }}</h3>{% endif %}<ul class="sc-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for item in menu.items %}<li><a class="sc-sidebar-link" href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link>{% if item.icon %}<svg class="sc-icon" width="16" height="16" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"/></svg>{% endif %}{{ item.label }}</a></li>{% endfor %}</ul>{% if highlightCurrent %}<script>
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.setAttribute('aria-current','page');}});})(); (function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.setAttribute('aria-current','page');}});})();
</script>{% endif %}</nav> </script>{% endif %}</nav>

View File

@ -33,8 +33,15 @@
}); });
}, { rootMargin: '0px 0px -8% 0px' }); }, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); }); items.forEach(function(el){ io.observe(el); });
/* Reveal anything already on screen now: first-screen readouts must not
wait on the async observer or a scroll (fixes ghosted above-the-fold
content in no-scroll captures / IO gaps). */
requestAnimationFrame(function(){
var vh = window.innerHeight || document.documentElement.clientHeight;
items.forEach(function(el){ if (el.getBoundingClientRect().top < vh) { el.classList.add("sc-in"); io.unobserve(el); } });
});
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */ /* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600); setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 400);
} else { } else {
items.forEach(function(el){ el.classList.add('sc-in'); }); items.forEach(function(el){ el.classList.add('sc-in'); });
} }

View File

@ -30,8 +30,15 @@
}); });
}, { rootMargin: '0px 0px -8% 0px' }); }, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); }); items.forEach(function(el){ io.observe(el); });
/* Reveal anything already on screen now: first-screen readouts must not
wait on the async observer or a scroll (fixes ghosted above-the-fold
content in no-scroll captures / IO gaps). */
requestAnimationFrame(function(){
var vh = window.innerHeight || document.documentElement.clientHeight;
items.forEach(function(el){ if (el.getBoundingClientRect().top < vh) { el.classList.add("sc-in"); io.unobserve(el); } });
});
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */ /* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600); setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 400);
} else { } else {
items.forEach(function(el){ el.classList.add('sc-in'); }); items.forEach(function(el){ el.classList.add('sc-in'); });
} }

View File

@ -36,8 +36,15 @@
}); });
}, { rootMargin: '0px 0px -8% 0px' }); }, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); }); items.forEach(function(el){ io.observe(el); });
/* Reveal anything already on screen now: first-screen readouts must not
wait on the async observer or a scroll (fixes ghosted above-the-fold
content in no-scroll captures / IO gaps). */
requestAnimationFrame(function(){
var vh = window.innerHeight || document.documentElement.clientHeight;
items.forEach(function(el){ if (el.getBoundingClientRect().top < vh) { el.classList.add("sc-in"); io.unobserve(el); } });
});
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */ /* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600); setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 400);
} else { } else {
items.forEach(function(el){ el.classList.add('sc-in'); }); items.forEach(function(el){ el.classList.add('sc-in'); });
} }

View File

@ -32,8 +32,15 @@
}); });
}, { rootMargin: '0px 0px -8% 0px' }); }, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); }); items.forEach(function(el){ io.observe(el); });
/* Reveal anything already on screen now: first-screen readouts must not
wait on the async observer or a scroll (fixes ghosted above-the-fold
content in no-scroll captures / IO gaps). */
requestAnimationFrame(function(){
var vh = window.innerHeight || document.documentElement.clientHeight;
items.forEach(function(el){ if (el.getBoundingClientRect().top < vh) { el.classList.add("sc-in"); io.unobserve(el); } });
});
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */ /* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600); setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 400);
} else { } else {
items.forEach(function(el){ el.classList.add('sc-in'); }); items.forEach(function(el){ el.classList.add('sc-in'); });
} }

View File

@ -28,8 +28,15 @@
}); });
}, { rootMargin: '0px 0px -8% 0px' }); }, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); }); items.forEach(function(el){ io.observe(el); });
/* Reveal anything already on screen now: first-screen readouts must not
wait on the async observer or a scroll (fixes ghosted above-the-fold
content in no-scroll captures / IO gaps). */
requestAnimationFrame(function(){
var vh = window.innerHeight || document.documentElement.clientHeight;
items.forEach(function(el){ if (el.getBoundingClientRect().top < vh) { el.classList.add("sc-in"); io.unobserve(el); } });
});
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */ /* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600); setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 400);
} else { } else {
items.forEach(function(el){ el.classList.add('sc-in'); }); items.forEach(function(el){ el.classList.add('sc-in'); });
} }

View File

@ -30,8 +30,15 @@
}); });
}, { rootMargin: '0px 0px -8% 0px' }); }, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); }); items.forEach(function(el){ io.observe(el); });
/* Reveal anything already on screen now: first-screen readouts must not
wait on the async observer or a scroll (fixes ghosted above-the-fold
content in no-scroll captures / IO gaps). */
requestAnimationFrame(function(){
var vh = window.innerHeight || document.documentElement.clientHeight;
items.forEach(function(el){ if (el.getBoundingClientRect().top < vh) { el.classList.add("sc-in"); io.unobserve(el); } });
});
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */ /* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600); setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 400);
} else { } else {
items.forEach(function(el){ el.classList.add('sc-in'); }); items.forEach(function(el){ el.classList.add('sc-in'); });
} }