diff --git a/manifest.yaml b/manifest.yaml
index 495d02e..e8f6f6b 100644
--- a/manifest.yaml
+++ b/manifest.yaml
@@ -338,3 +338,52 @@ css:
.noir-redact { transition: none; }
}
}
+
+ /* ============================================================
+ Explicit responsive layout (authored CSS — NOT purged).
+ The host Tailwind JIT does not reliably scan a theme's override
+ templates, so sm:/md:/lg: grid variants declared there can be
+ dropped from the build (a grid then never expands on desktop
+ and never collapses on mobile). These media-query rules own the
+ column counts deterministically and always collapse to a single
+ column on phones.
+ ============================================================ */
+ .noir-cols { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
+ @media (min-width: 640px) {
+ .noir-cols-2, .noir-cols-3, .noir-cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ }
+ @media (min-width: 1024px) {
+ .noir-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .noir-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+ }
+ /* Grids that stay two-up on phones (galleries, stat rows, footer links). */
+ .noir-cols-2up { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem; }
+ @media (min-width: 768px) {
+ .noir-cols-2up-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .noir-cols-2up-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+ }
+ /* Media/text split — one column on phones, two from the tablet width. */
+ .noir-split { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
+ @media (min-width: 768px) { .noir-split { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+ /* Gap modifiers (used alongside any grid class above). */
+ .noir-gap-sm { gap: 0.75rem; }
+ .noir-gap-lg { gap: 2rem; }
+
+ /* Hero vertical rhythm — replaces the unreliable min-h-[75vh] arbitrary
+ class so the masthead has bounded, predictable breathing room instead
+ of collapsing to nothing or ballooning to 75vh of empty space. */
+ .noir-hero-pad { padding-top: 3.5rem; padding-bottom: 3.5rem; }
+ @media (min-width: 768px) { .noir-hero-pad { padding-top: 5rem; padding-bottom: 5rem; } }
+ @media (min-width: 1024px) { .noir-hero-pad { padding-top: 6.5rem; padding-bottom: 6.5rem; } }
+
+ /* Empty-media graceful state — the demo seed ships no images, so any
+ media frame that renders an empty
must not balloon into a grey
+ box or show broken-image chrome. Images never exceed their column. */
+ .noir-file img, .bn-card img, .bn-gallery img, figure img { max-width: 100%; height: auto; }
+ img[src=""], img:not([src]) { display: none; }
+
+ /* Overflow safety — long typed/mono labels wrap rather than push the
+ page wider than a phone viewport (uppercase + wide tracking is the risk). */
+ .noir-label, .noir-mono, .noir-heading { overflow-wrap: break-word; }
+ /* Never let a block force a horizontal scrollbar on small screens. */
+ [data-block-override], [data-block] { max-width: 100%; }
diff --git a/templates/noir/default.ninjatpl b/templates/noir/default.ninjatpl
index 29aceb0..db28cea 100644
--- a/templates/noir/default.ninjatpl
+++ b/templates/noir/default.ninjatpl
@@ -22,8 +22,11 @@
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var els=document.querySelectorAll('.noir-reveal');
if(reduce||!('IntersectionObserver' in window)){els.forEach(function(el){el.classList.add('noir-in');});return;}
- var io=new IntersectionObserver(function(entries){entries.forEach(function(e){if(e.isIntersecting){e.target.classList.add('noir-in');io.unobserve(e.target);}});},{threshold:0.12});
- els.forEach(function(el){io.observe(el);});
+ var io=new IntersectionObserver(function(entries){entries.forEach(function(e){if(e.isIntersecting){e.target.classList.add('noir-in');io.unobserve(e.target);}});},{threshold:0,rootMargin:'0px 0px -8% 0px'});
+ function showAll(){els.forEach(function(el){el.classList.add('noir-in');});}
+ els.forEach(function(el){var r=el.getBoundingClientRect();var h=window.innerHeight||document.documentElement.clientHeight||0;if(r.top0){el.classList.add('noir-in');}else{io.observe(el);}});
+ window.addEventListener('load',showAll);
+ setTimeout(showAll,1600);
})();