diff --git a/static/css/marketing.css b/static/css/marketing.css
index 820b94f..72b6535 100644
--- a/static/css/marketing.css
+++ b/static/css/marketing.css
@@ -497,6 +497,9 @@
.z-\[9999\] {
z-index: 9999;
}
+ .col-span-1 {
+ grid-column: span 1 / span 1;
+ }
.container {
width: 100%;
@media (width >= 40rem) {
@@ -3669,6 +3672,11 @@
opacity: 50%;
}
}
+ .sm\:col-span-2 {
+ @media (width >= 40rem) {
+ grid-column: span 2 / span 2;
+ }
+ }
.sm\:-mx-6 {
@media (width >= 40rem) {
margin-inline: calc(var(--spacing) * -6);
@@ -4000,6 +4008,11 @@
}
}
}
+ .md\:col-span-3 {
+ @media (width >= 48rem) {
+ grid-column: span 3 / span 3;
+ }
+ }
.md\:mb-2 {
@media (width >= 48rem) {
margin-bottom: calc(var(--spacing) * 2);
diff --git a/templates/macros/bento.html b/templates/macros/bento.html
index 3823e80..d4cd692 100644
--- a/templates/macros/bento.html
+++ b/templates/macros/bento.html
@@ -1,12 +1,14 @@
-{# Reusable bento card macro. FlexiHub style: dark navy2 surface, decorative watermark number, gradient icon corner. #}
+{# Reusable bento card macro. FlexiHub style: dark navy2 surface, decorative watermark number, gradient icon corner.
+ `span` controls column span via a static lookup table (Tailwind's content scanner only sees literal class strings,
+ so dynamic `col-span-{{ span }}` would produce dead classes — the lookup keeps the utilities discoverable). #}
{% macro bento_card(number, title, description, icon='✦', span='1') %}
-
+{%- set span_classes = {'1': 'col-span-1', '2': 'sm:col-span-2', '3': 'sm:col-span-2 md:col-span-3'} -%}
+
{{ number }}
{{ icon }}
-
{{ title }}
-
{{ description }}
+
{{ title | safe }}
+
{{ description | safe }}
{% endmacro %}
diff --git a/tests/test_marketing_landing_template.py b/tests/test_marketing_landing_template.py
index 8049332..7af2430 100644
--- a/tests/test_marketing_landing_template.py
+++ b/tests/test_marketing_landing_template.py
@@ -255,6 +255,8 @@ def test_bento_has_6_features():
# Watermark numbers 01..06
for n in ['01', '02', '03', '04', '05', '06']:
assert f'>{n}<' in body, f"Missing bento watermark number {n}"
+ # Card 04 must use French Q&R, not English Q&A — primary identifier check
+ assert 'Q&R' in body or 'Q&R' in body, "Card 04 must use French Q&R, not Q&A"
def test_bento_uses_flexihub_styling():
@@ -281,3 +283,20 @@ def test_bento_uses_wcag_safe_text_on_dark():
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'text-white/70' in body, "Missing WCAG-safe /70 text opacity on dark cards"
+
+
+def test_bento_renders_nbsp_entities_not_escaped():
+ """Card 01 '95 %+' NBSP must render as a non-breaking space, not as literal ' ' text.
+
+ Regression guard: if the bento macro stops piping description through `| safe`,
+ Jinja autoescape will double-escape ' ' to ' ' and users see the
+ raw entity. The HTML response must contain the literal '95 %+' once
+ (single escape), never '95 %+'.
+ """
+ client = app.test_client()
+ body = client.get('/').data.decode('utf-8')
+ assert '95 %+' in body, "NBSP entity should appear single-escaped in card 01"
+ assert '95 ' not in body, "NBSP entity must not be double-escaped (missing | safe?)"
+ # Q&R card title: French ampersand must survive as & in HTML, not &
+ assert 'Q&R' in body, "Q&R title should appear single-escaped"
+ assert 'Q&R' not in body, "Q&R title must not be double-escaped"