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];
121
122/// Whether `class` is exempt from the `moss-` prefix rule.
123///
124/// See [`UNPREFIXED_LEGACY_CLASSES`]. The `callout-` prefix arm covers the
125/// open-ended Obsidian callout types (`callout-note`, `callout-warning`, …).
126pub fn is_unprefixed_legacy(class: &str) -> bool {
127    class.starts_with("callout-") || UNPREFIXED_LEGACY_CLASSES.contains(&class)
128}
129
130/// The full contract surface — every `moss-*` class moss currently emits.
131///
132/// Phase 0b seeds this with the CURRENT emitted vocabulary (not the
133/// v1-collapsed shape). Phase 1c rewrites to the collapsed form.
134pub const COMPONENTS: &[ComponentEntry] = &[
135    ComponentEntry {
136        class: "moss-cards",
137        kind: "container",
138        parent: "",
139        data_attrs: &[
140            DataAttr {
141                name: "data-layout",
142                values: &["grid", "list", "minimal"],
143                default: "grid",
144                description: "Card layout density. Grid: 2-3 cols with covers. List: single column with side covers. Minimal: text-only with year groupings.",
145            },
146            DataAttr {
147                name: "data-density",
148                values: &["default", "compact"],
149                default: "default",
150                description: "Vertical spacing density.",
151            },
152            DataAttr {
153                name: "data-list-axis",
154                values: &["date", "weight", "title"],
155                default: "title",
156                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).",
157            },
158            DataAttr {
159                name: "data-list-has-covers",
160                values: &[""],
161                default: "",
162                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.",
163            },
164        ],
165        example_html: r#"<div class="moss-cards-container">
166  <div class="moss-cards" data-layout="grid" data-list-axis="date" data-list-has-covers>
167    <a class="moss-card" href="...">...</a>
168    <a class="moss-card" href="...">...</a>
169  </div>
170</div>"#,
171        example_markdown: "",
172        status: Status::Confirmed,
173        since: "1",
174        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.",
175    },
176    ComponentEntry {
177        class: "moss-cards-container",
178        kind: "container",
179        parent: "",
180        data_attrs: &[],
181        example_html: r#"<div class="moss-cards-container">
182  <div class="moss-cards" data-layout="grid">...</div>
183</div>"#,
184        example_markdown: "",
185        status: Status::Confirmed,
186        since: "1",
187        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.",
188    },
189    ComponentEntry {
190        class: "moss-summary-layout",
191        kind: "container",
192        parent: "moss-cards",
193        data_attrs: &[],
194        example_html: r#"<div class="moss-cards" data-layout="list">...</div>"#,
195        example_markdown: "",
196        status: Status::Retired,
197        since: "1",
198        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.",
199    },
200    // -------------------------------------------------------------------
201    // Cards family — current emitted vocabulary (pre-Phase 1c collapsing).
202    // Three parallel layouts: grid, list, minimal. Each has its own
203    // container + instance + sub-classes.
204    // -------------------------------------------------------------------
205    ComponentEntry {
206        class: "moss-cards-grid",
207        kind: "container",
208        parent: "",
209        data_attrs: &[],
210        example_html: r#"<div class="moss-cards-grid">
211  <a class="moss-card-grid" href="...">...</a>
212</div>"#,
213        example_markdown: "",
214        status: Status::Retired,
215        since: "0",
216        description: "Retired in Phase 1c — collapsed into `.moss-cards[data-layout=grid]`.",
217    },
218    ComponentEntry {
219        class: "moss-cards-list",
220        kind: "container",
221        parent: "",
222        data_attrs: &[],
223        example_html: r#"<div class="moss-cards-list">
224  <a class="moss-card-list" href="...">...</a>
225</div>"#,
226        example_markdown: "",
227        status: Status::Retired,
228        since: "0",
229        description: "Retired in Phase 1c — collapsed into `.moss-cards[data-layout=list]`.",
230    },
231    ComponentEntry {
232        class: "moss-cards-minimal-year-group",
233        kind: "container",
234        parent: "",
235        data_attrs: &[],
236        example_html: r#"<section class="moss-cards-minimal-year-group">
237  <h3>2024</h3>
238  <div class="moss-card-minimal">...</div>
239</section>"#,
240        example_markdown: "",
241        status: Status::Confirmed,
242        since: "0",
243        description: "Year-grouped section in minimal card layout (e.g. blog index). Modifier `--summary` collapses past years.",
244    },
245    ComponentEntry {
246        class: "moss-cards-minimal-year-group--summary",
247        kind: "container",
248        parent: "moss-cards-minimal-year-group",
249        data_attrs: &[],
250        example_html: r#"<section class="moss-cards-minimal-year-group moss-cards-minimal-year-group--summary">...</section>"#,
251        example_markdown: "",
252        status: Status::Confirmed,
253        since: "0",
254        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).",
255    },
256    ComponentEntry {
257        class: "moss-card",
258        kind: "instance",
259        parent: "moss-cards",
260        data_attrs: &[
261            DataAttr {
262                name: "data-linkblog",
263                values: &[],
264                default: "",
265                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>`).",
266            },
267        ],
268        example_html: r#"<a class="moss-card" href="...">...</a>"#,
269        example_markdown: "",
270        status: Status::Confirmed,
271        since: "1",
272        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]`).",
273    },
274    ComponentEntry {
275        class: "moss-card-cover",
276        kind: "instance",
277        parent: "moss-card",
278        data_attrs: &[],
279        example_html: r#"<div class="moss-card-cover"><img src="..." /></div>"#,
280        example_markdown: "",
281        status: Status::Confirmed,
282        since: "1",
283        description: "Cover media slot inside `.moss-card`. Gets `.moss-card-no-cover` modifier when no image is present.",
284    },
285    ComponentEntry {
286        class: "moss-card-no-cover",
287        kind: "instance",
288        parent: "moss-card",
289        data_attrs: &[],
290        example_html: r#"<div class="moss-card-cover moss-card-no-cover"></div>"#,
291        example_markdown: "",
292        status: Status::Confirmed,
293        since: "1",
294        description: "Modifier applied to `.moss-card-cover` when no cover media is available.",
295    },
296    ComponentEntry {
297        class: "moss-card-content",
298        kind: "instance",
299        parent: "moss-card",
300        data_attrs: &[],
301        example_html: r#"<div class="moss-card-content">...</div>"#,
302        example_markdown: "",
303        status: Status::Confirmed,
304        since: "1",
305        description: "Text content slot inside a grid-layout `.moss-card` (kicker + title + meta).",
306    },
307    ComponentEntry {
308        class: "moss-card-row",
309        kind: "instance",
310        parent: "moss-card",
311        data_attrs: &[],
312        example_html: r#"<div class="moss-card-row">...</div>"#,
313        example_markdown: "",
314        status: Status::Confirmed,
315        since: "1",
316        description: "Row wrapper inside a list-layout `.moss-card` holding body + cover side-by-side.",
317    },
318    ComponentEntry {
319        class: "moss-card-body",
320        kind: "instance",
321        parent: "moss-card",
322        data_attrs: &[],
323        example_html: r#"<div class="moss-card-body">...</div>"#,
324        example_markdown: "",
325        status: Status::Confirmed,
326        since: "1",
327        description: "Text body slot of a list-layout `.moss-card`.",
328    },
329    ComponentEntry {
330        class: "moss-card-head",
331        kind: "instance",
332        parent: "moss-card",
333        data_attrs: &[],
334        example_html: r#"<div class="moss-card-head">...</div>"#,
335        example_markdown: "",
336        status: Status::Confirmed,
337        since: "1",
338        description: "Header row of a `.moss-card-body` (title + kicker + meta).",
339    },
340    ComponentEntry {
341        class: "moss-card-title",
342        kind: "instance",
343        parent: "moss-card",
344        data_attrs: &[],
345        example_html: r#"<h3 class="moss-card-title">Page title</h3>"#,
346        example_markdown: "",
347        status: Status::Confirmed,
348        since: "1",
349        description: "Title inside `.moss-card`.",
350    },
351    ComponentEntry {
352        class: "moss-card-meta",
353        kind: "instance",
354        parent: "moss-card",
355        data_attrs: &[],
356        example_html: r#"<div class="moss-card-meta">2024-01-15</div>"#,
357        example_markdown: "",
358        status: Status::Confirmed,
359        since: "1",
360        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.",
361    },
362    ComponentEntry {
363        class: "moss-card-kicker",
364        kind: "instance",
365        parent: "moss-card",
366        data_attrs: &[],
367        example_html: r#"<span class="moss-card-kicker">Category</span>"#,
368        example_markdown: "",
369        status: Status::Confirmed,
370        since: "1",
371        description: "Eyebrow / overline above the title inside `.moss-card`.",
372    },
373    ComponentEntry {
374        class: "moss-card-permalink",
375        kind: "instance",
376        parent: "moss-card-kicker",
377        data_attrs: &[],
378        example_html: r#"<a class="moss-card-permalink" href="/posts/foo/" title="Permalink to 'Title'">★</a>"#,
379        example_markdown: "",
380        status: Status::Emerging,
381        since: "1",
382        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`.",
383    },
384    ComponentEntry {
385        class: "moss-card-title-link",
386        kind: "instance",
387        parent: "moss-card-head",
388        data_attrs: &[],
389        example_html: r#"<a class="moss-card-title-link" href="https://outlet.example/article"><h3 class="moss-card-title">Article Title</h3></a>"#,
390        example_markdown: "",
391        status: Status::Emerging,
392        since: "1",
393        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`).",
394    },
395    ComponentEntry {
396        class: "moss-card-cover-link",
397        kind: "instance",
398        parent: "moss-card-row",
399        data_attrs: &[],
400        example_html: r#"<a class="moss-card-cover-link" href="https://outlet.example/article"><div class="moss-card-cover">...</div></a>"#,
401        example_markdown: "",
402        status: Status::Emerging,
403        since: "1",
404        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.",
405    },
406    ComponentEntry {
407        class: "moss-card-description-link",
408        kind: "instance",
409        parent: "moss-card-body",
410        data_attrs: &[],
411        example_html: r#"<a class="moss-card-description-link" href="https://outlet.example/article"><p class="moss-card-description">…</p></a>"#,
412        example_markdown: "",
413        status: Status::Emerging,
414        since: "1",
415        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.",
416    },
417    ComponentEntry {
418        class: "moss-card-description",
419        kind: "instance",
420        parent: "moss-card",
421        data_attrs: &[],
422        example_html: r#"<p class="moss-card-description">Excerpt...</p>"#,
423        example_markdown: "",
424        status: Status::Confirmed,
425        since: "1",
426        description: "Excerpt / description paragraph inside a `.moss-card` — below the title in both grid- and list-layout cards.",
427    },
428    ComponentEntry {
429        class: "moss-card-count",
430        kind: "instance",
431        parent: "moss-card",
432        data_attrs: &[],
433        example_html: r#"<div class="moss-card-count">4 articles</div>"#,
434        example_markdown: "",
435        status: Status::Confirmed,
436        since: "1",
437        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.",
438    },
439    ComponentEntry {
440        class: "moss-embed-more",
441        kind: "instance",
442        parent: "moss-cards-container",
443        data_attrs: &[],
444        example_html: r#"<p class="moss-embed-more"><a href="/news/">More →</a></p>"#,
445        example_markdown: "",
446        status: Status::Confirmed,
447        since: "1",
448        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).",
449    },
450    ComponentEntry {
451        class: "moss-card-grid",
452        kind: "instance",
453        parent: "moss-cards-grid",
454        data_attrs: &[],
455        example_html: r#"<a class="moss-card-grid" href="...">...</a>"#,
456        example_markdown: "",
457        status: Status::Retired,
458        since: "0",
459        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=grid]`).",
460    },
461    ComponentEntry {
462        class: "moss-card-grid-cover",
463        kind: "instance",
464        parent: "moss-card-grid",
465        data_attrs: &[],
466        example_html: r#"<div class="moss-card-grid-cover"><img src="..." /></div>"#,
467        example_markdown: "",
468        status: Status::Retired,
469        since: "0",
470        description: "Retired in Phase 1c — collapsed into `.moss-card-cover`.",
471    },
472    ComponentEntry {
473        class: "moss-card-grid-no-cover",
474        kind: "instance",
475        parent: "moss-card-grid",
476        data_attrs: &[],
477        example_html: r#"<div class="moss-card-grid-cover moss-card-grid-no-cover"></div>"#,
478        example_markdown: "",
479        status: Status::Retired,
480        since: "0",
481        description: "Retired in Phase 1c — collapsed into `.moss-card-no-cover`.",
482    },
483    ComponentEntry {
484        class: "moss-card-grid-content",
485        kind: "instance",
486        parent: "moss-card-grid",
487        data_attrs: &[],
488        example_html: r#"<div class="moss-card-grid-content">...</div>"#,
489        example_markdown: "",
490        status: Status::Retired,
491        since: "0",
492        description: "Retired in Phase 1c — collapsed into `.moss-card-content`.",
493    },
494    ComponentEntry {
495        class: "moss-card-grid-kicker",
496        kind: "instance",
497        parent: "moss-card-grid",
498        data_attrs: &[],
499        example_html: r#"<span class="moss-card-grid-kicker">Category</span>"#,
500        example_markdown: "",
501        status: Status::Retired,
502        since: "0",
503        description: "Retired in Phase 1c — collapsed into `.moss-card-kicker`.",
504    },
505    ComponentEntry {
506        class: "moss-card-grid-title",
507        kind: "instance",
508        parent: "moss-card-grid",
509        data_attrs: &[],
510        example_html: r#"<h3 class="moss-card-grid-title">Page title</h3>"#,
511        example_markdown: "",
512        status: Status::Retired,
513        since: "0",
514        description: "Retired in Phase 1c — collapsed into `.moss-card-title`.",
515    },
516    ComponentEntry {
517        class: "moss-card-grid-meta",
518        kind: "instance",
519        parent: "moss-card-grid",
520        data_attrs: &[],
521        example_html: r#"<div class="moss-card-grid-meta">2024-01-15</div>"#,
522        example_markdown: "",
523        status: Status::Retired,
524        since: "0",
525        description: "Retired in Phase 1c — collapsed into `.moss-card-meta`.",
526    },
527    ComponentEntry {
528        class: "moss-card-list",
529        kind: "instance",
530        parent: "moss-cards-list",
531        data_attrs: &[],
532        example_html: r#"<a class="moss-card-list" href="...">...</a>"#,
533        example_markdown: "",
534        status: Status::Retired,
535        since: "0",
536        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=list]`).",
537    },
538    ComponentEntry {
539        class: "moss-card-list-row",
540        kind: "instance",
541        parent: "moss-card-list",
542        data_attrs: &[],
543        example_html: r#"<div class="moss-card-list-row">...</div>"#,
544        example_markdown: "",
545        status: Status::Retired,
546        since: "0",
547        description: "Retired in Phase 1c — collapsed into `.moss-card-row`.",
548    },
549    ComponentEntry {
550        class: "moss-card-list-cover",
551        kind: "instance",
552        parent: "moss-card-list",
553        data_attrs: &[],
554        example_html: r#"<div class="moss-card-list-cover"><img src="..." /></div>"#,
555        example_markdown: "",
556        status: Status::Retired,
557        since: "0",
558        description: "Retired in Phase 1c — collapsed into `.moss-card-cover`.",
559    },
560    ComponentEntry {
561        class: "moss-card-list-body",
562        kind: "instance",
563        parent: "moss-card-list",
564        data_attrs: &[],
565        example_html: r#"<div class="moss-card-list-body">...</div>"#,
566        example_markdown: "",
567        status: Status::Retired,
568        since: "0",
569        description: "Retired in Phase 1c — collapsed into `.moss-card-body`.",
570    },
571    ComponentEntry {
572        class: "moss-card-list-head",
573        kind: "instance",
574        parent: "moss-card-list",
575        data_attrs: &[],
576        example_html: r#"<div class="moss-card-list-head">...</div>"#,
577        example_markdown: "",
578        status: Status::Retired,
579        since: "0",
580        description: "Retired in Phase 1c — collapsed into `.moss-card-head`.",
581    },
582    ComponentEntry {
583        class: "moss-card-list-kicker",
584        kind: "instance",
585        parent: "moss-card-list",
586        data_attrs: &[],
587        example_html: r#"<span class="moss-card-list-kicker">Category</span>"#,
588        example_markdown: "",
589        status: Status::Retired,
590        since: "0",
591        description: "Retired in Phase 1c — collapsed into `.moss-card-kicker`.",
592    },
593    ComponentEntry {
594        class: "moss-card-list-title",
595        kind: "instance",
596        parent: "moss-card-list",
597        data_attrs: &[],
598        example_html: r#"<h3 class="moss-card-list-title">Page title</h3>"#,
599        example_markdown: "",
600        status: Status::Retired,
601        since: "0",
602        description: "Retired in Phase 1c — collapsed into `.moss-card-title`.",
603    },
604    ComponentEntry {
605        class: "moss-card-list-meta",
606        kind: "instance",
607        parent: "moss-card-list",
608        data_attrs: &[],
609        example_html: r#"<div class="moss-card-list-meta">2024-01-15</div>"#,
610        example_markdown: "",
611        status: Status::Retired,
612        since: "0",
613        description: "Retired in Phase 1c — collapsed into `.moss-card-meta`.",
614    },
615    ComponentEntry {
616        class: "moss-card-list-description",
617        kind: "instance",
618        parent: "moss-card-list",
619        data_attrs: &[],
620        example_html: r#"<p class="moss-card-list-description">Excerpt...</p>"#,
621        example_markdown: "",
622        status: Status::Retired,
623        since: "0",
624        description: "Retired in Phase 1c — collapsed into `.moss-card-description`.",
625    },
626    ComponentEntry {
627        class: "moss-card-minimal",
628        kind: "instance",
629        parent: "moss-cards-minimal-year-group",
630        data_attrs: &[],
631        example_html: r#"<div class="moss-card-minimal">
632  <a class="moss-prefix-link" href="...">...</a>
633</div>"#,
634        example_markdown: "",
635        status: Status::Retired,
636        since: "0",
637        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=minimal]`).",
638    },
639    ComponentEntry {
640        class: "moss-folder-item",
641        kind: "instance",
642        parent: "moss-cards-minimal-year-group",
643        data_attrs: &[],
644        example_html: r#"<div class="moss-card-minimal moss-folder-item">
645  <a class="moss-prefix-link moss-folder-link" href="...">...</a>
646  <p class="moss-folder-description">...</p>
647</div>"#,
648        example_markdown: "",
649        status: Status::Confirmed,
650        since: "0",
651        description: "Modifier on `.moss-card-minimal` for folder-type entries in minimal listings.",
652    },
653    ComponentEntry {
654        class: "moss-folder-title",
655        kind: "instance",
656        parent: "moss-folder-item",
657        data_attrs: &[],
658        example_html: r#"<span class="moss-folder-title">Folder name</span>"#,
659        example_markdown: "",
660        status: Status::Confirmed,
661        since: "0",
662        description: "Title text of a folder entry in minimal listings.",
663    },
664    ComponentEntry {
665        class: "moss-folder-description",
666        kind: "instance",
667        parent: "moss-folder-item",
668        data_attrs: &[],
669        example_html: r#"<p class="moss-folder-description">Description...</p>"#,
670        example_markdown: "",
671        status: Status::Confirmed,
672        since: "0",
673        description: "Description paragraph of a folder entry in minimal listings.",
674    },
675    ComponentEntry {
676        class: "moss-folder-link",
677        kind: "instance",
678        parent: "moss-folder-item",
679        data_attrs: &[],
680        example_html: r#"<a class="moss-prefix-link moss-folder-link" href="...">...</a>"#,
681        example_markdown: "",
682        status: Status::Confirmed,
683        since: "0",
684        description: "Modifier on `.moss-prefix-link` for folder-type links in minimal listings.",
685    },
686    // -------------------------------------------------------------------
687    // Prefix-link primitive — used by minimal cards and other listings.
688    // -------------------------------------------------------------------
689    ComponentEntry {
690        class: "moss-prefix-link",
691        kind: "instance",
692        parent: "moss-card-minimal",
693        data_attrs: &[],
694        example_html: r#"<a class="moss-prefix-link" href="...">
695  <span class="moss-prefix-link-prefix">2024-01-15</span>
696  <span class="moss-prefix-link-title">Page title</span>
697</a>"#,
698        example_markdown: "",
699        status: Status::Emerging,
700        since: "0",
701        description: "Link with a prefix span (date or icon) and a title span. Used inside minimal cards.",
702    },
703    ComponentEntry {
704        class: "moss-prefix-link-prefix",
705        kind: "instance",
706        parent: "moss-prefix-link",
707        data_attrs: &[],
708        example_html: r#"<span class="moss-prefix-link-prefix">2024-01-15</span>"#,
709        example_markdown: "",
710        status: Status::Emerging,
711        since: "0",
712        description: "Prefix slot of a prefix-link (typically a date).",
713    },
714    ComponentEntry {
715        class: "moss-prefix-link-title",
716        kind: "instance",
717        parent: "moss-prefix-link",
718        data_attrs: &[],
719        example_html: r#"<span class="moss-prefix-link-title">Page title</span>"#,
720        example_markdown: "",
721        status: Status::Emerging,
722        since: "0",
723        description: "Title slot of a prefix-link.",
724    },
725    ComponentEntry {
726        class: "moss-prefix-link-suffix",
727        kind: "instance",
728        parent: "moss-prefix-link",
729        data_attrs: &[],
730        example_html: r#"<span class="moss-prefix-link-suffix">→</span>"#,
731        example_markdown: "",
732        status: Status::Emerging,
733        since: "0",
734        description: "Optional trailing slot of a prefix-link.",
735    },
736    // -------------------------------------------------------------------
737    // Callouts — Obsidian-style admonitions. Type variant goes on the
738    // container as `.callout-<type>`. Phase 1c may collapse into
739    // `.moss-callout[data-type]`.
740    // -------------------------------------------------------------------
741    ComponentEntry {
742        class: "moss-callout",
743        kind: "standalone",
744        parent: "",
745        data_attrs: &[],
746        example_html: r#"<div class="moss-callout callout" data-type="note">
747  <div class="callout-title">Note</div>
748  <div class="callout-content">Body...</div>
749</div>"#,
750        example_markdown: "> [!note]\n> Body...",
751        status: Status::Confirmed,
752        since: "0",
753        description: "Obsidian-style callout. The Obsidian-compat `.callout` class is co-emitted; type lives on `data-type` (v1).",
754    },
755    ComponentEntry {
756        class: "callout",
757        kind: "standalone",
758        parent: "",
759        data_attrs: &[
760            DataAttr {
761                name: "data-type",
762                values: &["note", "info", "tip", "warning", "pending"],
763                default: "note",
764                description: "v1 callout type. Theme authors target `.callout[data-type=...]` to style by variant.",
765            },
766        ],
767        example_html: r#"<div class="moss-callout callout" data-type="note">...</div>"#,
768        example_markdown: "",
769        status: Status::Confirmed,
770        since: "0",
771        description: "Obsidian-compat class co-emitted on every callout for theme parity. Type lives on `data-type` (v1).",
772    },
773    ComponentEntry {
774        class: "callout-title",
775        kind: "instance",
776        parent: "moss-callout",
777        data_attrs: &[],
778        example_html: r#"<div class="callout-title">Note</div>"#,
779        example_markdown: "",
780        status: Status::Confirmed,
781        since: "0",
782        description: "Title row of a callout.",
783    },
784    ComponentEntry {
785        class: "callout-content",
786        kind: "instance",
787        parent: "moss-callout",
788        data_attrs: &[],
789        example_html: r#"<div class="callout-content">Body...</div>"#,
790        example_markdown: "",
791        status: Status::Confirmed,
792        since: "0",
793        description: "Body container of a callout.",
794    },
795    ComponentEntry {
796        class: "callout-note",
797        kind: "instance",
798        parent: "moss-callout",
799        data_attrs: &[],
800        example_html: r#"<div class="moss-callout callout callout-note">...</div>"#,
801        example_markdown: "> [!note]\n> Body",
802        status: Status::Retired,
803        since: "0",
804        description: "Retired in Phase 1c — type lives on `.callout[data-type=note]`.",
805    },
806    ComponentEntry {
807        class: "callout-info",
808        kind: "instance",
809        parent: "moss-callout",
810        data_attrs: &[],
811        example_html: r#"<div class="moss-callout callout callout-info">...</div>"#,
812        example_markdown: "> [!info]\n> Body",
813        status: Status::Retired,
814        since: "0",
815        description: "Retired in Phase 1c — type lives on `.callout[data-type=info]`.",
816    },
817    ComponentEntry {
818        class: "callout-tip",
819        kind: "instance",
820        parent: "moss-callout",
821        data_attrs: &[],
822        example_html: r#"<div class="moss-callout callout callout-tip">...</div>"#,
823        example_markdown: "> [!tip]\n> Body",
824        status: Status::Retired,
825        since: "0",
826        description: "Retired in Phase 1c — type lives on `.callout[data-type=tip]`.",
827    },
828    ComponentEntry {
829        class: "callout-warning",
830        kind: "instance",
831        parent: "moss-callout",
832        data_attrs: &[],
833        example_html: r#"<div class="moss-callout callout callout-warning">...</div>"#,
834        example_markdown: "> [!warning]\n> Body",
835        status: Status::Retired,
836        since: "0",
837        description: "Retired in Phase 1c — type lives on `.callout[data-type=warning]`.",
838    },
839    ComponentEntry {
840        class: "callout-pending",
841        kind: "instance",
842        parent: "moss-callout",
843        data_attrs: &[],
844        example_html: r#"<div class="moss-callout callout callout-pending">...</div>"#,
845        example_markdown: "> [!pending]\n> Body",
846        status: Status::Retired,
847        since: "0",
848        description: "Retired in Phase 1c — type lives on `.callout[data-type=pending]`.",
849    },
850    // -------------------------------------------------------------------
851    // Embeds — `![[file.ext]]` shortcode renderers (audio, video, pdf,
852    // notebook, table, 3d, iframe).
853    // -------------------------------------------------------------------
854    ComponentEntry {
855        class: "moss-embed",
856        kind: "standalone",
857        parent: "",
858        data_attrs: &[
859            DataAttr {
860                name: "data-type",
861                values: &["audio", "video", "pdf", "notebook", "table", "iframe", "3d"],
862                default: "",
863                description: "v1 embed kind. Set on the embed element. Theme authors target `.moss-embed[data-type=...]`.",
864            },
865            DataAttr {
866                name: "data-loop",
867                values: &[],
868                default: "",
869                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.",
870            },
871            DataAttr {
872                name: "data-width",
873                values: &["body", "wide", "page", "screen"],
874                default: "body",
875                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
876            },
877            DataAttr {
878                name: "data-provider",
879                values: &["youtube", "vimeo", "codepen"],
880                default: "",
881                description: "Identifies the embed provider for external URL embeds. Absent for generic iframes and local HTML embeds.",
882            },
883        ],
884        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>"#,
885        example_markdown: "![[clip.mp4|loop]]",
886        status: Status::Confirmed,
887        since: "0",
888        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.",
889    },
890    ComponentEntry {
891        class: "moss-embed-audio",
892        kind: "instance",
893        parent: "moss-embed",
894        data_attrs: &[],
895        example_html: r#"<div class="moss-embed moss-embed-audio"><audio controls src="..."></audio></div>"#,
896        example_markdown: "![[track.mp3]]",
897        status: Status::Retired,
898        since: "0",
899        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=audio]`.",
900    },
901    ComponentEntry {
902        class: "moss-embed-video",
903        kind: "instance",
904        parent: "moss-embed",
905        data_attrs: &[],
906        example_html: r#"<div class="moss-embed moss-embed-video"><video controls src="..."></video></div>"#,
907        example_markdown: "![[clip.mp4]]",
908        status: Status::Retired,
909        since: "0",
910        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=video]`.",
911    },
912    ComponentEntry {
913        class: "moss-embed-pdf",
914        kind: "instance",
915        parent: "moss-embed",
916        data_attrs: &[],
917        example_html: r#"<div class="moss-embed moss-embed-pdf"><iframe src="..."></iframe></div>"#,
918        example_markdown: "![[paper.pdf]]",
919        status: Status::Retired,
920        since: "0",
921        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=pdf]`.",
922    },
923    ComponentEntry {
924        class: "moss-embed-iframe",
925        kind: "instance",
926        parent: "moss-embed",
927        data_attrs: &[],
928        example_html: r#"<div class="moss-embed moss-embed-iframe"><iframe src="..."></iframe></div>"#,
929        example_markdown: "![[page.html]]",
930        status: Status::Retired,
931        since: "0",
932        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=iframe]`.",
933    },
934    ComponentEntry {
935        class: "moss-embed-notebook",
936        kind: "instance",
937        parent: "moss-embed",
938        data_attrs: &[],
939        example_html: r#"<div class="moss-embed moss-embed-notebook">...</div>"#,
940        example_markdown: "![[analysis.ipynb]]",
941        status: Status::Retired,
942        since: "0",
943        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=notebook]`.",
944    },
945    ComponentEntry {
946        class: "moss-embed-ipynb",
947        kind: "instance",
948        parent: "moss-embed",
949        data_attrs: &[],
950        example_html: r#"<div class="moss-embed moss-embed-ipynb">...</div>"#,
951        example_markdown: "",
952        status: Status::Emerging,
953        since: "0",
954        description: "Alias of `.moss-embed-notebook`; consolidation pending.",
955    },
956    ComponentEntry {
957        class: "moss-embed-table",
958        kind: "instance",
959        parent: "moss-embed",
960        data_attrs: &[],
961        example_html: r#"<div class="moss-embed moss-embed-table"><table>...</table></div>"#,
962        example_markdown: "![[data.csv]]",
963        status: Status::Retired,
964        since: "0",
965        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=table]`.",
966    },
967    ComponentEntry {
968        class: "moss-embed-3d",
969        kind: "instance",
970        parent: "moss-embed",
971        data_attrs: &[],
972        example_html: r#"<div class="moss-embed moss-embed-3d">...</div>"#,
973        example_markdown: "![[model.glb]]",
974        status: Status::Retired,
975        since: "0",
976        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=3d]`.",
977    },
978    ComponentEntry {
979        class: "moss-embed-error",
980        kind: "instance",
981        parent: "moss-embed",
982        data_attrs: &[],
983        example_html: r#"<div class="moss-embed moss-embed-error">File not found: ...</div>"#,
984        example_markdown: "",
985        status: Status::Confirmed,
986        since: "0",
987        description: "Error state for embeds whose target cannot be resolved.",
988    },
989    ComponentEntry {
990        class: "moss-embed-missing",
991        kind: "instance",
992        parent: "moss-embed",
993        data_attrs: &[],
994        example_html: r#"<div class="moss-embed-missing">Folder not found: journal</div>"#,
995        example_markdown: "",
996        status: Status::Confirmed,
997        since: "1",
998        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.",
999    },
1000    // -------------------------------------------------------------------
1001    // Hero, image, visual primitives.
1002    // -------------------------------------------------------------------
1003    ComponentEntry {
1004        class: "moss-hero",
1005        kind: "standalone",
1006        parent: "",
1007        data_attrs: &[
1008            DataAttr {
1009                name: "data-width",
1010                values: &["body", "wide", "page", "screen"],
1011                default: "body",
1012                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.",
1013            },
1014            DataAttr {
1015                name: "data-slides",
1016                values: &["2", "3", "4", "5", "6"],
1017                default: "",
1018                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.",
1019            },
1020            DataAttr {
1021                name: "data-hero-tone",
1022                values: &["light"],
1023                default: "",
1024                description: "Marks a hero whose image is pale enough (scan-cached dominant colour above 0.4 relative luminance) that the default legibility scrim leaves white overlay text under 4.5:1; site.css swaps in a stronger gradient. Emitted only when the hero also carries overlay text. Absent = mid-tone or dark image, no overlay, or an unparseable colour — all keep the default ramp.",
1025            },
1026            DataAttr {
1027                name: "data-mobile",
1028                values: &["overlay"],
1029                default: "",
1030                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\"]`.",
1031            },
1032            DataAttr {
1033                name: "data-captioned",
1034                values: &[""],
1035                default: "",
1036                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.",
1037            },
1038        ],
1039        example_html: r#"<section class="moss-hero" data-width="page">
1040  <div class="moss-hero-content">...</div>
1041</section>"#,
1042        example_markdown: ":::hero {image=cover.jpg}\n:::\n\n:::hero {image=cover.jpg full mobile=overlay}\n# Title over the image\n:::\n",
1043        status: Status::Confirmed,
1044        since: "0",
1045        description: "Hero banner section at the top of a page (cover image + title). v1 adds `data-width` for author-controlled sizing.",
1046    },
1047    ComponentEntry {
1048        class: "moss-hero-content",
1049        kind: "instance",
1050        parent: "moss-hero",
1051        data_attrs: &[],
1052        example_html: r#"<div class="moss-hero-content">...</div>"#,
1053        example_markdown: "",
1054        status: Status::Confirmed,
1055        since: "0",
1056        description: "Text content slot inside `.moss-hero` — text laid ON the image. For text ABOUT the image, see `.moss-hero-caption`.",
1057    },
1058    ComponentEntry {
1059        class: "moss-hero-caption",
1060        kind: "standalone",
1061        parent: "",
1062        data_attrs: &[],
1063        example_html: r#"<p class="moss-hero-caption">封面:基輔米迦勒修道院門口的陣亡將士紀念牆(拍攝:糜緒洋)</p>"#,
1064        example_markdown: ":::hero {image=cover.jpg caption=\"Cover: the memorial wall (photo: A. Photographer)\"}\n:::\n",
1065        status: Status::Confirmed,
1066        since: "0",
1067        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.",
1068    },
1069    ComponentEntry {
1070        class: "moss-hero-slides",
1071        kind: "instance",
1072        parent: "moss-hero",
1073        data_attrs: &[],
1074        example_html: r#"<div class="moss-hero-slides"><div class="moss-hero-slide"><img src="portrait-1.jpg" alt="" /></div></div>"#,
1075        example_markdown: "",
1076        status: Status::Confirmed,
1077        since: "0",
1078        description: "Wrapper holding the `.moss-hero-slide` images of a multi-image hero; the CSS ambient crossfade cycles one slide visible at a time.",
1079    },
1080    ComponentEntry {
1081        class: "moss-hero-slide",
1082        kind: "instance",
1083        parent: "moss-hero",
1084        data_attrs: &[],
1085        example_html: r#"<div class="moss-hero-slide"><img src="portrait-1.jpg" alt="" /></div>"#,
1086        example_markdown: ":::hero
1087![[portrait-1.jpg]]
1088![[portrait-2.jpg]]
1089# Title
1090:::
1091",
1092        status: Status::Confirmed,
1093        since: "0",
1094        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.",
1095    },
1096    ComponentEntry {
1097        class: "moss-image",
1098        kind: "standalone",
1099        parent: "",
1100        data_attrs: &[
1101            DataAttr {
1102                name: "data-aspect",
1103                values: &["portrait", "square", "auto"],
1104                default: "auto",
1105                description: "v1 image aspect-ratio hint. Theme authors target `.moss-image[data-aspect=...]`. Emitter wiring lands in a follow-up.",
1106            },
1107            DataAttr {
1108                name: "data-width",
1109                values: &["body", "wide", "page", "screen"],
1110                default: "body",
1111                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1112            },
1113        ],
1114        example_html: r#"<figure class="moss-image" style="width:55%"><img src="..." alt="..." /></figure>"#,
1115        example_markdown: "![alt](image.jpg)",
1116        status: Status::Confirmed,
1117        since: "0",
1118        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.",
1119    },
1120    ComponentEntry {
1121        class: "moss-align-left",
1122        kind: "standalone",
1123        parent: "",
1124        data_attrs: &[],
1125        example_html: r#"<img src="..." alt="..." class="moss-align-left" />"#,
1126        example_markdown: "![[photo.jpg|align-left]]",
1127        status: Status::Confirmed,
1128        since: "0",
1129        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.",
1130    },
1131    ComponentEntry {
1132        class: "moss-align-right",
1133        kind: "standalone",
1134        parent: "",
1135        data_attrs: &[],
1136        example_html: r#"<img src="..." alt="..." class="moss-align-right" />"#,
1137        example_markdown: "![[photo.jpg|align-right]]",
1138        status: Status::Confirmed,
1139        since: "0",
1140        description: "Floats an image to the right of body text (editorial runaround). Symmetric counterpart to `.moss-align-left`. Mirrors WordPress's `alignright` convention.",
1141    },
1142    ComponentEntry {
1143        class: "moss-article-title",
1144        kind: "instance",
1145        parent: "",
1146        data_attrs: &[],
1147        example_html: r#"<h1 class="moss-article-title">Title</h1>"#,
1148        example_markdown: "",
1149        status: Status::Emerging,
1150        since: "0",
1151        description: "Article-page H1 title emitted from frontmatter.",
1152    },
1153    ComponentEntry {
1154        class: "moss-heading-anchor",
1155        kind: "instance",
1156        parent: "",
1157        data_attrs: &[],
1158        example_html: r##"<h2 id="setup">Setup<a class="moss-heading-anchor" href="#setup" aria-label="Permalink to this section"><span aria-hidden="true">#</span></a></h2>"##,
1159        example_markdown: "## Setup",
1160        status: Status::Emerging,
1161        since: "1",
1162        description: "Clickable permalink appended inside every author-written body heading that carries a slug id; links to the heading's `#`-fragment. The auto-injected `moss-article-title` H1 is emitted separately and gets no anchor.",
1163    },
1164    // -------------------------------------------------------------------
1165    // Grid + gallery + buttons containers (free-form layouts).
1166    // -------------------------------------------------------------------
1167    ComponentEntry {
1168        class: "moss-grid",
1169        kind: "container",
1170        parent: "",
1171        data_attrs: &[
1172            DataAttr {
1173                name: "data-width",
1174                values: &["body", "wide", "page", "screen"],
1175                default: "body",
1176                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1177            },
1178            DataAttr {
1179                name: "data-columns",
1180                values: &["1", "2", "3", "4"],
1181                default: "",
1182                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.",
1183            },
1184        ],
1185        example_html: r#"<div class="moss-grid" data-width="wide">
1186  <div class="moss-grid-card">...</div>
1187</div>"#,
1188        example_markdown: ":::grid {cols=2}\nLeft cell\n+++\nRight cell\n:::\n",
1189        status: Status::Confirmed,
1190        since: "0",
1191        description: "Generic grid container (used by profiles, link previews, etc.). Modifier classes: `profiles`, `featured`, `no-cards`. v1 adds `data-width` (P9).",
1192    },
1193    ComponentEntry {
1194        class: "moss-grid-card",
1195        kind: "instance",
1196        parent: "moss-grid",
1197        data_attrs: &[
1198            DataAttr {
1199                name: "data-kind",
1200                values: &["link", "friend", "card"],
1201                default: "card",
1202                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.",
1203            },
1204        ],
1205        example_html: r#"<a class="moss-grid-card" data-kind="link" href="...">...</a>"#,
1206        example_markdown: "",
1207        status: Status::Confirmed,
1208        since: "0",
1209        description: "Card instance inside `.moss-grid`. Today emits sibling classes `link-card` / `friend-card` / `no-cards`; v1 collapses to `data-kind`.",
1210    },
1211    ComponentEntry {
1212        class: "moss-gallery",
1213        kind: "container",
1214        parent: "",
1215        data_attrs: &[
1216            DataAttr {
1217                name: "data-width",
1218                values: &["body", "wide", "page", "screen"],
1219                default: "body",
1220                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1221            },
1222            DataAttr {
1223                name: "data-columns",
1224                values: &[],
1225                default: "",
1226                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.",
1227            },
1228        ],
1229        example_html: r#"<div class="moss-gallery" data-width="page">
1230  <div class="moss-gallery-item">...</div>
1231</div>"#,
1232        example_markdown: ":::gallery\nphoto.jpg\n:::\n",
1233        status: Status::Confirmed,
1234        since: "0",
1235        description: "Image gallery container. v1 adds `data-width` (P9).",
1236    },
1237    ComponentEntry {
1238        class: "moss-gallery-item",
1239        kind: "instance",
1240        parent: "moss-gallery",
1241        data_attrs: &[],
1242        example_html: r#"<div class="moss-gallery-item"><img src="..." /></div>"#,
1243        example_markdown: "",
1244        status: Status::Confirmed,
1245        since: "0",
1246        description: "Single image entry inside `.moss-gallery`.",
1247    },
1248    ComponentEntry {
1249        class: "moss-buttons",
1250        kind: "container",
1251        parent: "",
1252        data_attrs: &[
1253            DataAttr {
1254                name: "data-style",
1255                values: &["default", "inverted"],
1256                default: "default",
1257                description: "v1 button-row style. Theme authors target `.moss-buttons[data-style=...]`.",
1258            },
1259        ],
1260        example_html: r#"<div class="moss-buttons" data-style="inverted">
1261  <a class="moss-btn" href="...">Click</a>
1262</div>"#,
1263        example_markdown: ":::buttons\n[Get started](https://example.com)\n:::\n",
1264        status: Status::Confirmed,
1265        since: "0",
1266        description: "Container for a row of `.moss-btn` buttons. v1: the inverted variant is on `data-style=\"inverted\"`.",
1267    },
1268    // -------------------------------------------------------------------
1269    // Button primitive (used by subscribe + general CTAs).
1270    // -------------------------------------------------------------------
1271    ComponentEntry {
1272        class: "moss-btn",
1273        kind: "standalone",
1274        parent: "",
1275        data_attrs: &[
1276            DataAttr {
1277                name: "data-role",
1278                values: &["default", "primary", "secondary"],
1279                default: "default",
1280                description: "v1 button role. Theme authors target `.moss-btn[data-role=...]`.",
1281            },
1282        ],
1283        example_html: r#"<button class="moss-btn" data-role="primary">
1284  <span class="moss-btn__label">Submit</span>
1285</button>"#,
1286        example_markdown: "",
1287        status: Status::Confirmed,
1288        since: "0",
1289        description: "Generic button primitive. Role on `data-role` (v1).",
1290    },
1291    ComponentEntry {
1292        class: "moss-btn__label",
1293        kind: "instance",
1294        parent: "moss-btn",
1295        data_attrs: &[],
1296        example_html: r#"<span class="moss-btn__label">Submit</span>"#,
1297        example_markdown: "",
1298        status: Status::Confirmed,
1299        since: "0",
1300        description: "Label span inside `.moss-btn`.",
1301    },
1302    ComponentEntry {
1303        class: "moss-btn__check",
1304        kind: "instance",
1305        parent: "moss-btn",
1306        data_attrs: &[],
1307        example_html: r#"<span class="moss-btn__check">✓</span>"#,
1308        example_markdown: "",
1309        status: Status::Confirmed,
1310        since: "0",
1311        description: "Success checkmark slot inside `.moss-btn`.",
1312    },
1313    ComponentEntry {
1314        class: "moss-btn__spinner",
1315        kind: "instance",
1316        parent: "moss-btn",
1317        data_attrs: &[],
1318        example_html: r#"<span class="moss-btn__spinner"></span>"#,
1319        example_markdown: "",
1320        status: Status::Confirmed,
1321        since: "0",
1322        description: "Loading spinner slot inside `.moss-btn`.",
1323    },
1324    // -------------------------------------------------------------------
1325    // Subscribe form (newsletter / Buttondown / seta).
1326    // -------------------------------------------------------------------
1327    ComponentEntry {
1328        class: "moss-subscribe",
1329        kind: "standalone",
1330        parent: "",
1331        data_attrs: &[],
1332        example_html: r#"<div class="moss-subscribe">
1333  <form class="moss-subscribe-form">...</form>
1334</div>"#,
1335        example_markdown: ":::subscribe\n:::\n",
1336        status: Status::Confirmed,
1337        since: "0",
1338        description: "Newsletter subscribe block (auto-injected into footer when email channel configured).",
1339    },
1340    ComponentEntry {
1341        class: "moss-subscribe-form",
1342        kind: "instance",
1343        parent: "moss-subscribe",
1344        data_attrs: &[
1345            DataAttr {
1346                name: "data-position",
1347                values: &["inline", "apply"],
1348                default: "inline",
1349                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).",
1350            },
1351            DataAttr {
1352                name: "data-button-override",
1353                values: &["true"],
1354                default: "true",
1355                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.",
1356            },
1357            DataAttr {
1358                name: "data-moss-hosted",
1359                values: &["true"],
1360                default: "true",
1361                description: "Marks moss-hosted (seta) forms hydrated by subscribe.ts. Absent on 3rd-party provider forms.",
1362            },
1363            DataAttr {
1364                name: "data-state",
1365                values: &["idle", "loading", "success", "error"],
1366                default: "idle",
1367                description: "Runtime submit state machine, driven by subscribe.ts. Emitted as `idle`; theme authors target `.moss-subscribe-form[data-state=...]`.",
1368            },
1369            DataAttr {
1370                name: "data-moss-pending-site",
1371                values: &["true"],
1372                default: "true",
1373                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.",
1374            },
1375        ],
1376        example_html: r#"<form class="moss-subscribe-form">...</form>"#,
1377        example_markdown: "",
1378        status: Status::Emerging,
1379        since: "0",
1380        description: "Form element inside `.moss-subscribe`.",
1381    },
1382    ComponentEntry {
1383        class: "moss-btn-slot",
1384        kind: "instance",
1385        parent: "moss-subscribe",
1386        data_attrs: &[],
1387        example_html: r#"<div class="moss-btn-slot"><button class="moss-btn">...</button></div>"#,
1388        example_markdown: "",
1389        status: Status::Emerging,
1390        since: "0",
1391        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.",
1392    },
1393    ComponentEntry {
1394        class: "moss-subscribe-status",
1395        kind: "instance",
1396        parent: "moss-subscribe",
1397        data_attrs: &[],
1398        example_html: r#"<div class="moss-subscribe-status">
1399  <span class="moss-subscribe-status__icon"></span>
1400  Subscribed!
1401</div>"#,
1402        example_markdown: "",
1403        status: Status::Emerging,
1404        since: "0",
1405        description: "Status message shown after submit (success/error).",
1406    },
1407    ComponentEntry {
1408        class: "moss-subscribe-status__icon",
1409        kind: "instance",
1410        parent: "moss-subscribe-status",
1411        data_attrs: &[],
1412        example_html: r#"<span class="moss-subscribe-status__icon"></span>"#,
1413        example_markdown: "",
1414        status: Status::Emerging,
1415        since: "0",
1416        description: "Icon slot inside `.moss-subscribe-status`.",
1417    },
1418    ComponentEntry {
1419        class: "moss-subscribe-landing",
1420        kind: "standalone",
1421        parent: "",
1422        data_attrs: &[],
1423        example_html: r#"<section class="moss-subscribe-landing">...</section>"#,
1424        example_markdown: "",
1425        status: Status::Emerging,
1426        since: "0",
1427        description: "Standalone subscribe landing page surface (larger variant).",
1428    },
1429    // -------------------------------------------------------------------
1430    // Apply form (membership / contributor application).
1431    // -------------------------------------------------------------------
1432    ComponentEntry {
1433        class: "moss-apply",
1434        kind: "standalone",
1435        parent: "",
1436        data_attrs: &[],
1437        example_html: r#"<div class="moss-apply" data-state="idle">
1438  <form class="moss-subscribe-form moss-apply-form">...</form>
1439</div>"#,
1440        example_markdown: ":::apply\n:::\n",
1441        status: Status::Emerging,
1442        since: "0",
1443        description: "Apply / membership-request form block (:::apply shortcode).",
1444    },
1445    ComponentEntry {
1446        class: "moss-apply-form",
1447        kind: "instance",
1448        parent: "moss-apply",
1449        data_attrs: &[
1450            DataAttr {
1451                name: "data-position",
1452                values: &["apply"],
1453                default: "apply",
1454                description: "Position variant; always `apply` for this form. Drives CSS layout in email.css.",
1455            },
1456            DataAttr {
1457                name: "data-revert",
1458                values: &["false"],
1459                default: "false",
1460                description: "When `false`, success is terminal (no auto-revert). subscribe.ts reads this.",
1461            },
1462        ],
1463        example_html: r#"<form class="moss-subscribe-form moss-apply-form" data-position="apply" data-revert="false">...</form>"#,
1464        example_markdown: "",
1465        status: Status::Emerging,
1466        since: "0",
1467        description: "Form element inside `.moss-apply`. Also carries `.moss-subscribe-form` so subscribe.ts hydrates it.",
1468    },
1469    ComponentEntry {
1470        class: "moss-apply-matters",
1471        kind: "instance",
1472        parent: "moss-apply",
1473        data_attrs: &[],
1474        example_html: r#"<input type="text" name="matters" class="moss-input moss-apply-matters">"#,
1475        example_markdown: "",
1476        status: Status::Emerging,
1477        since: "0",
1478        description: "Second apply-form input inside `.moss-apply-form` — a Matters username OR a one-line pitch (placeholder-only, no visible label).",
1479    },
1480    ComponentEntry {
1481        class: "moss-apply-hp",
1482        kind: "instance",
1483        parent: "moss-apply",
1484        data_attrs: &[],
1485        example_html: r#"<input type="text" name="website" class="moss-apply-hp" tabindex="-1" aria-hidden="true">"#,
1486        example_markdown: "",
1487        status: Status::Emerging,
1488        since: "0",
1489        description: "Honeypot field (off-screen) inside `.moss-apply-form`. Bots fill it; humans don't.",
1490    },
1491    ComponentEntry {
1492        class: "moss-apply-status",
1493        kind: "instance",
1494        parent: "moss-apply",
1495        data_attrs: &[],
1496        example_html: r#"<div class="moss-subscribe-status moss-apply-status" aria-live="polite">...</div>"#,
1497        example_markdown: "",
1498        status: Status::Emerging,
1499        since: "0",
1500        description: "Status region inside `.moss-apply-form` (also carries `.moss-subscribe-status`).",
1501    },
1502    ComponentEntry {
1503        class: "moss-apply-helper",
1504        kind: "instance",
1505        parent: "moss-apply",
1506        data_attrs: &[],
1507        example_html: r#"<p class="moss-apply-helper" id="moss-apply-email-help">用于获取邀请及免费托管服务</p>"#,
1508        example_markdown: "",
1509        status: Status::Emerging,
1510        since: "0",
1511        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.",
1512    },
1513    // -------------------------------------------------------------------
1514    // Series navigation (prev/next + collection links).
1515    // -------------------------------------------------------------------
1516    ComponentEntry {
1517        class: "moss-series-nav",
1518        kind: "standalone",
1519        parent: "",
1520        data_attrs: &[],
1521        example_html: r#"<nav class="moss-series-nav">
1522  <div class="moss-series-nav-links">...</div>
1523</nav>"#,
1524        example_markdown: "",
1525        status: Status::Confirmed,
1526        since: "0",
1527        description: "Series navigation bar (prev/next/collection) on series pages.",
1528    },
1529    ComponentEntry {
1530        class: "moss-series-nav-links",
1531        kind: "instance",
1532        parent: "moss-series-nav",
1533        data_attrs: &[],
1534        example_html: r#"<div class="moss-series-nav-links">...</div>"#,
1535        example_markdown: "",
1536        status: Status::Confirmed,
1537        since: "0",
1538        description: "Row holding prev/next links in series nav.",
1539    },
1540    ComponentEntry {
1541        class: "moss-series-nav-link",
1542        kind: "instance",
1543        parent: "moss-series-nav",
1544        data_attrs: &[],
1545        example_html: r#"<a class="moss-series-nav-link moss-series-nav-prev" href="...">...</a>"#,
1546        example_markdown: "",
1547        status: Status::Confirmed,
1548        since: "0",
1549        description: "Individual link inside series nav. Modifiers: `moss-series-nav-prev`, `moss-series-nav-next`, `empty` (placeholder).",
1550    },
1551    ComponentEntry {
1552        class: "moss-series-nav-prev",
1553        kind: "instance",
1554        parent: "moss-series-nav",
1555        data_attrs: &[],
1556        example_html: r#"<a class="moss-series-nav-link moss-series-nav-prev" href="...">...</a>"#,
1557        example_markdown: "",
1558        status: Status::Confirmed,
1559        since: "0",
1560        description: "Previous-page modifier on a series nav link.",
1561    },
1562    ComponentEntry {
1563        class: "moss-series-nav-next",
1564        kind: "instance",
1565        parent: "moss-series-nav",
1566        data_attrs: &[],
1567        example_html: r#"<a class="moss-series-nav-link moss-series-nav-next" href="...">...</a>"#,
1568        example_markdown: "",
1569        status: Status::Confirmed,
1570        since: "0",
1571        description: "Next-page modifier on a series nav link.",
1572    },
1573    ComponentEntry {
1574        class: "moss-series-nav-arrow",
1575        kind: "instance",
1576        parent: "moss-series-nav",
1577        data_attrs: &[],
1578        example_html: r#"<span class="moss-series-nav-arrow">→</span>"#,
1579        example_markdown: "",
1580        status: Status::Confirmed,
1581        since: "0",
1582        description: "Arrow glyph inside a series-nav link.",
1583    },
1584    ComponentEntry {
1585        class: "moss-series-nav-title",
1586        kind: "instance",
1587        parent: "moss-series-nav",
1588        data_attrs: &[],
1589        example_html: r#"<span class="moss-series-nav-title">Next page title</span>"#,
1590        example_markdown: "",
1591        status: Status::Confirmed,
1592        since: "0",
1593        description: "Title text of the destination page in a series-nav link.",
1594    },
1595    ComponentEntry {
1596        class: "moss-series-nav-collection",
1597        kind: "instance",
1598        parent: "moss-series-nav",
1599        data_attrs: &[],
1600        example_html: r#"<div class="moss-series-nav-collection">...</div>"#,
1601        example_markdown: "",
1602        status: Status::Confirmed,
1603        since: "0",
1604        description: "Collection-listing slot in series nav (sibling pages).",
1605    },
1606    ComponentEntry {
1607        class: "moss-series-nav-collection-row",
1608        kind: "instance",
1609        parent: "moss-series-nav-collection",
1610        data_attrs: &[],
1611        example_html: r#"<div class="moss-series-nav-collection-row">...</div>"#,
1612        example_markdown: "",
1613        status: Status::Confirmed,
1614        since: "0",
1615        description: "Row inside the collection listing of series nav.",
1616    },
1617    ComponentEntry {
1618        class: "moss-series-nav-position",
1619        kind: "instance",
1620        parent: "moss-series-nav-collection-row",
1621        data_attrs: &[],
1622        example_html: r#"<span class="moss-series-nav-position">2 of 3</span>"#,
1623        example_markdown: "",
1624        status: Status::Confirmed,
1625        since: "0",
1626        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.",
1627    },
1628    // -------------------------------------------------------------------
1629    // Collection cover (collection landing pages).
1630    // -------------------------------------------------------------------
1631    ComponentEntry {
1632        class: "moss-collection-cover",
1633        kind: "standalone",
1634        parent: "",
1635        data_attrs: &[],
1636        example_html: r#"<section class="moss-collection-cover">
1637  <div class="moss-collection-cover-row">...</div>
1638</section>"#,
1639        example_markdown: "",
1640        status: Status::Emerging,
1641        since: "0",
1642        description: "Header surface on a collection landing page.",
1643    },
1644    ComponentEntry {
1645        class: "moss-collection-cover-row",
1646        kind: "instance",
1647        parent: "moss-collection-cover",
1648        data_attrs: &[],
1649        example_html: r#"<div class="moss-collection-cover-row">...</div>"#,
1650        example_markdown: "",
1651        status: Status::Emerging,
1652        since: "0",
1653        description: "Row inside `.moss-collection-cover`.",
1654    },
1655    ComponentEntry {
1656        class: "moss-collection-cover-body",
1657        kind: "instance",
1658        parent: "moss-collection-cover",
1659        data_attrs: &[],
1660        example_html: r#"<div class="moss-collection-cover-body">...</div>"#,
1661        example_markdown: "",
1662        status: Status::Emerging,
1663        since: "0",
1664        description: "Body content slot inside `.moss-collection-cover`.",
1665    },
1666    // -------------------------------------------------------------------
1667    // Form primitives (input, label, field, link).
1668    // -------------------------------------------------------------------
1669    ComponentEntry {
1670        class: "moss-input",
1671        kind: "standalone",
1672        parent: "",
1673        data_attrs: &[],
1674        example_html: r#"<input class="moss-input" type="email" />"#,
1675        example_markdown: "",
1676        status: Status::Confirmed,
1677        since: "0",
1678        description: "Generic form input primitive.",
1679    },
1680    ComponentEntry {
1681        class: "moss-field",
1682        kind: "container",
1683        parent: "",
1684        data_attrs: &[],
1685        example_html: r#"<div class="moss-field">
1686  <label class="moss-label">Email</label>
1687  <input class="moss-input" />
1688</div>"#,
1689        example_markdown: "",
1690        status: Status::Confirmed,
1691        since: "0",
1692        description: "Form field group (label + input). Modifier `--inline` for horizontal layout.",
1693    },
1694    ComponentEntry {
1695        class: "moss-label",
1696        kind: "instance",
1697        parent: "moss-field",
1698        data_attrs: &[],
1699        example_html: r#"<label class="moss-label">Email</label>"#,
1700        example_markdown: "",
1701        status: Status::Confirmed,
1702        since: "0",
1703        description: "Label primitive for `.moss-field`. Modifier `--small` for compact form.",
1704    },
1705    ComponentEntry {
1706        class: "moss-link",
1707        kind: "standalone",
1708        parent: "",
1709        data_attrs: &[],
1710        example_html: r#"<a class="moss-link" href="...">Click me</a>"#,
1711        example_markdown: "",
1712        status: Status::Confirmed,
1713        since: "0",
1714        description: "Inline-link primitive (resets `<button>` chrome too). Use `--subtle` for muted variant.",
1715    },
1716    ComponentEntry {
1717        class: "moss-field--inline",
1718        kind: "instance",
1719        parent: "moss-field",
1720        data_attrs: &[],
1721        example_html: r#"<div class="moss-field moss-field--inline">
1722  <label class="moss-label">Email</label>
1723  <input class="moss-input" />
1724</div>"#,
1725        example_markdown: "",
1726        status: Status::Confirmed,
1727        since: "0",
1728        description: "BEM modifier on `.moss-field` for horizontal label+input layout (used by settings UI primitives).",
1729    },
1730    ComponentEntry {
1731        class: "moss-label--small",
1732        kind: "instance",
1733        parent: "moss-label",
1734        data_attrs: &[],
1735        example_html: r#"<label class="moss-label moss-label--small">Compact label</label>"#,
1736        example_markdown: "",
1737        status: Status::Confirmed,
1738        since: "0",
1739        description: "BEM modifier on `.moss-label` for compact form (used by services settings rows).",
1740    },
1741    ComponentEntry {
1742        class: "moss-info-grid",
1743        kind: "container",
1744        parent: "",
1745        data_attrs: &[],
1746        example_html: r#"<div class="moss-info-grid">
1747  <div class="moss-field moss-field--inline">...</div>
1748  <div class="moss-field moss-field--inline">...</div>
1749</div>"#,
1750        example_markdown: "",
1751        status: Status::Emerging,
1752        since: "0",
1753        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.",
1754    },
1755    ComponentEntry {
1756        class: "moss-row",
1757        kind: "container",
1758        parent: "",
1759        data_attrs: &[],
1760        example_html: r#"<div class="moss-row">
1761  <div class="moss-field">...</div>
1762  <div class="moss-field">...</div>
1763</div>"#,
1764        example_markdown: "",
1765        status: Status::Emerging,
1766        since: "0",
1767        description: "Horizontal flex row of equal-flex `.moss-field` children. Form-row layout helper shipped in the default theme.",
1768    },
1769    ComponentEntry {
1770        class: "moss-input-feedback",
1771        kind: "instance",
1772        parent: "moss-field",
1773        data_attrs: &[],
1774        example_html: r#"<span class="moss-input-feedback">Saving…</span>"#,
1775        example_markdown: "",
1776        status: Status::Emerging,
1777        since: "0",
1778        description: "Auto-save status hint slot under `.moss-field`. Three state modifiers: `--success`, `--error`, `--fade-out`.",
1779    },
1780    ComponentEntry {
1781        class: "moss-input-feedback--success",
1782        kind: "instance",
1783        parent: "moss-input-feedback",
1784        data_attrs: &[],
1785        example_html: r#"<span class="moss-input-feedback moss-input-feedback--success">Saved</span>"#,
1786        example_markdown: "",
1787        status: Status::Emerging,
1788        since: "0",
1789        description: "Success state modifier on `.moss-input-feedback`.",
1790    },
1791    ComponentEntry {
1792        class: "moss-input-feedback--error",
1793        kind: "instance",
1794        parent: "moss-input-feedback",
1795        data_attrs: &[],
1796        example_html: r#"<span class="moss-input-feedback moss-input-feedback--error">Failed to save</span>"#,
1797        example_markdown: "",
1798        status: Status::Emerging,
1799        since: "0",
1800        description: "Error state modifier on `.moss-input-feedback`.",
1801    },
1802    ComponentEntry {
1803        class: "moss-input-feedback--fade-out",
1804        kind: "instance",
1805        parent: "moss-input-feedback",
1806        data_attrs: &[],
1807        example_html: r#"<span class="moss-input-feedback moss-input-feedback--success moss-input-feedback--fade-out">Saved</span>"#,
1808        example_markdown: "",
1809        status: Status::Emerging,
1810        since: "0",
1811        description: "Transient fade-out modifier on `.moss-input-feedback` (applied after a success message to dismiss it).",
1812    },
1813    // -------------------------------------------------------------------
1814    // Other emit surfaces (comments, colophon, shell frame, misc).
1815    // -------------------------------------------------------------------
1816    ComponentEntry {
1817        class: "moss-comments",
1818        kind: "standalone",
1819        parent: "",
1820        data_attrs: &[],
1821        example_html: r#"<section class="moss-comments">...</section>"#,
1822        example_markdown: "",
1823        status: Status::Confirmed,
1824        since: "0",
1825        description: "Comments surface (per-site SQLite backend or Artalk legacy).",
1826    },
1827    ComponentEntry {
1828        class: "moss-service-inactive",
1829        kind: "instance",
1830        parent: "",
1831        data_attrs: &[],
1832        example_html: r#"<section class="moss-comments moss-service-inactive">...</section>"#,
1833        example_markdown: "",
1834        status: Status::Confirmed,
1835        since: "0",
1836        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.",
1837    },
1838    // -------------------------------------------------------------------
1839    // Preview link popover — emitted by `assets/js/preview.js` runtime.
1840    // -------------------------------------------------------------------
1841    ComponentEntry {
1842        class: "moss-preview-popup",
1843        kind: "chrome",
1844        parent: "",
1845        data_attrs: &[],
1846        example_html: r#"<div class="moss-preview-popup" role="tooltip" aria-live="polite">
1847  <strong class="moss-preview-title">...</strong>
1848  <p class="moss-preview-desc">...</p>
1849  <p class="moss-preview-text">...</p>
1850</div>"#,
1851        example_markdown: "",
1852        status: Status::Confirmed,
1853        since: "0",
1854        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.",
1855    },
1856    ComponentEntry {
1857        class: "moss-preview-title",
1858        kind: "instance",
1859        parent: "moss-preview-popup",
1860        data_attrs: &[],
1861        example_html: r#"<strong class="moss-preview-title">Article title</strong>"#,
1862        example_markdown: "",
1863        status: Status::Confirmed,
1864        since: "0",
1865        description: "Title slot inside `.moss-preview-popup`.",
1866    },
1867    ComponentEntry {
1868        class: "moss-preview-desc",
1869        kind: "instance",
1870        parent: "moss-preview-popup",
1871        data_attrs: &[],
1872        example_html: r#"<p class="moss-preview-desc">Short description</p>"#,
1873        example_markdown: "",
1874        status: Status::Confirmed,
1875        since: "0",
1876        description: "Description slot inside `.moss-preview-popup` (from frontmatter `description`).",
1877    },
1878    ComponentEntry {
1879        class: "moss-preview-text",
1880        kind: "instance",
1881        parent: "moss-preview-popup",
1882        data_attrs: &[],
1883        example_html: r#"<p class="moss-preview-text">Excerpt of the linked article…</p>"#,
1884        example_markdown: "",
1885        status: Status::Confirmed,
1886        since: "0",
1887        description: "Excerpt slot inside `.moss-preview-popup` (auto-extracted from the linked article body).",
1888    },
1889    ComponentEntry {
1890        class: "moss-colophon",
1891        kind: "chrome",
1892        parent: "",
1893        data_attrs: &[],
1894        example_html: r#"<div class="moss-colophon">
1895  <a href="https://mosspub.com">
1896    <svg class="moss-colophon-icon"></svg>
1897    <span class="moss-colophon-label">Published with moss</span>
1898  </a>
1899</div>"#,
1900        example_markdown: "",
1901        status: Status::Confirmed,
1902        since: "0",
1903        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.",
1904    },
1905    ComponentEntry {
1906        class: "moss-colophon-icon",
1907        kind: "instance",
1908        parent: "moss-colophon",
1909        data_attrs: &[],
1910        example_html: r#"<svg class="moss-colophon-icon"></svg>"#,
1911        example_markdown: "",
1912        status: Status::Confirmed,
1913        since: "0",
1914        description: "The moss mark inside `.moss-colophon`. Decorative (`aria-hidden`) — `.moss-colophon-label` carries the accessible name.",
1915    },
1916    ComponentEntry {
1917        class: "moss-colophon-label",
1918        kind: "instance",
1919        parent: "moss-colophon",
1920        data_attrs: &[],
1921        example_html: r#"<span class="moss-colophon-label">Published with moss</span>"#,
1922        example_markdown: "",
1923        status: Status::Confirmed,
1924        since: "0",
1925        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.",
1926    },
1927    ComponentEntry {
1928        class: "moss-shell-frame",
1929        kind: "chrome",
1930        parent: "",
1931        data_attrs: &[],
1932        example_html: r#"<div class="moss-shell-frame">...</div>"#,
1933        example_markdown: "",
1934        status: Status::Emerging,
1935        since: "0",
1936        description: "App-shell frame surface (preview chrome).",
1937    },
1938    ComponentEntry {
1939        class: "moss-mobile-frame",
1940        kind: "chrome",
1941        parent: "moss-shell-frame",
1942        data_attrs: &[],
1943        example_html: r#"<html class="moss-shell-frame moss-mobile-frame">...</html>"#,
1944        example_markdown: "",
1945        status: Status::Emerging,
1946        since: "0",
1947        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.",
1948    },
1949    ComponentEntry {
1950        class: "main-nav",
1951        kind: "chrome",
1952        parent: "",
1953        data_attrs: &[],
1954        example_html: r#"<nav class="main-nav container">...</nav>"#,
1955        example_markdown: "",
1956        status: Status::Confirmed,
1957        since: "0",
1958        description: "Top site navigation bar. Legacy non-`moss-` prefix kept for theme parity.",
1959    },
1960    // The article masthead and nav interior. Legacy non-`moss-` prefixes, kept
1961    // for theme parity like `main-nav` above.
1962    //
1963    // These were emitted but undeclared until 2026-08-05, and the omission had
1964    // a measured cost: an agent restyling a journalism site reaches for the
1965    // byline row first, found nothing for it in `describe --json`, and had to
1966    // recover the class by reading built HTML — which the shipped guidance
1967    // sanctions only as a self-check, and which silently breaks on a rename.
1968    // Declaring them is what makes "never hardcode a class from memory"
1969    // followable for the masthead. `components_sync_test` cannot guard these:
1970    // it only matches `class="moss-..."` literals.
1971    ComponentEntry {
1972        class: "date-line",
1973        kind: "chrome",
1974        parent: "",
1975        data_attrs: &[],
1976        example_html: r#"<div class="date-line"><span class="date">March 3, 2026</span><div class="font-anchor">...</div></div>"#,
1977        example_markdown: "",
1978        status: Status::Confirmed,
1979        since: "0",
1980        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`.",
1981    },
1982    ComponentEntry {
1983        class: "date",
1984        kind: "instance",
1985        parent: "date-line",
1986        data_attrs: &[],
1987        example_html: r#"<span class="date">March 3, 2026</span>"#,
1988        example_markdown: "",
1989        status: Status::Confirmed,
1990        since: "0",
1991        description: "The formatted publication date inside `.date-line`. Text is localized to the page's language.",
1992    },
1993    ComponentEntry {
1994        class: "moss-byline",
1995        kind: "container",
1996        parent: "",
1997        data_attrs: &[],
1998        example_html: r#"<div class="moss-byline"><div class="moss-byline-row">作者 糜緒洋</div><div class="moss-byline-row">編輯 謝丁</div></div>"#,
1999        example_markdown: "",
2000        status: Status::Confirmed,
2001        since: "1",
2002        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.",
2003    },
2004    ComponentEntry {
2005        class: "moss-byline-row",
2006        kind: "instance",
2007        parent: "moss-byline",
2008        data_attrs: &[],
2009        example_html: r#"<div class="moss-byline-row">首發媒體 <a href="https://theinitium.com/a">端傳媒</a></div>"#,
2010        example_markdown: "",
2011        status: Status::Confirmed,
2012        since: "1",
2013        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.",
2014    },
2015    ComponentEntry {
2016        class: "moss-article-colophon",
2017        kind: "container",
2018        parent: "",
2019        data_attrs: &[],
2020        example_html: r#"<div class="moss-article-colophon"><div class="moss-article-colophon-row">首發媒體 <a href="https://theinitium.com/a">端傳媒</a></div></div>"#,
2021        example_markdown: "",
2022        status: Status::Confirmed,
2023        since: "1",
2024        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.",
2025    },
2026    ComponentEntry {
2027        class: "moss-article-colophon-row",
2028        kind: "instance",
2029        parent: "moss-article-colophon",
2030        data_attrs: &[],
2031        example_html: r#"<div class="moss-article-colophon-row">封面 基輔米迦勒修道院門口的陣亡將士紀念牆(拍攝:糜緒洋)</div>"#,
2032        example_markdown: "",
2033        status: Status::Confirmed,
2034        since: "1",
2035        description: "One foot-credit line, rendered as inline markdown exactly like `.moss-byline-row`.",
2036    },
2037    ComponentEntry {
2038        class: "site-name",
2039        kind: "instance",
2040        parent: "main-nav",
2041        data_attrs: &[],
2042        example_html: r#"<a href="/" class="site-name">在場</a>"#,
2043        example_markdown: "",
2044        status: Status::Confirmed,
2045        since: "0",
2046        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`.",
2047    },
2048    ComponentEntry {
2049        class: "breadcrumb-segment",
2050        kind: "instance",
2051        parent: "main-nav",
2052        data_attrs: &[],
2053        example_html: r#"<a href="/awards/" class="breadcrumb-segment">獎項</a>"#,
2054        example_markdown: "",
2055        status: Status::Confirmed,
2056        since: "0",
2057        description: "One ancestor link in the nav-left breadcrumb trail, used in place of `.site-name` once the page is below the site root.",
2058    },
2059    ComponentEntry {
2060        class: "nav-icons",
2061        kind: "chrome",
2062        parent: "main-nav",
2063        data_attrs: &[],
2064        example_html: r#"<div class="nav-icons">...</div>"#,
2065        example_markdown: "",
2066        status: Status::Confirmed,
2067        since: "0",
2068        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.",
2069    },
2070    // The rest of the nav interior and the default footer, on the same footing
2071    // as the masthead block above: emitted since 0, styled in `site.css`, and
2072    // undeclared until 2026-08-05.
2073    //
2074    // The 2026-08-05 trial found the concrete cost. An agent asked to restyle a
2075    // site went looking in `describe --json` for the language switcher, found
2076    // nothing, and recovered `.nav-lang-toggle` by grepping built HTML — the one
2077    // move the shipped guidance tells agents not to make, because it breaks
2078    // silently on a rename. `main-nav`, `.site-name` and `.nav-icons` were
2079    // declared; everything they contain was not, which is the worst of both
2080    // (the contract looks complete enough to trust).
2081    ComponentEntry {
2082        class: "nav-left",
2083        kind: "chrome",
2084        parent: "main-nav",
2085        data_attrs: &[],
2086        example_html: r#"<div class="nav-left"><a href="/" class="site-name">在場</a></div>"#,
2087        example_markdown: "",
2088        status: Status::Confirmed,
2089        since: "0",
2090        description: "Left group of the nav bar. Holds either `.site-name` or the breadcrumb trail, never both.",
2091    },
2092    ComponentEntry {
2093        class: "nav-right",
2094        kind: "chrome",
2095        parent: "main-nav",
2096        data_attrs: &[],
2097        example_html: r#"<div class="nav-right">…hamburger, .nav-links, .nav-icons…</div>"#,
2098        example_markdown: "",
2099        status: Status::Confirmed,
2100        since: "0",
2101        description: "Right group of the nav bar: the mobile menu button, the nav links, and the icon cluster, in that order.",
2102    },
2103    ComponentEntry {
2104        class: "nav-links",
2105        kind: "chrome",
2106        parent: "nav-right",
2107        data_attrs: &[],
2108        example_html: r#"<div class="nav-links"><a href="/about/" class="active">關於</a>…</div>"#,
2109        example_markdown: "",
2110        status: Status::Confirmed,
2111        since: "0",
2112        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.",
2113    },
2114    ComponentEntry {
2115        class: "site-logo",
2116        kind: "instance",
2117        parent: "site-name",
2118        data_attrs: &[],
2119        example_html: r#"<img class="site-logo" src="…" alt="" aria-hidden="true">"#,
2120        example_markdown: "",
2121        status: Status::Confirmed,
2122        since: "0",
2123        description: "Optional logo image inside the site-name link. Decorative by construction (`alt=\"\"` + `aria-hidden`), because the adjacent text already names the site.",
2124    },
2125    ComponentEntry {
2126        class: "breadcrumb-label",
2127        kind: "instance",
2128        parent: "nav-left",
2129        data_attrs: &[],
2130        example_html: r#"<span class="breadcrumb-label">獎項</span>"#,
2131        example_markdown: "",
2132        status: Status::Confirmed,
2133        since: "0",
2134        description: "The final, non-linked breadcrumb segment — the page you are on. `.breadcrumb-segment` is the linked form for ancestors.",
2135    },
2136    ComponentEntry {
2137        class: "breadcrumb-separator",
2138        kind: "instance",
2139        parent: "nav-left",
2140        data_attrs: &[],
2141        example_html: r#"<span class="breadcrumb-separator">/</span>"#,
2142        example_markdown: "",
2143        status: Status::Confirmed,
2144        since: "0",
2145        description: "The `/` between breadcrumb segments. Restyle or hide this rather than trying to remove it from the markup.",
2146    },
2147    ComponentEntry {
2148        class: "mobile-menu-button",
2149        kind: "chrome",
2150        parent: "nav-right",
2151        data_attrs: &[],
2152        example_html: r#"<button class="mobile-menu-button" aria-label="…"><svg>…</svg></button>"#,
2153        example_markdown: "",
2154        status: Status::Confirmed,
2155        since: "0",
2156        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.",
2157    },
2158    ComponentEntry {
2159        class: "nav-search-btn",
2160        kind: "instance",
2161        parent: "nav-icons",
2162        data_attrs: &[],
2163        example_html: r#"<button class="nav-search-btn" type="button" aria-label="…"><svg class="search-icon">…</svg></button>"#,
2164        example_markdown: "",
2165        status: Status::Confirmed,
2166        since: "0",
2167        description: "Search button in the nav icon cluster. Its glyph is `.search-icon`.",
2168    },
2169    ComponentEntry {
2170        class: "search-icon",
2171        kind: "instance",
2172        parent: "nav-search-btn",
2173        data_attrs: &[],
2174        example_html: r#"<svg class="search-icon" aria-hidden="true" width="1em" height="1em">…</svg>"#,
2175        example_markdown: "",
2176        status: Status::Confirmed,
2177        since: "0",
2178        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.",
2179    },
2180    ComponentEntry {
2181        class: "nav-theme-btn",
2182        kind: "instance",
2183        parent: "nav-icons",
2184        data_attrs: &[],
2185        example_html: r#"<button class="nav-theme-btn" type="button" aria-label="…"><svg class="theme-toggle-icon">…</svg></button>"#,
2186        example_markdown: "",
2187        status: Status::Confirmed,
2188        since: "0",
2189        description: "Light/dark toggle in the nav icon cluster. Its glyph is `.theme-toggle-icon`.",
2190    },
2191    ComponentEntry {
2192        class: "theme-toggle-icon",
2193        kind: "instance",
2194        parent: "nav-theme-btn",
2195        data_attrs: &[],
2196        example_html: r#"<svg class="theme-toggle-icon" aria-hidden="true" width="1em" height="1em">…</svg>"#,
2197        example_markdown: "",
2198        status: Status::Confirmed,
2199        since: "0",
2200        description: "The sun/moon glyph. One SVG whose clip path animates between states — restyle it, but do not expect two separate icons to swap.",
2201    },
2202    ComponentEntry {
2203        class: "nav-lang-toggle",
2204        kind: "chrome",
2205        parent: "nav-icons",
2206        data_attrs: &[],
2207        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>"#,
2208        example_markdown: "",
2209        status: Status::Confirmed,
2210        since: "0",
2211        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.",
2212    },
2213    ComponentEntry {
2214        class: "nav-lang-current",
2215        kind: "instance",
2216        parent: "nav-lang-toggle",
2217        data_attrs: &[],
2218        example_html: r#"<span class="nav-lang-current">繁</span>"#,
2219        example_markdown: "",
2220        status: Status::Confirmed,
2221        since: "0",
2222        description: "The edition being viewed, as inert text rather than a link — style the current-language affordance here.",
2223    },
2224    ComponentEntry {
2225        class: "nav-lang-link",
2226        kind: "instance",
2227        parent: "nav-lang-toggle",
2228        data_attrs: &[],
2229        example_html: r#"<a href="/en/" class="nav-lang-link" hreflang="en">EN</a>"#,
2230        example_markdown: "",
2231        status: Status::Confirmed,
2232        since: "0",
2233        description: "A link to another edition of the same page. Carries `hreflang`, so `[hreflang=\"en\"]` is a stable hook for per-language styling.",
2234    },
2235    // The floating nav island (ADR-049) — the small bar that appears when the
2236    // reader scrolls back up past the masthead on a long page. A SECOND object,
2237    // not the masthead re-pinned, which is why it has its own `moss-`-prefixed
2238    // vocabulary. Its trail is the one exception: it deliberately reuses
2239    // `.site-name` / `.breadcrumb-segment` / `.breadcrumb-label` /
2240    // `.breadcrumb-separator` from the masthead above, so a site that restyles
2241    // its breadcrumb restyles both at once.
2242    ComponentEntry {
2243        class: "moss-nav-island",
2244        kind: "chrome",
2245        parent: "",
2246        data_attrs: &[DataAttr {
2247            name: "data-shown",
2248            values: &["false", "true"],
2249            default: "false",
2250            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.",
2251        }],
2252        example_html: r#"<div class="moss-nav-island" data-shown="true"><div class="moss-nav-island-bar">…</div></div>"#,
2253        example_markdown: "",
2254        status: Status::Emerging,
2255        since: "0",
2256        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`.",
2257    },
2258    ComponentEntry {
2259        class: "moss-nav-island-bar",
2260        kind: "instance",
2261        parent: "moss-nav-island",
2262        data_attrs: &[],
2263        example_html: r#"<div class="moss-nav-island-bar">…trail, actions, progress…</div>"#,
2264        example_markdown: "",
2265        status: Status::Emerging,
2266        since: "0",
2267        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.",
2268    },
2269    ComponentEntry {
2270        class: "moss-nav-island-trail",
2271        kind: "instance",
2272        parent: "moss-nav-island-bar",
2273        data_attrs: &[],
2274        example_html: r#"<nav class="moss-nav-island-trail" aria-label="Breadcrumb">…</nav>"#,
2275        example_markdown: "",
2276        status: Status::Emerging,
2277        since: "0",
2278        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.",
2279    },
2280    ComponentEntry {
2281        class: "moss-nav-island-current",
2282        kind: "instance",
2283        parent: "moss-nav-island-trail",
2284        data_attrs: &[],
2285        example_html: r#"<span class="breadcrumb-segment moss-nav-island-current" aria-current="page">末代女礦工</span>"#,
2286        example_markdown: "",
2287        status: Status::Emerging,
2288        since: "0",
2289        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.",
2290    },
2291    ComponentEntry {
2292        class: "moss-nav-island-more",
2293        kind: "instance",
2294        parent: "moss-nav-island-trail",
2295        data_attrs: &[],
2296        example_html: r#"<button class="moss-nav-island-more" aria-expanded="false">…</button>"#,
2297        example_markdown: "",
2298        status: Status::Emerging,
2299        since: "0",
2300        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.",
2301    },
2302    ComponentEntry {
2303        class: "moss-nav-island-actions",
2304        kind: "instance",
2305        parent: "moss-nav-island-bar",
2306        data_attrs: &[],
2307        example_html: r#"<span class="moss-nav-island-actions">…</span>"#,
2308        example_markdown: "",
2309        status: Status::Emerging,
2310        since: "0",
2311        description: "Button cluster at the island's end edge. Holds the sections button only — theme, language and search stay in the masthead.",
2312    },
2313    ComponentEntry {
2314        class: "moss-nav-island-sections",
2315        kind: "instance",
2316        parent: "moss-nav-island-actions",
2317        data_attrs: &[],
2318        example_html: r#"<button class="moss-nav-island-sections" aria-expanded="false"><svg>…</svg></button>"#,
2319        example_markdown: "",
2320        status: Status::Emerging,
2321        since: "0",
2322        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.",
2323    },
2324    ComponentEntry {
2325        class: "moss-nav-island-menu",
2326        kind: "instance",
2327        parent: "moss-nav-island",
2328        data_attrs: &[DataAttr {
2329            name: "data-island-menu",
2330            values: &["levels", "sections"],
2331            default: "levels",
2332            description: "Which of the two menus this is: the folded ancestor levels, or the page's sections.",
2333        }],
2334        example_html: r#"<div class="moss-nav-island-menu" data-island-menu="sections">…</div>"#,
2335        example_markdown: "",
2336        status: Status::Emerging,
2337        since: "0",
2338        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.",
2339    },
2340    ComponentEntry {
2341        class: "moss-breadcrumb-more",
2342        kind: "instance",
2343        parent: "",
2344        data_attrs: &[],
2345        example_html: r#"<button class="moss-breadcrumb-more" aria-expanded="false">…</button>"#,
2346        example_markdown: "",
2347        status: Status::Emerging,
2348        since: "0",
2349        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.",
2350    },
2351    ComponentEntry {
2352        class: "moss-breadcrumb-menu",
2353        kind: "instance",
2354        parent: "",
2355        data_attrs: &[],
2356        example_html: r#"<div class="moss-breadcrumb-menu" hidden>…</div>"#,
2357        example_markdown: "",
2358        status: Status::Emerging,
2359        since: "0",
2360        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`.",
2361    },
2362    ComponentEntry {
2363        class: "moss-nav-island-progress",
2364        kind: "instance",
2365        parent: "moss-nav-island-bar",
2366        data_attrs: &[],
2367        example_html: r#"<span class="moss-nav-island-progress"><span class="moss-nav-island-progress-fill"></span></span>"#,
2368        example_markdown: "",
2369        status: Status::Emerging,
2370        since: "0",
2371        description: "Reading-progress track along the island's own bottom edge — not a separate bar across the window. Currently measures document scroll.",
2372    },
2373    ComponentEntry {
2374        class: "moss-nav-island-progress-fill",
2375        kind: "instance",
2376        parent: "moss-nav-island-progress",
2377        data_attrs: &[],
2378        example_html: r#"<span class="moss-nav-island-progress-fill" style="width: 42%"></span>"#,
2379        example_markdown: "",
2380        status: Status::Emerging,
2381        since: "0",
2382        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.",
2383    },
2384    ComponentEntry {
2385        class: "footer-default",
2386        kind: "chrome",
2387        parent: "",
2388        data_attrs: &[],
2389        example_html: r#"<p class="footer-default"><a href="/rss.xml" class="footer-link" data-external>RSS</a>…</p>"#,
2390        example_markdown: "",
2391        status: Status::Confirmed,
2392        since: "0",
2393        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.",
2394    },
2395    ComponentEntry {
2396        class: "footer-link",
2397        kind: "instance",
2398        parent: "footer-default",
2399        data_attrs: &[],
2400        example_html: r#"<a href="/rss.xml" class="footer-link" data-external>RSS</a>"#,
2401        example_markdown: "",
2402        status: Status::Confirmed,
2403        since: "0",
2404        description: "One link in the generated footer row (RSS and similar). Off-site ones also carry `data-external`.",
2405    },
2406    ComponentEntry {
2407        class: "moss-child-section-divider",
2408        kind: "instance",
2409        parent: "",
2410        data_attrs: &[],
2411        example_html: r#"<hr class="moss-child-section-divider" />"#,
2412        example_markdown: "",
2413        status: Status::Emerging,
2414        since: "0",
2415        description: "Divider rule between auto-generated child sections.",
2416    },
2417    ComponentEntry {
2418        // Source of truth: `crates/moss-core/src/ast/shortcode_extract.rs`
2419        // (the unknown-name branch around line 1282). components_sync_test
2420        // only greps emitter source for `class="moss-..."` literals — this
2421        // class is assembled via `render_div_open`, so a regression here
2422        // will NOT be caught by that test; keep this entry in sync by hand.
2423        class: "moss-unknown-shortcode",
2424        kind: "standalone",
2425        parent: "",
2426        data_attrs: &[DataAttr {
2427            name: "data-name",
2428            values: &[],
2429            default: "",
2430            description: "The unrecognised shortcode name, as written by the author.",
2431        }],
2432        example_html: r#"<div class="moss-unknown-shortcode" data-name="foo">
2433
2434<p>body parsed as markdown</p>
2435
2436</div>"#,
2437        example_markdown: ":::foo\nbody parsed as markdown\n:::",
2438        status: Status::Confirmed,
2439        since: "0",
2440        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.",
2441    },
2442    // -------------------------------------------------------------------
2443    // Syntax highlight tokens (emitted by syntect inside <code>).
2444    // -------------------------------------------------------------------
2445    ComponentEntry {
2446        class: "moss-hl-keyword",
2447        kind: "instance",
2448        parent: "",
2449        data_attrs: &[],
2450        example_html: r#"<span class="moss-hl-keyword">if</span>"#,
2451        example_markdown: "",
2452        status: Status::Emerging,
2453        since: "0",
2454        description: "Syntax-highlight token: keyword.",
2455    },
2456    ComponentEntry {
2457        class: "moss-hl-string",
2458        kind: "instance",
2459        parent: "",
2460        data_attrs: &[],
2461        example_html: r#"<span class="moss-hl-string">"hi"</span>"#,
2462        example_markdown: "",
2463        status: Status::Emerging,
2464        since: "0",
2465        description: "Syntax-highlight token: string literal.",
2466    },
2467    ComponentEntry {
2468        class: "moss-hl-comment",
2469        kind: "instance",
2470        parent: "",
2471        data_attrs: &[],
2472        example_html: r#"<span class="moss-hl-comment">// note</span>"#,
2473        example_markdown: "",
2474        status: Status::Emerging,
2475        since: "0",
2476        description: "Syntax-highlight token: comment.",
2477    },
2478    ComponentEntry {
2479        class: "moss-hl-function",
2480        kind: "instance",
2481        parent: "",
2482        data_attrs: &[],
2483        example_html: r#"<span class="moss-hl-function">render</span>"#,
2484        example_markdown: "",
2485        status: Status::Emerging,
2486        since: "0",
2487        description: "Syntax-highlight token: function name.",
2488    },
2489    ComponentEntry {
2490        class: "moss-hl-type",
2491        kind: "instance",
2492        parent: "",
2493        data_attrs: &[],
2494        example_html: r#"<span class="moss-hl-type">String</span>"#,
2495        example_markdown: "",
2496        status: Status::Emerging,
2497        since: "0",
2498        description: "Syntax-highlight token: type name.",
2499    },
2500    ComponentEntry {
2501        class: "moss-hl-number",
2502        kind: "instance",
2503        parent: "",
2504        data_attrs: &[],
2505        example_html: r#"<span class="moss-hl-number">42</span>"#,
2506        example_markdown: "",
2507        status: Status::Emerging,
2508        since: "0",
2509        description: "Syntax-highlight token: numeric literal.",
2510    },
2511    ComponentEntry {
2512        class: "moss-hl-operator",
2513        kind: "instance",
2514        parent: "",
2515        data_attrs: &[],
2516        example_html: r#"<span class="moss-hl-operator">+</span>"#,
2517        example_markdown: "",
2518        status: Status::Emerging,
2519        since: "0",
2520        description: "Syntax-highlight token: operator.",
2521    },
2522    ComponentEntry {
2523        class: "moss-hl-builtin",
2524        kind: "instance",
2525        parent: "",
2526        data_attrs: &[],
2527        example_html: r#"<span class="moss-hl-builtin">print</span>"#,
2528        example_markdown: "",
2529        status: Status::Emerging,
2530        since: "0",
2531        description: "Syntax-highlight token: builtin identifier.",
2532    },
2533    ComponentEntry {
2534        class: "moss-hl-tag",
2535        kind: "instance",
2536        parent: "",
2537        data_attrs: &[],
2538        example_html: r#"<span class="moss-hl-tag">div</span>"#,
2539        example_markdown: "",
2540        status: Status::Emerging,
2541        since: "0",
2542        description: "Syntax-highlight token: markup tag name.",
2543    },
2544    ComponentEntry {
2545        class: "moss-hl-attr",
2546        kind: "instance",
2547        parent: "",
2548        data_attrs: &[],
2549        example_html: r#"<span class="moss-hl-attr">class</span>"#,
2550        example_markdown: "",
2551        status: Status::Emerging,
2552        since: "0",
2553        description: "Syntax-highlight token: attribute name.",
2554    },
2555    ComponentEntry {
2556        class: "moss-hl-meta",
2557        kind: "instance",
2558        parent: "",
2559        data_attrs: &[],
2560        example_html: r#"<span class="moss-hl-meta">@derive</span>"#,
2561        example_markdown: "",
2562        status: Status::Emerging,
2563        since: "0",
2564        description: "Syntax-highlight token: meta/annotation.",
2565    },
2566    ComponentEntry {
2567        class: "moss-hl-addition-bg",
2568        kind: "instance",
2569        parent: "",
2570        data_attrs: &[],
2571        example_html: r#"<span class="moss-hl-addition-bg">+ added line</span>"#,
2572        example_markdown: "",
2573        status: Status::Emerging,
2574        since: "0",
2575        description: "Syntax-highlight diff token: added-line background.",
2576    },
2577    ComponentEntry {
2578        class: "moss-hl-deletion",
2579        kind: "instance",
2580        parent: "",
2581        data_attrs: &[],
2582        example_html: r#"<span class="moss-hl-deletion">- removed line</span>"#,
2583        example_markdown: "",
2584        status: Status::Emerging,
2585        since: "0",
2586        description: "Syntax-highlight diff token: removed-line text.",
2587    },
2588    ComponentEntry {
2589        class: "moss-hl-deletion-bg",
2590        kind: "instance",
2591        parent: "",
2592        data_attrs: &[],
2593        example_html: r#"<span class="moss-hl-deletion-bg">- removed line</span>"#,
2594        example_markdown: "",
2595        status: Status::Emerging,
2596        since: "0",
2597        description: "Syntax-highlight diff token: removed-line background.",
2598    },
2599    ComponentEntry {
2600        class: "moss-recent",
2601        kind: "container",
2602        parent: "",
2603        data_attrs: &[],
2604        example_html: r#"<ul class="moss-recent">
2605  <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>
2606</ul>"#,
2607        example_markdown: ":::recent {count=5 since=\"2026-01-01\"}\n:::\n",
2608        status: Status::Emerging,
2609        since: "0",
2610        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.",
2611    },
2612    ComponentEntry {
2613        class: "moss-recent__date",
2614        kind: "instance",
2615        parent: "moss-recent",
2616        data_attrs: &[],
2617        example_html: r#"<div class="moss-recent__date">2026-04-12</div>"#,
2618        example_markdown: "",
2619        status: Status::Emerging,
2620        since: "0",
2621        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.",
2622    },
2623    ComponentEntry {
2624        class: "moss-recent__desc",
2625        kind: "instance",
2626        parent: "moss-recent",
2627        data_attrs: &[],
2628        example_html: r#"<div class="moss-recent__desc">A walk through the garden.</div>"#,
2629        example_markdown: "",
2630        status: Status::Emerging,
2631        since: "0",
2632        description: "Per-entry description slot inside `.moss-recent` (BEM child). Sourced from frontmatter `description`; empty when unset.",
2633    },
2634    // -------------------------------------------------------------------
2635    // Ambient loop video — JS-injected wrapper + toggle (§3.5).
2636    // The <video data-loop> synthesizer emits `data-loop` on the <video>;
2637    // ambient-video.ts wraps it at init time.
2638    // -------------------------------------------------------------------
2639    ComponentEntry {
2640        class: "moss-ambient-video",
2641        kind: "standalone",
2642        parent: "",
2643        data_attrs: &[
2644            DataAttr {
2645                name: "data-paused",
2646                values: &[],
2647                default: "",
2648                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.",
2649            },
2650        ],
2651        example_html: r#"<div class="moss-ambient-video">
2652  <video data-loop src="clip.mp4" autoplay muted loop playsinline preload="metadata"></video>
2653  <button class="moss-ambient-toggle" type="button" aria-label="Pause video">⏸</button>
2654</div>"#,
2655        example_markdown: "![[clip.mp4|loop]]",
2656        status: Status::Emerging,
2657        since: "1",
2658        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.",
2659    },
2660    ComponentEntry {
2661        class: "moss-ambient-toggle",
2662        kind: "instance",
2663        parent: "moss-ambient-video",
2664        data_attrs: &[],
2665        example_html: r#"<button class="moss-ambient-toggle" type="button" aria-label="Pause video">⏸</button>"#,
2666        example_markdown: "",
2667        status: Status::Emerging,
2668        since: "1",
2669        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).",
2670    },
2671    // -------------------------------------------------------------------
2672    // LaTeX math (ADR-030). P1 emits the escaped source in a marked
2673    // `<code>`; P2 replaces the element's *contents* with a typeset
2674    // `<svg>` while keeping the class and `data-moss-math` stable, so a
2675    // theme selector written against P1 keeps working across the upgrade.
2676    // -------------------------------------------------------------------
2677    ComponentEntry {
2678        class: "moss-math",
2679        kind: "standalone",
2680        parent: "",
2681        data_attrs: &[
2682            DataAttr {
2683                name: "data-moss-math",
2684                values: &["inline", "display"],
2685                default: "inline",
2686                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.",
2687            },
2688        ],
2689        example_html: r#"<code class="moss-math" data-moss-math="inline">$E = mc^2$</code>"#,
2690        example_markdown: "Energy $E = mc^2$.",
2691        status: Status::Emerging,
2692        since: "1",
2693        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).",
2694    },
2695    ComponentEntry {
2696        class: "moss-math-scroll",
2697        kind: "container",
2698        parent: "",
2699        data_attrs: &[],
2700        example_html: r#"<div class="moss-math-scroll"><svg class="moss-math" data-moss-math="display">…</svg></div>"#,
2701        example_markdown: "$$E = mc^2$$",
2702        status: Status::Emerging,
2703        since: "1",
2704        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%`.",
2705    },
2706    ComponentEntry {
2707        class: "moss-table-scroll",
2708        kind: "container",
2709        parent: "",
2710        data_attrs: &[],
2711        example_html: r#"<div class="moss-table-scroll" tabindex="0">
2712  <table>…</table>
2713</div>"#,
2714        example_markdown: "| Name | Subs |\n| --- | --- |\n| a | 1,200 |",
2715        status: Status::Emerging,
2716        since: "1",
2717        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.",
2718    },
2719    ComponentEntry {
2720        class: "moss-col-right",
2721        kind: "instance",
2722        parent: "moss-table-scroll",
2723        data_attrs: &[],
2724        example_html: r#"<th class="moss-col-right">订阅数</th>
2725<td class="moss-col-right">1,457,776</td>"#,
2726        example_markdown: "| Subs |\n| --: |\n| 1,457,776 |",
2727        status: Status::Emerging,
2728        since: "1",
2729        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`.",
2730    },
2731    ComponentEntry {
2732        class: "moss-col-center",
2733        kind: "instance",
2734        parent: "moss-table-scroll",
2735        data_attrs: &[],
2736        example_html: r#"<th class="moss-col-center">Status</th>
2737<td class="moss-col-center">✓</td>"#,
2738        example_markdown: "| Status |\n| :-: |\n| ✓ |",
2739        status: Status::Emerging,
2740        since: "1",
2741        description: "Center-aligned table cell (`<th>`/`<td>`). Applied to a whole column the author center-aligned in GFM (`|:-:|`).",
2742    },
2743    ComponentEntry {
2744        class: "moss-search",
2745        kind: "chrome",
2746        parent: "",
2747        data_attrs: &[],
2748        example_html: r#"<div class="moss-search" id="moss-search" hidden>
2749  <div class="moss-search__backdrop"></div>
2750  <div class="moss-search__panel" role="dialog" aria-modal="true" aria-label="Search">
2751    <div class="moss-search__field"><input class="moss-search__input" role="combobox"></div>
2752    <div class="moss-search__progress" hidden></div>
2753    <div class="moss-search__seam"></div>
2754    <div class="moss-search__body">
2755      <p class="moss-search__status" role="status" hidden></p>
2756      <ul class="moss-search__results" role="listbox">
2757        <li class="moss-search__row">
2758          <a class="moss-search__link" role="option" href="/posts/foo/">
2759            <span class="moss-search__title">Title</span>
2760            <span class="moss-search__excerpt">…a <mark>match</mark>…</span>
2761          </a>
2762        </li>
2763      </ul>
2764    </div>
2765  </div>
2766</div>"#,
2767        example_markdown: "",
2768        status: Status::Emerging,
2769        since: "1",
2770        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.",
2771    },
2772    ComponentEntry {
2773        class: "moss-footnotes",
2774        kind: "container",
2775        parent: "",
2776        data_attrs: &[],
2777        example_html: r##"<section class="moss-footnotes" role="doc-endnotes">
2778<ol>
2779<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>
2780</li>
2781</ol>
2782</section>"##,
2783        example_markdown: "Text[^1].\n\n[^1]: The note.",
2784        status: Status::Emerging,
2785        since: "1",
2786        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.",
2787    },
2788    ComponentEntry {
2789        class: "moss-footnote-ref",
2790        kind: "instance",
2791        parent: "moss-footnotes",
2792        data_attrs: &[],
2793        example_html: r##"<sup class="moss-footnote-ref" id="fnref-1"><a href="#fn-1" role="doc-noteref">1</a></sup>"##,
2794        example_markdown: "Text[^1].\n\n[^1]: The note.",
2795        status: Status::Emerging,
2796        since: "1",
2797        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.",
2798    },
2799    ComponentEntry {
2800        class: "moss-footnote-backref",
2801        kind: "instance",
2802        parent: "moss-footnotes",
2803        data_attrs: &[],
2804        example_html: r##"<a class="moss-footnote-backref" href="#fnref-1" role="doc-backlink" aria-label="Back to reference 1">&#8617;&#xFE0E;</a>"##,
2805        example_markdown: "Text[^1].\n\n[^1]: The note.",
2806        status: Status::Emerging,
2807        since: "1",
2808        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.",
2809    },
2810];
2811
2812/// Implementation classes that are emitted by moss for internal functionality
2813/// but must not appear in the public theme-facing contract (`moss describe` /
2814/// `docs/reference/contract.md`). These classes ARE present in `COMPONENTS` for
2815/// the sync-test to validate their HTML class literals, but `is_public()` hides
2816/// them from agents, themes, and `reference.md` generation.
2817const INTERNAL_CLASSES: &[&str] = &[
2818    "moss-apply",
2819    "moss-apply-form",
2820    "moss-apply-matters",
2821    "moss-apply-hp",
2822    "moss-apply-status",
2823    "moss-apply-helper",
2824];
2825
2826impl ComponentEntry {
2827    /// True for entries that belong in the public, agent/theme-facing surface.
2828    /// v1 rule: not retired AND not an internal implementation class.
2829    ///
2830    /// Internal classes (e.g. all `moss-apply*`) stay in COMPONENTS so the
2831    /// sync-test can validate them, but they must not surface in `moss describe`
2832    /// or `docs/reference/contract.md` — they are subject to change at any time.
2833    pub fn is_public(&self) -> bool {
2834        self.status != Status::Retired && !INTERNAL_CLASSES.contains(&self.class)
2835    }
2836}
2837
2838/// Iterator over class names with `Status::Retired`. Used by the build
2839/// pipeline's theme lint to warn users about pre-v1 vocabulary.
2840///
2841/// Exposed as an iterator over `&'static str` so callers don't need to
2842/// import the `Status` enum (keeps moss-core's surface narrow).
2843pub fn retired_class_names() -> impl Iterator<Item = &'static str> {
2844    COMPONENTS.iter()
2845        .filter(|e| e.status == Status::Retired)
2846        .map(|e| e.class)
2847}
2848
2849#[cfg(test)]
2850mod tests {
2851    use super::*;
2852
2853    /// Orphan-gate: every class in `INTERNAL_CLASSES` must exist as a `class`
2854    /// in `COMPONENTS`. If a class is renamed in the emitter *and* in
2855    /// `INTERNAL_CLASSES` but forgotten in `COMPONENTS`, it would silently
2856    /// re-enter the public contract surface (`is_public()` only hides known
2857    /// internals). This test prevents that gap.
2858    #[test]
2859    fn every_internal_class_has_a_components_entry() {
2860        let component_classes: std::collections::HashSet<&'static str> =
2861            COMPONENTS.iter().map(|e| e.class).collect();
2862        for &internal in INTERNAL_CLASSES {
2863            assert!(
2864                component_classes.contains(internal),
2865                "INTERNAL_CLASSES entry '{}' has no matching entry in COMPONENTS — \
2866                 add a ComponentEntry for it or remove it from INTERNAL_CLASSES",
2867                internal
2868            );
2869        }
2870    }
2871}