fix(scifi-clean): repair blank hero + whitespace/layout + mobile responsiveness

Layout / whitespace:
- Root-cause blank hero: seed passed hero styled-text (eyebrow/headline/
  subheadline) as bare strings, but the hero template does field.text|default:field
  and the ninjatpl engine throws a hard error on attribute-access-on-string, so the
  whole hero (and the builtin fallback) errored and rendered as an empty ~160px band.
  Seed now passes {text:...} objects (the builtin-sample / admin-editor format).
- tech_spec table: value + unit were both right-aligned, leaving a cavernous mid-gap;
  label column now absorbs slack and value+unit group together on the right. Box
  narrowed (34rem) with tighter padding so the HUD corners frame a deliberate panel.
- Tightened section rhythm (sc-section 4->3.5rem, sc-section-tight 2.5->2.75rem) and
  hero bottom padding to remove oversized inter-section gaps.
- Defined sc-article-measure (was referenced but undefined -> unbounded line length).

Empty-media graceful state (seed ships no images):
- rich-section and hero (split) rendered an empty aspect-ratio muted box when media
  was absent; media frame is now guarded and the grid collapses to one column.

Mobile responsiveness (explicit sc-* classes + @media, not JIT-scanned Tailwind):
- sc-form-grid (global + contact-form scoped) collapses to 1 column <=640px.
- gallery dropped inline grid-template-columns that overrode its responsive classes,
  so 3/4-col galleries now collapse on small screens.
- nav toggle tap target bumped to 42x42px.

Motion robustness:
- Added a safety-net full-reveal timeout to the scroll-reveal script so below-fold
  [data-sc-reveal] content is guaranteed visible on no-scroll screenshot captures and
  IntersectionObserver edge cases. Reduced-motion / no-JS already render fully.

Semantic tokens only; check-safety 32 checks 19 ok 13 skip -> OK; codeless build OK.
No version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-05 17:27:27 +08:00
parent 2163eb669b
commit 6af6950f23
14 changed files with 30 additions and 14 deletions

View File

@ -1 +1 @@
<section data-block="scifi-clean:tech_spec" class="sc-section-tight {{ class }}"><div class="sc-content-well"><div class="sc-hud-corners" style="max-width: 44rem; padding: 1.5rem;"><table class="sc-spec">{% if caption %}<caption>{{ caption }}</caption>{% endif %}<tbody>{% if rows %}{% for row in rows %}<tr><td class="sc-spec-label">{{ row.label }}</td><td class="sc-spec-value">{{ row.value }}</td><td class="sc-spec-unit">{{ row.unit }}</td></tr>{% endfor %}{% else %}<tr><td class="sc-spec-label">No spec rows yet</td><td class="sc-spec-value">--</td><td class="sc-spec-unit"></td></tr>{% endif %}</tbody></table></div></div></section>
<section data-block="scifi-clean:tech_spec" class="sc-section-tight {{ class }}"><div class="sc-content-well"><div class="sc-hud-corners" style="max-width: 34rem; padding: 1.25rem 1.5rem; overflow-x: auto;"><table class="sc-spec">{% if caption %}<caption>{{ caption }}</caption>{% endif %}<tbody>{% if rows %}{% for row in rows %}<tr><td class="sc-spec-label">{{ row.label }}</td><td class="sc-spec-value">{{ row.value }}</td><td class="sc-spec-unit">{{ row.unit }}</td></tr>{% endfor %}{% else %}<tr><td class="sc-spec-label">No spec rows yet</td><td class="sc-spec-value">--</td><td class="sc-spec-unit"></td></tr>{% endif %}</tbody></table></div></div></section>

View File

