diff --git a/manifest.yaml b/manifest.yaml index 66fac6d..51b2af4 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -131,6 +131,17 @@ css: --font-body: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif; --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: + * 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. Semantic tokens only: hsl(var(--token)). No literal colors. diff --git a/presets.json b/presets.json index a99c457..4e0a770 100644 --- a/presets.json +++ b/presets.json @@ -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.", "theme": { "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": { "background": "210 14% 97%", "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.", "theme": { "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": { "background": "0 0% 100%", "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.", "theme": { "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": { "background": "195 20% 97%", "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.", "theme": { "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": { "background": "220 12% 96%", "foreground": "220 20% 14%", diff --git a/preview.png b/preview.png index eaf8b9c..fce5cf9 100644 Binary files a/preview.png and b/preview.png differ diff --git a/screenshots/01-home-light.png b/screenshots/01-home-light.png index eaf8b9c..fce5cf9 100644 Binary files a/screenshots/01-home-light.png and b/screenshots/01-home-light.png differ diff --git a/screenshots/02-home-dark.png b/screenshots/02-home-dark.png index 1d40c51..acc1505 100644 Binary files a/screenshots/02-home-dark.png and b/screenshots/02-home-dark.png differ diff --git a/screenshots/03-about-light.png b/screenshots/03-about-light.png index f11bb52..374e7f3 100644 Binary files a/screenshots/03-about-light.png and b/screenshots/03-about-light.png differ diff --git a/screenshots/04-about-dark.png b/screenshots/04-about-dark.png index 4cee40a..a170d41 100644 Binary files a/screenshots/04-about-dark.png and b/screenshots/04-about-dark.png differ diff --git a/screenshots/05-blog-control-loop-light.png b/screenshots/05-blog-control-loop-light.png index 596da37..1b80e32 100644 Binary files a/screenshots/05-blog-control-loop-light.png and b/screenshots/05-blog-control-loop-light.png differ diff --git a/screenshots/06-blog-control-loop-dark.png b/screenshots/06-blog-control-loop-dark.png index 5b431b9..ebbbb72 100644 Binary files a/screenshots/06-blog-control-loop-dark.png and b/screenshots/06-blog-control-loop-dark.png differ diff --git a/seed/seed.json b/seed/seed.json index 8059281..ea8db0c 100644 --- a/seed/seed.json +++ b/seed/seed.json @@ -2,6 +2,7 @@ "demo": true, "settings": { "merge": { + "title": "Halcyon Robotics", "site_name": "Halcyon Robotics", "tagline": "Autonomy systems for the field." } @@ -66,7 +67,7 @@ "reconcile_blocks": true, "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": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "

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.

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.

" } } + { "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

" } } ] }, { diff --git a/templates/overrides/scifi-clean/author-bio-hero.ninjatpl b/templates/overrides/scifi-clean/author-bio-hero.ninjatpl index a04af8c..50452f6 100644 --- a/templates/overrides/scifi-clean/author-bio-hero.ninjatpl +++ b/templates/overrides/scifi-clean/author-bio-hero.ninjatpl @@ -1 +1 @@ -{% if context.currentAuthor %}
{% if showAvatar and context.currentAuthor.avatarUrl %}{{ context.currentAuthor.displayName }}{% else %}{% endif %}

{{ context.currentAuthor.displayName }}

{% if context.currentAuthor.bio %}

{{ context.currentAuthor.bio }}

{% endif %}{% if showSocial %}
{% if context.currentAuthor.websiteUrl %}{% endif %}{% if context.currentAuthor.twitterHandle %}{% endif %}{% if context.currentAuthor.linkedinUrl %}{% endif %}
{% endif %}
{% endif %} +{% if context.currentAuthor %}
{% if showAvatar and context.currentAuthor.avatarUrl %}{{ context.currentAuthor.displayName }}{% else %}{% endif %}

{{ context.currentAuthor.displayName }}

{% if context.currentAuthor.bio %}

{{ context.currentAuthor.bio }}

