# Noir theme — codeless .bnp manifest (WO-TF-022, Wave A). # Detective / true-crime dossier. Synthesized into manifest.pb by # `ninja plugin build` (no plugin.wasm). theme_presets: presets.json bundled_fonts: fonts.json master_pages: master_pages.json system_templates: - key: noir title: Noir description: "Detective and true-crime dossier theme: case-file cards, typewriter type, redaction bars, evidence board and film grain. High-key gallery by day, darkroom black by night." page_templates: - system: noir key: default title: Default description: "Case-file page with a typed masthead and a dissolved footer rule." slots: [header, main, footer] - system: noir key: full-width title: Full Width description: "Edge-to-edge dossier layout for evidence boards and lightbox grids." slots: [header, main, footer] - system: noir key: landing title: Landing description: "Spotlit case hero over film grain, a caption rule and a closing CTA." slots: [hero, main, cta, footer] - system: noir key: article title: Case File description: "Long-form case write-up with a sticky evidence rail and typed prose." slots: [header, main, aside, footer] - system: noir key: blog-index title: Case Index description: "Blog index framed as a case log, with featured and post-grid slots." slots: [header, featured, main, footer] - system: noir key: contact title: Tip Line description: "Contact layout with an optional aside for hours and location." slots: [header, main, aside, footer] - system: noir key: auth title: Access description: "Centred access shell with a typed wordmark over a darkroom ground." slots: [main, footer] template_overrides: # Chrome (4) - { template: noir, block: navbar } - { template: noir, block: footer } - { template: noir, block: utility-bar } - { template: noir, block: announcement-bar } # Marketing (12) - { template: noir, block: hero } - { template: noir, block: feature-grid } - { template: noir, block: rich-section } - { template: noir, block: stats } - { template: noir, block: testimonial } - { template: noir, block: logo-strip } - { template: noir, block: pricing } - { template: noir, block: team } - { template: noir, block: faq } - { template: noir, block: timeline } - { template: noir, block: cta } - { template: noir, block: countdown } # Primitives (11) - { template: noir, block: heading } - { template: noir, block: text } - { template: noir, block: rich-text } - { template: noir, block: image } - { template: noir, block: quote } - { template: noir, block: divider } - { template: noir, block: button } - { template: noir, block: card } - { template: noir, block: gallery } - { template: noir, block: video-embed } - { template: noir, block: audio-embed } # Commerce / local (6) - { template: noir, block: menu-list } - { template: noir, block: event-list } - { template: noir, block: contact-form } - { template: noir, block: contact-card } - { template: noir, block: hours-location } - { template: noir, block: social-links } # Blog family (12) - { template: noir, block: blog-hero } - { template: noir, block: featured-posts } - { template: noir, block: related-posts } - { template: noir, block: popular-posts } - { template: noir, block: category-list } - { template: noir, block: post-hero } - { template: noir, block: post-metadata } - { template: noir, block: author-bio } - { template: noir, block: author-bio-hero } - { template: noir, block: newsletter-signup } - { template: noir, block: breadcrumbs } - { template: noir, block: sidebar-nav } # Auth surface (3) - { template: noir, block: auth-form } - { template: noir, block: password-reset-form } - { template: noir, block: auth-status } # System (1) - { template: noir, block: page-suggestions } email_wrappers: - noir css: input_css_append: | /* ============================================================ Noir Theme — Detective / true-crime dossier utility CSS. Injected via CSSManifest.InputCSSAppend. Colour is ALWAYS consumed via hsl(var(--token)) with kebab-case tokens (the CMS emits --muted-foreground, --card-foreground, ... from the camelCase preset keys). Font families ALWAYS flow through var(--font-heading|body|mono, ) — never a hardcoded face. ============================================================ */ @layer utilities { /* --- Typography slots -------------------------------------- */ /* Fallback stacks name the ideal typewriter faces first (Special Elite / Courier Prime, admin-assignable via Google Fonts) then the shipped OFL defaults (IBM Plex Mono, Source Serif 4). */ .noir-heading { font-family: var(--font-heading, "Special Elite", "IBM Plex Mono", "Courier New", monospace); letter-spacing: 0.01em; } .noir-body { font-family: var(--font-body, "Source Serif 4", Georgia, "Times New Roman", serif); } .noir-mono { font-family: var(--font-mono, "Courier Prime", "IBM Plex Mono", "Courier New", monospace); } /* .noir-label — typed evidence label / eyebrow. */ .noir-label { font-family: var(--font-mono, "Courier Prime", "IBM Plex Mono", "Courier New", monospace); text-transform: uppercase; letter-spacing: 0.22em; font-size: 0.7rem; line-height: 1.4; } /* --- Hairline rules ---------------------------------------- */ .noir-rule { border: none; border-top: 1px solid hsl(var(--border)); margin: 0; } .noir-rule-b { border-bottom: 1px solid hsl(var(--border)); } .noir-hairline { border: 1px solid hsl(var(--border)); } /* --- .noir-file — manila case-file card -------------------- */ /* Square-cornered paper panel with a hairline border and a top file-tab drawn as a border accent. */ .noir-file { position: relative; background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border)); border-top: 2px solid hsl(var(--primary)); } /* .noir-tab — folder-tab caption bar sitting above a file. */ .noir-tab { display: inline-block; background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); font-family: var(--font-mono, "Courier Prime", "IBM Plex Mono", "Courier New", monospace); text-transform: uppercase; letter-spacing: 0.18em; font-size: 0.65rem; padding: 0.2rem 0.7rem; } /* --- .noir-stamp — rotated case-status stamp --------------- */ .noir-stamp { display: inline-block; border: 2px solid currentColor; padding: 0.2rem 0.6rem; font-family: var(--font-mono, "Courier Prime", "IBM Plex Mono", "Courier New", monospace); text-transform: uppercase; letter-spacing: 0.2em; font-size: 0.7rem; transform: rotate(-7deg); opacity: 0.9; } /* --- .noir-redact — redaction bar over text ---------------- */ /* Solid ink bar; reveals on hover/focus. Reduced-motion safe: the reveal is a colour swap with no transform. */ .noir-redact { background-color: hsl(var(--foreground)); color: transparent; border-radius: 1px; padding: 0 0.15em; transition: background-color 0.18s ease, color 0.18s ease; cursor: help; } .noir-redact:hover, .noir-redact:focus-visible { background-color: transparent; color: hsl(var(--foreground)); outline: none; } /* --- .noir-pin — evidence-board push pin ------------------- */ .noir-pin { position: relative; } .noir-pin::before { content: ""; position: absolute; top: -7px; left: 50%; transform: translateX(-50%); width: 12px; height: 12px; border-radius: 9999px; background: hsl(var(--primary)); box-shadow: 0 1px 2px hsl(var(--foreground) / 0.5); z-index: 2; } /* --- .noir-tape — masking-tape corner strip ---------------- */ .noir-tape { position: relative; } .noir-tape::before { content: ""; position: absolute; top: -0.5rem; left: 50%; transform: translateX(-50%) rotate(-3deg); width: 4.5rem; height: 1.1rem; background: hsl(var(--muted-foreground) / 0.28); border-left: 1px dashed hsl(var(--muted-foreground) / 0.4); border-right: 1px dashed hsl(var(--muted-foreground) / 0.4); z-index: 2; } /* --- .noir-grain — 3.5% film-grain overlay ----------------- */ .noir-grain { position: relative; isolation: isolate; } .noir-grain::after { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: 0.035; background-image: repeating-radial-gradient(circle at 19% 27%, hsl(var(--foreground)) 0 1px, transparent 1px 3px), repeating-radial-gradient(circle at 71% 63%, hsl(var(--foreground)) 0 1px, transparent 1px 3px); mix-blend-mode: overlay; } /* --- .noir-spotlight — darkroom vignette ------------------- */ .noir-spotlight { position: relative; } .noir-spotlight::before { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; background: radial-gradient(120% 90% at 50% 35%, transparent 40%, hsl(var(--background) / 0.85) 100%); } /* --- .noir-btn — square outline button --------------------- */ .noir-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; background-color: transparent; color: hsl(var(--foreground)); border: 1px solid hsl(var(--foreground)); padding: 0.7rem 1.5rem; font-family: var(--font-mono, "Courier Prime", "IBM Plex Mono", "Courier New", monospace); text-transform: uppercase; letter-spacing: 0.18em; font-size: 0.72rem; text-decoration: none; transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease; cursor: pointer; } .noir-btn:hover, .noir-btn:focus-visible { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary)); } .noir-btn:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; } /* .noir-btn-solid — filled evidence-accent variant. */ .noir-btn-solid { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary)); } .noir-btn-solid:hover, .noir-btn-solid:focus-visible { background-color: transparent; color: hsl(var(--foreground)); border-color: hsl(var(--foreground)); } /* --- .noir-link — evidence-accent underline link ----------- */ .noir-link { color: hsl(var(--primary)); text-decoration: none; border-bottom: 1px solid hsl(var(--primary)); transition: color 0.18s ease, border-color 0.18s ease; } .noir-link:hover, .noir-link:focus-visible { color: hsl(var(--accent)); border-bottom-color: hsl(var(--accent)); } /* --- Scroll reveal (MID motion, progressive enhancement) --- */ /* No-JS / no class: content is fully visible. The page template adds `.noir-js` to only when JS runs. */ .noir-js .noir-reveal { opacity: 0; transform: translateY(14px); transition: opacity 0.6s ease, transform 0.6s ease; will-change: opacity, transform; } .noir-js .noir-reveal.noir-in { opacity: 1; transform: none; } @media (prefers-reduced-motion: reduce) { .noir-js .noir-reveal { opacity: 1; transform: none; transition: none; } .noir-redact { transition: none; } } }