@ -140,9 +140,10 @@ css:
/* --- Layout primitives --- */
.sc-content-well { width: 100%; max-width: 1280px; margin-left: auto; margin-right: auto; padding-left: 1.5rem; padding-right: 1.5rem; box-sizing: border-box; }
.sc-measure { max-width: 70ch; }
.sc-article-measure { max-width: 72ch; }
.sc-swiss-12 { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 1.5rem; row-gap: 1.5rem; }
.sc-section { padding-top: 4rem; padding-bottom: 4rem; }
.sc-section-tight { padding-top: 2.5rem; padding-bottom: 2.5rem; }
.sc-section { padding-top: 3.5rem; padding-bottom: 3.5rem; }
.sc-section-tight { padding-top: 2.75rem; padding-bottom: 2.75rem; }
.sc-hairline { border: 1px solid hsl(var(--border)); }
.sc-hairline-top { border-top: 1px solid hsl(var(--border)); }
.sc-hairline-bottom { border-bottom: 1px solid hsl(var(--border)); }
@ -218,7 +219,7 @@ css:
@media (min-width: 768px) { .sc-nav-links { display: flex; } }
.sc-nav-link { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--foreground)); text-decoration: none; padding-bottom: 2px; border-bottom: 2px solid transparent; transition: border-color 150ms ease; }
.sc-nav-link:hover { border-bottom-color: hsl(var(--accent)); }
.sc-nav-toggle { display: inline-flex; }
.sc-nav-toggle { display: inline-flex; align-items: center; justify-content: center; min-width: 42px; min-height: 42px; }
@media (min-width: 768px) { .sc-nav-toggle { display: none; } }
.sc-nav-drawer { display: none; flex-direction: column; gap: 0.25rem; padding: 0.5rem 0 1rem; border-top: 1px solid hsl(var(--border)); width: 100%; }
@media (min-width: 768px) { .sc-nav-drawer { display: none !important; } }
@ -329,6 +330,7 @@ css:
/* --- Forms --- */
.sc-form { display: grid; gap: 1.25rem; }
.sc-form-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 640px) { .sc-form-grid { grid-template-columns: 1fr; } }
.sc-field { display: flex; flex-direction: column; gap: 0.375rem; }
.sc-field--full { grid-column: 1 / -1; }
.sc-label { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 600; color: hsl(var(--foreground)); text-transform: uppercase; letter-spacing: 0.04em; }
@ -450,9 +452,9 @@ css:
.sc-spec { width: 100%; border-collapse: collapse; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.875rem; }
.sc-spec caption { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--muted-foreground)); text-align: left; padding-bottom: 0.75rem; }
.sc-spec tr { border-bottom: 1px solid hsl(var(--border)); }
.sc-spec-label { text-align: left; padding: 0.625rem 0.5rem; color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.75rem; }
.sc-spec-value { text-align: right; padding: 0.625rem 0.5rem; color: hsl(var(--foreground)); font-variant-numeric: tabular-nums; font-weight: 500; }
.sc-spec-unit { text-align: right; padding: 0.625rem 0.5rem; color: hsl(var(--muted-foreground)); font-size: 0.75rem; }
.sc-spec-label { text-align: left; width: 100%; padding: 0.625rem 0.5rem; color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.75rem; }
.sc-spec-value { text-align: right; white-space: nowrap; padding: 0.625rem 0.375rem 0.625rem 1rem; color: hsl(var(--foreground)); font-variant-numeric: tabular-nums; font-weight: 500; }
.sc-spec-unit { text-align: left; white-space: nowrap; width: 1%; padding: 0.625rem 0.5rem 0.625rem 0; color: hsl(var(--muted-foreground)); font-size: 0.75rem; }
.sc-bench-row { display: grid; grid-template-columns: 1fr auto; gap: 0.375rem 1rem; align-items: baseline; margin-bottom: 1rem; }
.sc-bench-name { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--foreground)); }

View File

@ -27,7 +27,7 @@
"template_key": "full-width",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 1, "content": { "template": "left-aligned", "eyebrow": "Field autonomy", "headline": "Robots that hold their own position", "subheadline": "Halcyon builds the perception and control stack for machines that work where the map runs out. One runtime, from sensor to actuator.", "ctaPrimary": { "text": "Read the spec", "url": "/about" }, "ctaSecondary": { "text": "Book a briefing", "url": "/contact" } } },
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 1, "content": { "template": "left-aligned", "eyebrow": { "text": "Field autonomy" }, "headline": { "text": "Robots that hold their own position" }, "subheadline": { "text": "Halcyon builds the perception and control stack for machines that work where the map runs out. One runtime, from sensor to actuator." }, "ctaPrimary": { "text": "Read the spec", "url": "/about" }, "ctaSecondary": { "text": "Book a briefing", "url": "/contact" } } },
{ "block_key": "scifi-clean:telemetry_grid", "title": "Telemetry", "slot": "main", "sort_order": 2, "content": { "eyebrow": "Runtime", "heading": "What the stack holds", "columns": 4, "tiles": [ { "label": "Control loop", "value": "1000", "unit": "Hz", "delta": "deterministic" }, { "label": "Localisation drift", "value": "4", "unit": "cm", "delta": "over 1 km" }, { "label": "Onboard latency", "value": "18", "unit": "ms", "delta": "sensor to command" }, { "label": "Power draw", "value": "42", "unit": "W", "delta": "full autonomy" } ] } },
{ "block_key": "scifi-clean:benchmark_table", "title": "Benchmarks", "slot": "main", "sort_order": 3, "content": { "eyebrow": "Perception", "heading": "Detection recall by surface", "rows": [ { "label": "Paved road", "value": "99.2", "unit": "%", "bar": 99, "highlight": false }, { "label": "Gravel and dirt", "value": "97.8", "unit": "%", "bar": 98, "highlight": false }, { "label": "Low light", "value": "94.1", "unit": "%", "bar": 94, "highlight": true }, { "label": "Heavy dust", "value": "91.6", "unit": "%", "bar": 92, "highlight": false } ], "note": "Measured on the internal Halcyon field set. Numbers are illustrative product data for this demo site." } },
{ "block_key": "stats", "title": "Deployment", "slot": "main", "sort_order": 4, "content": { "heading": "Built for the field", "columns": 3, "countUp": false, "items": [ { "value": "40", "suffix": " C", "label": "Rated operating ceiling" }, { "value": "8", "suffix": " h", "label": "Continuous mission time" }, { "value": "IP67", "label": "Sealed enclosure rating" } ] } },

