@omega.js/desktop 0.53.0 → 0.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +38 -38
  2. package/dist/cli-run.js +4 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/cdp/client.js +1 -1
  5. package/dist/commands/cdp.js +1 -1
  6. package/dist/commands/clean.js +2 -3
  7. package/dist/commands/dev.js +25 -0
  8. package/dist/commands/lib/ensure-target.js +12 -17
  9. package/dist/commands/lib/migrate.js +17 -0
  10. package/dist/commands/logs.js +1 -1
  11. package/dist/commands/release.js +1 -1
  12. package/dist/commands/test.js +4 -4
  13. package/dist/commands/update.js +5 -4
  14. package/dist/defaults/.github/workflows/build.yml +18 -18
  15. package/dist/defaults/_.gitignore +0 -2
  16. package/dist/defaults/_mas/README.md +3 -3
  17. package/dist/defaults/config/certs/README.md +1 -1
  18. package/dist/defaults/config/omega.json5 +36 -36
  19. package/dist/defaults/docs/README.md +3 -3
  20. package/dist/defaults/gulpfile.js +1 -1
  21. package/dist/defaults/hooks/build/post.js +1 -1
  22. package/dist/defaults/hooks/build/pre.js +1 -1
  23. package/dist/defaults/hooks/notarize/post.js +2 -2
  24. package/dist/defaults/hooks/release/post.js +1 -1
  25. package/dist/defaults/hooks/release/pre.js +1 -1
  26. package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
  27. package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
  28. package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
  29. package/dist/defaults/src/integrations/context-menu/index.js +11 -11
  30. package/dist/defaults/src/integrations/menu/index.js +5 -5
  31. package/dist/defaults/src/integrations/tray/index.js +9 -9
  32. package/dist/defaults/src/main.js +2 -2
  33. package/dist/defaults/src/preload.js +1 -1
  34. package/dist/defaults/test/README.md +3 -3
  35. package/dist/defaults/test/_init.js +1 -1
  36. package/dist/gulp/tasks/audit.js +5 -8
  37. package/dist/lib/restart-manager/index.js +1 -1
  38. package/dist/lib/restart-manager/install.js +1 -1
  39. package/dist/lib/restart-manager/protocol.js +1 -1
  40. package/dist/main.js +4 -3
  41. package/dist/preload.js +1 -1
  42. package/dist/test/suites/build/audit.test.js +20 -7
  43. package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
  44. package/dist/test/suites/build/cli.test.js +28 -0
  45. package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
  46. package/dist/test/suites/build/defaults-scaffold.test.js +19 -5
  47. package/dist/test/suites/build/deploy-direct.test.js +7 -5
  48. package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
  49. package/dist/test/suites/build/deploy-hook.test.js +4 -2
  50. package/dist/test/suites/build/dev-verb.test.js +67 -0
  51. package/dist/test/suites/build/ensure-target.test.js +11 -3
  52. package/dist/test/suites/build/merge-line-files.test.js +6 -6
  53. package/dist/test/suites/build/migrate.test.js +29 -0
  54. package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
  55. package/dist/test/suites/build/runner-env-write.test.js +73 -0
  56. package/dist/test/suites/build/runner.test.js +9 -8
  57. package/dist/test/suites/build/setup-scripts.test.js +27 -0
  58. package/dist/test/suites/build/validate-config.test.js +13 -2
  59. package/dist/test/suites/build/verb-logs.test.js +20 -0
  60. package/dist/test/suites/renderer/window-desktop-surface.test.js +1 -1
  61. package/dist/utils/build-pipeline.js +4 -4
  62. package/dist/utils/runner-env.js +13 -28
  63. package/dist/vendor/config/company.js +46 -14
  64. package/dist/vendor/config/defaults.js +30 -7
  65. package/dist/vendor/config/edit.js +25 -3
  66. package/dist/vendor/config/env-delivery.js +1 -1
  67. package/dist/vendor/config/env-schema.js +3 -6
  68. package/dist/vendor/config/env.js +34 -22
  69. package/dist/vendor/config/index.js +13 -17
  70. package/dist/vendor/config/load.js +15 -7
  71. package/dist/vendor/config/repo.js +10 -27
  72. package/dist/vendor/config/schema-client.js +64 -0
  73. package/dist/vendor/config/schema-cloud.js +38 -0
  74. package/dist/vendor/config/schema-manager.js +118 -0
  75. package/dist/vendor/config/schema-overrides.js +68 -0
  76. package/dist/vendor/config/schema.js +99 -152
  77. package/dist/vendor/config/validate.js +97 -77
  78. package/dist/vendor/devkit/agents-md.js +233 -0
  79. package/dist/vendor/devkit/attach-log-file.js +15 -1
  80. package/dist/vendor/devkit/ci-workflows.js +30 -30
  81. package/dist/vendor/devkit/cli-router.js +13 -7
  82. package/dist/vendor/devkit/defaults-engine.js +9 -43
  83. package/dist/vendor/devkit/deploy-snapshot.js +44 -9
  84. package/dist/vendor/devkit/env-lines.js +183 -0
  85. package/dist/vendor/devkit/local.js +62 -10
  86. package/dist/vendor/devkit/lockfile.js +32 -13
  87. package/dist/vendor/devkit/logger.js +7 -2
  88. package/dist/vendor/devkit/merge-line-files.js +219 -176
  89. package/dist/vendor/devkit/omega-bin.js +208 -111
  90. package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
  91. package/dist/vendor/devkit/preludes/index.js +1 -0
  92. package/dist/vendor/devkit/target-picker.js +45 -0
  93. package/dist/vendor/devkit/test/dashed-files.js +37 -0
  94. package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
  95. package/dist/vendor/devkit/update.js +15 -15
  96. package/dist/vendor/devkit/verb-scripts.js +40 -0
  97. package/dist/vendor/devkit/verbs.js +170 -0
  98. package/package.json +18 -24
  99. package/dist/commands/install.js +0 -37
  100. package/dist/defaults/AGENTS.md +0 -119
  101. package/dist/defaults/CLAUDE.md +0 -1
  102. package/dist/vendor/config/env-retired.js +0 -137
  103. package/dist/vendor/config/retired-keys.js +0 -635
  104. package/docs/analytics.md +0 -140
  105. package/docs/app-state.md +0 -92
  106. package/docs/audit.md +0 -69
  107. package/docs/auth.md +0 -284
  108. package/docs/auto-updater.md +0 -243
  109. package/docs/boot-sequence.md +0 -44
  110. package/docs/build-system.md +0 -169
  111. package/docs/cdp-debugging.md +0 -169
  112. package/docs/common-mistakes.md +0 -21
  113. package/docs/config-schema.md +0 -120
  114. package/docs/context-menu.md +0 -112
  115. package/docs/context.md +0 -81
  116. package/docs/css.md +0 -84
  117. package/docs/deep-link.md +0 -186
  118. package/docs/environment-detection.md +0 -112
  119. package/docs/fontawesome.md +0 -109
  120. package/docs/hooks.md +0 -89
  121. package/docs/icons.md +0 -79
  122. package/docs/index.md +0 -328
  123. package/docs/installer-options.md +0 -165
  124. package/docs/ipc.md +0 -61
  125. package/docs/lib-modules.md +0 -53
  126. package/docs/logging.md +0 -227
  127. package/docs/menu.md +0 -160
  128. package/docs/releasing.md +0 -239
  129. package/docs/remote-config.md +0 -118
  130. package/docs/remote-scripts.md +0 -144
  131. package/docs/restart-manager.md +0 -144
  132. package/docs/runner.md +0 -290
  133. package/docs/sentry.md +0 -97
  134. package/docs/shared/agent-docs.md +0 -89
  135. package/docs/shared/analytics.md +0 -612
  136. package/docs/shared/brands.md +0 -57
  137. package/docs/shared/breaking-changes.md +0 -917
  138. package/docs/shared/config.md +0 -1948
  139. package/docs/shared/deploys.md +0 -341
  140. package/docs/shared/icons.md +0 -219
  141. package/docs/shared/local-dev.md +0 -167
  142. package/docs/shared/logging.md +0 -205
  143. package/docs/shared/monitoring.md +0 -167
  144. package/docs/shared/publishing.md +0 -187
  145. package/docs/shared/rulings.md +0 -34
  146. package/docs/shared/testing.md +0 -147
  147. package/docs/shared/theming.md +0 -629
  148. package/docs/shared/translation.md +0 -342
  149. package/docs/shared/updates.md +0 -61
  150. package/docs/signing.md +0 -293
  151. package/docs/startup.md +0 -142
  152. package/docs/storage.md +0 -59
  153. package/docs/templating.md +0 -101
  154. package/docs/test-boot-layer.md +0 -157
  155. package/docs/test-framework.md +0 -362
  156. package/docs/themes.md +0 -149
  157. package/docs/tooltips.md +0 -99
  158. package/docs/tray.md +0 -164
  159. package/docs/usage.md +0 -58
  160. package/docs/verts.md +0 -62
  161. package/docs/windows.md +0 -149