{% endif %}{% if showSocial %}
{% if context.currentAuthor.websiteUrl %}{% endif %}{% if context.currentAuthor.twitterHandle %}{% endif %}{% if context.currentAuthor.linkedinUrl %}{% endif %}
{% endif %}
{% endif %} diff --git a/templates/overrides/scifi-clean/contact-form.ninjatpl b/templates/overrides/scifi-clean/contact-form.ninjatpl index 6deed57..afd27b5 100644 --- a/templates/overrides/scifi-clean/contact-form.ninjatpl +++ b/templates/overrides/scifi-clean/contact-form.ninjatpl @@ -30,5 +30,5 @@ {% endwith %} {% endif %} - + {% endwith %} diff --git a/templates/overrides/scifi-clean/sidebar-nav.ninjatpl b/templates/overrides/scifi-clean/sidebar-nav.ninjatpl index fac6a30..4b2c46f 100644 --- a/templates/overrides/scifi-clean/sidebar-nav.ninjatpl +++ b/templates/overrides/scifi-clean/sidebar-nav.ninjatpl @@ -1,3 +1,3 @@ - diff --git a/templates/scifi-clean/article.ninjatpl b/templates/scifi-clean/article.ninjatpl index 87d4ce6..6b6b277 100644 --- a/templates/scifi-clean/article.ninjatpl +++ b/templates/scifi-clean/article.ninjatpl @@ -33,8 +33,15 @@ }); }, { rootMargin: '0px 0px -8% 0px' }); 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. */ - 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 { items.forEach(function(el){ el.classList.add('sc-in'); }); } diff --git a/templates/scifi-clean/blog-index.ninjatpl b/templates/scifi-clean/blog-index.ninjatpl index c9ddc8a..a3d3605 100644 --- a/templates/scifi-clean/blog-index.ninjatpl +++ b/templates/scifi-clean/blog-index.ninjatpl @@ -30,8 +30,15 @@ }); }, { rootMargin: '0px 0px -8% 0px' }); 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. */ - 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 { items.forEach(function(el){ el.classList.add('sc-in'); }); } diff --git a/templates/scifi-clean/contact.ninjatpl b/templates/scifi-clean/contact.ninjatpl index 6defa07..349cf68 100644 --- a/templates/scifi-clean/contact.ninjatpl +++ b/templates/scifi-clean/contact.ninjatpl @@ -36,8 +36,15 @@ }); }, { rootMargin: '0px 0px -8% 0px' }); 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. */ - 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 { items.forEach(function(el){ el.classList.add('sc-in'); }); } diff --git a/templates/scifi-clean/default.ninjatpl b/templates/scifi-clean/default.ninjatpl index e840711..b23359f 100644 --- a/templates/scifi-clean/default.ninjatpl +++ b/templates/scifi-clean/default.ninjatpl @@ -32,8 +32,15 @@ }); }, { rootMargin: '0px 0px -8% 0px' }); 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. */ - 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 { items.forEach(function(el){ el.classList.add('sc-in'); }); } diff --git a/templates/scifi-clean/full-width.ninjatpl b/templates/scifi-clean/full-width.ninjatpl index bdbd2c1..56ed553 100644 --- a/templates/scifi-clean/full-width.ninjatpl +++ b/templates/scifi-clean/full-width.ninjatpl @@ -28,8 +28,15 @@ }); }, { rootMargin: '0px 0px -8% 0px' }); 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. */ - 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 { items.forEach(function(el){ el.classList.add('sc-in'); }); } diff --git a/templates/scifi-clean/landing.ninjatpl b/templates/scifi-clean/landing.ninjatpl index c28d3d3..7a7a1e7 100644 --- a/templates/scifi-clean/landing.ninjatpl +++ b/templates/scifi-clean/landing.ninjatpl @@ -30,8 +30,15 @@ }); }, { rootMargin: '0px 0px -8% 0px' }); 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. */ - 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 { items.forEach(function(el){ el.classList.add('sc-in'); }); }