View File

@ -30,5 +30,5 @@
{% endwith %}
{% endif %}
</div></section>
<style>#contact-form-{{ bid }} .sc-form-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.25rem;margin-top:2rem;}#contact-form-{{ bid }} .sc-field--full{grid-column:1 / -1;}#contact-form-{{ bid }} .sc-label{display:block;margin-bottom:0.375rem;font-size:0.8125rem;font-weight:500;color:hsl(var(--foreground));font-family:var(--font-heading, "Inter Tight", "Inter", system-ui, sans-serif);}#contact-form-{{ bid }} .sc-input{width:100%;padding:0.5rem 0.75rem;font-size:0.9375rem;font-family:var(--font-body, "Inter", system-ui, sans-serif);color:hsl(var(--foreground));background:hsl(var(--background));border:1px solid hsl(var(--input));border-radius:4px;box-sizing:border-box;}#contact-form-{{ bid }} .sc-input:focus{outline:2px solid hsl(var(--ring));outline-offset:1px;}</style>
<style>#contact-form-{{ bid }} .sc-form-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.25rem;margin-top:2rem;}@media (max-width:640px){#contact-form-{{ bid }} .sc-form-grid{grid-template-columns:1fr;}}#contact-form-{{ bid }} .sc-field--full{grid-column:1 / -1;}#contact-form-{{ bid }} .sc-label{display:block;margin-bottom:0.375rem;font-size:0.8125rem;font-weight:500;color:hsl(var(--foreground));font-family:var(--font-heading, "Inter Tight", "Inter", system-ui, sans-serif);}#contact-form-{{ bid }} .sc-input{width:100%;padding:0.5rem 0.75rem;font-size:0.9375rem;font-family:var(--font-body, "Inter", system-ui, sans-serif);color:hsl(var(--foreground));background:hsl(var(--background));border:1px solid hsl(var(--input));border-radius:4px;box-sizing:border-box;}#contact-form-{{ bid }} .sc-input:focus{outline:2px solid hsl(var(--ring));outline-offset:1px;}</style>
{% endwith %}

View File

@ -1,6 +1,6 @@
{# gallery override — HUD grid of hairline-framed images; carries data-bn-gallery / data-lightbox / data-bn-gallery-item so the host lightbox JS binds. #}
{% if columns == 4 %}{% set gcols = "sc-gallery-4" %}{% set gn = 4 %}{% elif columns == 2 %}{% set gcols = "" %}{% set gn = 2 %}{% else %}{% set gcols = "sc-gallery-3" %}{% set gn = 3 %}{% endif %}
{% if gap == "sm" %}{% set ggap = "0.5rem" %}{% elif gap == "lg" %}{% set ggap = "1.5rem" %}{% else %}{% set ggap = "1rem" %}{% endif %}
<div class="sc-gallery-grid {{ gcols }} {{ class }}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}" style="margin:1.5rem 0;display:grid;grid-template-columns:repeat({{ gn }}, minmax(0, 1fr));gap:{{ ggap }};">
<div class="sc-gallery-grid {{ gcols }} {{ class }}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}" style="margin:1.5rem 0;gap:{{ ggap }};">
{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:'' }}" class="sc-media-frame" style="display:block;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--muted));">{% img image.src alt=image.alt|default:"" class="sc-gallery-img" %}</a>{% endfor %}
</div><style>.sc-gallery-grid .sc-gallery-img{display:block;width:100%;height:100%;aspect-ratio:1;object-fit:cover;}</style>

View File

