Skip to main content

moss_core/contract/
components.rs

1//! moss component contract — Source 2 of the federated contract.
2//!
3//! Single source of truth for every `moss-*` class moss currently emits.
4//! Each entry declares: the class name, its kind (container/instance/standalone/chrome),
5//! accepted `data-*` attributes with value spaces, example HTML, example markdown.
6//!
7//! ## Adding a new emitter class
8//!
9//! 1. Emit the class from your renderer module (`build/markdown/*`, `build/components/*`).
10//! 2. Add a `ComponentEntry` to [`COMPONENTS`] here.
11//! 3. Run `cargo test --test components_sync_test` from src-tauri/ — the
12//!    scanner test will fail if you forget.
13//! 4. Run `cargo run --bin generate-artifacts --features dev-tools -- contract-docs` to
14//!    refresh `docs/reference/contract.md`.
15//!
16//! ## Why a const table, not a derive macro?
17//!
18//! Mirrors the BUILTIN_FIELDS precedent in `schema_fields.rs`. The synchronization
19//! is enforced by a sync test (`emitter_classes_match_components_table`) that
20//! scans emitter Rust source for `class="moss-..."` literals. This is a
21//! best-effort scanner (won't catch classes assembled via `format!()`), not a
22//! type-checked guarantee like BUILTIN_FIELDS' compile-time mirror. The
23//! limitation is documented in the spec § Source 2.
24
25/// Status of a component entry.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Status {
28    /// In active use; theme authors can rely on it.
29    Confirmed,
30    /// Emerging convention; may evolve.
31    Emerging,
32    /// Scheduled for removal; theme authors should migrate.
33    Retired,
34}
35
36/// A declared `data-*` attribute on a component.
37pub struct DataAttr {
38    /// Attribute name including `data-` prefix (e.g. `"data-layout"`).
39    pub name: &'static str,
40    /// Allowed values (e.g. `&["grid", "list", "minimal"]`). Empty means free-form.
41    pub values: &'static [&'static str],
42    /// Default value (first in `values`, or `""` for free-form).
43    pub default: &'static str,
44    /// Short description shown in reference.md.
45    pub description: &'static str,
46}
47
48/// A single component contract entry.
49pub struct ComponentEntry {
50    /// Class name without leading `.` (e.g. `"moss-cards"`).
51    pub class: &'static str,
52    /// Container / Instance / Standalone / Chrome.
53    pub kind: &'static str,
54    /// For Instance kinds, the parent container's class (or `""`).
55    pub parent: &'static str,
56    /// Declared `data-*` attributes on the element with this class.
57    pub data_attrs: &'static [DataAttr],
58    /// Example HTML snippet showing the class in context. Multi-line allowed.
59    pub example_html: &'static str,
60    /// Example markdown that produces this HTML. Empty for HTML-only chrome.
61    pub example_markdown: &'static str,
62    /// Status: confirmed / emerging / retired.
63    pub status: Status,
64    /// Contract version this entry was introduced in.
65    pub since: &'static str,
66    /// Optional human-readable description.
67    pub description: &'static str,
68}
69
70/// Classes in [`COMPONENTS`] that deliberately carry no `moss-` prefix.
71///
72/// Every other entry must be `moss-`-prefixed; `every_component_has_a_class_name`
73/// enforces that and consults this list for the exceptions. The list lives here,
74/// beside the table, rather than in the test: an unprefixed class is a decision
75/// made when the entry is *written*, and a reviewer reading the entry has to be
76/// able to see that the decision was made. It was in the test file until
77/// 2026-08-06, and the split cost a red build — declaring the masthead and nav
78/// interior added 21 unprefixed entries whose exemption had to be recorded in a
79/// file nobody editing the table had open.
80///
81/// Two families, both emitted for **theme parity** — themes written against
82/// these names predate the `moss-` convention, so renaming them would break
83/// styling moss does not own:
84///
85/// - Obsidian-style callouts (`callout`, `callout-<type>`), emitted alongside
86///   their `moss-callout` equivalents.
87/// - The nav interior and article masthead (`main-nav`, `date-line`, …).
88///
89/// Prefix matching is deliberate for the callout family only: `callout-<type>`
90/// is an open set that grows with the callout vocabulary. The chrome names are
91/// a closed set and are listed exactly, so a typo'd new one still fails.
92pub const UNPREFIXED_LEGACY_CLASSES: &[&str] = &[
93    // Obsidian callout parity — `callout-*` is matched by prefix, see below.
94    "callout",
95    // Nav interior.
96    "main-nav",
97    "nav-left",
98    "nav-right",
99    "nav-links",
100    "nav-icons",
101    "nav-search-btn",
102    "nav-theme-btn",
103    "nav-lang-toggle",
104    "nav-lang-current",
105    "nav-lang-link",
106    "search-icon",
107    "theme-toggle-icon",
108    "mobile-menu-button",
109    "site-name",
110    "site-logo",
111    "breadcrumb-segment",
112    "breadcrumb-label",
113    "breadcrumb-separator",
114    // Article masthead.
115    "date-line",
116    "date",
117    // Default footer.
118    "footer-default",
119    "footer-link",
120    // Surfaces the site JavaScript reads (declared 2026-08-09). Same reason as
121    // the nav: these names were emitted, styled, and queried by name long before
122    // the `moss-` convention, and a theme that targets them today would break if
123    // they were renamed for tidiness.
124    "container",
125    "nav-content",
126    "font-anchor",
127    "font-trigger",
128    "cover-thumb",
129    "media-item",
130    "lightbox-content",
131    "lightbox-image",
132    "lightbox-video",
133    "lightbox-iframe",
134    "lightbox-title",
135    "lightbox-article-link",
136    "lightbox-close",
137    "lightbox-next",
138    "lightbox-prev",
139    "comments-toggle",
140    "comment-list",
141    "comment-item",
142    "comment-replies",
143    "comment-reply-btn",
144    // Unprefixed names moss ships that were governed by nothing until the
145    // sync test learned to read escaped-quote emission and the feature
146    // stylesheets (2026-08-21). These are not "legacy parity" cases like the
147    // callout and nav families above — no theme predates them. They are simply
148    // names moss emits without a prefix, which under ADR-063 is what the whole
149    // contract is heading towards; they are listed here because the prefix
150    // rule is still in force until that migration runs.
151    "active",
152    "comment-author",
153    "comment-body",
154    "comment-date",
155    "comment-field",
156    "comment-form",
157    "comment-form-meta",
158    "comment-form-slot",
159    "comment-form-status",
160    "comment-form-submit",
161    "comment-header",
162    "comment-source-link",
163    "comments-chevron",
164    "comments-icon",
165    "empty",
166    "font-pill",
167    "has-sidebar-layout",
168    "latest-sidebar",
169    "lightbox",
170    "lightbox-caption",
171    "lightbox-nav",
172    "link-preview",
173    "link-preview-domain",
174    "link-preview-title",
175    "main-content",
176    "media-grid",
177    "media-overlay",
178    "media-title",
179    "minimal",
180    "page-wrapper",
181    "review-biblio",
182    "review-colophon",
183    "review-colophon-details",
184    "review-colophon-identity",
185    "review-colophon-subtitle",
186    "review-colophon-title",
187    "review-community-rating",
188    "review-links",
189    "review-rating",
190    "review-sep",
191    "sidebar-more",
192    "size-std",
193    "title",
194    "wikilink",
195];
196
197/// Whether `class` is exempt from the `moss-` prefix rule.
198///
199/// See [`UNPREFIXED_LEGACY_CLASSES`]. The `callout-` prefix arm covers the
200/// open-ended Obsidian callout types (`callout-note`, `callout-warning`, …).
201pub fn is_unprefixed_legacy(class: &str) -> bool {
202    class.starts_with("callout-") || UNPREFIXED_LEGACY_CLASSES.contains(&class)
203}
204
205/// The full contract surface — every `moss-*` class moss currently emits.
206///
207/// Phase 0b seeds this with the CURRENT emitted vocabulary (not the
208/// v1-collapsed shape). Phase 1c rewrites to the collapsed form.
209pub const COMPONENTS: &[ComponentEntry] = &[
210    ComponentEntry {
211        class: "moss-cards",
212        kind: "container",
213        parent: "",
214        data_attrs: &[
215            DataAttr {
216                name: "data-layout",
217                values: &["grid", "list", "minimal"],
218                default: "grid",
219                description: "Card layout density. Grid: 2-3 cols with covers. List: single column with side covers. Minimal: text-only with year groupings.",
220            },
221            DataAttr {
222                name: "data-density",
223                values: &["default", "compact"],
224                default: "default",
225                description: "Vertical spacing density.",
226            },
227            DataAttr {
228                name: "data-list-axis",
229                values: &["date", "weight", "title"],
230                default: "title",
231                description: "Sort axis for the listing (mirrors the folder's `sort:` frontmatter). Drives `--moss-card-min` density tuning and decides whether each `.moss-card-meta` slot is filled (date axis) or omitted (weight/title axes).",
232            },
233            DataAttr {
234                name: "data-list-has-covers",
235                values: &[""],
236                default: "",
237                description: "Boolean presence flag: emitted iff any child card has a cover. Combines with `data-list-axis` to widen `--moss-card-min` for cover-led layouts. Use `[data-list-has-covers]` in CSS to target it.",
238            },
239        ],
240        example_html: r#"<div class="moss-cards-container">
241  <div class="moss-cards" data-layout="grid" data-list-axis="date" data-list-has-covers>
242    <a class="moss-card" href="...">...</a>
243    <a class="moss-card" href="...">...</a>
244  </div>
245</div>"#,
246        example_markdown: "",
247        status: Status::Confirmed,
248        since: "1",
249        description: "Auto-generated listing of child pages. The single canonical container; layout density on `data-layout` (`grid` for cover-led tiles, `list` for cover+excerpt rows, `minimal` for text-only year-grouped indexes). Wrapped in `.moss-cards-container` to scope CSS container queries.",
250    },
251    ComponentEntry {
252        class: "moss-cards-container",
253        kind: "container",
254        parent: "",
255        data_attrs: &[],
256        example_html: r#"<div class="moss-cards-container">
257  <div class="moss-cards" data-layout="grid">...</div>
258</div>"#,
259        example_markdown: "",
260        status: Status::Confirmed,
261        since: "1",
262        description: "Outer wrapper around `.moss-cards` that carries `container-type: inline-size` so the grid can use `@container` queries instead of viewport `@media` queries. Layout-agnostic — wraps any `data-layout` variant.",
263    },
264    ComponentEntry {
265        class: "moss-summary-layout",
266        kind: "container",
267        parent: "moss-cards",
268        data_attrs: &[],
269        example_html: r#"<div class="moss-cards" data-layout="list">...</div>"#,
270        example_markdown: "",
271        status: Status::Retired,
272        since: "1",
273        description: "Retired: the additional co-class on `.moss-cards[data-layout=\"list\"]` had no matching rules in the default CSS once `children_style: summary` collapsed into the list-layout block, and its lingering emission broke themes that hid the class (e.g. SoCiviC's `.moss/theme/style.css` keyed `display: none` on it, erasing folder-embed listings). Theme authors targeting summary listings should use `.moss-cards[data-layout=\"list\"]` directly.",
274    },
275    // -------------------------------------------------------------------
276    // Cards family — current emitted vocabulary (pre-Phase 1c collapsing).
277    // Three parallel layouts: grid, list, minimal. Each has its own
278    // container + instance + sub-classes.
279    // -------------------------------------------------------------------
280    ComponentEntry {
281        class: "moss-cards-grid",
282        kind: "container",
283        parent: "",
284        data_attrs: &[],
285        example_html: r#"<div class="moss-cards-grid">
286  <a class="moss-card-grid" href="...">...</a>
287</div>"#,
288        example_markdown: "",
289        status: Status::Retired,
290        since: "0",
291        description: "Retired in Phase 1c — collapsed into `.moss-cards[data-layout=grid]`.",
292    },
293    ComponentEntry {
294        class: "moss-cards-list",
295        kind: "container",
296        parent: "",
297        data_attrs: &[],
298        example_html: r#"<div class="moss-cards-list">
299  <a class="moss-card-list" href="...">...</a>
300</div>"#,
301        example_markdown: "",
302        status: Status::Retired,
303        since: "0",
304        description: "Retired in Phase 1c — collapsed into `.moss-cards[data-layout=list]`.",
305    },
306    ComponentEntry {
307        class: "moss-cards-minimal-year-group",
308        kind: "container",
309        parent: "",
310        data_attrs: &[],
311        example_html: r#"<section class="moss-cards-minimal-year-group">
312  <h3>2024</h3>
313  <div class="moss-card-minimal">...</div>
314</section>"#,
315        example_markdown: "",
316        status: Status::Confirmed,
317        since: "0",
318        description: "Year-grouped section in minimal card layout (e.g. blog index). Modifier `--summary` collapses past years.",
319    },
320    ComponentEntry {
321        class: "minimal",
322        kind: "instance",
323        parent: "moss-cards-minimal-year-group",
324        data_attrs: &[],
325        example_html: r#"<section class="moss-cards-minimal-year-group minimal">…</section>"#,
326        example_markdown: "",
327        status: Status::Confirmed,
328        since: "0",
329        description: "Bare co-class on a minimal year group, alongside the prefixed name. Retained because themes in the wild key on it.",
330    },
331    ComponentEntry {
332        class: "moss-cards-minimal-year-group--summary",
333        kind: "container",
334        parent: "moss-cards-minimal-year-group",
335        data_attrs: &[],
336        example_html: r#"<section class="moss-cards-minimal-year-group moss-cards-minimal-year-group--summary">...</section>"#,
337        example_markdown: "",
338        status: Status::Confirmed,
339        since: "0",
340        description: "BEM modifier on `.moss-cards-minimal-year-group`. Applied to year groups that should render in collapsed summary form (e.g. past years on a blog index).",
341    },
342    ComponentEntry {
343        class: "moss-card",
344        kind: "instance",
345        parent: "moss-cards",
346        data_attrs: &[
347            DataAttr {
348                name: "data-linkblog",
349                values: &[],
350                default: "",
351                description: "Presence flag: emitted IFF the card's source page has an `external_url:` frontmatter (linkblog pattern). When set, the element is a `<div>` rather than `<a>` so the kicker can host a nested `<a>★</a>` archive link; title, cover, and description carry their own inner anchors to the canonical URL. Absent on ordinary cards (single whole-card `<a>`).",
352            },
353            DataAttr {
354                name: "data-cover-color",
355                values: &[],
356                default: "",
357                description: "Presence flag: emitted IFF the cover-colour ladder produced a colour, which arrives alongside it as `--moss-cover-color` in the element's `style`. Absent for a card with no cover, and for an image cover whose file moss could not read.",
358            },
359        ],
360        example_html: r#"<a class="moss-card" href="...">...</a>"#,
361        example_markdown: "",
362        status: Status::Confirmed,
363        since: "1",
364        description: "v1 collapsed shape — single canonical instance class inside `.moss-cards`. Layout-specific styling targets `.moss-cards[data-layout=X] .moss-card`. Tag is `<a>` for ordinary cards and `<div>` for linkblog cards (`[data-linkblog]`).",
365    },
366    ComponentEntry {
367        class: "moss-card-cover",
368        kind: "instance",
369        parent: "moss-card",
370        data_attrs: &[],
371        example_html: r#"<div class="moss-card-cover"><img src="..." /></div>"#,
372        example_markdown: "",
373        status: Status::Confirmed,
374        since: "1",
375        description: "Cover media slot inside `.moss-card`. Gets `.moss-card-no-cover` modifier when no image is present.",
376    },
377    ComponentEntry {
378        class: "moss-card-no-cover",
379        kind: "instance",
380        parent: "moss-card",
381        data_attrs: &[],
382        example_html: r#"<div class="moss-card-cover moss-card-no-cover"></div>"#,
383        example_markdown: "",
384        status: Status::Confirmed,
385        since: "1",
386        description: "Modifier applied to `.moss-card-cover` when no cover media is available.",
387    },
388    ComponentEntry {
389        class: "moss-card-content",
390        kind: "instance",
391        parent: "moss-card",
392        data_attrs: &[],
393        example_html: r#"<div class="moss-card-content">...</div>"#,
394        example_markdown: "",
395        status: Status::Confirmed,
396        since: "1",
397        description: "Text content slot inside a grid-layout `.moss-card` (kicker + title + meta).",
398    },
399    ComponentEntry {
400        class: "moss-card-row",
401        kind: "instance",
402        parent: "moss-card",
403        data_attrs: &[],
404        example_html: r#"<div class="moss-card-row">...</div>"#,
405        example_markdown: "",
406        status: Status::Confirmed,
407        since: "1",
408        description: "Row wrapper inside a list-layout `.moss-card` holding body + cover side-by-side.",
409    },
410    ComponentEntry {
411        class: "moss-card-body",
412        kind: "instance",
413        parent: "moss-card",
414        data_attrs: &[],
415        example_html: r#"<div class="moss-card-body">...</div>"#,
416        example_markdown: "",
417        status: Status::Confirmed,
418        since: "1",
419        description: "Text body slot of a list-layout `.moss-card`.",
420    },
421    ComponentEntry {
422        class: "moss-card-head",
423        kind: "instance",
424        parent: "moss-card",
425        data_attrs: &[],
426        example_html: r#"<div class="moss-card-head">...</div>"#,
427        example_markdown: "",
428        status: Status::Confirmed,
429        since: "1",
430        description: "Header row of a `.moss-card-body` (title + kicker + meta).",
431    },
432    ComponentEntry {
433        class: "moss-card-title",
434        kind: "instance",
435        parent: "moss-card",
436        data_attrs: &[],
437        example_html: r#"<h3 class="moss-card-title">Page title</h3>"#,
438        example_markdown: "",
439        status: Status::Confirmed,
440        since: "1",
441        description: "Title inside `.moss-card`.",
442    },
443    ComponentEntry {
444        class: "moss-card-meta",
445        kind: "instance",
446        parent: "moss-card",
447        data_attrs: &[],
448        example_html: r#"<div class="moss-card-meta">2024-01-15</div>"#,
449        example_markdown: "",
450        status: Status::Confirmed,
451        since: "1",
452        description: "Type-aware metadata slot (date for articles, count for folders, domain for links). Renders ABOVE the title in horizontal mode — filling the kicker position when the explicit `kicker` slot is unset, per `docs/reference/design/preview-cards.md:22-30`. To the right of the title in vertical CJK mode (the horizontal kicker position transposed). Meta IS the visual kicker, with the same uppercase overline treatment.",
453    },
454    ComponentEntry {
455        class: "moss-card-kicker",
456        kind: "instance",
457        parent: "moss-card",
458        data_attrs: &[],
459        example_html: r#"<span class="moss-card-kicker">Category</span>"#,
460        example_markdown: "",
461        status: Status::Confirmed,
462        since: "1",
463        description: "Eyebrow / overline above the title inside `.moss-card`.",
464    },
465    ComponentEntry {
466        class: "moss-card-permalink",
467        kind: "instance",
468        parent: "moss-card-kicker",
469        data_attrs: &[],
470        example_html: r#"<a class="moss-card-permalink" href="/posts/foo/" title="Permalink to 'Title'">★</a>"#,
471        example_markdown: "",
472        status: Status::Emerging,
473        since: "1",
474        description: "Author's-archive link mark (★, U+2605) emitted INSIDE `.moss-card-kicker` for linkblog cards (those whose child page has `external_url:`). The card title links to the external canonical (publisher); the `★` links to the local archival copy at the page's slug. Reads as part of the kicker line — \"Publisher · Year ★\". Semantically distinct from Daring-Fireball's linkblog ★ (which marks discussion permalink alongside commentary) — here the local copy is the same content preserved for resilience and stable bylines, not added commentary. Putting `<a>★</a>` inside the kicker is valid because linkblog cards emit `<div class=\"moss-card\" data-linkblog>` (not `<a>`) as the outer element — see the `data-linkblog` attribute described on `.moss-card`.",
475    },
476    ComponentEntry {
477        class: "moss-card-title-link",
478        kind: "instance",
479        parent: "moss-card-head",
480        data_attrs: &[],
481        example_html: r#"<a class="moss-card-title-link" href="https://outlet.example/article"><h3 class="moss-card-title">Article Title</h3></a>"#,
482        example_markdown: "",
483        status: Status::Emerging,
484        since: "1",
485        description: "Anchor wrapping the `.moss-card-title` `<h3>` on linkblog cards. Ordinary cards have the whole-card `<a class=\"moss-card\">` as the link target — but linkblog cards switch the outer to `<div>` so the kicker can host a nested `★` anchor, which means the title needs its own anchor to stay clickable. Same canonical-URL target as the other inner anchors (`moss-card-cover-link`, `moss-card-description-link`).",
486    },
487    ComponentEntry {
488        class: "moss-card-cover-link",
489        kind: "instance",
490        parent: "moss-card-row",
491        data_attrs: &[],
492        example_html: r#"<a class="moss-card-cover-link" href="https://outlet.example/article"><div class="moss-card-cover">...</div></a>"#,
493        example_markdown: "",
494        status: Status::Emerging,
495        since: "1",
496        description: "Anchor wrapping the `.moss-card-cover` on linkblog cards — same role as `.moss-card-title-link` but for the cover image / media. Targets the canonical (external) URL.",
497    },
498    ComponentEntry {
499        class: "moss-card-description-link",
500        kind: "instance",
501        parent: "moss-card-body",
502        data_attrs: &[],
503        example_html: r#"<a class="moss-card-description-link" href="https://outlet.example/article"><p class="moss-card-description">…</p></a>"#,
504        example_markdown: "",
505        status: Status::Emerging,
506        since: "1",
507        description: "Anchor wrapping the `.moss-card-description` on linkblog cards — same role as `.moss-card-title-link` but for the description excerpt. Targets the canonical (external) URL.",
508    },
509    ComponentEntry {
510        class: "moss-card-description",
511        kind: "instance",
512        parent: "moss-card",
513        data_attrs: &[],
514        example_html: r#"<p class="moss-card-description">Excerpt...</p>"#,
515        example_markdown: "",
516        status: Status::Confirmed,
517        since: "1",
518        description: "Excerpt / description paragraph inside a `.moss-card` — below the title in both grid- and list-layout cards.",
519    },
520    ComponentEntry {
521        class: "moss-card-count",
522        kind: "instance",
523        parent: "moss-card",
524        data_attrs: &[],
525        example_html: r#"<div class="moss-card-count">4 articles</div>"#,
526        example_markdown: "",
527        status: Status::Confirmed,
528        since: "1",
529        description: "Tertiary subtitle line showing `N articles` for a folder card. Renders only on non-date listings when the folder card has no `description` to display.",
530    },
531    ComponentEntry {
532        class: "moss-embed-more",
533        kind: "instance",
534        parent: "moss-cards-container",
535        data_attrs: &[],
536        example_html: r#"<p class="moss-embed-more"><a href="/news/">More →</a></p>"#,
537        example_markdown: "",
538        status: Status::Confirmed,
539        since: "1",
540        description: "Trailing \"More →\" link on a truncated children listing (emitted when `children_limit` caps the embed); links to the folder's full index. Rendered as a sibling immediately after `.moss-cards-container`, so it sits outside the listing's flex `gap` and binds to the list via its own `margin-top` (see docs/reference/design/spacing.md).",
541    },
542    ComponentEntry {
543        class: "moss-card-grid",
544        kind: "instance",
545        parent: "moss-cards-grid",
546        data_attrs: &[],
547        example_html: r#"<a class="moss-card-grid" href="...">...</a>"#,
548        example_markdown: "",
549        status: Status::Retired,
550        since: "0",
551        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=grid]`).",
552    },
553    ComponentEntry {
554        class: "moss-card-grid-cover",
555        kind: "instance",
556        parent: "moss-card-grid",
557        data_attrs: &[],
558        example_html: r#"<div class="moss-card-grid-cover"><img src="..." /></div>"#,
559        example_markdown: "",
560        status: Status::Retired,
561        since: "0",
562        description: "Retired in Phase 1c — collapsed into `.moss-card-cover`.",
563    },
564    ComponentEntry {
565        class: "moss-card-grid-no-cover",
566        kind: "instance",
567        parent: "moss-card-grid",
568        data_attrs: &[],
569        example_html: r#"<div class="moss-card-grid-cover moss-card-grid-no-cover"></div>"#,
570        example_markdown: "",
571        status: Status::Retired,
572        since: "0",
573        description: "Retired in Phase 1c — collapsed into `.moss-card-no-cover`.",
574    },
575    ComponentEntry {
576        class: "moss-card-grid-content",
577        kind: "instance",
578        parent: "moss-card-grid",
579        data_attrs: &[],
580        example_html: r#"<div class="moss-card-grid-content">...</div>"#,
581        example_markdown: "",
582        status: Status::Retired,
583        since: "0",
584        description: "Retired in Phase 1c — collapsed into `.moss-card-content`.",
585    },
586    ComponentEntry {
587        class: "moss-card-grid-kicker",
588        kind: "instance",
589        parent: "moss-card-grid",
590        data_attrs: &[],
591        example_html: r#"<span class="moss-card-grid-kicker">Category</span>"#,
592        example_markdown: "",
593        status: Status::Retired,
594        since: "0",
595        description: "Retired in Phase 1c — collapsed into `.moss-card-kicker`.",
596    },
597    ComponentEntry {
598        class: "moss-card-grid-title",
599        kind: "instance",
600        parent: "moss-card-grid",
601        data_attrs: &[],
602        example_html: r#"<h3 class="moss-card-grid-title">Page title</h3>"#,
603        example_markdown: "",
604        status: Status::Retired,
605        since: "0",
606        description: "Retired in Phase 1c — collapsed into `.moss-card-title`.",
607    },
608    ComponentEntry {
609        class: "moss-card-grid-meta",
610        kind: "instance",
611        parent: "moss-card-grid",
612        data_attrs: &[],
613        example_html: r#"<div class="moss-card-grid-meta">2024-01-15</div>"#,
614        example_markdown: "",
615        status: Status::Retired,
616        since: "0",
617        description: "Retired in Phase 1c — collapsed into `.moss-card-meta`.",
618    },
619    ComponentEntry {
620        class: "moss-card-list",
621        kind: "instance",
622        parent: "moss-cards-list",
623        data_attrs: &[],
624        example_html: r#"<a class="moss-card-list" href="...">...</a>"#,
625        example_markdown: "",
626        status: Status::Retired,
627        since: "0",
628        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=list]`).",
629    },
630    ComponentEntry {
631        class: "moss-card-list-row",
632        kind: "instance",
633        parent: "moss-card-list",
634        data_attrs: &[],
635        example_html: r#"<div class="moss-card-list-row">...</div>"#,
636        example_markdown: "",
637        status: Status::Retired,
638        since: "0",
639        description: "Retired in Phase 1c — collapsed into `.moss-card-row`.",
640    },
641    ComponentEntry {
642        class: "moss-card-list-cover",
643        kind: "instance",
644        parent: "moss-card-list",
645        data_attrs: &[],
646        example_html: r#"<div class="moss-card-list-cover"><img src="..." /></div>"#,
647        example_markdown: "",
648        status: Status::Retired,
649        since: "0",
650        description: "Retired in Phase 1c — collapsed into `.moss-card-cover`.",
651    },
652    ComponentEntry {
653        class: "moss-card-list-body",
654        kind: "instance",
655        parent: "moss-card-list",
656        data_attrs: &[],
657        example_html: r#"<div class="moss-card-list-body">...</div>"#,
658        example_markdown: "",
659        status: Status::Retired,
660        since: "0",
661        description: "Retired in Phase 1c — collapsed into `.moss-card-body`.",
662    },
663    ComponentEntry {
664        class: "moss-card-list-head",
665        kind: "instance",
666        parent: "moss-card-list",
667        data_attrs: &[],
668        example_html: r#"<div class="moss-card-list-head">...</div>"#,
669        example_markdown: "",
670        status: Status::Retired,
671        since: "0",
672        description: "Retired in Phase 1c — collapsed into `.moss-card-head`.",
673    },
674    ComponentEntry {
675        class: "moss-card-list-kicker",
676        kind: "instance",
677        parent: "moss-card-list",
678        data_attrs: &[],
679        example_html: r#"<span class="moss-card-list-kicker">Category</span>"#,
680        example_markdown: "",
681        status: Status::Retired,
682        since: "0",
683        description: "Retired in Phase 1c — collapsed into `.moss-card-kicker`.",
684    },
685    ComponentEntry {
686        class: "moss-card-list-title",
687        kind: "instance",
688        parent: "moss-card-list",
689        data_attrs: &[],
690        example_html: r#"<h3 class="moss-card-list-title">Page title</h3>"#,
691        example_markdown: "",
692        status: Status::Retired,
693        since: "0",
694        description: "Retired in Phase 1c — collapsed into `.moss-card-title`.",
695    },
696    ComponentEntry {
697        class: "moss-card-list-meta",
698        kind: "instance",
699        parent: "moss-card-list",
700        data_attrs: &[],
701        example_html: r#"<div class="moss-card-list-meta">2024-01-15</div>"#,
702        example_markdown: "",
703        status: Status::Retired,
704        since: "0",
705        description: "Retired in Phase 1c — collapsed into `.moss-card-meta`.",
706    },
707    ComponentEntry {
708        class: "moss-card-list-description",
709        kind: "instance",
710        parent: "moss-card-list",
711        data_attrs: &[],
712        example_html: r#"<p class="moss-card-list-description">Excerpt...</p>"#,
713        example_markdown: "",
714        status: Status::Retired,
715        since: "0",
716        description: "Retired in Phase 1c — collapsed into `.moss-card-description`.",
717    },
718    ComponentEntry {
719        class: "moss-card-minimal",
720        kind: "instance",
721        parent: "moss-cards-minimal-year-group",
722        data_attrs: &[],
723        example_html: r#"<div class="moss-card-minimal">
724  <a class="moss-prefix-link" href="...">...</a>
725</div>"#,
726        example_markdown: "",
727        status: Status::Retired,
728        since: "0",
729        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=minimal]`).",
730    },
731    ComponentEntry {
732        class: "moss-folder-item",
733        kind: "instance",
734        parent: "moss-cards-minimal-year-group",
735        data_attrs: &[],
736        example_html: r#"<div class="moss-card-minimal moss-folder-item">
737  <a class="moss-prefix-link moss-folder-link" href="...">...</a>
738  <p class="moss-folder-description">...</p>
739</div>"#,
740        example_markdown: "",
741        status: Status::Confirmed,
742        since: "0",
743        description: "Modifier on `.moss-card-minimal` for folder-type entries in minimal listings.",
744    },
745    ComponentEntry {
746        class: "moss-folder-title",
747        kind: "instance",
748        parent: "moss-folder-item",
749        data_attrs: &[],
750        example_html: r#"<span class="moss-folder-title">Folder name</span>"#,
751        example_markdown: "",
752        status: Status::Confirmed,
753        since: "0",
754        description: "Title text of a folder entry in minimal listings.",
755    },
756    ComponentEntry {
757        class: "moss-folder-description",
758        kind: "instance",
759        parent: "moss-folder-item",
760        data_attrs: &[],
761        example_html: r#"<p class="moss-folder-description">Description...</p>"#,
762        example_markdown: "",
763        status: Status::Confirmed,
764        since: "0",
765        description: "Description paragraph of a folder entry in minimal listings.",
766    },
767    ComponentEntry {
768        class: "moss-folder-link",
769        kind: "instance",
770        parent: "moss-folder-item",
771        data_attrs: &[],
772        example_html: r#"<a class="moss-prefix-link moss-folder-link" href="...">...</a>"#,
773        example_markdown: "",
774        status: Status::Confirmed,
775        since: "0",
776        description: "Modifier on `.moss-prefix-link` for folder-type links in minimal listings.",
777    },
778    // -------------------------------------------------------------------
779    // Prefix-link primitive — used by minimal cards and other listings.
780    // -------------------------------------------------------------------
781    ComponentEntry {
782        class: "moss-prefix-link",
783        kind: "instance",
784        parent: "moss-card-minimal",
785        data_attrs: &[],
786        example_html: r#"<a class="moss-prefix-link" href="...">
787  <span class="moss-prefix-link-prefix">2024-01-15</span>
788  <span class="moss-prefix-link-title">Page title</span>
789</a>"#,
790        example_markdown: "",
791        status: Status::Emerging,
792        since: "0",
793        description: "Link with a prefix span (date or icon) and a title span. Used inside minimal cards.",
794    },
795    ComponentEntry {
796        class: "moss-prefix-link-prefix",
797        kind: "instance",
798        parent: "moss-prefix-link",
799        data_attrs: &[],
800        example_html: r#"<span class="moss-prefix-link-prefix">2024-01-15</span>"#,
801        example_markdown: "",
802        status: Status::Emerging,
803        since: "0",
804        description: "Prefix slot of a prefix-link (typically a date).",
805    },
806    ComponentEntry {
807        class: "moss-prefix-link-title",
808        kind: "instance",
809        parent: "moss-prefix-link",
810        data_attrs: &[],
811        example_html: r#"<span class="moss-prefix-link-title">Page title</span>"#,
812        example_markdown: "",
813        status: Status::Emerging,
814        since: "0",
815        description: "Title slot of a prefix-link.",
816    },
817    ComponentEntry {
818        class: "title",
819        kind: "instance",
820        parent: "moss-prefix-link-title",
821        data_attrs: &[],
822        example_html: r#"<span class="moss-prefix-link-title title">Article Title</span>"#,
823        example_markdown: "",
824        status: Status::Confirmed,
825        since: "0",
826        description: "Bare co-class beside `moss-prefix-link-title`, the counterpart of `date` on the prefix span. Both predate the prefixed names and are still emitted for themes that key on them.",
827    },
828    ComponentEntry {
829        class: "moss-prefix-link-suffix",
830        kind: "instance",
831        parent: "moss-prefix-link",
832        data_attrs: &[],
833        example_html: r#"<span class="moss-prefix-link-suffix">→</span>"#,
834        example_markdown: "",
835        status: Status::Emerging,
836        since: "0",
837        description: "Optional trailing slot of a prefix-link.",
838    },
839    // -------------------------------------------------------------------
840    // Callouts — Obsidian-style admonitions. Type variant goes on the
841    // container as `.callout-<type>`. Phase 1c may collapse into
842    // `.moss-callout[data-type]`.
843    // -------------------------------------------------------------------
844    ComponentEntry {
845        class: "moss-callout",
846        kind: "standalone",
847        parent: "",
848        data_attrs: &[],
849        example_html: r#"<div class="moss-callout callout" data-type="note">
850  <div class="callout-title">Note</div>
851  <div class="callout-content">Body...</div>
852</div>"#,
853        example_markdown: "> [!note]\n> Body...",
854        status: Status::Confirmed,
855        since: "0",
856        description: "Obsidian-style callout. The Obsidian-compat `.callout` class is co-emitted; type lives on `data-type` (v1).",
857    },
858    ComponentEntry {
859        class: "callout",
860        kind: "standalone",
861        parent: "",
862        data_attrs: &[
863            DataAttr {
864                name: "data-type",
865                values: &["note", "info", "tip", "warning", "pending"],
866                default: "note",
867                description: "v1 callout type. Theme authors target `.callout[data-type=...]` to style by variant.",
868            },
869        ],
870        example_html: r#"<div class="moss-callout callout" data-type="note">...</div>"#,
871        example_markdown: "",
872        status: Status::Confirmed,
873        since: "0",
874        description: "Obsidian-compat class co-emitted on every callout for theme parity. Type lives on `data-type` (v1).",
875    },
876    ComponentEntry {
877        class: "callout-title",
878        kind: "instance",
879        parent: "moss-callout",
880        data_attrs: &[],
881        example_html: r#"<div class="callout-title">Note</div>"#,
882        example_markdown: "",
883        status: Status::Confirmed,
884        since: "0",
885        description: "Title row of a callout.",
886    },
887    ComponentEntry {
888        class: "callout-content",
889        kind: "instance",
890        parent: "moss-callout",
891        data_attrs: &[],
892        example_html: r#"<div class="callout-content">Body...</div>"#,
893        example_markdown: "",
894        status: Status::Confirmed,
895        since: "0",
896        description: "Body container of a callout.",
897    },
898    ComponentEntry {
899        class: "callout-note",
900        kind: "instance",
901        parent: "moss-callout",
902        data_attrs: &[],
903        example_html: r#"<div class="moss-callout callout callout-note">...</div>"#,
904        example_markdown: "> [!note]\n> Body",
905        status: Status::Retired,
906        since: "0",
907        description: "Retired in Phase 1c — type lives on `.callout[data-type=note]`.",
908    },
909    ComponentEntry {
910        class: "callout-info",
911        kind: "instance",
912        parent: "moss-callout",
913        data_attrs: &[],
914        example_html: r#"<div class="moss-callout callout callout-info">...</div>"#,
915        example_markdown: "> [!info]\n> Body",
916        status: Status::Retired,
917        since: "0",
918        description: "Retired in Phase 1c — type lives on `.callout[data-type=info]`.",
919    },
920    ComponentEntry {
921        class: "callout-tip",
922        kind: "instance",
923        parent: "moss-callout",
924        data_attrs: &[],
925        example_html: r#"<div class="moss-callout callout callout-tip">...</div>"#,
926        example_markdown: "> [!tip]\n> Body",
927        status: Status::Retired,
928        since: "0",
929        description: "Retired in Phase 1c — type lives on `.callout[data-type=tip]`.",
930    },
931    ComponentEntry {
932        class: "callout-warning",
933        kind: "instance",
934        parent: "moss-callout",
935        data_attrs: &[],
936        example_html: r#"<div class="moss-callout callout callout-warning">...</div>"#,
937        example_markdown: "> [!warning]\n> Body",
938        status: Status::Retired,
939        since: "0",
940        description: "Retired in Phase 1c — type lives on `.callout[data-type=warning]`.",
941    },
942    ComponentEntry {
943        class: "callout-pending",
944        kind: "instance",
945        parent: "moss-callout",
946        data_attrs: &[],
947        example_html: r#"<div class="moss-callout callout callout-pending">...</div>"#,
948        example_markdown: "> [!pending]\n> Body",
949        status: Status::Retired,
950        since: "0",
951        description: "Retired in Phase 1c — type lives on `.callout[data-type=pending]`.",
952    },
953    // -------------------------------------------------------------------
954    // Embeds — `![[file.ext]]` shortcode renderers (audio, video, pdf,
955    // notebook, table, 3d, iframe).
956    // -------------------------------------------------------------------
957    ComponentEntry {
958        class: "moss-embed",
959        kind: "standalone",
960        parent: "",
961        data_attrs: &[
962            DataAttr {
963                name: "data-type",
964                values: &["audio", "video", "pdf", "notebook", "table", "iframe", "3d"],
965                default: "",
966                description: "v1 embed kind. Set on the embed element. Theme authors target `.moss-embed[data-type=...]`.",
967            },
968            DataAttr {
969                name: "data-loop",
970                values: &[],
971                default: "",
972                description: "Ambient background video: autoplay + muted + loop + playsinline, controls off. Authored as `![[clip.mp4|loop]]`. Boolean presence flag (value is empty). JS reads it to apply the reduced-motion guard and mount the pause/play toggle.",
973            },
974            DataAttr {
975                name: "data-width",
976                values: &["body", "wide", "page", "screen"],
977                default: "body",
978                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
979            },
980            DataAttr {
981                name: "data-provider",
982                values: &["youtube", "vimeo", "codepen"],
983                default: "",
984                description: "Identifies the embed provider for external URL embeds. Absent for generic iframes and local HTML embeds.",
985            },
986        ],
987        example_html: r#"<video class="moss-embed moss-embed-video" data-type="video" data-loop src="clip.mp4" autoplay muted loop playsinline preload="metadata"></video>"#,
988        example_markdown: "![[clip.mp4|loop]]",
989        status: Status::Confirmed,
990        since: "0",
991        description: "Base class on every typed embed. Kind on `data-type` (v1). Ambient video: add `data-loop` via `![[clip.mp4|loop]]`. `.moss-embed-audio` / `-video` / `-pdf` / `-notebook` / `-table` / `-iframe` / `-3d` retired in Phase 1c.",
992    },
993    ComponentEntry {
994        class: "moss-embed-pending",
995        kind: "instance",
996        parent: "moss-embed",
997        data_attrs: &[],
998        example_html: r#"<div class="moss-embed moss-embed-pending">Downloading data.csv…</div>"#,
999        example_markdown: "",
1000        status: Status::Confirmed,
1001        since: "0",
1002        description: "Co-class on `.moss-embed` while the embedded file is still coming down from iCloud Drive. moss neither waits for the download nor bakes in an error box: it asks for the file, says plainly that it is still arriving, and lets the watcher rebuild the page when it lands. The default CSS does not style this — it is a hook for themes that want to mark the placeholder as provisional.",
1003    },
1004    ComponentEntry {
1005        class: "moss-embed-audio",
1006        kind: "instance",
1007        parent: "moss-embed",
1008        data_attrs: &[],
1009        example_html: r#"<div class="moss-embed moss-embed-audio"><audio controls src="..."></audio></div>"#,
1010        example_markdown: "![[track.mp3]]",
1011        status: Status::Retired,
1012        since: "0",
1013        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=audio]`.",
1014    },
1015    ComponentEntry {
1016        class: "moss-embed-video",
1017        kind: "instance",
1018        parent: "moss-embed",
1019        data_attrs: &[],
1020        example_html: r#"<div class="moss-embed moss-embed-video"><video controls src="..."></video></div>"#,
1021        example_markdown: "![[clip.mp4]]",
1022        status: Status::Retired,
1023        since: "0",
1024        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=video]`.",
1025    },
1026    ComponentEntry {
1027        class: "moss-embed-pdf",
1028        kind: "instance",
1029        parent: "moss-embed",
1030        data_attrs: &[],
1031        example_html: r#"<div class="moss-embed moss-embed-pdf"><iframe src="..."></iframe></div>"#,
1032        example_markdown: "![[paper.pdf]]",
1033        status: Status::Retired,
1034        since: "0",
1035        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=pdf]`.",
1036    },
1037    ComponentEntry {
1038        class: "moss-embed-iframe",
1039        kind: "instance",
1040        parent: "moss-embed",
1041        data_attrs: &[],
1042        example_html: r#"<div class="moss-embed moss-embed-iframe"><iframe src="..."></iframe></div>"#,
1043        example_markdown: "![[page.html]]",
1044        status: Status::Retired,
1045        since: "0",
1046        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=iframe]`.",
1047    },
1048    ComponentEntry {
1049        class: "moss-embed-notebook",
1050        kind: "instance",
1051        parent: "moss-embed",
1052        data_attrs: &[],
1053        example_html: r#"<div class="moss-embed moss-embed-notebook">...</div>"#,
1054        example_markdown: "![[analysis.ipynb]]",
1055        status: Status::Retired,
1056        since: "0",
1057        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=notebook]`.",
1058    },
1059    ComponentEntry {
1060        class: "moss-embed-ipynb",
1061        kind: "instance",
1062        parent: "moss-embed",
1063        data_attrs: &[],
1064        example_html: r#"<div class="moss-embed moss-embed-ipynb">...</div>"#,
1065        example_markdown: "",
1066        status: Status::Emerging,
1067        since: "0",
1068        description: "Alias of `.moss-embed-notebook`; consolidation pending.",
1069    },
1070    ComponentEntry {
1071        class: "moss-embed-table",
1072        kind: "instance",
1073        parent: "moss-embed",
1074        data_attrs: &[],
1075        example_html: r#"<div class="moss-embed moss-embed-table"><table>...</table></div>"#,
1076        example_markdown: "![[data.csv]]",
1077        status: Status::Retired,
1078        since: "0",
1079        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=table]`.",
1080    },
1081    ComponentEntry {
1082        class: "moss-embed-3d",
1083        kind: "instance",
1084        parent: "moss-embed",
1085        data_attrs: &[],
1086        example_html: r#"<div class="moss-embed moss-embed-3d">...</div>"#,
1087        example_markdown: "![[model.glb]]",
1088        status: Status::Retired,
1089        since: "0",
1090        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=3d]`.",
1091    },
1092    ComponentEntry {
1093        class: "moss-embed-error",
1094        kind: "instance",
1095        parent: "moss-embed",
1096        data_attrs: &[],
1097        example_html: r#"<div class="moss-embed moss-embed-error">File not found: ...</div>"#,
1098        example_markdown: "",
1099        status: Status::Confirmed,
1100        since: "0",
1101        description: "Error state for embeds whose target cannot be resolved.",
1102    },
1103    ComponentEntry {
1104        class: "moss-embed-missing",
1105        kind: "instance",
1106        parent: "moss-embed",
1107        data_attrs: &[],
1108        example_html: r#"<div class="moss-embed-missing">Folder not found: journal</div>"#,
1109        example_markdown: "",
1110        status: Status::Confirmed,
1111        since: "1",
1112        description: "Fallback rendered when a folder-list embed (`![[journal/]]`) targets a folder that does not exist or cannot be resolved. Distinct from `.moss-embed-error` (file/wikilink resolution failure) — this one is specifically the folder-listing path.",
1113    },
1114    // -------------------------------------------------------------------
1115    // Hero, image, visual primitives.
1116    // -------------------------------------------------------------------
1117    ComponentEntry {
1118        class: "moss-hero",
1119        kind: "standalone",
1120        parent: "",
1121        data_attrs: &[
1122            DataAttr {
1123                name: "data-width",
1124                values: &["body", "wide", "page", "screen"],
1125                default: "body",
1126                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9. Emitted from the authoring shortcode (e.g. `:::hero {full}` -> `data-width=\"screen\"`); on article children, site.css sizes the band via the content-width escape (ADR-021 Corollary 2). The hero itself escapes by DOM position (outside `<main>`), not by these rules.",
1127            },
1128            DataAttr {
1129                name: "data-slides",
1130                values: &["2", "3", "4", "5", "6"],
1131                default: "",
1132                description: "Slide count of a multi-image hero (consecutive leading media lines). Present only when > 1; drives the ambient CSS crossfade — one slide visible at a time, no controls. Absent = single-image hero, today's exact markup.",
1133            },
1134            DataAttr {
1135                name: "data-hero-tone",
1136                values: &["light"],
1137                default: "",
1138                description: "Marks a hero whose image is pale enough (scan-cached dominant colour above 0.4 relative luminance) that white overlay text cannot clear 4.5:1 over it. site.css responds by removing the legibility scrim and setting the overlay text DARK — a pale image needs no scrim to carry dark type, and darkening it hard enough to carry white type (this rule used to reach 0.78 black) greys out the picture the author chose. Applies to the two overlaid layouts; mobile-stacked text sits on `--moss-cover-color` and stays white. Emitted only when the hero also carries overlay text. Absent = mid-tone or dark image, no overlay, or an unparseable colour — all keep white type over the default ramp.",
1139            },
1140            DataAttr {
1141                name: "data-mobile",
1142                values: &["overlay"],
1143                default: "",
1144                description: "Below **48rem** (moss's mobile threshold), `overlay` keeps the title on top of the image instead of stacking it underneath. Emitted **only** for `:::hero {mobile=overlay}` — an author must ask for it; a hero with overlay text does not get it by default. The selector to fight if you want the other behaviour is `.moss-hero[data-mobile=\"overlay\"]`.",
1145            },
1146            DataAttr {
1147                name: "data-captioned",
1148                values: &[""],
1149                default: "",
1150                description: "Present when the hero carries `caption=\"…\"`. Such a hero is a photograph on display rather than a backdrop for overlay text, and a caption that names a subject is a promise the subject is in frame — so site.css shows the whole image instead of the default crop-to-fill: the box takes the picture's own shape, centred, bounded by `--moss-hero-max-height` rather than filling the frame. Put the crop back on the image itself (`image=cover.jpg|cover top`), which lands as an inline style and wins.",
1151            },
1152        ],
1153        example_html: r#"<section class="moss-hero" data-width="page">
1154  <div class="moss-hero-content">...</div>
1155</section>"#,
1156        example_markdown: ":::hero {image=cover.jpg}\n:::\n\n:::hero {image=cover.jpg full mobile=overlay}\n# Title over the image\n:::\n",
1157        status: Status::Confirmed,
1158        since: "0",
1159        description: "Hero banner section at the top of a page (cover image + title). v1 adds `data-width` for author-controlled sizing.",
1160    },
1161    ComponentEntry {
1162        class: "moss-hero-content",
1163        kind: "instance",
1164        parent: "moss-hero",
1165        data_attrs: &[],
1166        example_html: r#"<div class="moss-hero-content">...</div>"#,
1167        example_markdown: "",
1168        status: Status::Confirmed,
1169        since: "0",
1170        description: "Text content slot inside `.moss-hero` — text laid ON the image. For text ABOUT the image, see `.moss-hero-caption`.",
1171    },
1172    ComponentEntry {
1173        class: "moss-hero-caption",
1174        kind: "standalone",
1175        parent: "",
1176        data_attrs: &[],
1177        example_html: r#"<p class="moss-hero-caption">封面:基輔米迦勒修道院門口的陣亡將士紀念牆(拍攝:糜緒洋)</p>"#,
1178        example_markdown: ":::hero {image=cover.jpg caption=\"Cover: the memorial wall (photo: A. Photographer)\"}\n:::\n",
1179        status: Status::Confirmed,
1180        since: "0",
1181        description: "Caption or credit for a hero image, from `:::hero {caption=\"…\"}`. A SIBLING of `.moss-hero`, immediately after it — not a child: the section is a fixed-height cropping frame, and a photographer's credit has to survive as text below the picture rather than be printed across it. Rendered as inline markdown, so a credit can be a link, exactly like a `byline:` / `colophon:` row.",
1182    },
1183    ComponentEntry {
1184        class: "moss-hero-slides",
1185        kind: "instance",
1186        parent: "moss-hero",
1187        data_attrs: &[],
1188        example_html: r#"<div class="moss-hero-slides"><div class="moss-hero-slide"><img src="portrait-1.jpg" alt="" /></div></div>"#,
1189        example_markdown: "",
1190        status: Status::Confirmed,
1191        since: "0",
1192        description: "Wrapper holding the `.moss-hero-slide` images of a multi-image hero; the CSS ambient crossfade cycles one slide visible at a time.",
1193    },
1194    ComponentEntry {
1195        class: "moss-hero-slide",
1196        kind: "instance",
1197        parent: "moss-hero",
1198        data_attrs: &[],
1199        example_html: r#"<div class="moss-hero-slide"><img src="portrait-1.jpg" alt="" /></div>"#,
1200        example_markdown: ":::hero
1201![[portrait-1.jpg]]
1202![[portrait-2.jpg]]
1203# Title
1204:::
1205",
1206        status: Status::Confirmed,
1207        since: "0",
1208        description: "One background slide of a multi-image hero. Emitted only when the hero has 2+ images; slides crossfade ambiently via site.css keyed on the section's data-slides. First slide is the reduced-motion static fallback.",
1209    },
1210    ComponentEntry {
1211        class: "moss-image",
1212        kind: "standalone",
1213        parent: "",
1214        data_attrs: &[
1215            DataAttr {
1216                name: "data-aspect",
1217                values: &["portrait", "square", "auto"],
1218                default: "auto",
1219                description: "v1 image aspect-ratio hint. Theme authors target `.moss-image[data-aspect=...]`. Emitter wiring lands in a follow-up.",
1220            },
1221            DataAttr {
1222                name: "data-width",
1223                values: &["body", "wide", "page", "screen"],
1224                default: "body",
1225                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1226            },
1227        ],
1228        example_html: r#"<figure class="moss-image" style="width:55%"><img src="..." alt="..." /></figure>"#,
1229        example_markdown: "![alt](image.jpg)",
1230        status: Status::Confirmed,
1231        since: "0",
1232        description: "Wrapper around an inline `<img>` for sizing and figure semantics. `data-width` carries a named width token (body|wide|page|screen); a content-relative width is instead emitted as inline `style=\"width:NN%\"` (set by the editor drag-resize), which also forces the inner image to fill that percent box. Images narrower than the content column center horizontally.",
1233    },
1234    ComponentEntry {
1235        class: "moss-align-left",
1236        kind: "standalone",
1237        parent: "",
1238        data_attrs: &[],
1239        example_html: r#"<img src="..." alt="..." class="moss-align-left" />"#,
1240        example_markdown: "![[photo.jpg|align-left]]",
1241        status: Status::Confirmed,
1242        since: "0",
1243        description: "Floats an image to the left of body text (editorial runaround). Defaults max-width to 50% on desktop, collapses to full-width below 48rem. CSS `:has()` escalates the float to a wrapping `<figure class=\"moss-image\">` or `<picture>` when present. Mirrors WordPress's `alignleft` convention.",
1244    },
1245    ComponentEntry {
1246        class: "moss-align-right",
1247        kind: "standalone",
1248        parent: "",
1249        data_attrs: &[],
1250        example_html: r#"<img src="..." alt="..." class="moss-align-right" />"#,
1251        example_markdown: "![[photo.jpg|align-right]]",
1252        status: Status::Confirmed,
1253        since: "0",
1254        description: "Floats an image to the right of body text (editorial runaround). Symmetric counterpart to `.moss-align-left`. Mirrors WordPress's `alignright` convention.",
1255    },
1256    ComponentEntry {
1257        class: "moss-article-title",
1258        kind: "instance",
1259        parent: "",
1260        data_attrs: &[],
1261        example_html: r#"<h1 class="moss-article-title">Title</h1>"#,
1262        example_markdown: "",
1263        status: Status::Emerging,
1264        since: "0",
1265        description: "Article-page H1 title emitted from frontmatter.",
1266    },
1267    ComponentEntry {
1268        class: "moss-heading-anchor",
1269        kind: "instance",
1270        parent: "",
1271        data_attrs: &[],
1272        example_html: r##"<h2 id="setup">Setup<a class="moss-heading-anchor" href="#setup" aria-label="Permalink to this section"></a></h2>"##,
1273        example_markdown: "## Setup",
1274        status: Status::Emerging,
1275        since: "1",
1276        description: "Clickable permalink appended inside every author-written body heading that carries a slug id; links to the heading's `#`-fragment. The element is EMPTY — the `#` a reader sees is drawn by `site.css` as `::after` content, so selecting a heading never copies it. Not emitted for a display title: the auto-injected `moss-article-title` H1, a `:::hero` overlay heading, and a `:::grid` cell heading all get none.",
1277    },
1278    // -------------------------------------------------------------------
1279    // Grid + gallery + buttons containers (free-form layouts).
1280    // -------------------------------------------------------------------
1281    ComponentEntry {
1282        class: "moss-grid",
1283        kind: "container",
1284        parent: "",
1285        data_attrs: &[
1286            DataAttr {
1287                name: "data-width",
1288                values: &["body", "wide", "page", "screen"],
1289                default: "body",
1290                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1291            },
1292            DataAttr {
1293                name: "data-columns",
1294                values: &["1", "2", "3", "4"],
1295                default: "",
1296                description: "Column count, from `:::grid N`. Note the responsive default: below 768px moss collapses `[data-columns]` to a single column, which is right for a grid of cards and wrong for a grid of short text lines. Re-assert `grid-template-columns` inside your own media query if yours is the latter. A ratio (`:::grid 2 1:2`) arrives as the custom property `--moss-grid-ratio` on the element, so it stays overridable — the collapse applies to ratio grids too.",
1297            },
1298        ],
1299        example_html: r#"<div class="moss-grid" data-width="wide">
1300  <div class="moss-grid-card">...</div>
1301</div>"#,
1302        example_markdown: ":::grid {cols=2}\nLeft cell\n+++\nRight cell\n:::\n",
1303        status: Status::Confirmed,
1304        since: "0",
1305        description: "Generic grid container (used by profiles, link previews, etc.). Modifier classes: `profiles`, `featured`, `no-cards`. v1 adds `data-width` (P9).",
1306    },
1307    ComponentEntry {
1308        class: "moss-grid-card",
1309        kind: "instance",
1310        parent: "moss-grid",
1311        data_attrs: &[
1312            DataAttr {
1313                name: "data-kind",
1314                values: &["link", "friend", "card"],
1315                default: "card",
1316                description: "v1 grid-card variant. Today expressed via co-emitted classes (`.link-card`, `.friend-card`, `.no-cards`); Phase 1c collapses to this `data-kind` attribute.",
1317            },
1318            DataAttr {
1319                name: "data-cover-color",
1320                values: &[],
1321                default: "",
1322                description: "Presence flag: emitted IFF the cell's first block is an image, alone in a paragraph or in a figure. The image's dominant colour arrives alongside it as `--moss-cover-color` in the element's `style`. moss paints nothing with it — the attribute exists so a theme can give a hand-built cell (cover, then the author's own text) the same colour band a collection card gets.",
1323            },
1324        ],
1325        example_html: r#"<a class="moss-grid-card" data-kind="link" href="...">...</a>"#,
1326        example_markdown: "",
1327        status: Status::Confirmed,
1328        since: "0",
1329        description: "Card instance inside `.moss-grid`. Today emits sibling classes `link-card` / `friend-card` / `no-cards`; v1 collapses to `data-kind`.",
1330    },
1331    ComponentEntry {
1332        class: "link-preview",
1333        kind: "instance",
1334        parent: "moss-grid-card",
1335        data_attrs: &[],
1336        example_html: r#"<a href="…" class="moss-grid-card link-preview" target="_blank" rel="noopener">…</a>"#,
1337        example_markdown: "",
1338        status: Status::Confirmed,
1339        since: "0",
1340        description: "Co-class on a grid card that links out to an external page, alongside `moss-grid-card`. Marks the card as a preview of somewhere else rather than of a page on this site.",
1341    },
1342    ComponentEntry {
1343        class: "link-preview-title",
1344        kind: "instance",
1345        parent: "link-preview",
1346        data_attrs: &[],
1347        example_html: r#"<span class="link-preview-title">…</span>"#,
1348        example_markdown: "",
1349        status: Status::Confirmed,
1350        since: "0",
1351        description: "Title of the linked page inside a link preview.",
1352    },
1353    ComponentEntry {
1354        class: "link-preview-domain",
1355        kind: "instance",
1356        parent: "link-preview",
1357        data_attrs: &[],
1358        example_html: r#"<span class="link-preview-domain">example.com</span>"#,
1359        example_markdown: "",
1360        status: Status::Confirmed,
1361        since: "0",
1362        description: "Domain of the linked page, shown so a reader can see where the link goes before following it.",
1363    },
1364    ComponentEntry {
1365        class: "moss-gallery",
1366        kind: "container",
1367        parent: "",
1368        data_attrs: &[
1369            DataAttr {
1370                name: "data-width",
1371                values: &["body", "wide", "page", "screen"],
1372                default: "body",
1373                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1374            },
1375            DataAttr {
1376                name: "data-columns",
1377                values: &[],
1378                default: "",
1379                description: "Column count, from `:::gallery N` — the author names it rather than moss inferring it. The **opposite** of `.moss-grid[data-columns]` on mobile: below 48rem the grid collapses to one column, while the gallery uses `auto-fill` to keep as many tracks as clear 88px. N becomes a maximum rather than a mandate, and a gallery that already fits stays at its authored count. Collapsing a wall of thumbnails to one column is wrong; collapsing prose cells is right.",
1380            },
1381        ],
1382        example_html: r#"<div class="moss-gallery" data-width="page">
1383  <div class="moss-gallery-item">...</div>
1384</div>"#,
1385        example_markdown: ":::gallery\nphoto.jpg\n:::\n",
1386        status: Status::Confirmed,
1387        since: "0",
1388        description: "Image gallery container. v1 adds `data-width` (P9).",
1389    },
1390    ComponentEntry {
1391        class: "moss-gallery-item",
1392        kind: "instance",
1393        parent: "moss-gallery",
1394        data_attrs: &[],
1395        example_html: r#"<div class="moss-gallery-item"><img src="..." /></div>"#,
1396        example_markdown: "",
1397        status: Status::Confirmed,
1398        since: "0",
1399        description: "Single image entry inside `.moss-gallery`.",
1400    },
1401    ComponentEntry {
1402        class: "moss-buttons",
1403        kind: "container",
1404        parent: "",
1405        data_attrs: &[
1406            DataAttr {
1407                name: "data-style",
1408                values: &["default", "inverted"],
1409                default: "default",
1410                description: "v1 button-row style. Theme authors target `.moss-buttons[data-style=...]`.",
1411            },
1412        ],
1413        example_html: r#"<div class="moss-buttons" data-style="inverted">
1414  <a class="moss-btn" href="...">Click</a>
1415</div>"#,
1416        example_markdown: ":::buttons\n[Get started](https://example.com)\n:::\n",
1417        status: Status::Confirmed,
1418        since: "0",
1419        description: "Container for a row of `.moss-btn` buttons. v1: the inverted variant is on `data-style=\"inverted\"`.",
1420    },
1421    // -------------------------------------------------------------------
1422    // Button primitive (used by subscribe + general CTAs).
1423    // -------------------------------------------------------------------
1424    ComponentEntry {
1425        class: "moss-btn",
1426        kind: "standalone",
1427        parent: "",
1428        data_attrs: &[
1429            DataAttr {
1430                name: "data-role",
1431                values: &["default", "primary", "secondary"],
1432                default: "default",
1433                description: "v1 button role. Theme authors target `.moss-btn[data-role=...]`.",
1434            },
1435        ],
1436        example_html: r#"<button class="moss-btn" data-role="primary">
1437  <span class="moss-btn__label">Submit</span>
1438</button>"#,
1439        example_markdown: "",
1440        status: Status::Confirmed,
1441        since: "0",
1442        description: "Generic button primitive. Role on `data-role` (v1).",
1443    },
1444    ComponentEntry {
1445        class: "moss-btn__label",
1446        kind: "instance",
1447        parent: "moss-btn",
1448        data_attrs: &[],
1449        example_html: r#"<span class="moss-btn__label">Submit</span>"#,
1450        example_markdown: "",
1451        status: Status::Confirmed,
1452        since: "0",
1453        description: "Label span inside `.moss-btn`.",
1454    },
1455    ComponentEntry {
1456        class: "moss-btn__check",
1457        kind: "instance",
1458        parent: "moss-btn",
1459        data_attrs: &[],
1460        example_html: r#"<span class="moss-btn__check">✓</span>"#,
1461        example_markdown: "",
1462        status: Status::Confirmed,
1463        since: "0",
1464        description: "Success checkmark slot inside `.moss-btn`.",
1465    },
1466    ComponentEntry {
1467        class: "moss-btn__spinner",
1468        kind: "instance",
1469        parent: "moss-btn",
1470        data_attrs: &[],
1471        example_html: r#"<span class="moss-btn__spinner"></span>"#,
1472        example_markdown: "",
1473        status: Status::Confirmed,
1474        since: "0",
1475        description: "Loading spinner slot inside `.moss-btn`.",
1476    },
1477    // -------------------------------------------------------------------
1478    // Subscribe form (newsletter / Buttondown / seta).
1479    // -------------------------------------------------------------------
1480    ComponentEntry {
1481        class: "moss-subscribe",
1482        kind: "standalone",
1483        parent: "",
1484        data_attrs: &[],
1485        example_html: r#"<div class="moss-subscribe">
1486  <form class="moss-subscribe-form">...</form>
1487</div>"#,
1488        example_markdown: ":::subscribe\n:::\n",
1489        status: Status::Confirmed,
1490        since: "0",
1491        description: "Newsletter subscribe block (auto-injected into footer when email channel configured).",
1492    },
1493    ComponentEntry {
1494        class: "moss-subscribe-script",
1495        kind: "instance",
1496        parent: "",
1497        data_attrs: &[],
1498        example_html: r#"<script class="moss-subscribe-script">/* subscribe form handler */</script>"#,
1499        example_markdown: "",
1500        status: Status::Confirmed,
1501        since: "0",
1502        description: "Marks the `<script>` moss injects into `<head>` when a page carries a subscribe form. It is a handle, not a styling hook — a theme or a test uses it to find (or suppress) moss's own subscribe behaviour without matching on script contents.",
1503    },
1504    ComponentEntry {
1505        class: "moss-subscribe-form",
1506        kind: "instance",
1507        parent: "moss-subscribe",
1508        data_attrs: &[
1509            DataAttr {
1510                name: "data-position",
1511                values: &["inline", "apply"],
1512                default: "inline",
1513                description: "Placement/behavior variant. All moss-hosted subscribe forms are `inline` (the auto-injected footer form and the `:::subscribe` shortcode emit identical HTML — footer vs in-page styling keys on the `footer` ancestor in CSS, not this attribute). `apply` marks the `:::apply` form (terminal success, FormData body).",
1514            },
1515            DataAttr {
1516                name: "data-button-override",
1517                values: &["true"],
1518                default: "true",
1519                description: "Emitted only when the author overrides the button label (`:::subscribe{button=\"...\"}`). Signals subscribe.ts to leave the button label AND placeholder as authored instead of overwriting them with the language-default copy.",
1520            },
1521            DataAttr {
1522                name: "data-moss-hosted",
1523                values: &["true"],
1524                default: "true",
1525                description: "Marks moss-hosted (seta) forms hydrated by subscribe.ts. Absent on 3rd-party provider forms.",
1526            },
1527            DataAttr {
1528                name: "data-state",
1529                values: &["idle", "loading", "success", "error"],
1530                default: "idle",
1531                description: "Runtime submit state machine, driven by subscribe.ts. Emitted as `idle`; theme authors target `.moss-subscribe-form[data-state=...]`.",
1532            },
1533            DataAttr {
1534                name: "data-moss-pending-site",
1535                values: &["true"],
1536                default: "true",
1537                description: "Pre-first-publish pending wiring (`action=\"#\"`, no site_id yet). Hidden on published pages (body without `data-moss-preview`) via the email.css defense rule so a pending form never faces real readers.",
1538            },
1539        ],
1540        example_html: r#"<form class="moss-subscribe-form">...</form>"#,
1541        example_markdown: "",
1542        status: Status::Emerging,
1543        since: "0",
1544        description: "Form element inside `.moss-subscribe`.",
1545    },
1546    ComponentEntry {
1547        class: "moss-btn-slot",
1548        kind: "instance",
1549        parent: "moss-subscribe",
1550        data_attrs: &[],
1551        example_html: r#"<div class="moss-btn-slot"><button class="moss-btn">...</button></div>"#,
1552        example_markdown: "",
1553        status: Status::Emerging,
1554        since: "0",
1555        description: "Fixed-width slot wrapping a form's submit button; used by both the subscribe and comment forms to prevent layout shift across idle/loading/success states.",
1556    },
1557    ComponentEntry {
1558        class: "moss-subscribe-status",
1559        kind: "instance",
1560        parent: "moss-subscribe",
1561        data_attrs: &[],
1562        example_html: r#"<div class="moss-subscribe-status">
1563  <span class="moss-subscribe-status__icon"></span>
1564  Subscribed!
1565</div>"#,
1566        example_markdown: "",
1567        status: Status::Emerging,
1568        since: "0",
1569        description: "Status message shown after submit (success/error).",
1570    },
1571    ComponentEntry {
1572        class: "moss-subscribe-status__icon",
1573        kind: "instance",
1574        parent: "moss-subscribe-status",
1575        data_attrs: &[],
1576        example_html: r#"<span class="moss-subscribe-status__icon"></span>"#,
1577        example_markdown: "",
1578        status: Status::Emerging,
1579        since: "0",
1580        description: "Icon slot inside `.moss-subscribe-status`.",
1581    },
1582    ComponentEntry {
1583        class: "moss-subscribe-landing",
1584        kind: "standalone",
1585        parent: "",
1586        data_attrs: &[],
1587        example_html: r#"<section class="moss-subscribe-landing">...</section>"#,
1588        example_markdown: "",
1589        status: Status::Emerging,
1590        since: "0",
1591        description: "Standalone subscribe landing page surface (larger variant).",
1592    },
1593    // -------------------------------------------------------------------
1594    // Apply form (membership / contributor application).
1595    // -------------------------------------------------------------------
1596    ComponentEntry {
1597        class: "moss-apply",
1598        kind: "standalone",
1599        parent: "",
1600        data_attrs: &[],
1601        example_html: r#"<div class="moss-apply" data-state="idle">
1602  <form class="moss-subscribe-form moss-apply-form">...</form>
1603</div>"#,
1604        example_markdown: ":::apply\n:::\n",
1605        status: Status::Emerging,
1606        since: "0",
1607        description: "Apply / membership-request form block (:::apply shortcode).",
1608    },
1609    ComponentEntry {
1610        class: "moss-apply-form",
1611        kind: "instance",
1612        parent: "moss-apply",
1613        data_attrs: &[
1614            DataAttr {
1615                name: "data-position",
1616                values: &["apply"],
1617                default: "apply",
1618                description: "Position variant; always `apply` for this form. Drives CSS layout in email.css.",
1619            },
1620            DataAttr {
1621                name: "data-revert",
1622                values: &["false"],
1623                default: "false",
1624                description: "When `false`, success is terminal (no auto-revert). subscribe.ts reads this.",
1625            },
1626        ],
1627        example_html: r#"<form class="moss-subscribe-form moss-apply-form" data-position="apply" data-revert="false">...</form>"#,
1628        example_markdown: "",
1629        status: Status::Emerging,
1630        since: "0",
1631        description: "Form element inside `.moss-apply`. Also carries `.moss-subscribe-form` so subscribe.ts hydrates it.",
1632    },
1633    ComponentEntry {
1634        class: "moss-apply-matters",
1635        kind: "instance",
1636        parent: "moss-apply",
1637        data_attrs: &[],
1638        example_html: r#"<input type="text" name="matters" class="moss-input moss-apply-matters">"#,
1639        example_markdown: "",
1640        status: Status::Emerging,
1641        since: "0",
1642        description: "Second apply-form input inside `.moss-apply-form` — a Matters username OR a one-line pitch (placeholder-only, no visible label).",
1643    },
1644    ComponentEntry {
1645        class: "moss-apply-hp",
1646        kind: "instance",
1647        parent: "moss-apply",
1648        data_attrs: &[],
1649        example_html: r#"<input type="text" name="website" class="moss-apply-hp" tabindex="-1" aria-hidden="true">"#,
1650        example_markdown: "",
1651        status: Status::Emerging,
1652        since: "0",
1653        description: "Honeypot field (off-screen) inside `.moss-apply-form`. Bots fill it; humans don't.",
1654    },
1655    ComponentEntry {
1656        class: "moss-apply-status",
1657        kind: "instance",
1658        parent: "moss-apply",
1659        data_attrs: &[],
1660        example_html: r#"<div class="moss-subscribe-status moss-apply-status" aria-live="polite">...</div>"#,
1661        example_markdown: "",
1662        status: Status::Emerging,
1663        since: "0",
1664        description: "Status region inside `.moss-apply-form` (also carries `.moss-subscribe-status`).",
1665    },
1666    ComponentEntry {
1667        class: "moss-apply-helper",
1668        kind: "instance",
1669        parent: "moss-apply",
1670        data_attrs: &[],
1671        example_html: r#"<p class="moss-apply-helper" id="moss-apply-email-help">用于获取邀请及免费托管服务</p>"#,
1672        example_markdown: "",
1673        status: Status::Emerging,
1674        since: "0",
1675        description: "Helper text line beneath each field in `.moss-apply-form` (referenced by the field's aria-describedby). Internal — not part of the public component contract.",
1676    },
1677    // -------------------------------------------------------------------
1678    // Series navigation (prev/next + collection links).
1679    // -------------------------------------------------------------------
1680    ComponentEntry {
1681        class: "moss-series-nav",
1682        kind: "standalone",
1683        parent: "",
1684        data_attrs: &[],
1685        example_html: r#"<nav class="moss-series-nav">
1686  <div class="moss-series-nav-links">...</div>
1687</nav>"#,
1688        example_markdown: "",
1689        status: Status::Confirmed,
1690        since: "0",
1691        description: "Series navigation bar (prev/next/collection) on series pages.",
1692    },
1693    ComponentEntry {
1694        class: "moss-series-nav-links",
1695        kind: "instance",
1696        parent: "moss-series-nav",
1697        data_attrs: &[],
1698        example_html: r#"<div class="moss-series-nav-links">...</div>"#,
1699        example_markdown: "",
1700        status: Status::Confirmed,
1701        since: "0",
1702        description: "Row holding prev/next links in series nav.",
1703    },
1704    ComponentEntry {
1705        class: "moss-series-nav-link",
1706        kind: "instance",
1707        parent: "moss-series-nav",
1708        data_attrs: &[],
1709        example_html: r#"<a class="moss-series-nav-link moss-series-nav-prev" href="...">...</a>"#,
1710        example_markdown: "",
1711        status: Status::Confirmed,
1712        since: "0",
1713        description: "Individual link inside series nav. Modifiers: `moss-series-nav-prev`, `moss-series-nav-next`, `empty` (placeholder).",
1714    },
1715    ComponentEntry {
1716        class: "empty",
1717        kind: "instance",
1718        parent: "moss-series-nav-link",
1719        data_attrs: &[],
1720        example_html: r#"<span class="moss-series-nav-link moss-series-nav-prev empty"></span>"#,
1721        example_markdown: "",
1722        status: Status::Confirmed,
1723        since: "0",
1724        description: "Co-class on a series-nav link with nowhere to go — the previous link on the first entry, the next link on the last. The element is still emitted so the pair keeps its layout.",
1725    },
1726    ComponentEntry {
1727        class: "moss-series-nav-prev",
1728        kind: "instance",
1729        parent: "moss-series-nav",
1730        data_attrs: &[],
1731        example_html: r#"<a class="moss-series-nav-link moss-series-nav-prev" href="...">...</a>"#,
1732        example_markdown: "",
1733        status: Status::Confirmed,
1734        since: "0",
1735        description: "Previous-page modifier on a series nav link.",
1736    },
1737    ComponentEntry {
1738        class: "moss-series-nav-next",
1739        kind: "instance",
1740        parent: "moss-series-nav",
1741        data_attrs: &[],
1742        example_html: r#"<a class="moss-series-nav-link moss-series-nav-next" href="...">...</a>"#,
1743        example_markdown: "",
1744        status: Status::Confirmed,
1745        since: "0",
1746        description: "Next-page modifier on a series nav link.",
1747    },
1748    ComponentEntry {
1749        class: "moss-series-nav-arrow",
1750        kind: "instance",
1751        parent: "moss-series-nav",
1752        data_attrs: &[],
1753        example_html: r#"<span class="moss-series-nav-arrow">→</span>"#,
1754        example_markdown: "",
1755        status: Status::Confirmed,
1756        since: "0",
1757        description: "Arrow glyph inside a series-nav link.",
1758    },
1759    ComponentEntry {
1760        class: "moss-series-nav-title",
1761        kind: "instance",
1762        parent: "moss-series-nav",
1763        data_attrs: &[],
1764        example_html: r#"<span class="moss-series-nav-title">Next page title</span>"#,
1765        example_markdown: "",
1766        status: Status::Confirmed,
1767        since: "0",
1768        description: "Title text of the destination page in a series-nav link.",
1769    },
1770    ComponentEntry {
1771        class: "moss-series-nav-collection",
1772        kind: "instance",
1773        parent: "moss-series-nav",
1774        data_attrs: &[],
1775        example_html: r#"<div class="moss-series-nav-collection">...</div>"#,
1776        example_markdown: "",
1777        status: Status::Confirmed,
1778        since: "0",
1779        description: "Collection-listing slot in series nav (sibling pages).",
1780    },
1781    ComponentEntry {
1782        class: "moss-series-nav-collection-row",
1783        kind: "instance",
1784        parent: "moss-series-nav-collection",
1785        data_attrs: &[],
1786        example_html: r#"<div class="moss-series-nav-collection-row">...</div>"#,
1787        example_markdown: "",
1788        status: Status::Confirmed,
1789        since: "0",
1790        description: "Row inside the collection listing of series nav.",
1791    },
1792    ComponentEntry {
1793        class: "moss-series-nav-position",
1794        kind: "instance",
1795        parent: "moss-series-nav-collection-row",
1796        data_attrs: &[],
1797        example_html: r#"<span class="moss-series-nav-position">2 of 3</span>"#,
1798        example_markdown: "",
1799        status: Status::Confirmed,
1800        since: "0",
1801        description: "Where this page sits in its series — \"2 of 3\" / 「第 2 篇,共 3 篇」. Counts only the pages still in the reading order, so a page that stepped out with `series: false` is not in the total. Omitted when the folder holds a single page.",
1802    },
1803    // -------------------------------------------------------------------
1804    // Collection cover (collection landing pages).
1805    // -------------------------------------------------------------------
1806    ComponentEntry {
1807        class: "moss-collection-cover",
1808        kind: "standalone",
1809        parent: "",
1810        data_attrs: &[],
1811        example_html: r#"<section class="moss-collection-cover">
1812  <div class="moss-collection-cover-row">...</div>
1813</section>"#,
1814        example_markdown: "",
1815        status: Status::Emerging,
1816        since: "0",
1817        description: "Header surface on a collection landing page.",
1818    },
1819    ComponentEntry {
1820        class: "moss-collection-cover-row",
1821        kind: "instance",
1822        parent: "moss-collection-cover",
1823        data_attrs: &[],
1824        example_html: r#"<div class="moss-collection-cover-row">...</div>"#,
1825        example_markdown: "",
1826        status: Status::Emerging,
1827        since: "0",
1828        description: "Row inside `.moss-collection-cover`.",
1829    },
1830    ComponentEntry {
1831        class: "moss-collection-cover-body",
1832        kind: "instance",
1833        parent: "moss-collection-cover",
1834        data_attrs: &[],
1835        example_html: r#"<div class="moss-collection-cover-body">...</div>"#,
1836        example_markdown: "",
1837        status: Status::Emerging,
1838        since: "0",
1839        description: "Body content slot inside `.moss-collection-cover`.",
1840    },
1841    // -------------------------------------------------------------------
1842    // Form primitives (input, label, field, link).
1843    // -------------------------------------------------------------------
1844    ComponentEntry {
1845        class: "moss-input",
1846        kind: "standalone",
1847        parent: "",
1848        data_attrs: &[],
1849        example_html: r#"<input class="moss-input" type="email" />"#,
1850        example_markdown: "",
1851        status: Status::Confirmed,
1852        since: "0",
1853        description: "Generic form input primitive.",
1854    },
1855    ComponentEntry {
1856        class: "moss-field",
1857        kind: "container",
1858        parent: "",
1859        data_attrs: &[],
1860        example_html: r#"<div class="moss-field">
1861  <label class="moss-label">Email</label>
1862  <input class="moss-input" />
1863</div>"#,
1864        example_markdown: "",
1865        status: Status::Confirmed,
1866        since: "0",
1867        description: "Form field group (label + input). Modifier `--inline` for horizontal layout.",
1868    },
1869    ComponentEntry {
1870        class: "moss-label",
1871        kind: "instance",
1872        parent: "moss-field",
1873        data_attrs: &[],
1874        example_html: r#"<label class="moss-label">Email</label>"#,
1875        example_markdown: "",
1876        status: Status::Confirmed,
1877        since: "0",
1878        description: "Label primitive for `.moss-field`. Modifier `--small` for compact form.",
1879    },
1880    ComponentEntry {
1881        class: "moss-link",
1882        kind: "standalone",
1883        parent: "",
1884        data_attrs: &[],
1885        example_html: r#"<a class="moss-link" href="...">Click me</a>"#,
1886        example_markdown: "",
1887        status: Status::Confirmed,
1888        since: "0",
1889        description: "Inline-link primitive (resets `<button>` chrome too). Use `--subtle` for muted variant.",
1890    },
1891    ComponentEntry {
1892        class: "moss-field--inline",
1893        kind: "instance",
1894        parent: "moss-field",
1895        data_attrs: &[],
1896        example_html: r#"<div class="moss-field moss-field--inline">
1897  <label class="moss-label">Email</label>
1898  <input class="moss-input" />
1899</div>"#,
1900        example_markdown: "",
1901        status: Status::Confirmed,
1902        since: "0",
1903        description: "BEM modifier on `.moss-field` for horizontal label+input layout (used by settings UI primitives).",
1904    },
1905    ComponentEntry {
1906        class: "moss-label--small",
1907        kind: "instance",
1908        parent: "moss-label",
1909        data_attrs: &[],
1910        example_html: r#"<label class="moss-label moss-label--small">Compact label</label>"#,
1911        example_markdown: "",
1912        status: Status::Confirmed,
1913        since: "0",
1914        description: "BEM modifier on `.moss-label` for compact form (used by services settings rows).",
1915    },
1916    ComponentEntry {
1917        class: "moss-info-grid",
1918        kind: "container",
1919        parent: "",
1920        data_attrs: &[],
1921        example_html: r#"<div class="moss-info-grid">
1922  <div class="moss-field moss-field--inline">...</div>
1923  <div class="moss-field moss-field--inline">...</div>
1924</div>"#,
1925        example_markdown: "",
1926        status: Status::Emerging,
1927        since: "0",
1928        description: "Two-column aligned label+value rows (CSS grid with `display: contents` children). Used by the deployment settings panel; ships in the default theme so authors can reuse the layout.",
1929    },
1930    ComponentEntry {
1931        class: "moss-row",
1932        kind: "container",
1933        parent: "",
1934        data_attrs: &[],
1935        example_html: r#"<div class="moss-row">
1936  <div class="moss-field">...</div>
1937  <div class="moss-field">...</div>
1938</div>"#,
1939        example_markdown: "",
1940        status: Status::Emerging,
1941        since: "0",
1942        description: "Horizontal flex row of equal-flex `.moss-field` children. Form-row layout helper shipped in the default theme.",
1943    },
1944    ComponentEntry {
1945        class: "moss-input-feedback",
1946        kind: "instance",
1947        parent: "moss-field",
1948        data_attrs: &[],
1949        example_html: r#"<span class="moss-input-feedback">Saving…</span>"#,
1950        example_markdown: "",
1951        status: Status::Emerging,
1952        since: "0",
1953        description: "Auto-save status hint slot under `.moss-field`. Three state modifiers: `--success`, `--error`, `--fade-out`.",
1954    },
1955    ComponentEntry {
1956        class: "moss-input-feedback--success",
1957        kind: "instance",
1958        parent: "moss-input-feedback",
1959        data_attrs: &[],
1960        example_html: r#"<span class="moss-input-feedback moss-input-feedback--success">Saved</span>"#,
1961        example_markdown: "",
1962        status: Status::Emerging,
1963        since: "0",
1964        description: "Success state modifier on `.moss-input-feedback`.",
1965    },
1966    ComponentEntry {
1967        class: "moss-input-feedback--error",
1968        kind: "instance",
1969        parent: "moss-input-feedback",
1970        data_attrs: &[],
1971        example_html: r#"<span class="moss-input-feedback moss-input-feedback--error">Failed to save</span>"#,
1972        example_markdown: "",
1973        status: Status::Emerging,
1974        since: "0",
1975        description: "Error state modifier on `.moss-input-feedback`.",
1976    },
1977    ComponentEntry {
1978        class: "moss-input-feedback--fade-out",
1979        kind: "instance",
1980        parent: "moss-input-feedback",
1981        data_attrs: &[],
1982        example_html: r#"<span class="moss-input-feedback moss-input-feedback--success moss-input-feedback--fade-out">Saved</span>"#,
1983        example_markdown: "",
1984        status: Status::Emerging,
1985        since: "0",
1986        description: "Transient fade-out modifier on `.moss-input-feedback` (applied after a success message to dismiss it).",
1987    },
1988    // -------------------------------------------------------------------
1989    // Other emit surfaces (comments, colophon, shell frame, misc).
1990    // -------------------------------------------------------------------
1991    ComponentEntry {
1992        class: "moss-comments",
1993        kind: "standalone",
1994        parent: "",
1995        data_attrs: &[],
1996        example_html: r#"<section class="moss-comments">...</section>"#,
1997        example_markdown: "",
1998        status: Status::Confirmed,
1999        since: "0",
2000        description: "Comments surface (per-site SQLite backend or Artalk legacy).",
2001    },
2002    ComponentEntry {
2003        class: "comment-form-slot",
2004        kind: "instance",
2005        parent: "moss-comments",
2006        data_attrs: &[],
2007        example_html: r#"<div class="comment-form-slot" id="default-form-slot">…</div>"#,
2008        example_markdown: "",
2009        status: Status::Confirmed,
2010        since: "0",
2011        description: "Placeholder the comment form is rendered into. Kept distinct from the form itself so a reply form can be moved between slots without re-rendering the form.",
2012    },
2013    ComponentEntry {
2014        class: "comment-form",
2015        kind: "instance",
2016        parent: "comment-form-slot",
2017        data_attrs: &[],
2018        example_html: r#"<form class="comment-form" data-state="idle" id="moss-comment-form">…</form>"#,
2019        example_markdown: "",
2020        status: Status::Confirmed,
2021        since: "0",
2022        description: "The comment submission form. `data-state` carries the submission lifecycle, so a theme can style submitting and error states without watching the network.",
2023    },
2024    ComponentEntry {
2025        class: "comment-form-meta",
2026        kind: "instance",
2027        parent: "comment-form",
2028        data_attrs: &[],
2029        example_html: r#"<div class="comment-form-meta"><input class="comment-field" name="name"></div>"#,
2030        example_markdown: "",
2031        status: Status::Confirmed,
2032        since: "0",
2033        description: "Row holding the commenter identity fields (name, and email where configured), as distinct from the comment body textarea.",
2034    },
2035    ComponentEntry {
2036        class: "comment-form-status",
2037        kind: "instance",
2038        parent: "comment-form",
2039        data_attrs: &[],
2040        example_html: r#"<div class="comment-form-status" id="moss-comment-status" aria-live="assertive" aria-atomic="true"></div>"#,
2041        example_markdown: "",
2042        status: Status::Confirmed,
2043        since: "0",
2044        description: "Live region announcing the result of a comment submission. an assertive live region is deliberate: the reader has just acted and is waiting on the answer.",
2045    },
2046    ComponentEntry {
2047        class: "comment-form-submit",
2048        kind: "instance",
2049        parent: "comment-form",
2050        data_attrs: &[],
2051        example_html: r#"<button type="submit" class="moss-btn comment-form-submit" aria-busy="false">…</button>"#,
2052        example_markdown: "",
2053        status: Status::Confirmed,
2054        since: "0",
2055        description: "Co-class on the comment form's submit button, alongside `moss-btn`. `aria-busy` tracks the in-flight submission.",
2056    },
2057    ComponentEntry {
2058        class: "comment-field",
2059        kind: "instance",
2060        parent: "comment-form-meta",
2061        data_attrs: &[],
2062        example_html: r#"<input type="text" name="name" class="comment-field" autocomplete="name" required>"#,
2063        example_markdown: "",
2064        status: Status::Confirmed,
2065        since: "0",
2066        description: "A text input inside the comment form. Applied to every field so a theme styles them once rather than per name.",
2067    },
2068    ComponentEntry {
2069        class: "moss-service-inactive",
2070        kind: "instance",
2071        parent: "",
2072        data_attrs: &[],
2073        example_html: r#"<section class="moss-comments moss-service-inactive">...</section>"#,
2074        example_markdown: "",
2075        status: Status::Confirmed,
2076        since: "0",
2077        description: "Co-class applied to `.moss-comments` and `.moss-subscribe-form` when the backing service is not configured. Hidden by default in published sites and revealed inside the preview chrome so authors can see the inactive surface during editing.",
2078    },
2079    // -------------------------------------------------------------------
2080    // Preview link popover — emitted by `assets/js/preview.js` runtime.
2081    // -------------------------------------------------------------------
2082    ComponentEntry {
2083        class: "moss-preview-popup",
2084        kind: "chrome",
2085        parent: "",
2086        data_attrs: &[],
2087        example_html: r#"<div class="moss-preview-popup" role="tooltip" aria-live="polite">
2088  <strong class="moss-preview-title">...</strong>
2089  <p class="moss-preview-desc">...</p>
2090  <p class="moss-preview-text">...</p>
2091</div>"#,
2092        example_markdown: "",
2093        status: Status::Confirmed,
2094        since: "0",
2095        description: "Floating link-preview popover injected at `document.body` level by the runtime `preview.js`. Fetches `/_moss/previews.json` and renders a hover card with title, description, and excerpt for internal links.",
2096    },
2097    ComponentEntry {
2098        class: "moss-preview-title",
2099        kind: "instance",
2100        parent: "moss-preview-popup",
2101        data_attrs: &[],
2102        example_html: r#"<strong class="moss-preview-title">Article title</strong>"#,
2103        example_markdown: "",
2104        status: Status::Confirmed,
2105        since: "0",
2106        description: "Title slot inside `.moss-preview-popup`.",
2107    },
2108    ComponentEntry {
2109        class: "moss-preview-desc",
2110        kind: "instance",
2111        parent: "moss-preview-popup",
2112        data_attrs: &[],
2113        example_html: r#"<p class="moss-preview-desc">Short description</p>"#,
2114        example_markdown: "",
2115        status: Status::Confirmed,
2116        since: "0",
2117        description: "Description slot inside `.moss-preview-popup` (from frontmatter `description`).",
2118    },
2119    ComponentEntry {
2120        class: "moss-preview-text",
2121        kind: "instance",
2122        parent: "moss-preview-popup",
2123        data_attrs: &[],
2124        example_html: r#"<p class="moss-preview-text">Excerpt of the linked article…</p>"#,
2125        example_markdown: "",
2126        status: Status::Confirmed,
2127        since: "0",
2128        description: "Excerpt slot inside `.moss-preview-popup` (auto-extracted from the linked article body).",
2129    },
2130    ComponentEntry {
2131        class: "moss-skip-link",
2132        kind: "chrome",
2133        parent: "",
2134        data_attrs: &[],
2135        example_html: r##"<a class="moss-skip-link" href="#main-content">Skip to content</a>"##,
2136        example_markdown: "",
2137        status: Status::Confirmed,
2138        since: "0",
2139        description: "First focusable element in `<body>`, before the nav island and header. Visually hidden until it receives keyboard focus, then jumps to `<main id=\"main-content\">` (WCAG 2.4.1). Themes may restyle it but should keep it off-screen at rest and visible on `:focus`.",
2140    },
2141    ComponentEntry {
2142        class: "moss-colophon",
2143        kind: "chrome",
2144        parent: "",
2145        data_attrs: &[],
2146        example_html: r#"<div class="moss-colophon">
2147  <a href="https://mosspub.com">
2148    <svg class="moss-colophon-icon"></svg>
2149    <span class="moss-colophon-label">Published with moss</span>
2150  </a>
2151</div>"#,
2152        example_markdown: "",
2153        status: Status::Confirmed,
2154        since: "0",
2155        description: "Footer colophon credit appended by moss. Shows the moss mark alone at rest; the wording fades in beneath it on hover or keyboard focus, without moving the mark.",
2156    },
2157    ComponentEntry {
2158        class: "moss-colophon-icon",
2159        kind: "instance",
2160        parent: "moss-colophon",
2161        data_attrs: &[],
2162        example_html: r#"<svg class="moss-colophon-icon"></svg>"#,
2163        example_markdown: "",
2164        status: Status::Confirmed,
2165        since: "0",
2166        description: "The moss mark inside `.moss-colophon`. Decorative (`aria-hidden`) — `.moss-colophon-label` carries the accessible name.",
2167    },
2168    ComponentEntry {
2169        class: "moss-colophon-label",
2170        kind: "instance",
2171        parent: "moss-colophon",
2172        data_attrs: &[],
2173        example_html: r#"<span class="moss-colophon-label">Published with moss</span>"#,
2174        example_markdown: "",
2175        status: Status::Confirmed,
2176        since: "0",
2177        description: "Localized attribution wording inside `.moss-colophon`. Transparent at rest and faded in on hover/focus, positioned out of flow beneath the mark so the reveal shifts nothing — it stays in the DOM because it is the link's accessible name.",
2178    },
2179    ComponentEntry {
2180        class: "moss-shell-frame",
2181        kind: "chrome",
2182        parent: "",
2183        data_attrs: &[],
2184        example_html: r#"<div class="moss-shell-frame">...</div>"#,
2185        example_markdown: "",
2186        status: Status::Emerging,
2187        since: "0",
2188        description: "App-shell frame surface (preview chrome).",
2189    },
2190    ComponentEntry {
2191        class: "moss-mobile-frame",
2192        kind: "chrome",
2193        parent: "moss-shell-frame",
2194        data_attrs: &[],
2195        example_html: r#"<html class="moss-shell-frame moss-mobile-frame">...</html>"#,
2196        example_markdown: "",
2197        status: Status::Emerging,
2198        since: "0",
2199        description: "Runtime marker the preview bridge adds to `<html>` when the shell is in mobile device-preview mode. Since ADR-039 the shell owns chrome clearance by insetting the preview iframe, so no CSS keys off this class and it currently has no effect; it is retained as a revert path and may be removed.",
2200    },
2201    ComponentEntry {
2202        class: "main-nav",
2203        kind: "chrome",
2204        parent: "",
2205        data_attrs: &[],
2206        example_html: r#"<nav class="main-nav container">...</nav>"#,
2207        example_markdown: "",
2208        status: Status::Confirmed,
2209        since: "0",
2210        description: "Top site navigation bar. Legacy non-`moss-` prefix kept for theme parity.",
2211    },
2212    // The article masthead and nav interior. Legacy non-`moss-` prefixes, kept
2213    // for theme parity like `main-nav` above.
2214    //
2215    // These were emitted but undeclared until 2026-08-05, and the omission had
2216    // a measured cost: an agent restyling a journalism site reaches for the
2217    // byline row first, found nothing for it in `describe --json`, and had to
2218    // recover the class by reading built HTML — which the shipped guidance
2219    // sanctions only as a self-check, and which silently breaks on a rename.
2220    // Declaring them is what makes "never hardcode a class from memory"
2221    // followable for the masthead. `components_sync_test` cannot guard these:
2222    // it only matches `class="moss-..."` literals.
2223    ComponentEntry {
2224        class: "date-line",
2225        kind: "chrome",
2226        parent: "",
2227        data_attrs: &[],
2228        example_html: r#"<div class="date-line"><span class="date">March 3, 2026</span><div class="font-anchor">...</div></div>"#,
2229        example_markdown: "",
2230        status: Status::Confirmed,
2231        since: "0",
2232        description: "Byline row under an article title: the publication date on the left, the reading-size control on the right. Emitted only when the page has a `date`.",
2233    },
2234    ComponentEntry {
2235        class: "date",
2236        kind: "instance",
2237        parent: "date-line",
2238        data_attrs: &[],
2239        example_html: r#"<span class="date">March 3, 2026</span>"#,
2240        example_markdown: "",
2241        status: Status::Confirmed,
2242        since: "0",
2243        description: "The formatted publication date inside `.date-line`. Text is localized to the page's language.",
2244    },
2245    ComponentEntry {
2246        class: "moss-byline",
2247        kind: "container",
2248        parent: "",
2249        data_attrs: &[],
2250        example_html: r#"<div class="moss-byline"><div class="moss-byline-row">作者 糜緒洋</div><div class="moss-byline-row">編輯 謝丁</div></div>"#,
2251        example_markdown: "",
2252        status: Status::Confirmed,
2253        since: "1",
2254        description: "Credit block under the page title, below `.date-line` when there is one. Emitted from the `byline` frontmatter field on every page kind — articles, folder indexes, the homepage and plain pages alike — one `.moss-byline-row` per authored line. On a page moss gives no title of its own (the homepage, a `home: true` folder page, a plain page) it sits under the author's own opening `<h1>`, or at the top of the page content when the body has none. Absent when the field is.",
2255    },
2256    ComponentEntry {
2257        class: "moss-byline-row",
2258        kind: "instance",
2259        parent: "moss-byline",
2260        data_attrs: &[],
2261        example_html: r#"<div class="moss-byline-row">首發媒體 <a href="https://theinitium.com/a">端傳媒</a></div>"#,
2262        example_markdown: "",
2263        status: Status::Confirmed,
2264        since: "1",
2265        description: "One credit line. Its content is the author's text rendered as inline markdown, so a row may contain links or emphasis. moss does not know which part is a role and which is a name — style the whole row.",
2266    },
2267    ComponentEntry {
2268        class: "moss-article-colophon",
2269        kind: "container",
2270        parent: "",
2271        data_attrs: &[],
2272        example_html: r#"<div class="moss-article-colophon"><div class="moss-article-colophon-row">首發媒體 <a href="https://theinitium.com/a">端傳媒</a></div></div>"#,
2273        example_markdown: "",
2274        status: Status::Confirmed,
2275        since: "1",
2276        description: "Credit block at the FOOT of the page, emitted from the `colophon` frontmatter field — where the piece first ran, contributor biographies, production credits. Same rows as `.moss-byline`, different end of the page. Emitted on every page kind: inside `<article>` on an article page, and last in the page content everywhere else — after the children listing on a folder index or the homepage — where the enclosing element is not an `<article>` despite the class name. Unrelated to `.review-colophon`, which is the review feature's book card.",
2277    },
2278    ComponentEntry {
2279        class: "moss-article-colophon-row",
2280        kind: "instance",
2281        parent: "moss-article-colophon",
2282        data_attrs: &[],
2283        example_html: r#"<div class="moss-article-colophon-row">封面 基輔米迦勒修道院門口的陣亡將士紀念牆(拍攝:糜緒洋)</div>"#,
2284        example_markdown: "",
2285        status: Status::Confirmed,
2286        since: "1",
2287        description: "One foot-credit line, rendered as inline markdown exactly like `.moss-byline-row`.",
2288    },
2289    ComponentEntry {
2290        class: "site-name",
2291        kind: "instance",
2292        parent: "main-nav",
2293        data_attrs: &[],
2294        example_html: r#"<a href="/" class="site-name">在場</a>"#,
2295        example_markdown: "",
2296        status: Status::Confirmed,
2297        since: "0",
2298        description: "The site title link at the left of the nav bar. On a non-home page the same slot may instead carry `.breadcrumb-segment`.",
2299    },
2300    ComponentEntry {
2301        class: "breadcrumb-segment",
2302        kind: "instance",
2303        parent: "main-nav",
2304        data_attrs: &[],
2305        example_html: r#"<a href="/awards/" class="breadcrumb-segment">獎項</a>"#,
2306        example_markdown: "",
2307        status: Status::Confirmed,
2308        since: "0",
2309        description: "One ancestor link in the nav-left breadcrumb trail, used in place of `.site-name` once the page is below the site root.",
2310    },
2311    ComponentEntry {
2312        class: "nav-icons",
2313        kind: "chrome",
2314        parent: "main-nav",
2315        data_attrs: &[],
2316        example_html: r#"<div class="nav-icons">...</div>"#,
2317        example_markdown: "",
2318        status: Status::Confirmed,
2319        since: "0",
2320        description: "Right-hand icon cluster in the nav bar (search, theme toggle, and similar). Stationary chrome, present whether or not the site has nav links.",
2321    },
2322    // The rest of the nav interior and the default footer, on the same footing
2323    // as the masthead block above: emitted since 0, styled in `site.css`, and
2324    // undeclared until 2026-08-05.
2325    //
2326    // The 2026-08-05 trial found the concrete cost. An agent asked to restyle a
2327    // site went looking in `describe --json` for the language switcher, found
2328    // nothing, and recovered `.nav-lang-toggle` by grepping built HTML — the one
2329    // move the shipped guidance tells agents not to make, because it breaks
2330    // silently on a rename. `main-nav`, `.site-name` and `.nav-icons` were
2331    // declared; everything they contain was not, which is the worst of both
2332    // (the contract looks complete enough to trust).
2333    ComponentEntry {
2334        class: "nav-left",
2335        kind: "chrome",
2336        parent: "main-nav",
2337        data_attrs: &[],
2338        example_html: r#"<div class="nav-left"><a href="/" class="site-name">在場</a></div>"#,
2339        example_markdown: "",
2340        status: Status::Confirmed,
2341        since: "0",
2342        description: "Left group of the nav bar. Holds either `.site-name` or the breadcrumb trail, never both.",
2343    },
2344    ComponentEntry {
2345        class: "nav-right",
2346        kind: "chrome",
2347        parent: "main-nav",
2348        data_attrs: &[],
2349        example_html: r#"<div class="nav-right">…hamburger, .nav-links, .nav-icons…</div>"#,
2350        example_markdown: "",
2351        status: Status::Confirmed,
2352        since: "0",
2353        description: "Right group of the nav bar: the mobile menu button, the nav links, and the icon cluster, in that order.",
2354    },
2355    ComponentEntry {
2356        class: "nav-links",
2357        kind: "chrome",
2358        parent: "nav-right",
2359        data_attrs: &[],
2360        example_html: r#"<div class="nav-links"><a href="/about/" class="active">關於</a>…</div>"#,
2361        example_markdown: "",
2362        status: Status::Confirmed,
2363        since: "0",
2364        description: "The nav link list. The link for the page currently being viewed additionally carries the bare class `active` — style `.nav-links .active`, not a `moss-` class.",
2365    },
2366    ComponentEntry {
2367        class: "site-logo",
2368        kind: "instance",
2369        parent: "site-name",
2370        data_attrs: &[],
2371        example_html: r#"<img class="site-logo" src="…" alt="" aria-hidden="true">"#,
2372        example_markdown: "",
2373        status: Status::Confirmed,
2374        since: "0",
2375        description: "Optional logo image inside the site-name link. Decorative by construction (`alt=\"\"` + `aria-hidden`), because the adjacent text already names the site.",
2376    },
2377    ComponentEntry {
2378        class: "breadcrumb-label",
2379        kind: "instance",
2380        parent: "nav-left",
2381        data_attrs: &[],
2382        example_html: r#"<span class="breadcrumb-label">獎項</span>"#,
2383        example_markdown: "",
2384        status: Status::Confirmed,
2385        since: "0",
2386        description: "The final, non-linked breadcrumb segment — the page you are on. `.breadcrumb-segment` is the linked form for ancestors.",
2387    },
2388    ComponentEntry {
2389        class: "breadcrumb-separator",
2390        kind: "instance",
2391        parent: "nav-left",
2392        data_attrs: &[],
2393        example_html: r#"<span class="breadcrumb-separator">/</span>"#,
2394        example_markdown: "",
2395        status: Status::Confirmed,
2396        since: "0",
2397        description: "The `/` between breadcrumb segments. Restyle or hide this rather than trying to remove it from the markup.",
2398    },
2399    ComponentEntry {
2400        class: "mobile-menu-button",
2401        kind: "chrome",
2402        parent: "nav-right",
2403        data_attrs: &[],
2404        example_html: r#"<button class="mobile-menu-button" aria-label="…"><svg>…</svg></button>"#,
2405        example_markdown: "",
2406        status: Status::Confirmed,
2407        since: "0",
2408        description: "The hamburger. Emitted on every page and hidden by media query above the mobile breakpoint — it is not conditionally rendered, so a rule that shows it always will.",
2409    },
2410    ComponentEntry {
2411        class: "nav-search-btn",
2412        kind: "instance",
2413        parent: "nav-icons",
2414        data_attrs: &[],
2415        example_html: r#"<button class="nav-search-btn" type="button" aria-label="…"><svg class="search-icon">…</svg></button>"#,
2416        example_markdown: "",
2417        status: Status::Confirmed,
2418        since: "0",
2419        description: "Search button in the nav icon cluster. Its glyph is `.search-icon`.",
2420    },
2421    ComponentEntry {
2422        class: "search-icon",
2423        kind: "instance",
2424        parent: "nav-search-btn",
2425        data_attrs: &[],
2426        example_html: r#"<svg class="search-icon" aria-hidden="true" width="1em" height="1em">…</svg>"#,
2427        example_markdown: "",
2428        status: Status::Confirmed,
2429        since: "0",
2430        description: "The magnifier glyph. Sized in `em` and stroked with `currentColor`, so it follows the button's font-size and color rather than needing its own rule.",
2431    },
2432    ComponentEntry {
2433        class: "nav-theme-btn",
2434        kind: "instance",
2435        parent: "nav-icons",
2436        data_attrs: &[],
2437        example_html: r#"<button class="nav-theme-btn" type="button" aria-label="…"><svg class="theme-toggle-icon">…</svg></button>"#,
2438        example_markdown: "",
2439        status: Status::Confirmed,
2440        since: "0",
2441        description: "Light/dark toggle in the nav icon cluster. Its glyph is `.theme-toggle-icon`.",
2442    },
2443    ComponentEntry {
2444        class: "theme-toggle-icon",
2445        kind: "instance",
2446        parent: "nav-theme-btn",
2447        data_attrs: &[],
2448        example_html: r#"<svg class="theme-toggle-icon" aria-hidden="true" width="1em" height="1em">…</svg>"#,
2449        example_markdown: "",
2450        status: Status::Confirmed,
2451        since: "0",
2452        description: "The sun/moon glyph. One SVG whose clip path animates between states — restyle it, but do not expect two separate icons to swap.",
2453    },
2454    ComponentEntry {
2455        class: "nav-lang-toggle",
2456        kind: "chrome",
2457        parent: "nav-icons",
2458        data_attrs: &[],
2459        example_html: r#"<div class="nav-lang-toggle" aria-label="…"><span class="nav-lang-current">繁</span><a href="/en/" class="nav-lang-link" hreflang="en">EN</a></div>"#,
2460        example_markdown: "",
2461        status: Status::Confirmed,
2462        since: "0",
2463        description: "The language switcher. Present only when the site has more than one edition among the three the switcher resolves (English, Simplified Chinese, Traditional Chinese) — a `ja/` or `fr/` tree publishes but adds no entry here.",
2464    },
2465    ComponentEntry {
2466        class: "nav-lang-current",
2467        kind: "instance",
2468        parent: "nav-lang-toggle",
2469        data_attrs: &[],
2470        example_html: r#"<span class="nav-lang-current">繁</span>"#,
2471        example_markdown: "",
2472        status: Status::Confirmed,
2473        since: "0",
2474        description: "The edition being viewed, as inert text rather than a link — style the current-language affordance here.",
2475    },
2476    ComponentEntry {
2477        class: "nav-lang-link",
2478        kind: "instance",
2479        parent: "nav-lang-toggle",
2480        data_attrs: &[],
2481        example_html: r#"<a href="/en/" class="nav-lang-link" hreflang="en">EN</a>"#,
2482        example_markdown: "",
2483        status: Status::Confirmed,
2484        since: "0",
2485        description: "A link to another edition of the same page. Carries `hreflang`, so `[hreflang=\"en\"]` is a stable hook for per-language styling.",
2486    },
2487    // The floating nav island (ADR-049) — the small bar that appears when the
2488    // reader scrolls back up past the masthead on a long page. A SECOND object,
2489    // not the masthead re-pinned, which is why it has its own `moss-`-prefixed
2490    // vocabulary. Its trail is the one exception: it deliberately reuses
2491    // `.site-name` / `.breadcrumb-segment` / `.breadcrumb-label` /
2492    // `.breadcrumb-separator` from the masthead above, so a site that restyles
2493    // its breadcrumb restyles both at once.
2494    ComponentEntry {
2495        class: "moss-nav-island",
2496        kind: "chrome",
2497        parent: "",
2498        data_attrs: &[DataAttr {
2499            name: "data-shown",
2500            values: &["false", "true"],
2501            default: "false",
2502            description: "Whether the island is currently revealed. Written by the site runtime; ABSENT in the emitted HTML, which is what keeps the island invisible with JavaScript off.",
2503        }],
2504        example_html: r#"<div class="moss-nav-island" data-shown="true"><div class="moss-nav-island-bar">…</div></div>"#,
2505        example_markdown: "",
2506        status: Status::Emerging,
2507        since: "0",
2508        description: "Floating navigation island: a one-line bar, aligned to the text column, revealed on scroll-up once the masthead has left the screen. Emitted on any page with a breadcrumb trail, but it only ever shows where it can take the reader somewhere — a trail of three or more crumbs (down the tree) or a page with headings (the sections panel). On a top-level page with neither, the markup stays dormant (ADR-049 §10). Turn it off site-wide with `[site].floating_nav = false` or `--moss-nav-island-display: none`.",
2509    },
2510    ComponentEntry {
2511        class: "moss-nav-island-bar",
2512        kind: "instance",
2513        parent: "moss-nav-island",
2514        data_attrs: &[],
2515        example_html: r#"<div class="moss-nav-island-bar">…trail, actions, progress…</div>"#,
2516        example_markdown: "",
2517        status: Status::Emerging,
2518        since: "0",
2519        description: "The visible rounded bar. Its width follows `--moss-nav-width`/`--moss-content-width`, so it lines up with the article text rather than with the window.",
2520    },
2521    ComponentEntry {
2522        class: "moss-nav-island-trail",
2523        kind: "instance",
2524        parent: "moss-nav-island-bar",
2525        data_attrs: &[],
2526        example_html: r#"<nav class="moss-nav-island-trail" aria-label="Breadcrumb">…</nav>"#,
2527        example_markdown: "",
2528        status: Status::Emerging,
2529        since: "0",
2530        description: "The breadcrumb inside the island. Same segment classes as the masthead's, plus the current page as a final crumb. It never wraps: ancestors fold into `.moss-nav-island-more` until the row fits.",
2531    },
2532    ComponentEntry {
2533        class: "moss-nav-island-current",
2534        kind: "instance",
2535        parent: "moss-nav-island-trail",
2536        data_attrs: &[],
2537        example_html: r#"<span class="breadcrumb-segment moss-nav-island-current" aria-current="page">末代女礦工</span>"#,
2538        example_markdown: "",
2539        status: Status::Emerging,
2540        since: "0",
2541        description: "The page you are on, as the trail's last crumb. The only crumb permitted to truncate — an ancestor either fits whole or folds away.",
2542    },
2543    ComponentEntry {
2544        class: "moss-nav-island-more",
2545        kind: "instance",
2546        parent: "moss-nav-island-trail",
2547        data_attrs: &[],
2548        example_html: r#"<button class="moss-nav-island-more" aria-expanded="false">…</button>"#,
2549        example_markdown: "",
2550        status: Status::Emerging,
2551        since: "0",
2552        description: "Stands in for the ancestor levels the trail had to drop. Opens the levels menu on click; names them on hover via `data-tooltip`. Never opens on hover — a touch device has none, and that is the width where folding happens.",
2553    },
2554    ComponentEntry {
2555        class: "moss-nav-island-actions",
2556        kind: "instance",
2557        parent: "moss-nav-island-bar",
2558        data_attrs: &[],
2559        example_html: r#"<span class="moss-nav-island-actions">…</span>"#,
2560        example_markdown: "",
2561        status: Status::Emerging,
2562        since: "0",
2563        description: "Button cluster at the island's end edge. Holds the sections button only — theme, language and search stay in the masthead.",
2564    },
2565    ComponentEntry {
2566        class: "moss-nav-island-sections",
2567        kind: "instance",
2568        parent: "moss-nav-island-actions",
2569        data_attrs: &[],
2570        example_html: r#"<button class="moss-nav-island-sections" aria-expanded="false"><svg>…</svg></button>"#,
2571        example_markdown: "",
2572        status: Status::Emerging,
2573        since: "0",
2574        description: "Opens this page's section list. Ships `hidden` and is unhidden only once headings have been found, so a page with no headings shows no dead glyph.",
2575    },
2576    ComponentEntry {
2577        class: "moss-nav-island-menu",
2578        kind: "instance",
2579        parent: "moss-nav-island",
2580        data_attrs: &[DataAttr {
2581            name: "data-island-menu",
2582            values: &["levels", "sections"],
2583            default: "levels",
2584            description: "Which of the two menus this is: the folded ancestor levels, or the page's sections.",
2585        }],
2586        example_html: r#"<div class="moss-nav-island-menu" data-island-menu="sections">…</div>"#,
2587        example_markdown: "",
2588        status: Status::Emerging,
2589        since: "0",
2590        description: "Popover opened by `.moss-nav-island-more` or `.moss-nav-island-sections`. Every row reserves a leading gutter for the current-row rule, so the labels line up in one column whether or not a row is marked.",
2591    },
2592    ComponentEntry {
2593        class: "moss-breadcrumb-more",
2594        kind: "instance",
2595        parent: "",
2596        data_attrs: &[],
2597        example_html: r#"<button class="moss-breadcrumb-more" aria-expanded="false">…</button>"#,
2598        example_markdown: "",
2599        status: Status::Emerging,
2600        since: "0",
2601        description: "The masthead trail's counterpart to `.moss-nav-island-more`: stands in for the ancestor levels the trail folded, opens the levels menu on click, names them on hover via `data-tooltip`. Emitted (hidden) only when the trail has a middle to fold — three or more crumbs.",
2602    },
2603    ComponentEntry {
2604        class: "moss-breadcrumb-menu",
2605        kind: "instance",
2606        parent: "",
2607        data_attrs: &[],
2608        example_html: r#"<div class="moss-breadcrumb-menu" hidden>…</div>"#,
2609        example_markdown: "",
2610        status: Status::Emerging,
2611        since: "0",
2612        description: "Popover listing the masthead trail's folded ancestor levels, opened by `.moss-breadcrumb-more`. A sibling of `.nav-left` (which clips its own overflow), positioned against `.nav-content`. Same shape as `.moss-nav-island-menu`.",
2613    },
2614    ComponentEntry {
2615        class: "moss-nav-island-progress",
2616        kind: "instance",
2617        parent: "moss-nav-island-bar",
2618        data_attrs: &[],
2619        example_html: r#"<span class="moss-nav-island-progress"><span class="moss-nav-island-progress-fill"></span></span>"#,
2620        example_markdown: "",
2621        status: Status::Emerging,
2622        since: "0",
2623        description: "Reading-progress track along the island's own bottom edge — not a separate bar across the window. Currently measures document scroll.",
2624    },
2625    ComponentEntry {
2626        class: "moss-nav-island-progress-fill",
2627        kind: "instance",
2628        parent: "moss-nav-island-progress",
2629        data_attrs: &[],
2630        example_html: r#"<span class="moss-nav-island-progress-fill" style="width: 42%"></span>"#,
2631        example_markdown: "",
2632        status: Status::Emerging,
2633        since: "0",
2634        description: "The filled portion of the progress track. Its `width` is written inline by the site runtime; with JavaScript off it stays at 0 and the track reads as empty.",
2635    },
2636    ComponentEntry {
2637        class: "footer-default",
2638        kind: "chrome",
2639        parent: "",
2640        data_attrs: &[],
2641        example_html: r#"<p class="footer-default"><a href="/rss.xml" class="footer-link" data-external>RSS</a>…</p>"#,
2642        example_markdown: "",
2643        status: Status::Confirmed,
2644        since: "0",
2645        description: "The generated footer link row, emitted only when the site has no authored `footer.md`. Authoring a footer replaces it, so a rule targeting this stops applying the moment the site gains one.",
2646    },
2647    ComponentEntry {
2648        class: "footer-link",
2649        kind: "instance",
2650        parent: "footer-default",
2651        data_attrs: &[],
2652        example_html: r#"<a href="/rss.xml" class="footer-link" data-external>RSS</a>"#,
2653        example_markdown: "",
2654        status: Status::Confirmed,
2655        since: "0",
2656        description: "One link in the generated footer row (RSS and similar). Off-site ones also carry `data-external`.",
2657    },
2658    ComponentEntry {
2659        class: "moss-child-section-divider",
2660        kind: "instance",
2661        parent: "",
2662        data_attrs: &[],
2663        example_html: r#"<hr class="moss-child-section-divider" />"#,
2664        example_markdown: "",
2665        status: Status::Emerging,
2666        since: "0",
2667        description: "Divider rule between auto-generated child sections.",
2668    },
2669    ComponentEntry {
2670        // Source of truth: `crates/moss-core/src/ast/shortcode_extract.rs`
2671        // (the unknown-name branch around line 1282). components_sync_test
2672        // only greps emitter source for `class="moss-..."` literals — this
2673        // class is assembled via `render_div_open`, so a regression here
2674        // will NOT be caught by that test; keep this entry in sync by hand.
2675        class: "moss-unknown-shortcode",
2676        kind: "standalone",
2677        parent: "",
2678        data_attrs: &[DataAttr {
2679            name: "data-name",
2680            values: &[],
2681            default: "",
2682            description: "The unrecognised shortcode name, as written by the author.",
2683        }],
2684        example_html: r#"<div class="moss-unknown-shortcode" data-name="foo">
2685
2686<p>body parsed as markdown</p>
2687
2688</div>"#,
2689        example_markdown: ":::foo\nbody parsed as markdown\n:::",
2690        status: Status::Confirmed,
2691        since: "0",
2692        description: "Fallback wrapper emitted for any `:::name` fence whose name is not a registered shortcode. The body is still parsed as markdown and a build warning names the shortcode, so a misspelling degrades to a styled region rather than losing content.",
2693    },
2694    // -------------------------------------------------------------------
2695    // Syntax highlight tokens (emitted by syntect inside <code>).
2696    // -------------------------------------------------------------------
2697    ComponentEntry {
2698        class: "moss-hl-keyword",
2699        kind: "instance",
2700        parent: "",
2701        data_attrs: &[],
2702        example_html: r#"<span class="moss-hl-keyword">if</span>"#,
2703        example_markdown: "",
2704        status: Status::Emerging,
2705        since: "0",
2706        description: "Syntax-highlight token: keyword.",
2707    },
2708    ComponentEntry {
2709        class: "moss-hl-string",
2710        kind: "instance",
2711        parent: "",
2712        data_attrs: &[],
2713        example_html: r#"<span class="moss-hl-string">"hi"</span>"#,
2714        example_markdown: "",
2715        status: Status::Emerging,
2716        since: "0",
2717        description: "Syntax-highlight token: string literal.",
2718    },
2719    ComponentEntry {
2720        class: "moss-hl-comment",
2721        kind: "instance",
2722        parent: "",
2723        data_attrs: &[],
2724        example_html: r#"<span class="moss-hl-comment">// note</span>"#,
2725        example_markdown: "",
2726        status: Status::Emerging,
2727        since: "0",
2728        description: "Syntax-highlight token: comment.",
2729    },
2730    ComponentEntry {
2731        class: "moss-hl-function",
2732        kind: "instance",
2733        parent: "",
2734        data_attrs: &[],
2735        example_html: r#"<span class="moss-hl-function">render</span>"#,
2736        example_markdown: "",
2737        status: Status::Emerging,
2738        since: "0",
2739        description: "Syntax-highlight token: function name.",
2740    },
2741    ComponentEntry {
2742        class: "moss-hl-type",
2743        kind: "instance",
2744        parent: "",
2745        data_attrs: &[],
2746        example_html: r#"<span class="moss-hl-type">String</span>"#,
2747        example_markdown: "",
2748        status: Status::Emerging,
2749        since: "0",
2750        description: "Syntax-highlight token: type name.",
2751    },
2752    ComponentEntry {
2753        class: "moss-hl-number",
2754        kind: "instance",
2755        parent: "",
2756        data_attrs: &[],
2757        example_html: r#"<span class="moss-hl-number">42</span>"#,
2758        example_markdown: "",
2759        status: Status::Emerging,
2760        since: "0",
2761        description: "Syntax-highlight token: numeric literal.",
2762    },
2763    ComponentEntry {
2764        class: "moss-hl-operator",
2765        kind: "instance",
2766        parent: "",
2767        data_attrs: &[],
2768        example_html: r#"<span class="moss-hl-operator">+</span>"#,
2769        example_markdown: "",
2770        status: Status::Emerging,
2771        since: "0",
2772        description: "Syntax-highlight token: operator.",
2773    },
2774    ComponentEntry {
2775        class: "moss-hl-builtin",
2776        kind: "instance",
2777        parent: "",
2778        data_attrs: &[],
2779        example_html: r#"<span class="moss-hl-builtin">print</span>"#,
2780        example_markdown: "",
2781        status: Status::Emerging,
2782        since: "0",
2783        description: "Syntax-highlight token: builtin identifier.",
2784    },
2785    ComponentEntry {
2786        class: "moss-hl-tag",
2787        kind: "instance",
2788        parent: "",
2789        data_attrs: &[],
2790        example_html: r#"<span class="moss-hl-tag">div</span>"#,
2791        example_markdown: "",
2792        status: Status::Emerging,
2793        since: "0",
2794        description: "Syntax-highlight token: markup tag name.",
2795    },
2796    ComponentEntry {
2797        class: "moss-hl-attr",
2798        kind: "instance",
2799        parent: "",
2800        data_attrs: &[],
2801        example_html: r#"<span class="moss-hl-attr">class</span>"#,
2802        example_markdown: "",
2803        status: Status::Emerging,
2804        since: "0",
2805        description: "Syntax-highlight token: attribute name.",
2806    },
2807    ComponentEntry {
2808        class: "moss-hl-meta",
2809        kind: "instance",
2810        parent: "",
2811        data_attrs: &[],
2812        example_html: r#"<span class="moss-hl-meta">@derive</span>"#,
2813        example_markdown: "",
2814        status: Status::Emerging,
2815        since: "0",
2816        description: "Syntax-highlight token: meta/annotation.",
2817    },
2818    ComponentEntry {
2819        class: "moss-hl-addition-bg",
2820        kind: "instance",
2821        parent: "",
2822        data_attrs: &[],
2823        example_html: r#"<span class="moss-hl-addition-bg">+ added line</span>"#,
2824        example_markdown: "",
2825        status: Status::Emerging,
2826        since: "0",
2827        description: "Syntax-highlight diff token: added-line background.",
2828    },
2829    ComponentEntry {
2830        class: "moss-hl-deletion",
2831        kind: "instance",
2832        parent: "",
2833        data_attrs: &[],
2834        example_html: r#"<span class="moss-hl-deletion">- removed line</span>"#,
2835        example_markdown: "",
2836        status: Status::Emerging,
2837        since: "0",
2838        description: "Syntax-highlight diff token: removed-line text.",
2839    },
2840    ComponentEntry {
2841        class: "moss-hl-deletion-bg",
2842        kind: "instance",
2843        parent: "",
2844        data_attrs: &[],
2845        example_html: r#"<span class="moss-hl-deletion-bg">- removed line</span>"#,
2846        example_markdown: "",
2847        status: Status::Emerging,
2848        since: "0",
2849        description: "Syntax-highlight diff token: removed-line background.",
2850    },
2851    ComponentEntry {
2852        class: "moss-recent",
2853        kind: "container",
2854        parent: "",
2855        data_attrs: &[],
2856        example_html: r#"<ul class="moss-recent">
2857  <li><a href="/posts/spring-notes/">Spring notes</a><div class="moss-recent__date">2026-04-12</div><div class="moss-recent__desc">A walk through the garden.</div></li>
2858</ul>"#,
2859        example_markdown: ":::recent {count=5 since=\"2026-01-01\"}\n:::\n",
2860        status: Status::Emerging,
2861        since: "0",
2862        description: "Auto-generated list of recent posts. Sorted newest-first; date and description slots are filled per child. No default CSS in the bundled theme — theme authors style it freely. IMPORTANT: emitted by the EMAIL/newsletter path only. On a web page, `:::recent` renders its fallback body as ordinary markdown and emits no list, because the per-page processor has no access to the build's aggregate document slice; a `:::recent` block with an empty body therefore produces nothing at all on a web page. To list posts on a page today, rely on the automatic child listing a folder home emits (`moss-cards`), and give any `:::recent` block a fallback body.",
2863    },
2864    ComponentEntry {
2865        class: "moss-recent__date",
2866        kind: "instance",
2867        parent: "moss-recent",
2868        data_attrs: &[],
2869        example_html: r#"<div class="moss-recent__date">2026-04-12</div>"#,
2870        example_markdown: "",
2871        status: Status::Emerging,
2872        since: "0",
2873        description: "Per-entry date slot inside `.moss-recent` (BEM child). Format is `YYYY-MM-DD`, derived from frontmatter `date`. Empty string when the post lacks a parseable date.",
2874    },
2875    ComponentEntry {
2876        class: "moss-recent__desc",
2877        kind: "instance",
2878        parent: "moss-recent",
2879        data_attrs: &[],
2880        example_html: r#"<div class="moss-recent__desc">A walk through the garden.</div>"#,
2881        example_markdown: "",
2882        status: Status::Emerging,
2883        since: "0",
2884        description: "Per-entry description slot inside `.moss-recent` (BEM child). Sourced from frontmatter `description`; empty when unset.",
2885    },
2886    // -------------------------------------------------------------------
2887    // Ambient loop video — JS-injected wrapper + toggle (§3.5).
2888    // The <video data-loop> synthesizer emits `data-loop` on the <video>;
2889    // ambient-video.ts wraps it at init time.
2890    // -------------------------------------------------------------------
2891    ComponentEntry {
2892        class: "moss-ambient-video",
2893        kind: "standalone",
2894        parent: "",
2895        data_attrs: &[
2896            DataAttr {
2897                name: "data-paused",
2898                values: &[],
2899                default: "",
2900                description: "Boolean presence flag set by ambient-video.ts when the video is paused (user-initiated or reduced-motion guard). CSS uses `[data-paused]` to keep the toggle visible.",
2901            },
2902        ],
2903        example_html: r#"<div class="moss-ambient-video">
2904  <video data-loop src="clip.mp4" autoplay muted loop playsinline preload="metadata"></video>
2905  <button class="moss-ambient-toggle" type="button" aria-label="Pause video">⏸</button>
2906</div>"#,
2907        example_markdown: "![[clip.mp4|loop]]",
2908        status: Status::Emerging,
2909        since: "1",
2910        description: "JS-injected wrapper around a `video[data-loop]` element. Provides the positioning context for `.moss-ambient-toggle` and the `[data-paused]` state hook. Not emitted by the Rust synthesizer — ambient-video.ts creates it at init.",
2911    },
2912    ComponentEntry {
2913        class: "moss-ambient-toggle",
2914        kind: "instance",
2915        parent: "moss-ambient-video",
2916        data_attrs: &[],
2917        example_html: r#"<button class="moss-ambient-toggle" type="button" aria-label="Pause video">⏸</button>"#,
2918        example_markdown: "",
2919        status: Status::Emerging,
2920        since: "1",
2921        description: "Chrome-free pause/play toggle button for ambient loop videos. Injected by ambient-video.ts. Keyboard-focusable; `aria-label` toggles between \"Pause video\" and \"Play video\". Visible on hover/focus of `.moss-ambient-video` and always visible when `[data-paused]`. Satisfies WCAG 2.2.2 Level A (Pause, Stop, Hide).",
2922    },
2923    // -------------------------------------------------------------------
2924    // LaTeX math (ADR-030). P1 emits the escaped source in a marked
2925    // `<code>`; P2 replaces the element's *contents* with a typeset
2926    // `<svg>` while keeping the class and `data-moss-math` stable, so a
2927    // theme selector written against P1 keeps working across the upgrade.
2928    // -------------------------------------------------------------------
2929    ComponentEntry {
2930        class: "moss-math",
2931        kind: "standalone",
2932        parent: "",
2933        data_attrs: &[
2934            DataAttr {
2935                name: "data-moss-math",
2936                values: &["inline", "display"],
2937                default: "inline",
2938                description: "Which delimiter produced the equation: `inline` for `$…$`, `display` for `$$…$$`. Carries the distinction to CSS and to the typesetter so neither has to re-derive it from context — a theme can select on it today to centre display math; moss ships no math stylesheet of its own yet, so both variants currently inherit plain `<code>` styling.",
2939            },
2940        ],
2941        example_html: r#"<code class="moss-math" data-moss-math="inline">$E = mc^2$</code>"#,
2942        example_markdown: "Energy $E = mc^2$.",
2943        status: Status::Emerging,
2944        since: "1",
2945        description: "A LaTeX equation. In P1 the element holds the author's own markdown source — `$` / `$$` delimiters included — HTML-escaped: an honest fallback that never shows a blank where an equation was written, and never deletes the delimiters of prose that merely looked like math. Requires `[site].math` (default on).",
2946    },
2947    ComponentEntry {
2948        class: "moss-math-scroll",
2949        kind: "container",
2950        parent: "",
2951        data_attrs: &[],
2952        example_html: r#"<div class="moss-math-scroll"><svg class="moss-math" data-moss-math="display">…</svg></div>"#,
2953        example_markdown: "$$E = mc^2$$",
2954        status: Status::Emerging,
2955        since: "1",
2956        description: "Horizontal-scroll container the build path wraps around typeset display math (`svg.moss-math[data-moss-math=\"display\"]`). On a narrow viewport a wide equation scrolls inside this box at its natural size rather than shrinking to unreadability or pushing the page into horizontal overflow. Emitted only by P2's typeset path; the P1 `<code>` fallback is never wrapped. A display SVG left unwrapped still cannot overflow the page — it falls back to scaling down via `max-width: 100%`.",
2957    },
2958    ComponentEntry {
2959        class: "moss-table-scroll",
2960        kind: "container",
2961        parent: "",
2962        data_attrs: &[],
2963        example_html: r#"<div class="moss-table-scroll" tabindex="0">
2964  <table>…</table>
2965</div>"#,
2966        example_markdown: "| Name | Subs |\n| --- | --- |\n| a | 1,200 |",
2967        status: Status::Emerging,
2968        since: "1",
2969        description: "Horizontal-scroll wrapper the renderer emits around every Markdown table and every CSV/TSV embed (`.moss-embed[data-type=\"table\"]`). Keeps the `<table>` semantically intact (unlike a `display:block` table, which breaks column layout and assistive-tech table semantics) while letting a wide table scroll inside its own box instead of pushing the page into horizontal overflow. `tabindex=\"0\"` makes an overflowing table keyboard-scrollable.",
2970    },
2971    ComponentEntry {
2972        class: "moss-col-right",
2973        kind: "instance",
2974        parent: "moss-table-scroll",
2975        data_attrs: &[],
2976        example_html: r#"<th class="moss-col-right">订阅数</th>
2977<td class="moss-col-right">1,457,776</td>"#,
2978        example_markdown: "| Subs |\n| --: |\n| 1,457,776 |",
2979        status: Status::Emerging,
2980        since: "1",
2981        description: "Right-aligned table cell (`<th>`/`<td>`). Applied to a whole column when the author right-aligned it in GFM (`|--:|`) or when the column auto-detects as numeric, so figures register on their trailing digits. Pairs with the table's `font-variant-numeric: tabular-nums`.",
2982    },
2983    ComponentEntry {
2984        class: "moss-col-center",
2985        kind: "instance",
2986        parent: "moss-table-scroll",
2987        data_attrs: &[],
2988        example_html: r#"<th class="moss-col-center">Status</th>
2989<td class="moss-col-center">✓</td>"#,
2990        example_markdown: "| Status |\n| :-: |\n| ✓ |",
2991        status: Status::Emerging,
2992        since: "1",
2993        description: "Center-aligned table cell (`<th>`/`<td>`). Applied to a whole column the author center-aligned in GFM (`|:-:|`).",
2994    },
2995    ComponentEntry {
2996        class: "moss-search",
2997        kind: "chrome",
2998        parent: "",
2999        data_attrs: &[],
3000        example_html: r#"<div class="moss-search" id="moss-search" hidden>
3001  <div class="moss-search__backdrop"></div>
3002  <div class="moss-search__panel" role="dialog" aria-modal="true" aria-label="Search">
3003    <div class="moss-search__field"><input class="moss-search__input" role="combobox"></div>
3004    <div class="moss-search__progress" hidden></div>
3005    <div class="moss-search__seam"></div>
3006    <div class="moss-search__body">
3007      <p class="moss-search__status" role="status" hidden></p>
3008      <ul class="moss-search__results" role="listbox">
3009        <li class="moss-search__row">
3010          <a class="moss-search__link" role="option" href="/posts/foo/">
3011            <span class="moss-search__title">Title</span>
3012            <span class="moss-search__excerpt">…a <mark>match</mark>…</span>
3013          </a>
3014        </li>
3015      </ul>
3016    </div>
3017  </div>
3018</div>"#,
3019        example_markdown: "",
3020        status: Status::Emerging,
3021        since: "1",
3022        description: "Site-search overlay. Not emitted by the build — the client runtime (`_moss/js/search.<hash>.js`, shipped only when the build wrote a Pagefind index) constructs this subtree lazily on the first open, so a reader who never searches downloads no index and materializes no DOM. Opened by the nav's `.nav-search-btn`, by `/`, or by ⌘K/Ctrl+K. BEM children carry the interior: `__backdrop` (translucent page-coloured scrim, not an opaque modal takeover), `__panel` (top-anchored at 18vh, fixed 18px radius at any height), `__field`/`__input`, `__progress` (1px accent hairline, delayed 200ms so fast queries never flash it), `__seam` (hairline inset by the corner radius), `__status` (idle / no-matches line, sharing one vertical slot with the results so the panel never jumps), `__results`/`__row`/`__link`/`__title`/`__excerpt`. Selection is a 2px `--moss-color-ui-accent` left border plus a ~4% accent tint — never a solid fill block. `<mark>` inside `__excerpt` is Pagefind's own term highlighting, restyled to colour emphasis rather than a highlighter box.",
3023    },
3024    ComponentEntry {
3025        class: "moss-footnotes",
3026        kind: "container",
3027        parent: "",
3028        data_attrs: &[],
3029        example_html: r##"<section class="moss-footnotes" role="doc-endnotes">
3030<ol>
3031<li id="fn-1"><p>The note. <a class="moss-footnote-backref" href="#fnref-1" role="doc-backlink" aria-label="Back to reference 1">&#8617;&#xFE0E;</a></p>
3032</li>
3033</ol>
3034</section>"##,
3035        example_markdown: "Text[^1].\n\n[^1]: The note.",
3036        status: Status::Emerging,
3037        since: "1",
3038        description: "The document's endnote section, appended after the body by the renderer. Holds one `<li id=\"fn-N\">` per footnote in first-reference order, whatever depth the author wrote the definition at — a definition inside a blockquote or a list item is hoisted here too. Present only on pages that define at least one footnote. `role=\"doc-endnotes\"` (DPUB-ARIA) names the region for assistive tech.",
3039    },
3040    ComponentEntry {
3041        class: "moss-footnote-ref",
3042        kind: "instance",
3043        parent: "moss-footnotes",
3044        data_attrs: &[],
3045        example_html: r##"<sup class="moss-footnote-ref" id="fnref-1"><a href="#fn-1" role="doc-noteref">1</a></sup>"##,
3046        example_markdown: "Text[^1].\n\n[^1]: The note.",
3047        status: Status::Emerging,
3048        since: "1",
3049        description: "The in-body footnote marker: a superscript number linking down to its note. The number is first-reference order, not the author's label, so `[^method]` and `[^1]` both print as ordinals. A second marker for the same note takes id `fnref-N-2`, `fnref-N-3`, … so each has its own back-link.",
3050    },
3051    ComponentEntry {
3052        class: "moss-footnote-backref",
3053        kind: "instance",
3054        parent: "moss-footnotes",
3055        data_attrs: &[],
3056        example_html: r##"<a class="moss-footnote-backref" href="#fnref-1" role="doc-backlink" aria-label="Back to reference 1">&#8617;&#xFE0E;</a>"##,
3057        example_markdown: "Text[^1].\n\n[^1]: The note.",
3058        status: Status::Emerging,
3059        since: "1",
3060        description: "The return arrow at the end of a note, linking back to the marker that sent the reader there. One per marker, so a note referenced twice ends with two arrows. A note nobody referenced has none. The arrow carries VARIATION SELECTOR-15 (`&#xFE0E;`) so mobile Chrome renders it as plain text rather than a coloured emoji.",
3061    },
3062    // Classes the site JavaScript reads, declared 2026-08-09.
3063    //
3064    // moss ships JS into every built site, and that JS finds what it acts on by
3065    // class name. So a class the script queries is as much a published name as
3066    // one a stylesheet targets — rename it and the feature dies silently, since
3067    // a selector that matches nothing does not throw. `site_js_selectors_match_
3068    // components_table` (src-tauri/tests/components_sync_test.rs) now checks
3069    // that direction; these are the twenty classes it found undeclared. All of
3070    // them predate the `moss-` convention, hence the block below in
3071    // `UNPREFIXED_LEGACY_CLASSES`.
3072    ComponentEntry {
3073        class: "container",
3074        kind: "chrome",
3075        parent: "",
3076        data_attrs: &[
3077            DataAttr {
3078                name: "data-share-cover",
3079                values: &[],
3080                default: "",
3081                description: "The page's own cover image, as a same-origin URL: its `:::hero` image, else its `cover:` frontmatter. Emitted on the `<article>` only, and only when the page has one of those — absent means the page genuinely has no cover, and never the auto-generated og card. The share-card runtime reads it instead of hunting for the cover in the markup; a theme can use `article.container[data-share-cover]` to tell a page that has a cover picture from one that does not.",
3082            },
3083        ],
3084        example_html: r#"<article class="container" data-share-cover="/img/cover.webp">…</article>"#,
3085        example_markdown: "",
3086        status: Status::Confirmed,
3087        since: "0",
3088        description: "The reading-width wrapper. It is the `<article>` on a page or post, and also the `<nav class=\"main-nav container\">` — one class, one measure, so the masthead lines up with the text under it. Site JS treats `article.container` as \"the current document\": immersive mode promotes its direct-child iframes, and the share card reads its `data-share-cover`.",
3089    },
3090    ComponentEntry {
3091        class: "nav-content",
3092        kind: "chrome",
3093        parent: "main-nav",
3094        data_attrs: &[],
3095        example_html: r#"<nav class="main-nav container"><div class="nav-content">…</div></nav>"#,
3096        example_markdown: "",
3097        status: Status::Confirmed,
3098        since: "0",
3099        description: "The row inside the nav bar that holds `.nav-left` and `.nav-right`. It is the box the responsive nav measures itself against: when the two groups no longer fit on one line, `nav-split.ts` sets `data-nav-split` on this element and the links move to a second row.",
3100    },
3101    ComponentEntry {
3102        class: "font-anchor",
3103        kind: "chrome",
3104        parent: "date-line",
3105        data_attrs: &[],
3106        example_html: r#"<div class="font-anchor"><button class="font-trigger size-std" aria-expanded="false"></button><div class="font-pill" id="fontPill">…</div></div>"#,
3107        example_markdown: "",
3108        status: Status::Confirmed,
3109        since: "0",
3110        description: "Positioning box for the reading-preferences control that sits at the end of an article's date line. Holds the trigger and the size pills; exists so the pills can be positioned against the trigger rather than the page.",
3111    },
3112    ComponentEntry {
3113        class: "font-pill",
3114        kind: "instance",
3115        parent: "font-anchor",
3116        data_attrs: &[],
3117        example_html: r#"<div class="font-pill" id="fontPill"><button data-scale="small"></button>…</div>"#,
3118        example_markdown: "",
3119        status: Status::Confirmed,
3120        since: "0",
3121        description: "The reading-size control that opens from the font trigger. Each button carries `data-scale`; the one matching the current size additionally carries the bare class `active`.",
3122    },
3123    ComponentEntry {
3124        class: "font-trigger",
3125        kind: "instance",
3126        parent: "font-anchor",
3127        data_attrs: &[],
3128        example_html: r#"<button class="font-trigger size-std" aria-label="Reading preferences" aria-expanded="false" type="button"></button>"#,
3129        example_markdown: "",
3130        status: Status::Confirmed,
3131        since: "0",
3132        description: "The button that opens the reading-size pills. Its second class tracks the chosen size (`size-std` by default), so a theme can restyle the trigger per size without reading state from JS.",
3133    },
3134    ComponentEntry {
3135        class: "size-std",
3136        kind: "instance",
3137        parent: "font-trigger",
3138        data_attrs: &[],
3139        example_html: r#"<button class="font-trigger size-std" aria-expanded="false"></button>"#,
3140        example_markdown: "",
3141        status: Status::Confirmed,
3142        since: "0",
3143        description: "Co-class on the font trigger reflecting the current reading size (`size-std` at the default). Lets a theme show the active size on the closed control.",
3144    },
3145    ComponentEntry {
3146        class: "cover-thumb",
3147        kind: "instance",
3148        parent: "moss-card-cover",
3149        data_attrs: &[],
3150        example_html: r#"<div class="moss-card-cover"><video src="clip.mp4" muted loop playsinline preload="metadata"></video><img src="clip.thumb.jpg" alt="A clip" class="cover-thumb" /></div>"#,
3151        example_markdown: "",
3152        status: Status::Confirmed,
3153        since: "0",
3154        description: "The still frame stacked over a video cover. It is what the reader sees until they hover: `card-video.ts` fades this image out and starts the `<video>` underneath, and fades it back in on leave. Emitted only for video covers, inside `.moss-card-cover` or `.moss-collection-cover`.",
3155    },
3156    ComponentEntry {
3157        class: "media-item",
3158        kind: "instance",
3159        parent: "",
3160        data_attrs: &[
3161            DataAttr { name: "data-type", values: &["image", "video", "iframe"], default: "image", description: "Which lightbox surface opens for this item." },
3162            DataAttr { name: "data-src", values: &[], default: "", description: "Full-size source the lightbox loads." },
3163            DataAttr { name: "data-title", values: &[], default: "", description: "Caption shown under the lightbox." },
3164            DataAttr { name: "data-article", values: &[], default: "", description: "URL of the article the item came from, linked from the caption." },
3165        ],
3166        example_html: r#"<figure class="media-item" tabindex="0" data-type="image" data-src="/img/full.webp" data-title="Kyiv" data-article="/posts/kyiv/">…</figure>"#,
3167        example_markdown: "",
3168        status: Status::Confirmed,
3169        since: "0",
3170        description: "One tile on the media-collection page. `fullscreen.ts` collects these in document order to build the lightbox playlist, so their order on the page is the order the arrows step through.",
3171    },
3172    ComponentEntry {
3173        class: "lightbox-content",
3174        kind: "chrome",
3175        parent: "",
3176        data_attrs: &[],
3177        example_html: r#"<div class="lightbox-content"><img class="lightbox-image" hidden /><video class="lightbox-video" controls hidden></video><iframe class="lightbox-iframe" hidden></iframe></div>"#,
3178        example_markdown: "",
3179        status: Status::Confirmed,
3180        since: "0",
3181        description: "The stage of the media-collection lightbox. Holds all three players at once; `fullscreen.ts` unhides whichever one matches the opened item's `data-type` and leaves the others hidden.",
3182    },
3183    ComponentEntry {
3184        class: "lightbox-image",
3185        kind: "instance",
3186        parent: "lightbox-content",
3187        data_attrs: &[],
3188        example_html: r#"<img class="lightbox-image" src="" alt="" hidden />"#,
3189        example_markdown: "",
3190        status: Status::Confirmed,
3191        since: "0",
3192        description: "The lightbox's image player. Emitted empty and hidden; its `src` is filled in when an image item opens.",
3193    },
3194    ComponentEntry {
3195        class: "lightbox-video",
3196        kind: "instance",
3197        parent: "lightbox-content",
3198        data_attrs: &[],
3199        example_html: r#"<video class="lightbox-video" controls hidden></video>"#,
3200        example_markdown: "",
3201        status: Status::Confirmed,
3202        since: "0",
3203        description: "The lightbox's video player. Emitted empty and hidden; paused and cleared when the lightbox closes so audio never outlives the overlay.",
3204    },
3205    ComponentEntry {
3206        class: "lightbox-iframe",
3207        kind: "instance",
3208        parent: "lightbox-content",
3209        data_attrs: &[],
3210        example_html: r#"<iframe class="lightbox-iframe" hidden></iframe>"#,
3211        example_markdown: "",
3212        status: Status::Confirmed,
3213        since: "0",
3214        description: "The lightbox's embed surface, for media items that are an external player rather than a file.",
3215    },
3216    ComponentEntry {
3217        class: "lightbox-title",
3218        kind: "instance",
3219        parent: "",
3220        data_attrs: &[],
3221        example_html: r#"<p class="lightbox-title"></p>"#,
3222        example_markdown: "",
3223        status: Status::Confirmed,
3224        since: "0",
3225        description: "Caption line under the lightbox stage. Filled from the open item's `data-title`.",
3226    },
3227    ComponentEntry {
3228        class: "lightbox-article-link",
3229        kind: "instance",
3230        parent: "",
3231        data_attrs: &[],
3232        example_html: r#"<a class="lightbox-article-link" href="">View in article →</a>"#,
3233        example_markdown: "",
3234        status: Status::Confirmed,
3235        since: "0",
3236        description: "The way back from a media tile to the article it appeared in. Its `href` is filled from the open item's `data-article`, and it is hidden when the item has none.",
3237    },
3238    ComponentEntry {
3239        class: "lightbox-close",
3240        kind: "instance",
3241        parent: "",
3242        data_attrs: &[],
3243        example_html: r#"<button class="lightbox-close" aria-label="Close">&times;</button>"#,
3244        example_markdown: "",
3245        status: Status::Confirmed,
3246        since: "0",
3247        description: "Dismisses the media-collection lightbox. Escape does the same thing.",
3248    },
3249    ComponentEntry {
3250        class: "lightbox-next",
3251        kind: "instance",
3252        parent: "",
3253        data_attrs: &[],
3254        example_html: r#"<button class="lightbox-nav lightbox-next" aria-label="Next">&rsaquo;</button>"#,
3255        example_markdown: "",
3256        status: Status::Confirmed,
3257        since: "0",
3258        description: "Steps forward through the `.media-item` playlist, wrapping at the end. Carries `lightbox-nav` as well, which styles both arrows together.",
3259    },
3260    ComponentEntry {
3261        class: "lightbox-prev",
3262        kind: "instance",
3263        parent: "",
3264        data_attrs: &[],
3265        example_html: r#"<button class="lightbox-nav lightbox-prev" aria-label="Previous">&lsaquo;</button>"#,
3266        example_markdown: "",
3267        status: Status::Confirmed,
3268        since: "0",
3269        description: "Steps backward through the `.media-item` playlist, wrapping at the start.",
3270    },
3271    ComponentEntry {
3272        class: "comments-toggle",
3273        kind: "instance",
3274        parent: "moss-comments",
3275        data_attrs: &[],
3276        example_html: r#"<summary class="comments-toggle"><svg class="comments-icon">…</svg><span>3 comments</span><svg class="comments-chevron">…</svg></summary>"#,
3277        example_markdown: "",
3278        status: Status::Confirmed,
3279        since: "0",
3280        description: "The `<summary>` that opens and closes the comment thread. Its `<span>` holds the count, which the client rewrites as comments arrive — so the span is a contract of its own, not decoration.",
3281    },
3282    ComponentEntry {
3283        class: "comments-icon",
3284        kind: "instance",
3285        parent: "comments-toggle",
3286        data_attrs: &[],
3287        example_html: r#"<summary class="comments-toggle"><svg class="comments-icon" width="18" height="18">…</svg></summary>"#,
3288        example_markdown: "",
3289        status: Status::Confirmed,
3290        since: "0",
3291        description: "Speech-bubble icon inside the comments disclosure summary.",
3292    },
3293    ComponentEntry {
3294        class: "comments-chevron",
3295        kind: "instance",
3296        parent: "comments-toggle",
3297        data_attrs: &[],
3298        example_html: r#"<summary class="comments-toggle">…<svg class="comments-chevron">…</svg></summary>"#,
3299        example_markdown: "",
3300        status: Status::Confirmed,
3301        since: "0",
3302        description: "Disclosure arrow inside the comments summary. Rotates with the `<details>` open state; the rotation is the only affordance saying the section collapses.",
3303    },
3304    ComponentEntry {
3305        class: "comment-list",
3306        kind: "container",
3307        parent: "moss-comments",
3308        data_attrs: &[],
3309        example_html: r#"<ol class="comment-list"><li class="comment-item" …>…</li></ol>"#,
3310        example_markdown: "",
3311        status: Status::Confirmed,
3312        since: "0",
3313        description: "The top-level comment thread, rendered server-side at build time and then hydrated in place (ADR-025). New comments are appended here by the client rather than replacing the list, so server-rendered and live comments share one shape.",
3314    },
3315    ComponentEntry {
3316        class: "comment-item",
3317        kind: "instance",
3318        parent: "comment-list",
3319        data_attrs: &[
3320            DataAttr { name: "data-comment-source", values: &["artalk"], default: "artalk", description: "Where the comment came from — moss's own server, or a syndicated platform." },
3321            DataAttr { name: "data-comment-id", values: &[], default: "", description: "Identifier within that source. Unique only per source, which is why nesting keys on the pair." },
3322        ],
3323        example_html: r#"<li class="comment-item" id="comment-artalk-12" data-comment-source="artalk" data-comment-id="12">…</li>"#,
3324        example_markdown: "",
3325        status: Status::Confirmed,
3326        since: "0",
3327        description: "One comment. Holds a `.comment-header`, a `.comment-body`, and — if it has replies — a nested `.comment-replies`. The `data-comment-*` pair is how the client matches a live comment to the one already on the page instead of rendering it twice.",
3328    },
3329    ComponentEntry {
3330        class: "comment-header",
3331        kind: "instance",
3332        parent: "comment-item",
3333        data_attrs: &[],
3334        example_html: r#"<div class="comment-header"><a class="comment-author">…</a><time class="comment-date">…</time></div>"#,
3335        example_markdown: "",
3336        status: Status::Confirmed,
3337        since: "0",
3338        description: "Attribution row of a single comment: who wrote it, when, and where it came from.",
3339    },
3340    ComponentEntry {
3341        class: "comment-body",
3342        kind: "instance",
3343        parent: "comment-item",
3344        data_attrs: &[],
3345        example_html: r#"<div class="comment-body">…</div>"#,
3346        example_markdown: "",
3347        status: Status::Confirmed,
3348        since: "0",
3349        description: "The comment text itself, separated from the attribution header so the two can be styled independently.",
3350    },
3351    ComponentEntry {
3352        class: "comment-author",
3353        kind: "instance",
3354        parent: "comment-header",
3355        data_attrs: &[],
3356        example_html: r#"<a href="…" class="comment-author" rel="nofollow ugc noopener" target="_blank">Name</a>"#,
3357        example_markdown: "",
3358        status: Status::Confirmed,
3359        since: "0",
3360        description: "The commenter's name, linked to the URL they supplied. Always carries `rel=`nofollow ugc`` — the link is reader-supplied content, not an endorsement by the site.",
3361    },
3362    ComponentEntry {
3363        class: "comment-date",
3364        kind: "instance",
3365        parent: "comment-header",
3366        data_attrs: &[],
3367        example_html: r#"<time class="comment-date" datetime="2026-08-21T10:00:00Z">21 August 2026</time>"#,
3368        example_markdown: "",
3369        status: Status::Confirmed,
3370        since: "0",
3371        description: "Publication time of a comment, as a `<time>` element carrying a machine-readable `datetime`.",
3372    },
3373    ComponentEntry {
3374        class: "comment-source-link",
3375        kind: "instance",
3376        parent: "comment-header",
3377        data_attrs: &[],
3378        example_html: r#"<a class="comment-source-link" href="…" target="_blank" rel="noopener nofollow">…</a>"#,
3379        example_markdown: "",
3380        status: Status::Confirmed,
3381        since: "0",
3382        description: "Link back to where a syndicated comment originated, for comments moss did not receive directly.",
3383    },
3384    ComponentEntry {
3385        class: "comment-replies",
3386        kind: "container",
3387        parent: "comment-item",
3388        data_attrs: &[],
3389        example_html: r#"<ol class="comment-replies"><li class="comment-item" …>…</li></ol>"#,
3390        example_markdown: "",
3391        status: Status::Confirmed,
3392        since: "0",
3393        description: "Nested replies under a comment, same shape as `.comment-list`. Emitted only when a comment has replies; the client creates one on demand when the first reply arrives.",
3394    },
3395    ComponentEntry {
3396        class: "comment-reply-btn",
3397        kind: "instance",
3398        parent: "comment-item",
3399        data_attrs: &[
3400            DataAttr { name: "data-reply-id", values: &[], default: "", description: "The comment being replied to." },
3401            DataAttr { name: "data-reply-name", values: &[], default: "", description: "Display name of its author, used to prefill the form." },
3402        ],
3403        example_html: r#"<button type="button" class="comment-reply-btn" data-reply-id="12" data-reply-name="Yi">↩︎ Reply</button>"#,
3404        example_markdown: "",
3405        status: Status::Confirmed,
3406        since: "0",
3407        description: "Moves the comment form under this comment so the reply is written where it will appear. Emitted only for comments moss can reply to; a syndicated comment gets a link out to its own platform instead.",
3408    },
3409    ComponentEntry {
3410        class: "review-colophon",
3411        kind: "standalone",
3412        parent: "",
3413        data_attrs: &[],
3414        example_html: r#"<footer class="review-colophon">…</footer>"#,
3415        example_markdown: "",
3416        status: Status::Confirmed,
3417        since: "0",
3418        description: "Footer block of a review page carrying the details of the work being reviewed. A `<footer>` because it describes the subject rather than continuing the argument.",
3419    },
3420    ComponentEntry {
3421        class: "review-colophon-details",
3422        kind: "instance",
3423        parent: "review-colophon",
3424        data_attrs: &[],
3425        example_html: r#"<div class="review-colophon-details">…</div>"#,
3426        example_markdown: "",
3427        status: Status::Confirmed,
3428        since: "0",
3429        description: "Container for the reviewed work's identifying fields inside the colophon.",
3430    },
3431    ComponentEntry {
3432        class: "review-biblio",
3433        kind: "instance",
3434        parent: "review-colophon",
3435        data_attrs: &[],
3436        example_html: r#"<div class="review-biblio">…</div>"#,
3437        example_markdown: "",
3438        status: Status::Confirmed,
3439        since: "0",
3440        description: "Full bibliographic citation of the reviewed work, formatted as one line.",
3441    },
3442    ComponentEntry {
3443        class: "review-rating",
3444        kind: "instance",
3445        parent: "review-colophon",
3446        data_attrs: &[],
3447        example_html: r#"<div class="review-rating">★★★★☆</div>"#,
3448        example_markdown: "",
3449        status: Status::Confirmed,
3450        since: "0",
3451        description: "The reviewer's own rating of the work.",
3452    },
3453    ComponentEntry {
3454        class: "review-community-rating",
3455        kind: "instance",
3456        parent: "review-colophon",
3457        data_attrs: &[],
3458        example_html: r#"<div class="review-community-rating">★ 7.8/10 · 1,204 ratings</div>"#,
3459        example_markdown: "",
3460        status: Status::Confirmed,
3461        since: "0",
3462        description: "Aggregate rating carried over from the source catalogue, kept visually distinct from the reviewer's own so the two are never read as one judgement.",
3463    },
3464    ComponentEntry {
3465        class: "review-links",
3466        kind: "instance",
3467        parent: "review-colophon",
3468        data_attrs: &[],
3469        example_html: r#"<nav class="review-links"><a href="…">…</a><span class="review-sep"> · </span><a href="…">…</a></nav>"#,
3470        example_markdown: "",
3471        status: Status::Confirmed,
3472        since: "0",
3473        description: "Navigation to the reviewed work elsewhere — catalogue entries, purchase pages, the publisher.",
3474    },
3475    ComponentEntry {
3476        class: "review-sep",
3477        kind: "instance",
3478        parent: "review-links",
3479        data_attrs: &[],
3480        example_html: r#"<span class="review-sep"> · </span>"#,
3481        example_markdown: "",
3482        status: Status::Confirmed,
3483        since: "0",
3484        description: "Separator between review links. An element rather than a `::before` so it can be hidden when links wrap.",
3485    },
3486    ComponentEntry {
3487        class: "review-colophon-title",
3488        kind: "instance",
3489        parent: "review-colophon-details",
3490        data_attrs: &[],
3491        example_html: r#"<div class="review-colophon-title">…</div>"#,
3492        example_markdown: "",
3493        status: Status::Confirmed,
3494        since: "0",
3495        description: "Title of the work being reviewed — not the title of the review itself, which is the page heading.",
3496    },
3497    ComponentEntry {
3498        class: "review-colophon-subtitle",
3499        kind: "instance",
3500        parent: "review-colophon-details",
3501        data_attrs: &[],
3502        example_html: r#"<div class="review-colophon-subtitle">…</div>"#,
3503        example_markdown: "",
3504        status: Status::Confirmed,
3505        since: "0",
3506        description: "Subtitle of the reviewed work, rendered only when one is present.",
3507    },
3508    ComponentEntry {
3509        class: "review-colophon-identity",
3510        kind: "instance",
3511        parent: "review-colophon-details",
3512        data_attrs: &[],
3513        example_html: r#"<div class="review-colophon-identity">…</div>"#,
3514        example_markdown: "",
3515        status: Status::Confirmed,
3516        since: "0",
3517        description: "The reviewed work's identifier (ISBN, DOI or equivalent), which is what makes a review resolvable to a specific edition.",
3518    },
3519    ComponentEntry {
3520        class: "page-wrapper",
3521        kind: "container",
3522        parent: "",
3523        data_attrs: &[],
3524        example_html: r#"<div class="page-wrapper"><div class="main-content">…</div></div>"#,
3525        example_markdown: "",
3526        status: Status::Confirmed,
3527        since: "0",
3528        description: "Outermost wrapper of a generated media-collection page.",
3529    },
3530    ComponentEntry {
3531        class: "main-content",
3532        kind: "instance",
3533        parent: "page-wrapper",
3534        data_attrs: &[],
3535        example_html: r#"<div class="main-content"><div class="media-grid">…</div></div>"#,
3536        example_markdown: "",
3537        status: Status::Confirmed,
3538        since: "0",
3539        description: "Primary content column of a media-collection page, inside the page wrapper.",
3540    },
3541    ComponentEntry {
3542        class: "media-grid",
3543        kind: "instance",
3544        parent: "main-content",
3545        data_attrs: &[],
3546        example_html: r#"<div class="media-grid">…</div>"#,
3547        example_markdown: "",
3548        status: Status::Confirmed,
3549        since: "0",
3550        description: "The grid of media items on a media-collection page.",
3551    },
3552    ComponentEntry {
3553        class: "media-overlay",
3554        kind: "instance",
3555        parent: "media-grid",
3556        data_attrs: &[],
3557        example_html: r#"<div class="media-overlay"><p class="media-title">…</p></div>"#,
3558        example_markdown: "",
3559        status: Status::Confirmed,
3560        since: "0",
3561        description: "Caption overlay drawn over a media thumbnail, typically revealed on hover or focus.",
3562    },
3563    ComponentEntry {
3564        class: "media-title",
3565        kind: "instance",
3566        parent: "media-overlay",
3567        data_attrs: &[],
3568        example_html: r#"<p class="media-title">…</p>"#,
3569        example_markdown: "",
3570        status: Status::Confirmed,
3571        since: "0",
3572        description: "Title of a single media item, inside its overlay.",
3573    },
3574    ComponentEntry {
3575        class: "lightbox",
3576        kind: "standalone",
3577        parent: "",
3578        data_attrs: &[],
3579        example_html: r#"<div id="lightbox" class="lightbox" hidden tabindex="-1">…</div>"#,
3580        example_markdown: "",
3581        status: Status::Confirmed,
3582        since: "0",
3583        description: "Full-screen viewer for a media collection. Ships `hidden` and `tabindex=`-1`` so it is out of the tab order until opened.",
3584    },
3585    ComponentEntry {
3586        class: "lightbox-caption",
3587        kind: "instance",
3588        parent: "lightbox",
3589        data_attrs: &[],
3590        example_html: r#"<div class="lightbox-caption">…</div>"#,
3591        example_markdown: "",
3592        status: Status::Confirmed,
3593        since: "0",
3594        description: "Caption area of the lightbox, describing the item currently shown.",
3595    },
3596    ComponentEntry {
3597        class: "lightbox-nav",
3598        kind: "instance",
3599        parent: "lightbox",
3600        data_attrs: &[],
3601        example_html: r#"<button class="lightbox-nav lightbox-prev" aria-label="Previous">&lsaquo;</button>"#,
3602        example_markdown: "",
3603        status: Status::Confirmed,
3604        since: "0",
3605        description: "Previous/next control in the lightbox. Carries a direction co-class (`lightbox-prev` / `lightbox-next`) and always an `aria-label`, since the visible glyph is a chevron.",
3606    },
3607    ComponentEntry {
3608        class: "latest-sidebar",
3609        kind: "standalone",
3610        parent: "",
3611        data_attrs: &[],
3612        example_html: r#"<nav class="latest-sidebar"><h3>Latest</h3><ul>…</ul></nav>"#,
3613        example_markdown: "",
3614        status: Status::Confirmed,
3615        since: "0",
3616        description: "Sidebar listing the most recent entries. A `<nav>` because it is a navigational aid, not part of the page's argument.",
3617    },
3618    ComponentEntry {
3619        class: "sidebar-more",
3620        kind: "instance",
3621        parent: "latest-sidebar",
3622        data_attrs: &[],
3623        example_html: r#"<a href="…" class="sidebar-more">More →</a>"#,
3624        example_markdown: "",
3625        status: Status::Confirmed,
3626        since: "0",
3627        description: "Link from the latest-entries sidebar to the full listing.",
3628    },
3629    ComponentEntry {
3630        class: "active",
3631        kind: "instance",
3632        parent: "",
3633        data_attrs: &[],
3634        example_html: r#"<button data-scale="" class="active" aria-label="Standard"></button>"#,
3635        example_markdown: "",
3636        status: Status::Confirmed,
3637        since: "0",
3638        description: "Bare state co-class marking the currently-selected item in a set — the current page in `.nav-links`, the current size in `.font-pill`. Style it scoped to its container (`.nav-links .active`), never on its own.",
3639    },
3640    ComponentEntry {
3641        class: "has-sidebar-layout",
3642        kind: "instance",
3643        parent: "",
3644        data_attrs: &[],
3645        example_html: r#"<main class="has-sidebar-layout">…</main>"#,
3646        example_markdown: "",
3647        status: Status::Confirmed,
3648        since: "0",
3649        description: "Marks a page laid out with a sidebar, so the main column can reserve room for it without the sidebar having rendered yet.",
3650    },
3651    ComponentEntry {
3652        class: "wikilink",
3653        kind: "instance",
3654        parent: "",
3655        data_attrs: &[],
3656        example_html: r#"<a class="wikilink" href="/other-page/">Other page</a>"#,
3657        example_markdown: "",
3658        status: Status::Confirmed,
3659        since: "0",
3660        description: "Marks a link that came from `[[wikilink]]` syntax rather than a markdown link, so a theme can distinguish internal cross-references from ordinary links.",
3661    },
3662];
3663
3664/// Implementation classes that are emitted by moss for internal functionality
3665/// but must not appear in the public theme-facing contract (`moss describe` /
3666/// `docs/reference/contract.md`). These classes ARE present in `COMPONENTS` for
3667/// the sync-test to validate their HTML class literals, but `is_public()` hides
3668/// them from agents, themes, and `reference.md` generation.
3669const INTERNAL_CLASSES: &[&str] = &[
3670    "moss-apply",
3671    "moss-apply-form",
3672    "moss-apply-matters",
3673    "moss-apply-hp",
3674    "moss-apply-status",
3675    "moss-apply-helper",
3676];
3677
3678impl ComponentEntry {
3679    /// True for entries that belong in the public, agent/theme-facing surface.
3680    /// v1 rule: not retired AND not an internal implementation class.
3681    ///
3682    /// Internal classes (e.g. all `moss-apply*`) stay in COMPONENTS so the
3683    /// sync-test can validate them, but they must not surface in `moss describe`
3684    /// or `docs/reference/contract.md` — they are subject to change at any time.
3685    pub fn is_public(&self) -> bool {
3686        self.status != Status::Retired && !INTERNAL_CLASSES.contains(&self.class)
3687    }
3688}
3689
3690/// Iterator over class names with `Status::Retired`. Used by the build
3691/// pipeline's theme lint to warn users about pre-v1 vocabulary.
3692///
3693/// Exposed as an iterator over `&'static str` so callers don't need to
3694/// import the `Status` enum (keeps moss-core's surface narrow).
3695pub fn retired_class_names() -> impl Iterator<Item = &'static str> {
3696    COMPONENTS.iter()
3697        .filter(|e| e.status == Status::Retired)
3698        .map(|e| e.class)
3699}
3700
3701#[cfg(test)]
3702mod tests {
3703    use super::*;
3704
3705    /// Orphan-gate: every class in `INTERNAL_CLASSES` must exist as a `class`
3706    /// in `COMPONENTS`. If a class is renamed in the emitter *and* in
3707    /// `INTERNAL_CLASSES` but forgotten in `COMPONENTS`, it would silently
3708    /// re-enter the public contract surface (`is_public()` only hides known
3709    /// internals). This test prevents that gap.
3710    #[test]
3711    fn every_internal_class_has_a_components_entry() {
3712        let component_classes: std::collections::HashSet<&'static str> =
3713            COMPONENTS.iter().map(|e| e.class).collect();
3714        for &internal in INTERNAL_CLASSES {
3715            assert!(
3716                component_classes.contains(internal),
3717                "INTERNAL_CLASSES entry '{}' has no matching entry in COMPONENTS — \
3718                 add a ComponentEntry for it or remove it from INTERNAL_CLASSES",
3719                internal
3720            );
3721        }
3722    }
3723}