themes-noir/manifest.yaml
Alex Dunmow 1451f06d9f feat(noir): WO-TF-022 Wave A — detective/true-crime dossier rebuild
Re-persona noir from photography to the locked detective/true-crime
direction: case-file panels, typewriter type, redaction bars, evidence
board, film grain. High-key gallery (light) / darkroom black + amber
safelight (dark).

- 49 mandatory builtin overrides (chrome, marketing, primitives,
  commerce, blog family, auth, page-suggestions), re-skinned from the
  frozen builtin defaults with every field/provider/tag/JS preserved;
  legacy button/card field reads reconciled to the new contract.
- 7 page templates (default, full-width, landing, article, blog-index,
  contact, auth) + themed email wrapper.
- 4 persona blocks (case_file, evidence_board, redacted_quote,
  case_dossier); old photography blocks deleted (now overrides).
- 5 dual-mode presets (19 tokens, light+dark tuned).
- Bundled OFL fonts (Source Serif 4, IBM Plex Mono) + licenses;
  RECOMMENDED_FONTS/ICONS updated; required_icon_packs=lucide.
- Master pages + seed demo ("The Cold File": home/about/blog x3/contact
  with case_tips data table + row_inserted email-admins workflow/login).
- noir CSS vocabulary via manifest css.input_css_append; MID motion
  (scroll-reveal + gallery lightbox, reduced-motion honored, no canvas).

Verified: make build exit 0; check-safety 32 checks 19 ok 13 skip -> OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:48:44 +08:00

341 lines
12 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.
============================================================ */
@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; }
}
}