Skip to main content

moss_core/contract/
components.rs

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