@@ -1,629 +0,0 @@
1
- # Theming — the OMEGA design system contract
2
-
3
- > classy v2 (the flagship skin) + the shared machinery every theme and, at C4,
4
- > every target rides. Visual spec: [docs/web/classy-v2/DIRECTION.md](../web/classy-v2/DIRECTION.md).
5
-
6
- The one-hex/one-token half of this contract is checkable per edit: the plugin's `omega:brandcheck` skill,
7
- [agent-plugins/claude/skills/brandcheck/SKILL.md](../../agent-plugins/claude/skills/brandcheck/SKILL.md), and
8
- its quality hook fires on every stylesheet edit (contrast, focus, and reduced-motion checks ride `omega:accessibility`).
9
-
10
- ## The three layers
11
-
12
- 1. **Tokens** — `packages/web/core/css/tokens/_index.scss` emits the
13
- `--omega-*` custom-property contract: neutrals (`ground`, `surface`,
14
- `surface-2`, `ink`, `ink-muted`, `ink-faint`, `line`, `line-strong`), the
15
- accent family (`accent`, `-hover`, `-active`, `-subtle`, `-ink`, `-ring`),
16
- status (`ok`/`warn`/`danger`, each with an `-rgb` channel twin — see
17
- "One status hue site-wide" below), the categorical ramp (`chart-1`…`chart-6` —
18
- series-1 rides the accent, the rest are CVD-validated muted hues, cool half
19
- before warm), shadows
20
- (`shadow-1`/`shadow-2`), shape (`radius-xs/s/m/l/xl`), motion (`speed`,
21
- `speed-slow`, `speed-first-paint`, `ease`), and the type slots (`font-ui`, `font-serif`,
22
- `font-mono`, plus the pairing slots `font-marketing` and `font-display`).
23
- Light + dark values ship together: OS preference carries via
24
- `@media (prefers-color-scheme: dark)`, and the `data-bs-theme` stamp
25
- (core/js/core/appearance.js) beats it in both directions. **Names are the
26
- stable API; values are the skin.**
27
- 2. **Mechanics** — `core/css/shell/_index.scss` (the `.omega-shell` app
28
- chrome: 264px sidebar / 68px rail / 60px topbar, drawer under 1200px; the
29
- rail clips nothing — its nav scrolls inside the `__sidebar-scroll` region
30
- so rail popovers can escape, [#319](https://github.com/Omega-JS-Stack/omega/issues/319);
31
- main claims the `1fr` row explicitly, so a page that turns the topbar off
32
- still scrolls inside main instead of handing the scroll back to the
33
- document, [#741](https://github.com/Omega-JS-Stack/omega/issues/741)),
34
- `core/css/motion/_index.scss` (below), and `core/css/components/_index.scss`
35
- (the shared component vocabulary: `omega-chip` and its `--accent`/`--ink`
36
- modifiers, plus the app-chrome pair the base topbar/sidebar renders —
37
- `.btn-icon`, the square ghost icon button, and `.omega-search`, the ⌘K field
38
- with its input and its `kbd` shortcut badge). All three paint exclusively through tokens so a skin restyles
39
- them without touching structure, and all three emit BEFORE the theme so a
40
- skin's own rules win. **Promotion rule
41
- ([#242](https://github.com/Omega-JS-Stack/omega/issues/242))**: a component
42
- the BASE layer renders belongs in the component sheet, never in one theme's
43
- partials — the icon-CSS ruling (presentation SSOT in the shared sheet).
44
- Theme-only components (classy's marketing vocabulary) stay in their theme.
45
- 3. **Theme** — `themes/<id>/`, a skin over `themes/base` (the structural
46
- `omega-*` markup layer every theme chain terminates at; classy is the
47
- flagship and the default skin). classy v2's `css/base/_root.scss` bridges Bootstrap's CSS
48
- variables onto the tokens, so every Bootstrap component follows the theme,
49
- the brand ramp, and consumer overrides with zero recompilation.
50
-
51
- ## The three surface tiers (and which way elevation runs)
52
-
53
- `--omega-ground` is the page, `--omega-surface` is a card lifted off it, and
54
- `--omega-surface-2` is the second tier: table heads, chips, code blocks, hover
55
- fills, receipt panels, segmented tracks. The tiers move in OPPOSITE directions
56
- per mode, and a component may not assume one of them: dark elevates by getting
57
- LIGHTER (`#0d0d0e` ground → `#151516` surface → `#1d1d1f` surface-2), light
58
- elevates with white plus a shadow and recesses by getting DARKER (`#f5f5f4`
59
- ground → `#ffffff` surface → `#ececeb` surface-2, the well).
60
-
61
- Light surface-2 shipped at `#f6f6f5`, one point off the ground, so every
62
- component that painted it directly on the page was invisible in light mode and
63
- correct in dark ([#152](https://github.com/Omega-JS-Stack/omega/issues/152)).
64
- The value is now a real well: readable on the ground AND inside a white card,
65
- still lighter than `--omega-line`. Its Bootstrap twins move with it:
66
- `$classy-surface-2-light` (compile-time `$body-secondary-bg`) and
67
- `--bs-secondary-bg-rgb` in classy's root bridge.
68
-
69
- Two readings of a segmented control are both sanctioned, and both work in both
70
- modes: a RAISED track (surface + `--omega-line-strong` + `shadow-1`, selected
71
- segment recessed to surface-2, the billing toggle) or a RECESSED track
72
- (surface-2 straight on the ground, selected segment raised to white, the
73
- platform rail).
74
-
75
- ## brand.color → the accent ramps
76
-
77
- `omega.json5 → brand.color` is an optional hex string (`#RGB` / `#RRGGBB`,
78
- schema-validated since [#272](https://github.com/Omega-JS-Stack/omega/issues/272);
79
- unset leaves the token sheet's neutral placeholder standing) and it drives
80
- `composeBrandTokens()` (`packages/devkit/src/brand-tokens.js`): a light ramp plus a **dark-mode variant**
81
- (darker brands lift into a legible lightness band for the charcoal ground;
82
- already-light brands pass through). `core/_includes/core/head.html` emits both
83
- as inline `:root` blocks AFTER the css bundles — same three-stamp plumbing as
84
- the token sheet — so the ramp wins the cascade everywhere, including
85
- `.btn-primary`, links, focus rings, `.form-check-input:checked`,
86
- `.progress-bar`, and `.text-primary`/`.bg-primary` (classy re-points those at
87
- the tokens).
88
-
89
- ONE hex, three surfaces ([#912](https://github.com/Omega-JS-Stack/omega/issues/912)).
90
- Desktop and the extension have no `<head>` of their own to inline into, so their
91
- sass tasks write the SAME ramp to a generated partial before every compile:
92
- `<dist>/assets/scss/_brand.scss` on desktop, `<dist>/assets/css/_brand.scss` on
93
- the extension, both rendered by `renderBrandScss()` beside the ramp math. The
94
- partial carries `$primary` (the compile-time accent Bootstrap's color ramp
95
- derives from) plus a `ramp` mixin holding the runtime `--omega-accent` family,
96
- and the scaffold's `main.scss` reads both: `@use 'brand'` above the framework
97
- import for the variable, `@include brand.ramp;` below it for the css. The mixin
98
- is why the include sits below: a used module's css is emitted at its LOAD
99
- position, which is ahead of the token sheet and theme it has to beat, so a
100
- root-level rule in the partial would lose to the very placeholders it replaces.
101
- A brand that WANTS a different accent from `brand.color` puts a literal back in
102
- the `with (...)` block, in place of `brand.$primary`. No `brand.color` at all,
103
- and the renderer's own fallback (`#2563EB`, the hex the classy theme declares
104
- with `!default`) is what compiles.
105
-
106
- ## Consumer customization (tier 1 — main.scss)
107
-
108
- - **Discovery**: `omega customize --list` prints the layered override map —
109
- every shadowable file (sections, includes, css, pages), its owning layer
110
- (framework / `theme:<id>` / consumer), and whether you already shadow it.
111
- `omega customize <path>` materializes ONE of them at that path with a
112
- provenance header; reading the theme source tree to guess a path is over.
113
- The css lane lists only what sass actually layers by — `main.scss` and the
114
- page sheets — because a bare relative `@use`/`@import` inside a theme sheet
115
- resolves against the importing file, so a consumer copy of a partial would
116
- never load.
117
- - **Recolor**: set `brand.color` in omega.json5. No CSS.
118
- - **Sass knobs**: every variable in `themes/classy/_config.scss` is `!default`
119
- — `@use 'omega:main' with ($primary: …, $font-family-sans-serif: …,
120
- $border-radius: …)` from the consumer main.scss.
121
- - **Token overrides**: redefine any `--omega-*` property in `:root` /
122
- `[data-bs-theme='dark']` for surgical re-vibing (grays, radii, speeds,
123
- shadows, type).
124
- - **Type presets**: stamp `data-omega-type="sans|serif|mono"` on `<html>`
125
- (via `theme.html.attributes`) or re-point `--omega-font-marketing`
126
- / `--omega-font-display` directly. Default is the mix pairing — serif
127
- marketing display over sans UI.
128
- - **Fonts (D5, shipped)**: classy vendors **Inter** (UI grotesk) and
129
- **Newsreader** (marketing serif); newsflash vendors **Schibsted Grotesk**
130
- and **Fraunces** the same way (cp187 — its Google Fonts CDN link is dead) —
131
- variable woff2, latin + latin-ext, OFL —
132
- in `themes/<theme>/fonts/`; the asset pipeline copies every layer's `fonts/`
133
- dir to `/assets/fonts` (first layer wins, and a consumer-local theme is
134
- followed by the packaged theme it SHADOWS — inheriting that skin's sheet
135
- through the hatch inherits the files its `@font-face` rules point at,
136
- [#773](https://github.com/Omega-JS-Stack/omega/issues/773)), `css/base/_fonts.scss` carries
137
- the `@font-face` blocks (`font-display: swap`), and `css/base/_root.scss`
138
- re-points `--omega-font-ui` / `--omega-font-serif` at them. The core token
139
- sheet keeps system stacks as the framework default AND the fallback tail —
140
- swapping faces stays a values-only change (two custom properties + the
141
- files). Ordering note: core main.scss MUST load the token sheet before the
142
- theme `@forward` (Sass emits a module's CSS at its first load), or theme
143
- token overrides lose the cascade. The head PRELOADS the first-paint faces,
144
- and the list is read off those `@font-face` rules in the compiled sheet
145
- ([#765](https://github.com/Omega-JS-Stack/omega/issues/765)) — a face with no
146
- `unicode-range`, or one whose range reaches basic latin (U+0000-00FF), is a
147
- first-paint face; the `-latin-ext` subsets stay out. So a theme naming its
148
- files its own way, or vendoring ONE variable face, preloads correctly with
149
- nothing to declare: the declaration IS the `@font-face`
150
- ([docs/web/index.md](../web/index.md)).
151
- And the face that paints BEFORE it lands is metric-matched
152
- ([#768](https://github.com/Omega-JS-Stack/omega/issues/768)): the css lane
153
- measures each vendored family's own font file, measures the system family the
154
- theme's stack already names next, and generates
155
- `@font-face { font-family: '<Family> Fallback'; src: local('<system family>'), ...;
156
- size-adjust; ascent-override; descent-override; line-gap-override }` (one
157
- `local()` per measured family the stack names, so the face resolves on macOS
158
- and Windows alike; the overrides follow the first), then
159
- names that face right after the web family in the `--omega-font-*` stack. The
160
- system font occupies the same lines the webfont will, so `font-display: swap`
161
- moves nothing (classy: Inter over `Helvetica Neue` at size-adjust 105.508%,
162
- Newsreader over `georgia` at 91.224%). A theme gets this for FREE: the
163
- transform reads the compiled sheet, so a theme's own faces and its own stacks
164
- are all it has to declare, and no scss file names a fallback family. What a
165
- theme owes is the tail: a stack that reaches none of the measured system
166
- families (Arial, Helvetica, Helvetica Neue, Georgia, Times New Roman,
167
- Verdana) gets no fallback face and one build warning naming the family.
168
-
169
- Tier 2 stays: fork `themes/_template` for a full theme; `themes/base` remains
170
- the fall-through layer for anything the theme doesn't cover, and base's
171
- `_theme.scss` forwards classy's theme as the default skin css. The tier is
172
- PROVEN in a real build by the brand-shape corpus's `shape-theme-override` cell
173
- ([#773](https://github.com/Omega-JS-Stack/omega/issues/773)): a consumer-local
174
- theme shadows the packaged one of the same id, and every file kind the cascade
175
- resolves — layout, include, the scss entry, a section's html, its own section
176
- js, and a lane left to the chain with `inherit: ['js']` — is read back out of
177
- `dist/` ([testing.md](testing.md)).
178
-
179
- ### CSS fall-through — the two lanes (cp190, closes the audit's asymmetry)
180
-
181
- Layouts/includes fall through per-file, but a theme's `_theme.scss` never
182
- did — a non-classy theme rendered the shared `omega-*` vocabulary
183
- unstyled on fall-through pages. Two blessed lanes now cover it, both
184
- proven:
185
-
186
- - **Partial/consumer themes** (no Bootstrap of their own): put
187
- `@forward 'omega:theme';` at the top of the theme's `_theme.scss` — the
188
- importer resolves through the layer roots and SELF-SKIPS the requesting
189
- file, so the forward lands on base's `_theme.scss` (a bridge forwarding
190
- classy's theme as the default skin css) and emits classy's whole
191
- chain (Bootstrap included, configured through the forward); the theme's
192
- own rules land after and win the cascade. Pinned in
193
- `test/themes.test.js` ("inheritance hatch").
194
- - **Full sibling themes** (own Bootstrap config — newsflash): do NOT
195
- inherit wholesale (two Bootstraps); import classy's app/auth partials
196
- directly as the vocabulary FLOOR — they are deliberately TOKEN-PURE
197
- (zero Sass config coupling), so they paint through the importing theme's
198
- token re-values. The set: `layout/shell` (+ `.page-header`),
199
- `layout/footer` (the shared footer include speaks `omega-footer`
200
- vocabulary on every page — the floor supplies structure, the theme
201
- re-inks it), `app/panels` (table/statgrid/iconbtn/count), `pages/auth`,
202
- `components/receipt`, `components/badges` (dot-status; the chip left for the
203
- core component sheet in #242, so every theme gets it with no floor at all). Import
204
- EARLY (the floor sits UNDER the theme's voice, so later theme rules win
205
- collisions like classy's `.badge` base). Live models:
206
- `themes/newsflash/_theme.scss`, `themes/neobrutalism/_theme.scss`.
207
- NOT in the floor and never needed there: `components/buttons` and
208
- `components/forms` are Sass-config coupled, so the two app-chrome pieces they
209
- used to hide — `.btn-icon` and `.omega-search` — moved to the core component
210
- sheet in [#303](https://github.com/Omega-JS-Stack/omega/issues/303). A sibling
211
- theme that skipped them rendered a native UA button box beside the breadcrumb
212
- and a native search field with reboot's inverted `kbd` (a solid cream slab on
213
- a dark skin). Anything else a base surface renders takes the same route: the
214
- core sheet, never a widened floor.
215
-
216
- **The guard ([#98](https://github.com/Omega-JS-Stack/omega/issues/98))**: the web
217
- asset lane reads the COMPILED main bundle for three sentinels the two lanes both
218
- guarantee — `.omega-auth`, `.omega-statgrid`, `.omega-footer`, one per
219
- fall-through surface (the other floor partials ride along unsentineled) — and a
220
- non-classy theme missing ANY of them gets one loud build WARNING (never a failure)
221
- naming the theme, the missing piece (hatch vs floor — decided by whether the
222
- bundle defines `--bs-body-bg`, i.e. whether the theme ships its own Bootstrap),
223
- and the exact line to add. `packages/web/src/theme-vocabulary.js`; every bundled
224
- theme is silent, pinned in `test/themes.test.js` ("fall-through guard").
225
-
226
- Rule for classy authors: app/auth vocabulary partials MUST stay token-pure
227
- — a Sass config dependency there breaks every sibling theme's floor.
228
-
229
- ## Content-page vocabulary (default pages + blueprints)
230
-
231
- Every classy frontend default page composes from one shared set
232
- (`themes/classy/css/marketing/_content.scss` + the existing section/bento
233
- vocab): `omega-page-hero` (+ `omega-display--page`) opener, `omega-prose`
234
- long-form (blog posts, legal md, bios), `omega-timeline`, `omega-post-card`
235
- (the ONE blog card — `_includes/frontend/components/post-card.html`, shared
236
- by index/related/category/tag grids), `omega-person`, `omega-facts`
237
- (hairline-divided columns; `__value--num` serif numerals, `__sub` footnote),
238
- `omega-chip-cloud`, `omega-blog-search`, and token-driven `.pagination`.
239
- Consumer frontmatter keys are unchanged — pages re-rendered, data contracts
240
- kept.
241
-
242
- **Compositional set (cp147 — pages are COMPOSED, not centered)**:
243
- `omega-hero-split` (asymmetric opener: statement + side rail),
244
- `omega-duo` (label/head column + body column, sticky aside; column split
245
- overridable via `--omega-duo-cols`), `omega-statement` (editorial letter
246
- text — big serif with italic `<em>`), `omega-numbered` (principles list
247
- with serif italic indices), `omega-channel` (contact/support rows),
248
- `omega-band` (one wide hairline row — enterprise, platform strips),
249
- `omega-form-panel` (the hairline container every long form sits in), and
250
- the `.omega-quiet` fine-print voice.
251
-
252
- **Post media + author contracts** (post-card AND the post page honor them):
253
- `post.image: false` → designed no-media panel (serif italic category
254
- monogram on dotgrid), never a 404 `<img>`; `post.image: "<path>"` → that
255
- image lazy-loaded; absent → the legacy `/assets/images/blog/post-<id>/`
256
- convention. An author that resolves to no team member renders a neutral
257
- pen-nib mark + the brand name (no broken avatar rows).
258
-
259
- **Footer pattern** (`frontend/sections/footer.html` + `css/layout/_footer.scss`,
260
- draft-2): brand block (lockup + one-liner + social icon row) beside auto-fit
261
- link columns; ONE hairline base row where copyright, legal links, the
262
- language dropup pill, and the segmented appearance control all share the
263
- same 1.75rem scale. The appearance segments are plain `data-appearance-set`
264
- buttons — core appearance.js stamps `.active`/`aria-pressed`. The brand
265
- lockup class (`.omega-nav__brand`) is root-scoped and shared by nav +
266
- footer.
267
-
268
- **Per-page theme css slot**: `themes/<theme>/css/pages/<page>/index.scss`
269
- compiles to its own bundle and loads after main css AND core's page sheet — one
270
- rule, every layer's sheet in layer order ([#624](https://github.com/Omega-JS-Stack/omega/issues/624)). That's where a theme outranks core page
271
- rules; classy's `status` and `feedback` entries repaint the JS-toggled `bg-*`
272
- state classes (a class contract — never rename them) into the hairline
273
- language there.
274
-
275
- ## App panels (App DNA — dashboard/admin content)
276
-
277
- `themes/classy/css/app/_panels.scss`: `omega-statgrid` (the DIRECTION
278
- merged stat card — ONE card, hairline column dividers, micro-label +
279
- tabular value + delta chip per cell; it sizes off ITS OWN width, never the
280
- viewport — `--omega-statgrid-cols` sets the column CEILING and cells reflow
281
- below a 10rem floor, so a statgrid nested in a half-width card wraps instead
282
- of overflowing), `omega-panel-title` (the 14/650 card-title voice), and
283
- `omega-activity` (hairline-divided feed rows with neutral icon chips).
284
- The backend dashboard layout and the admin dashboard/users blueprints
285
- render on these; every JS-populated id (`stat-*`, tables, charts) is a
286
- contract and stays.
287
-
288
- ## Categorical tones + the interactive affordance (#72)
289
-
290
- Two shared utility sets in `core/css` — core, not classy, so every theme
291
- inherits them:
292
-
293
- - **Tones** (`core/css/core/_tones.scss`): `.omega-tone-1`…`.omega-tone-6` set
294
- `--omega-tone` to the matching `--omega-chart-*` slot, and
295
- `.omega-badge-tone` paints a chip with it (`color-mix` tint, no
296
- uppercasing — a tone label is an identifier, not a word). ONE ramp for two
297
- surfaces: a chart series and the badge naming the same thing are the same
298
- color. Which name falls in which slot is the page's business. The sheet
299
- loads after the theme forward, so it outranks a theme's own chip rules.
300
- - **`.omega-interactive`** (`core/css/motion/_index.scss`): the whole-surface
301
- click affordance — the surface warms on hover AND `:focus-visible`, an
302
- accent ring on focus, an accent-subtle tint on press. `--lift` adds the
303
- card lift (2px up, shadow-2) that presses back down; a row takes the base
304
- class alone. `prefers-reduced-motion` drops the transition and the lift, and
305
- classy drops the lift outright (see "classy never raises on hover" below).
306
-
307
- Charts read the same ramp: `core/js/libs/charts.js` (`chartColors()`) resolves
308
- `--omega-chart-1…6` off `:root` and hands them to the chart definition as its
309
- theme palette, so a chart follows the brand ramp and dark mode without knowing
310
- either exists. A surface that means a STATUS passes the
311
- status tokens explicitly instead (`colors: ['var(--omega-ok)', …]` — the
312
- helper's `resolveColor` reads them off the live sheet); the admin dashboard's
313
- plan doughnut is the reference case ([#74](https://github.com/Omega-JS-Stack/omega/issues/74)).
314
-
315
- Graphs read the same ramp again: `core/js/libs/graph.js` (`graphTheme()`) maps
316
- `--omega-chart-1…6` onto mermaid's per-item slots (`cScale0…5` and `pie1…6`),
317
- and the rest of a diagram off `--omega-ink`, `--omega-surface`/`--omega-surface-2`,
318
- `--omega-line` and `--omega-accent` — the accent is the node outline, the one
319
- place the brand color lands in a diagram
320
- ([#169](https://github.com/Omega-JS-Stack/omega/issues/169)). Mermaid paints
321
- with literal colors, so every token is resolved before it is handed over, and
322
- the read happens at draw time: like charts, a light/dark flip lands on the next
323
- redraw and nothing listens for it.
324
-
325
- ## One status hue site-wide
326
-
327
- `--omega-ok` / `--omega-warn` / `--omega-danger` are the ONLY greens, ambers
328
- and reds on a site. Each ships with an `-rgb` channel twin (`--omega-ok-rgb:
329
- 18, 146, 92`) because Bootstrap's translucency utilities paint from
330
- `rgba(var(--bs-success-rgb), …)`, which no `var()` can feed a hex to. Every
331
- theme's root bridge points `--bs-success`/`-warning`/`-danger` and their `-rgb`
332
- companions at the tokens, AFTER Bootstrap compiles, so `bg-success`,
333
- `.text-success` and a token-painted glyph are the same color in both modes —
334
- the /status page's "all systems operational" and the uptime bars beneath it
335
- were two different greens in dark mode until this landed
336
- ([#13](https://github.com/Omega-JS-Stack/omega/issues/13)).
337
-
338
- Affirmation ticks ride that same bridge: every "you get this" check (plan
339
- features, benefit lists, hero meta, comparison "yes" cells, signup benefits)
340
- wears Bootstrap's `.text-success` on the icon or its wrapper, and nothing else.
341
- There is no framework class for it: the `.omega-check`/`--omega-check` seam
342
- was deleted (Ian's ruling 2026-07-31, #44) because it computed exactly
343
- `.text-success`: a concept Bootstrap already names is expressed through
344
- Bootstrap's hook, and `omega-*` is only for concepts Bootstrap has no name for.
345
- A call site NEVER paints a check its own color (#11's call-site rule stands).
346
-
347
- **A theme that re-values a status hue MUST re-value its `-rgb` twin** (only
348
- newsflash does today) or the two drift apart again. `test/tokens.test.js` pins
349
- both halves: the twins exist in every stamp, and the bridge is the LAST
350
- `--bs-success` in every theme's compiled css.
351
-
352
- ## Motion library
353
-
354
- CSS: `core/css/motion/_index.scss`. Engine: `@omega.js/client/modules/motion.js`
355
- (`createMotion()`), started by `core/js/first-paint.js` and registered on the
356
- omega library by `core/js/core/motion.js` — shared with desktop/extension at C4
357
- exactly like icon-renderer.
358
-
359
- | Surface | Use |
360
- |---|---|
361
- | `data-omega-reveal="up\|fade\|left\|right\|scale"` | reveal once on scroll-in |
362
- | `data-omega-reveal-stagger="60"` (parent) | staggers child reveals (ms step) |
363
- | `data-omega-first-paint` (band) | the band IS the first viewport: its reveals are started at PARSE time by head.html's inline starter (no fetch), and a rotator inside it opens on its first word (css) |
364
- | `data-omega-countup` | counts to the number already in the markup |
365
- | `data-omega-rotate="2600"` | children cycle (hero word rotator, quotes) |
366
- | `data-omega-marquee` + `.omega-marquee__track/__item` | seamless loop — the set is cloned until half the track covers the container (never runs dry), constant px/s (attr value overrides); clones are `aria-hidden` with focusables detabbed (`tabindex=-1`, still mouse-clickable) so interactive sets (newsflash ticker headlines) stay accessible |
367
- | `data-omega-scroll-watch="24"` | stamps `data-omega-scrolled` (glassy nav) |
368
- | `data-omega-segmented` | gliding-thumb segmented control: engine injects `.omega-segmented__thumb` and tracks the checked/`.active` segment (billing toggle, platform rails, footer appearance) |
369
- | `data-omega-dotfield="22"` | canvas dot grid (value = px spacing): slow traveling wave, dots tint along ONE drifting rainbow gradient, pointer glow (tracked window-level so the fixed nav can't blind it); the loop starts only once first paint has settled and repaints at 30fps (below), reduced motion gets ONE still grid, and static CSS dots remain for no-JS |
370
- | `.omega-hover-lift/-raise/-dim`, `.omega-pressable`, `.omega-hover-nudge .omega-nudge` | pure-CSS hover/press effects |
371
- | `.omega-interactive` (+ `--lift`) | whole-surface click affordance: warm on hover/focus-visible, ring on focus, tint on press |
372
- | `.omega-float`, `.omega-caret` | ambient float, terminal caret |
373
-
374
- Resilience rules (load-bearing):
375
-
376
- - Reveal styles only hide content under the inline `html[data-omega-motion]`
377
- stamp (head.html) — **no JS means a fully visible page**.
378
- - **A reveal never waits for the big bundle**
379
- ([#585](https://github.com/Omega-JS-Stack/omega/issues/585)). The stamp lands
380
- before first paint, so a hidden reveal that waited on the engine's
381
- IntersectionObserver was blank text for the whole JS download on a cold cache
382
- — LCP measured on an empty band. The cause was never the animation but WHERE
383
- the starter lived: `core/js/main.js`, behind firebase/auth/analytics init. The
384
- fix is `core/js/first-paint.js` — its own asset entry, loaded from `head.html`
385
- as a deferred `type="module"` script, so it runs the moment the DOM is parsed:
386
- ahead of the main bundle's module in the foot, and independent of images,
387
- fonts and firebase. Same engine, same CSS transition, same feel; it just
388
- starts when the DOM is ready instead of when the app is.
389
- - **The band in the FIRST viewport starts its reveals at PARSE time**
390
- ([#763](https://github.com/Omega-JS-Stack/omega/issues/763), Ian's ruling
391
- 2026-09-02). Booting the engine early made the gate cheap, not free: the
392
- reveal lane is a SCROLL lane, and the opening band is never scrolled to.
393
- Holding it at opacity 0 until the observer fires cost the playground homepage
394
- ~2.0s of a 2,588ms mobile LCP, measured on `p.omega-hero__sub`
395
- ([#749](https://github.com/Omega-JS-Stack/omega/issues/749)). #749 answered
396
- that by taking the hero copy out of the lane entirely; the above-the-fold fade
397
- is part of the design, so the attributes came back and the ATTRIBUTE changed
398
- meaning instead. A band that declares `data-omega-first-paint` is run by an
399
- inline starter in `head.html`, right after the motion stamp: a plain script,
400
- no module and no fetch. A MutationObserver on `document.documentElement`
401
- collects the reveals in each batch of added nodes, and **the guard is per
402
- ELEMENT, never per band** — the parser hands a band over at its OPEN tag, when
403
- it holds no copy yet, so a band-level "handled" flag stamps nothing and leaves
404
- the work to `DOMContentLoaded`, which waits for the deferred bundles: the very
405
- wait this removes. Each batch coalesces TWO animation frames — the first
406
- paints what was collected at opacity 0, which is the transition's starting
407
- state, the second stamps it — then re-applies the stagger across the whole
408
- container (`--omega-reveal-delay` per child, same 60ms default; parse order is
409
- document order, so an index never changes and re-setting a value restarts
410
- nothing) and sets `data-omega-inview`. `DOMContentLoaded` disconnects the
411
- observer after one last pass, as a backstop. The engine's `observeReveal`
412
- returns early on a stamped element, so its later boot changes nothing inside
413
- the band, and the starter needs no reduced-motion guard of its own: the reveal
414
- lane hides only under `(prefers-reduced-motion: no-preference)`, so a visitor
415
- who asked for less motion gets the final state either way, JS or no JS.
416
- **The inline critical block has to carry the reveal lane** for any of that to
417
- be visible: every reveal rule is scoped `html[data-omega-motion] …`, a token
418
- no markup scan can see, so PurgeCSS purged the lane out of the block and a
419
- built page painted its copy VISIBLE, stamped it, and met the deferred sheet
420
- with the element already resolved — no fade at all. The critical extractor
421
- safelists that one lane (`src/assets.js`, ~1.1 KB), pinned by
422
- `test/critical-css.test.js`. So the marketing
423
- hero's copy stack (badge, headline, sub, CTAs, meta, frame) is back on the
424
- lane behind its `data-omega-reveal-stagger="40"` container, and so is any
425
- markup the template never sees (an authored `demo_html` slot, a custom hero
426
- animation folder). The headline's word rotator is the one above-the-fold gate
427
- the starter does NOT resolve — the engine owns the cycle — so its css rule
428
- stays: inside a first-paint band the FIRST word paints with the document and
429
- yields the moment the engine stamps `data-omega-active`, which it does on that
430
- same child. Below-the-fold bands keep the one lane, untouched.
431
-
432
- **A first-paint band fades on its own, shorter token.** Chrome reports the
433
- largest element painted only when its fade ENDS, so the first viewport's
434
- transition and the stagger ahead of it land on LCP in full; the proof on the
435
- playground home (mobile, slow 4G) put the 550ms reveal behind a 90ms stagger
436
- 750ms past first paint. `motion/_index.scss` therefore times
437
- `[data-omega-first-paint] [data-omega-reveal]` with `--omega-speed-first-paint`
438
- (300ms in core) and the hero's stagger is 40ms a step: the same fade, the same
439
- lane, 400ms past first paint. Below the fold nothing changed, because no
440
- metric watches a scroll-in reveal. A theme re-times the first viewport by
441
- setting the token, exactly as it sets `--omega-speed-slow` for the rest.
442
-
443
- **Every first-viewport band declares the attribute, not just the hero**
444
- ([#467](https://github.com/Omega-JS-Stack/omega/issues/467) Phase 2). The
445
- five the #749 worker found still gated took the identical treatment:
446
- `about/hero` (both its layouts, photo-lead and split), the document masthead
447
- in `_layouts/frontend/core/minimal.html`, and the opening bands of
448
- `download.html`, `contact.html` and `pricing.html`. Four of them compose
449
- their copy through the shared `heading/masthead` component, which emits the
450
- reveal attributes itself, so the component takes a `first_paint: true`
451
- switch rather than losing them. The follow-up pass then walked the REST of
452
- that component's callers, and every one of them proved to be its page's
453
- opening band too: `status.html`, `feedback.html`, `blog/index.html` and the
454
- four blog taxonomy layouts, `team/index.html`, `legal/document.html` (whose
455
- band wears `omega-legal__head`, not a dotgrid), `alternatives/index.html`
456
- and `alternatives/alternative.html`, the three `collection/` layouts,
457
- `extension/index.html` and `updates/index.html`. The switch stays on the
458
- component because a MID-page caller (a consumer's own composition, the
459
- component gallery) still belongs on the reveal lane. Those masthead clusters
460
- therefore carry no reveal attributes at all, which #763 left exactly as it
461
- found them: the starter animates whatever a first-paint band carries, and
462
- these bands carry nothing. Below the fold on every one of these pages,
463
- nothing changed. Pinned by
464
- `packages/web/test/first-paint-bands.test.js`.
465
- - **The first-paint script is the one early seam, and it stays small.** Anything
466
- that must beat the big bundle belongs there (brand custom hero animations,
467
- [#441](https://github.com/Omega-JS-Stack/omega/issues/441), ride the same
468
- engine and get the same early start) — weighed against that budget. It must
469
- never import `@omega.js/client`: the runtime import drags the whole client into
470
- the bundle and rebuilds the very problem it exists to solve. The motion
471
- module's subpath is standalone by design, and `core/js/core/motion.js` adopts
472
- the running instance rather than starting a second one.
473
- - **One lane, not two** (Ian's ruling 2026-08-26). An earlier round answered
474
- #585 with a second, pure-CSS entrance lane (paint-time keyframes, a 120ms
475
- lead-in, an `:nth-child` stagger cap, a 1s auto-resolve net, `will-change`
476
- staging). It put the text on screen but at a different rhythm and a different
477
- start moment than the reveal everywhere else — a visibly worse animation. It
478
- is deleted: no `omega-reveal-in` keyframe, no `--omega-reveal-wait`, no
479
- `html[data-omega-motion-ready]` gate, no `data-omega-reveal-lead` hook. A
480
- band's stagger is again the engine's job alone, from the one number its author
481
- writes on the cluster (`data-omega-reveal-stagger="70"` → `--omega-reveal-delay`
482
- per child).
483
- - **Ambient motion waits for first paint; a mutation never reads geometry
484
- behind itself** ([#752](https://github.com/Omega-JS-Stack/omega/issues/752)).
485
- The homepage hero carries `data-omega-dotfield`, and the engine used to start
486
- its loop the moment it scanned: a full canvas grid repainted on every one of
487
- the display's frames, on the page's own thread, through the whole LCP window.
488
- The rules below answer it, and they are the pattern for anything ambient
489
- this engine grows next:
490
- - **The settle signal.** No measure, no style read and no paint until the
491
- first `requestIdleCallback` AFTER the load event (a 2s timeout, so a
492
- permanently busy page still gets its field), or the load event itself where
493
- idle callbacks do not exist. The `whenSettled(doc, fn)` helper in
494
- `motion.js` is the one implementation.
495
- - **The handoff stamp rides the first painted grid.** classy fades its CSS
496
- fallback dots out on `.omega-dotgrid[data-omega-dotfield-ready]::before`,
497
- so `data-omega-dotfield-ready` lands with the grid that replaces them and
498
- never at scan: stamped early it left the hero backdrop blank for the whole
499
- wait. The engine keeps its own re-entry set, so a rescan before the stamp
500
- exists still installs exactly one field.
501
- - **A capped cadence.** The field then repaints at 30fps, not at whatever the
502
- display offers: the wave crawls (~900px crest, a 20s rainbow cycle), so 30
503
- reads exactly like 60 and costs half the main thread, and a 120Hz display
504
- pays the same 30, not double. The pointer trail still eases on every frame
505
- (cheap), so its feel is unchanged. Under `prefers-reduced-motion` the field
506
- paints ONE grid, held at the start of the wave, with no loop and no pointer
507
- tracking; it repaints only on a resize or a `data-bs-theme` flip, and
508
- re-reads `--omega-line-strong` there, because the once-a-second color poll
509
- lives inside the loop that path does not have.
510
- - **The marquee measures in a later frame.** `build()` replaces the track's
511
- children and reads the set width one `requestAnimationFrame` later, never
512
- in the tick that just mutated it: that read was a forced synchronous
513
- reflow on every build, and a build runs on resize, on `fonts.ready` and on
514
- every image load in the set. A burst of those collapses to ONE measure, so
515
- the second never sizes the loop from a track the first just cloned.
516
- - `prefers-reduced-motion` renders final states: reveals resolve instantly,
517
- count-ups show their target, rotators hold the first word, marquees park.
518
- EVERY continuous loop in `core/css` and in the theme sheets this package
519
- authors parks with them (vendored Bootstrap keeps upstream's own behavior):
520
- the `.animation-*` utilities (spin, pulse, pulse-right, bounce, wiggle, flex,
521
- shimmer), the motion library's float, marquee, and caret blink, the lazy-load
522
- and binding shimmers, the exit-popup wave, the studio record pulse, the
523
- download/extension walkthrough pointers, and the theme loops (the typing
524
- dots, the org-chart flow lines, the ticker pulse, the CTA rings, the
525
- infinite-scroll track). The one-shot fades, slides, and popups already end on
526
- their final state. `test/animations.test.js` derives its roster from every
527
- core and theme sheet that declares `infinite`, so a new unparked loop fails
528
- the suite.
529
- - **Autoplaying video parks in JS, because CSS cannot pause one**
530
- ([#499](https://github.com/Omega-JS-Stack/omega/issues/499)). The motion
531
- engine's scan is the ONE lane: under `prefers-reduced-motion` it strips
532
- `autoplay` from every `video[autoplay]`, pauses it, and turns its controls
533
- on, so the poster holds and the visitor decides. It applies to every
534
- autoplaying band at once (the hero's video demo, `marketing/product-demo`) —
535
- a section never forks its own pause. Once parked, a rescan leaves a video
536
- the visitor started alone.
537
- - The PurgeCSS safelist keeps every `omega-`-namespaced selector plus
538
- Bootstrap's own JS-toggled transition classes (`collapse`/`collapsing`/
539
- `show`/`showing`/`fade` — `src/assets.js`) because that state is
540
- runtime-stamped and never visible to the content scan. **New
541
- runtime-stamped classes must live in the `omega-` namespace** (or join the
542
- safelist explicitly).
543
-
544
- **Classy never raises on hover** (Ian 2026-08-29,
545
- [#686](https://github.com/Omega-JS-Stack/omega/issues/686)). Nothing travels
546
- upward under the pointer in classy — not a button, not a card, not a tile.
547
- Everything else a hover does stays: the warm, the ring, the shadow, the line,
548
- the press, and the hero frame's tilt. The raise itself is SHARED css (core's
549
- motion library serves every skin, and newsflash/neobrutalism import classy's
550
- floor partials), so classy OPTS OUT in `themes/classy/css/base/_no-raise.scss`
551
- — loaded last by its `_theme.scss` — and every other theme keeps its lifts.
552
- Raises classy owns alone are gone from their own files instead. The legacy
553
- `.hover-up` utility needs one specificity step (`html .hover-up:hover`) because
554
- `core/css/core/_animations.scss` lands AFTER every theme, and a re-declared
555
- hover transform carries its `prefers-reduced-motion` park with it, since a
556
- media query adds no specificity. `test/hover-raise.test.js` compiles both the
557
- classy and a sibling bundle, so a new raise reaching classy fails the suite.
558
-
559
- **Every loading state carries a visible animation** (Ian 2026-08-15). Any
560
- surface that WAITS — a poll, a fetch, a build, a webhook that has not landed —
561
- shows an animated waiting indicator: the Bootstrap `spinner-border` idiom
562
- (`role="status"` plus a `visually-hidden` label; `spinner-border-lg` for a
563
- full-panel wait) for a spinner, the binding skeleton's shimmer for
564
- placeholder content. Bare text alone is never a waiting state: with nothing
565
- moving, a page that is working looks broken. Under `prefers-reduced-motion`
566
- the indicator swaps to a STATIC state that still says what is happening —
567
- park the loop (`animation: none`) and keep visible copy naming the wait
568
- beside it, because vendored Bootstrap only slows its own spinner, so the page
569
- rendering one owns the park (the confirmation page's
570
- `core/css/pages/payment/confirmation/index.scss` is the reference).
571
-
572
- ## Copy register
573
-
574
- Product copy — every string a visitor reads: layout and section copy,
575
- section-defaults `json5`, and the JS string literals that render into the
576
- page — **never uses em dashes**. Use a comma, a semicolon, a period, or
577
- parentheses instead, whichever the sentence actually wants (Ian 2026-08-15).
578
- The rule is about COPY, not about code: comments, `docs/`, frontmatter
579
- headers, and `logger.*` messages are not rendered and are untouched, and a
580
- bare `—` standing in as a placeholder glyph or a range separator is
581
- typography rather than a sentence.
582
-
583
- ## classy v2 (the flagship skin)
584
-
585
- Warm-paper light / de-blued charcoal dark; zero gradients (the only permitted
586
- fades are alpha masks and monotone chart fills); ink primary buttons
587
- (`.btn-adaptive` — built from `$dark`/`$light` by the shared overrides layer);
588
- accent reserved for links, active states, focus, meters, chart series-1, and
589
- small signals. Marketing display voice is the serif `--omega-font-marketing`
590
- (`.omega-display`); app surfaces stay on the grotesk. The app chrome rides
591
- `.omega-shell` with the content area drawn as one big rounded surface card
592
- (matching margins/radii on topbar + main — the markup contract is untouched).
593
-
594
- Structure: `_config.scss` (consumer knobs + Bootstrap forward) → `css/base`
595
- (root bridge, typography, utilities) → `css/components`
596
- (buttons/cards/forms/badges/dropdowns) → `css/layout`
597
- (general/nav/footer/shell) → `css/marketing` (hero/bento/sections) →
598
- `css/pages` (auth) → shared `../bootstrap/overrides`.
599
-
600
- Section chrome is data-driven: `_includes/frontend/sections/{nav,footer}.html`
601
- and the shared app chrome `_includes/global/sections/{app-sidebar,app-topbar,
602
- page-header}.html` render JSON section data (`core/_includes/**.json`,
603
- consumer-overridable per file). The sidebar supports an optional project
604
- selector module (`selector:`), nested collapsible groups, badges, an ad slot
605
- (`bottom.ad.enabled`), and the auth-bound user row; the topbar carries the
606
- drawer/rail toggles, the breadcrumb trail (from `theme.header.breadcrumbs`),
607
- the ⌘K search pill (`search:`), custom actions, and the account dropdown. Each
608
- region rides its own page switch: `theme.sidebar.enabled: false` drops the rail
609
- AND the two topbar toggles that drive it (dead buttons otherwise, naming an id
610
- the page no longer carries,
611
- [#740](https://github.com/Omega-JS-Stack/omega/issues/740)), and
612
- `theme.topbar.enabled: false` drops the topbar.
613
-
614
- A link's `icon` carries the FULL Font Awesome class string
615
- ([#903](https://github.com/Omega-JS-Stack/omega/issues/903)): `icon: 'fa-brands
616
- fa-github'`, rendered exactly as authored, so the whole chrome spells one key one
617
- way, the nav, the sidebar, the topbar (its notifications bell included), the page
618
- header (its title's `theme.header.title.icon` included), the account dropdown,
619
- the account section header and the footer, and any family the set carries is
620
- reachable. The mechanism behind the markup: [icons.md](icons.md).
621
-
622
- ## Stable-API line (don't churn once consumers exist)
623
-
624
- Token NAMES · `_config.scss` variable names · `.omega-shell` markup contract
625
- (which since [#319](https://github.com/Omega-JS-Stack/omega/issues/319) includes
626
- the `__sidebar-scroll` region — hand-rolled shell markup without it gets a rail
627
- that no longer scrolls its nav) · `_includes` section names + their JSON data
628
- shapes · the motion attribute contract. Everything behind that line — values, partial internals, page
629
- markup — iterates freely with the skin arc.