themes-noir/manifest.yaml
Alex Dunmow 1283b86ffa fix(noir): self-served @font-face + preset typography (bundled fonts are inert)
The CMS never consumes LoadedPlugin.BundledFonts, so fonts.json alone renders
fallback sans. Declare @font-face against the served artifact paths
(/templates/noir/fonts/*.woff2) in the appended CSS and set
typography.fontHeading/body/mono = template:noir:<Family> in all five presets
so --font-heading/--font-body/--font-mono resolve to the bundled faces
(IBM Plex Mono / Source Serif 4). gotham@1.2.0 precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:55:04 +08:00

364 lines
14 KiB
YAML

# 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, <fallback>) — never a hardcoded face.
============================================================ */
/* Bundled faces. The host does not yet emit @font-face for
bundled_fonts (LoadedPlugin.BundledFonts is unconsumed), so the theme
declares its own faces against the served artifact paths
(/templates/noir/fonts/...). Presets set typography.fontHeading/body/mono
to template:noir:<Family> so --font-heading/--font-body/--font-mono
resolve to these families. (gotham@1.2.0 precedent.) */
@font-face { font-family: 'Source Serif 4'; src: url('/templates/noir/fonts/source-serif-4-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Source Serif 4'; src: url('/templates/noir/fonts/source-serif-4-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/noir/fonts/ibm-plex-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/noir/fonts/ibm-plex-mono-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
@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 <html> 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; }
}
}
/* Empty-media graceful state — the demo seed ships no images, so any
media frame that renders an empty <img> 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%; }