@ -20,7 +20,7 @@
{% if trustLogos %}<div class="sc-hairline-top" style="margin-top: 3rem; padding-top: 1.5rem;"><p class="sc-kicker sc-eyebrow" style="margin-bottom: 1rem; color: hsl(var(--muted-foreground));">Trusted by</p><div class="sc-logo-row" style="display: flex; flex-wrap: wrap; align-items: center; gap: 2rem;">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}">{% img logo.image alt=logo.alt class="sc-logo" %}</a>{% else %}{% img logo.image alt=logo.alt class="sc-logo" %}{% endif %}{% endfor %}</div></div>{% endif %}
{% endmacro %}
{% if variant == "split" %}
<section class="sc-hero sc-hero-split {{ class }}" style="padding-top: 4rem; padding-bottom: 4rem;"><div class="sc-content-well"><div class="sc-grid sc-grid-2" style="align-items: center;"><div>{{ herobody() }}</div><div class="sc-media-frame" style="aspect-ratio: 4 / 3; background: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; {% if mediaPosition == "left" %}order: -1;{% endif %}">{% if backgroundImage %}{% img backgroundImage alt=headlineText class="sc-hero-media" %}{% endif %}</div></div></div></section>
<section class="sc-hero sc-hero-split {{ class }}" style="padding-top: 4rem; padding-bottom: 4rem;"><div class="sc-content-well"><div class="sc-grid {% if backgroundImage %}sc-grid-2{% endif %}" style="align-items: center;"><div>{{ herobody() }}</div>{% if backgroundImage %}<div class="sc-media-frame" style="aspect-ratio: 4 / 3; background: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; {% if mediaPosition == "left" %}order: -1;{% endif %}">{% img backgroundImage alt=headlineText class="sc-hero-media" %}</div>{% endif %}</div></div></section>
{% else %}
<section class="sc-hero {{ class }}" style="padding-top: 5rem; padding-bottom: 5rem; {% if variant == "left-aligned" %}text-align: left;{% else %}text-align: center;{% endif %}"><div class="sc-content-well" style="{% if variant != "left-aligned" %}display: flex; flex-direction: column; align-items: center;{% endif %}"><div style="max-width: 60ch; width: 100%;">{{ herobody() }}</div></div></section>
<section class="sc-hero {{ class }}" style="padding-top: 4.5rem; padding-bottom: 3.5rem; {% if variant == "left-aligned" %}text-align: left;{% else %}text-align: center;{% endif %}"><div class="sc-content-well" style="{% if variant != "left-aligned" %}display: flex; flex-direction: column; align-items: center;{% endif %}"><div style="max-width: 60ch; width: 100%;">{{ herobody() }}</div></div></section>
{% endif %}

View File

@ -1,10 +1,10 @@
{# rich-section override — two-column media + rich text, hairline surface options. #}
<section class="sc-section {% if background == "muted" %}sc-surface-muted{% elif background == "card" %}sc-surface-card{% endif %} {{ class }}"><div class="sc-content-well"><div class="sc-grid sc-grid-2" style="align-items: center;">
<section class="sc-section {% if background == "muted" %}sc-surface-muted{% elif background == "card" %}sc-surface-card{% endif %} {{ class }}"><div class="sc-content-well"><div class="sc-grid {% if media %}sc-grid-2{% endif %}" style="align-items: center;">
<div>
{% if eyebrow %}<p class="sc-eyebrow">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 1rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if body %}<div class="sc-text" style="color: hsl(var(--muted-foreground)); line-height: 1.7;">{{ body|safe }}</div>{% endif %}
{% if ctaUrl and ctaLabel %}<a class="sc-btn-ghost" href="{{ ctaUrl }}" style="margin-top: 1.5rem;">{{ ctaLabel }}::lucide:arrow-right:sm::</a>{% endif %}
</div>
<div class="sc-media-frame" style="aspect-ratio: 4 / 3; background: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; {% if mediaPosition == "left" %}order: -1;{% endif %}">{% if media %}{% img media alt=mediaAlt class="sc-rich-media" %}{% endif %}</div>
{% if media %}<div class="sc-media-frame" style="aspect-ratio: 4 / 3; background: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; {% if mediaPosition == "left" %}order: -1;{% endif %}">{% img media alt=mediaAlt class="sc-rich-media" %}</div>{% endif %}
</div></div></section>

View File

@ -33,6 +33,8 @@
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}

View File

@ -30,6 +30,8 @@
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}

View File

@ -30,6 +30,8 @@
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}

View File

@ -36,6 +36,8 @@
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}

View File

@ -32,6 +32,8 @@
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}

View File

@ -28,6 +28,8 @@
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}

View File

@ -30,6 +30,8 @@
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}