@3xhaust/oh-my-design 0.16.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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +282 -0
  3. package/README.md +282 -0
  4. package/adapters/build.ts +184 -0
  5. package/adapters/claude.ts +111 -0
  6. package/adapters/codex.ts +68 -0
  7. package/adapters/tokens.ts +21 -0
  8. package/adapters/tool-map.json +7 -0
  9. package/bin/omd-install.ts +76 -0
  10. package/bin/omd.ts +1774 -0
  11. package/core/asset-sourcing/index.ts +187 -0
  12. package/core/coach/index.ts +98 -0
  13. package/core/composition/app-shell-workbench.md +168 -0
  14. package/core/composition/asymmetric-diagonal-grid.md +178 -0
  15. package/core/composition/bento-grid.md +223 -0
  16. package/core/composition/editorial-index-labels.md +173 -0
  17. package/core/composition/form-wizard-stepper.md +132 -0
  18. package/core/composition/master-detail-flow.md +129 -0
  19. package/core/composition/section-inversion.md +182 -0
  20. package/core/composition/sidebar-margin-annotation.md +184 -0
  21. package/core/composition/split-screen-hero.md +224 -0
  22. package/core/composition/sticky-sidebar-scroll.md +219 -0
  23. package/core/composition/typographic-hero.md +100 -0
  24. package/core/composition-contract/index.ts +233 -0
  25. package/core/composition-contract/visual-richness.ts +101 -0
  26. package/core/config/index.ts +27 -0
  27. package/core/copy/index.ts +296 -0
  28. package/core/craft/finish-pass.md +356 -0
  29. package/core/craft/index.ts +33 -0
  30. package/core/design/index.ts +581 -0
  31. package/core/design/interaction-states.ts +189 -0
  32. package/core/eval-harness/index.ts +225 -0
  33. package/core/evidence/final.ts +444 -0
  34. package/core/evidence/task.ts +459 -0
  35. package/core/figma/client.ts +312 -0
  36. package/core/figma/diff.ts +261 -0
  37. package/core/figma/responsive.ts +310 -0
  38. package/core/figma/system.ts +466 -0
  39. package/core/figma/types.ts +185 -0
  40. package/core/frame/check-ux.ts +180 -0
  41. package/core/frame/index.ts +39 -0
  42. package/core/frame/write.ts +168 -0
  43. package/core/graphics/css-illustration-primitives.md +189 -0
  44. package/core/graphics/duotone-image-presets.md +173 -0
  45. package/core/graphics/gradient-mesh.md +178 -0
  46. package/core/graphics/noise-grain-texture.md +150 -0
  47. package/core/graphics/placeholder-policy.md +185 -0
  48. package/core/graphics/svg-geometric-patterns.md +145 -0
  49. package/core/history/index.ts +39 -0
  50. package/core/install/detect.ts +32 -0
  51. package/core/install/install.ts +321 -0
  52. package/core/install/patch-claude.ts +109 -0
  53. package/core/install/patch-codex.ts +82 -0
  54. package/core/interaction/index.ts +101 -0
  55. package/core/interaction/recipes/signature-lighting.md +203 -0
  56. package/core/ir/dom.ts +352 -0
  57. package/core/ir/normalize.ts +177 -0
  58. package/core/motion/easing.md +151 -0
  59. package/core/motion/energy.ts +188 -0
  60. package/core/motion/recipes/image-hover-distortion.md +198 -0
  61. package/core/motion/recipes/magnetic-hover.md +229 -0
  62. package/core/motion/recipes/marquee.md +202 -0
  63. package/core/motion/recipes/number-counter.md +242 -0
  64. package/core/motion/recipes/page-loader.md +240 -0
  65. package/core/motion/recipes/parallax.md +233 -0
  66. package/core/motion/recipes/scroll-reveal.md +210 -0
  67. package/core/motion/recipes/section-color-inversion.md +217 -0
  68. package/core/motion/recipes/split-text-entrance.md +222 -0
  69. package/core/motion/recipes/stagger-orchestrator.md +227 -0
  70. package/core/motion/recipes/sticky-scene-transition.md +246 -0
  71. package/core/motion/recipes/view-transitions.md +239 -0
  72. package/core/probe/index.ts +186 -0
  73. package/core/probe/schema.json +46 -0
  74. package/core/protocol/composition-contract.md +192 -0
  75. package/core/protocol/copy-deck.md +96 -0
  76. package/core/protocol/human-design-loop.md +447 -0
  77. package/core/protocol/slop-review.md +71 -0
  78. package/core/ref/batch.ts +94 -0
  79. package/core/ref/blueprint.ts +199 -0
  80. package/core/ref/distance.ts +225 -0
  81. package/core/ref/invariants.ts +162 -0
  82. package/core/ref/signal.ts +37 -0
  83. package/core/ref/store.ts +122 -0
  84. package/core/render/index.ts +607 -0
  85. package/core/rules/attribution.ts +159 -0
  86. package/core/rules/builtin/contrast.yaml +8 -0
  87. package/core/rules/builtin/focus.yaml +8 -0
  88. package/core/rules/builtin/hit-area.yaml +8 -0
  89. package/core/rules/builtin/ko.yaml +6 -0
  90. package/core/rules/builtin/motion.yaml +80 -0
  91. package/core/rules/builtin/slop.yaml +365 -0
  92. package/core/rules/builtin/spacing.yaml +9 -0
  93. package/core/rules/builtin/system.yaml +42 -0
  94. package/core/rules/builtin/token.yaml +8 -0
  95. package/core/rules/builtin/ux.yaml +125 -0
  96. package/core/rules/engine.ts +88 -0
  97. package/core/rules/leakage.ts +72 -0
  98. package/core/rules/motion-spec.ts +204 -0
  99. package/core/site/index.ts +100 -0
  100. package/core/slop/index.ts +427 -0
  101. package/core/slop/text-slop.ts +220 -0
  102. package/core/source-seal/index.ts +174 -0
  103. package/core/stack/index.ts +53 -0
  104. package/core/target/index.ts +183 -0
  105. package/core/theory/color.md +285 -0
  106. package/core/theory/components.md +272 -0
  107. package/core/theory/craft.md +270 -0
  108. package/core/theory/expressive.md +213 -0
  109. package/core/theory/imagegen.md +105 -0
  110. package/core/theory/layout.md +159 -0
  111. package/core/theory/motion.md +283 -0
  112. package/core/theory/typography.md +134 -0
  113. package/core/theory/ux.md +580 -0
  114. package/core/theory/voice.md +595 -0
  115. package/core/types.ts +684 -0
  116. package/package.json +40 -0
  117. package/src/agents/composer.agent.yaml +174 -0
  118. package/src/agents/eye.agent.yaml +208 -0
  119. package/src/agents/framer.agent.yaml +52 -0
  120. package/src/agents/glance.agent.yaml +19 -0
  121. package/src/agents/hand.agent.yaml +200 -0
  122. package/src/agents/scout.agent.yaml +71 -0
  123. package/src/agents/sketch.agent.yaml +64 -0
  124. package/src/agents/typesetter.agent.yaml +52 -0
  125. package/src/agents/writer.agent.yaml +63 -0
  126. package/src/skills/omd-coach/SKILL.md +40 -0
  127. package/src/skills/omd-critique/SKILL.md +77 -0
  128. package/src/skills/omd-figma/SKILL.md +256 -0
  129. package/src/skills/omd-humanize/SKILL.md +160 -0
  130. package/src/skills/omd-scout/SKILL.md +77 -0
  131. package/src/skills/omd-ultradesign/SKILL.md +521 -0
@@ -0,0 +1,173 @@
1
+ # Duotone / treated image presets
2
+
3
+ A duotone treatment replaces an image's tonal range with two colours — one for the
4
+ shadows, one for the highlights — using a CSS `filter` chain. The result reads as
5
+ intentional art direction rather than stock photography: the same face or landscape
6
+ in two hues aligned to the brand palette becomes a designed asset, not a found one.
7
+ The technique unifies images from different sources under a single visual language.
8
+
9
+ The CSS implementation uses `filter: grayscale(1) contrast() brightness() sepia() hue-rotate()` chained in a specific order to achieve the duotone effect without SVG
10
+ feColorMatrix or Canvas manipulation. A secondary blend-mode approach using
11
+ `mix-blend-mode: multiply` or `color` can achieve richer duotones when browser support
12
+ allows for the stacking context requirements.
13
+
14
+ ## When it earns its place / When it does not
15
+
16
+ Condition: when real images exist but come from varied sources — team photography at
17
+ different times, stock imagery from multiple libraries, user-generated content — and
18
+ visual unification is needed without re-photographing everything. A duotone treatment
19
+ makes diverse source material read as a designed set. Also: when the brand palette is
20
+ strong and the concept benefits from photographs that are unmistakably on-brand; the
21
+ treatment removes the image's original colour story and replaces it with the brand's.
22
+
23
+ `core/theory/layout.md` on similarity: "elements that look the same are read as
24
+ belonging to the same category." A grid of duotone-treated images reads as a unified
25
+ collection; the same grid with untreated photographs reads as a collection of separate
26
+ images that happen to share space.
27
+
28
+ Condition against: product photography where accurate colour matters — food, fashion,
29
+ product hardware. Medical or scientific imagery where colour carries meaning. Any context
30
+ where the user needs to see the actual colours of the subject. Documentation images where
31
+ clarity outweighs aesthetic treatment.
32
+
33
+ ## Parameters
34
+
35
+ ```css
36
+ :root {
37
+ /* The CSS filter duotone uses a grayscale base plus hue rotation and saturation.
38
+ Exact values depend on the target shadow and highlight colours.
39
+ The values below produce a warm amber shadow / cool blue highlight duotone.
40
+ Calibrate these against the brand palette — these are starting points. */
41
+
42
+ /* Step 1: desaturate completely. */
43
+ --dt-grayscale: grayscale(1);
44
+
45
+ /* Step 2: boost contrast so shadows and highlights are distinct zones. */
46
+ --dt-contrast: contrast(1.2);
47
+
48
+ /* Step 3: add the shadow tone via sepia (shifts greys to warm amber). */
49
+ --dt-sepia: sepia(0.8);
50
+
51
+ /* Step 4: rotate hue to place the image in the brand palette zone.
52
+ 0deg: warm amber (sepia base).
53
+ 60deg: warm yellow-green.
54
+ 180deg: cyan-teal.
55
+ 240deg: blue.
56
+ 300deg: purple-pink.
57
+ Combine with saturation adjustment for exact colour targeting. */
58
+ --dt-hue: hue-rotate(200deg);
59
+
60
+ /* Step 5: saturation of the resulting toned image.
61
+ 0.6–0.8: subtle colour wash — reads as toned, not fully coloured.
62
+ 1.0–1.4: strong colour — reads as deliberately duotoned. */
63
+ --dt-saturate: saturate(1.2);
64
+
65
+ /* Final brightness correction: duotone often darkens; compensate here. */
66
+ --dt-brightness: brightness(1.05);
67
+ }
68
+ ```
69
+
70
+ ## Implementation
71
+
72
+ **CSS filter chain (single-colour tint approach)**
73
+
74
+ ```css
75
+ /* The filter chain order matters. Changing the order changes the output. */
76
+ .duotone {
77
+ filter:
78
+ var(--dt-grayscale)
79
+ var(--dt-contrast)
80
+ var(--dt-sepia)
81
+ var(--dt-hue)
82
+ var(--dt-saturate)
83
+ var(--dt-brightness);
84
+ }
85
+
86
+ /* Preset: brand blue duotone */
87
+ .duotone--blue {
88
+ filter: grayscale(1) contrast(1.2) sepia(0.9) hue-rotate(180deg) saturate(1.3) brightness(1.0);
89
+ }
90
+
91
+ /* Preset: brand amber duotone */
92
+ .duotone--amber {
93
+ filter: grayscale(1) contrast(1.15) sepia(1) hue-rotate(20deg) saturate(1.1) brightness(1.08);
94
+ }
95
+
96
+ /* Preset: monochrome (single tone, minimal saturation) */
97
+ .duotone--mono {
98
+ filter: grayscale(1) contrast(1.1) brightness(1.05);
99
+ }
100
+ ```
101
+
102
+ **Mix-blend-mode approach (richer control, requires stacking context)**
103
+
104
+ ```html
105
+ <!-- The image sits in a container. A pseudo-element with the brand colour overlays
106
+ it via mix-blend-mode: color, which replaces the image's hue while preserving
107
+ luminosity. This produces a true two-stop duotone. -->
108
+ <div class="duotone-blend">
109
+ <img src="image.jpg" alt="[description]" class="duotone-blend__image">
110
+ </div>
111
+ ```
112
+
113
+ ```css
114
+ .duotone-blend {
115
+ position: relative;
116
+ isolation: isolate; /* required for mix-blend-mode to work correctly */
117
+ overflow: hidden;
118
+ }
119
+
120
+ .duotone-blend__image {
121
+ display: block;
122
+ width: 100%;
123
+ height: 100%;
124
+ object-fit: cover;
125
+ filter: grayscale(1) contrast(1.1); /* desaturate first */
126
+ }
127
+
128
+ /* The colour overlay uses mix-blend-mode: color to apply the brand hue
129
+ to the greyscale image below. */
130
+ .duotone-blend::after {
131
+ content: '';
132
+ position: absolute;
133
+ inset: 0;
134
+ background-color: var(--color-brand-primary, #1a4d8f);
135
+ opacity: 0.6; /* lower = more greyscale visible; higher = more brand colour */
136
+ mix-blend-mode: color;
137
+ pointer-events: none;
138
+ }
139
+ ```
140
+
141
+ ## Linter notes
142
+
143
+ - Duotone treatments applied in the flagged SLOP-GRADIENT hue bands (hue-rotate values
144
+ that produce indigo/violet or purple/pink tones) carry the same `SLOP-GRADIENT` risk as
145
+ mesh gradients in those bands. The treatment is applied to an image rather than a
146
+ background, so the `SLOP-GRADIENT` rule does not fire directly — but the aesthetic
147
+ outcome (a purple-tinted image on a purple gradient background) reads as the same AI-
148
+ default visual language. Record the colour choice if it lands in those bands.
149
+
150
+ - The `mix-blend-mode: color` approach requires the parent to have `isolation: isolate` set.
151
+ Without the stacking context, the blend mode may affect elements outside the intended
152
+ container. Always verify the containment before shipping.
153
+
154
+ - Duotone treatments reduce effective contrast for users with colour vision deficiencies.
155
+ After applying a treatment, run `omd check` to verify that any text overlaid on the
156
+ duotone image still meets 4.5:1 contrast. The treatment changes the image's luminance
157
+ distribution; a dark duotone image may produce lower contrast than the untreated version.
158
+
159
+ ## Do not combine with
160
+
161
+ **noise-grain-texture.md at opacity above 8%** — grain over a duotone image creates
162
+ triple-layered processing (image → duotone filter → grain overlay) that produces an
163
+ image that reads as heavily processed rather than treated. Light grain (3–5%) over a
164
+ duotone can add material quality; heavy grain obscures the treatment.
165
+
166
+ **css-illustration-primitives.md on the same visual zone** — a duotone image and a CSS
167
+ blob in the same section compete for the decorative register. The treated image is itself
168
+ the art-directed visual; a blob behind or over it dilutes its effect.
169
+
170
+ **Accurate-colour product contexts** — if the image is a product photograph and the
171
+ user needs to see the actual colour of the product, do not apply a duotone treatment.
172
+ The treatment overrides the image's colour story with the brand's, which is correct
173
+ for editorial and brand contexts and incorrect for product accuracy contexts.
@@ -0,0 +1,178 @@
1
+ # Gradient mesh backgrounds
2
+
3
+ A gradient mesh layers multiple `radial-gradient()` declarations on a single element,
4
+ producing a soft, multi-source light field that reads as dimensional rather than flat.
5
+ Unlike a simple two-stop linear gradient, a mesh gradient has no visible direction — it
6
+ pools and diffuses, the way light fills a room through multiple windows. The technique
7
+ is CSS-only: no SVG, no image file, no canvas.
8
+
9
+ ## When it earns its place / When it does not
10
+
11
+ Condition: the page has large flat sections that read as too digital — blank backgrounds
12
+ that feel like developer defaults rather than design decisions. A mesh gradient makes
13
+ the surface a presence without adding visual weight that competes with the content.
14
+ Showpiece and confident registers both benefit: the mesh adds depth to the hero or a
15
+ full-bleed section background without introducing imagery or illustration overhead.
16
+ `expressive.md` on grain texture (the natural companion to mesh): "Flat backgrounds or
17
+ smooth gradients that read as too digital — noise adds the tactile quality that solid
18
+ fills lack." A mesh gradient addresses the same problem at the colour level before grain
19
+ addresses it at the texture level.
20
+
21
+ Condition against: pages where the brand palette is already active and saturated — adding
22
+ a mesh to a high-saturation layout creates a chromatic argument between the mesh and the
23
+ content. Any dark section where the mesh's colour pools compete with text legibility;
24
+ verify 4.5:1 contrast between every text element and the mesh behind it at every mesh
25
+ position, not just the average. And for AI-generated-looking aesthetics: see the linter
26
+ notes below — specific hue combinations trigger `SLOP-GRADIENT` because they are the
27
+ statistical mean of AI design output, not a brand decision.
28
+
29
+ ## Parameters
30
+
31
+ ```css
32
+ :root {
33
+ /* Each radial gradient defines one light source in the mesh.
34
+ Three to five sources produce a convincing mesh; more than six
35
+ tends toward muddiness rather than depth.
36
+ Position each source at a different corner or edge — distribute
37
+ them to avoid clustering. */
38
+
39
+ /* Source 1: warm anchor — primary brand hue */
40
+ --mesh-source-1-color: hsl(220deg 60% 70% / 0.8);
41
+ --mesh-source-1-pos: 0% 0%;
42
+ --mesh-source-1-size: 60% 60%;
43
+
44
+ /* Source 2: cool contrast — secondary or complementary hue */
45
+ --mesh-source-2-color: hsl(280deg 50% 60% / 0.6);
46
+ --mesh-source-2-pos: 100% 0%;
47
+ --mesh-source-2-size: 50% 50%;
48
+
49
+ /* Source 3: neutral or analogous midground */
50
+ --mesh-source-3-color: hsl(200deg 40% 80% / 0.5);
51
+ --mesh-source-3-pos: 50% 100%;
52
+ --mesh-source-3-size: 70% 50%;
53
+
54
+ /* Base fill colour: what shows where the gradients do not reach.
55
+ Should be the lightest or darkest value in the palette, never a mid-tone.
56
+ Mid-tone bases with mid-tone gradients produce uniform murk. */
57
+ --mesh-base: #f0f0ee;
58
+ }
59
+ ```
60
+
61
+ ## Implementation
62
+
63
+ ```css
64
+ /* ── CSS-only mesh gradient ──────────────────────────────────────────────── */
65
+ .mesh-bg {
66
+ background-color: var(--mesh-base);
67
+ background-image:
68
+ radial-gradient(
69
+ ellipse var(--mesh-source-1-size) at var(--mesh-source-1-pos),
70
+ var(--mesh-source-1-color),
71
+ transparent
72
+ ),
73
+ radial-gradient(
74
+ ellipse var(--mesh-source-2-size) at var(--mesh-source-2-pos),
75
+ var(--mesh-source-2-color),
76
+ transparent
77
+ ),
78
+ radial-gradient(
79
+ ellipse var(--mesh-source-3-size) at var(--mesh-source-3-pos),
80
+ var(--mesh-source-3-color),
81
+ transparent
82
+ );
83
+ /* No background-blend-mode needed for basic mesh — the stacking of transparent
84
+ gradients handles the blending. background-blend-mode: multiply or screen can
85
+ intensify the effect but is harder to calibrate for legibility. */
86
+ }
87
+
88
+ /* ── Full-section mesh hero ──────────────────────────────────────────────── */
89
+ .mesh-hero {
90
+ min-height: 100svh;
91
+ display: flex;
92
+ align-items: flex-end;
93
+ padding: var(--space-page-margin, 5vw);
94
+ }
95
+
96
+ /* ── Mesh as a positional decorative element (not full-bleed) ─────────────── */
97
+ /* Place the mesh behind specific components without covering the entire background. */
98
+ .mesh-spot {
99
+ position: relative;
100
+ isolation: isolate; /* creates stacking context for the pseudo-element mesh */
101
+ }
102
+
103
+ .mesh-spot::before {
104
+ content: '';
105
+ position: absolute;
106
+ inset: -20%;
107
+ z-index: -1;
108
+ border-radius: 50%; /* circular mesh pool reads as ambient light source */
109
+ background-image:
110
+ radial-gradient(
111
+ ellipse 80% 80% at 30% 40%,
112
+ var(--mesh-source-1-color),
113
+ transparent
114
+ ),
115
+ radial-gradient(
116
+ ellipse 60% 60% at 70% 60%,
117
+ var(--mesh-source-2-color),
118
+ transparent
119
+ );
120
+ filter: blur(40px); /* blur softens the ellipse edges for a true mesh feel */
121
+ pointer-events: none;
122
+ }
123
+ ```
124
+
125
+ ```html
126
+ <!-- Full-section mesh: -->
127
+ <section class="mesh-bg mesh-hero">
128
+ <h1>Content over the mesh.</h1>
129
+ </section>
130
+
131
+ <!-- Spot mesh behind a card or feature: -->
132
+ <div class="mesh-spot">
133
+ <div class="card">Content gains an ambient glow.</div>
134
+ </div>
135
+ ```
136
+
137
+ ## Linter notes
138
+
139
+ The `SLOP-GRADIENT` rule fires on the following hue combinations because they are the
140
+ statistical signature of AI-generated design output — the default gradient every model
141
+ reaches for when asked to "make it look good":
142
+
143
+ - **Indigo → violet** (hue range 240–290deg): the single most overused gradient in AI
144
+ design. Any `background: linear-gradient` or `radial-gradient` whose two dominant
145
+ stops land in this band fires `SLOP-GRADIENT`. If the brand genuinely uses indigo and
146
+ violet, the decision must be recorded in `omd decision` with the brand's hex values
147
+ and the reference that supports the choice.
148
+
149
+ - **Purple → pink** (hue range 280–330deg): a secondary AI-default combination.
150
+ Frequently appears in "premium" and "AI product" contexts. Same rule applies.
151
+
152
+ - **Electric blue → teal** (hue range 190–240deg): common in tech startup contexts.
153
+ Fires `SLOP-GRADIENT` in these ranges at saturation above 70%.
154
+
155
+ The mesh gradient technique is not itself flagged — layered radial gradients at lower
156
+ saturation using the brand's actual hue values are expected and correct. The linter
157
+ fires on the specific hue bands above, not on mesh gradients per se. A mesh using
158
+ `hsl(30deg 40% 80%)` (warm sand) and `hsl(170deg 30% 75%)` (cool sage) is clean.
159
+ A mesh using `hsl(260deg 80% 60%)` and `hsl(310deg 70% 60%)` fires the rule.
160
+
161
+ **The test**: before committing a mesh gradient, read the HSL values of the dominant
162
+ stops. If both stops land in the flagged hue bands, record the brand reason or choose
163
+ a different hue combination. The mesh technique is sound; the hue selection is the risk.
164
+
165
+ ## Do not combine with
166
+
167
+ **noise-grain-texture.md at opacity above 10%** — both techniques add depth signals to
168
+ the background. Grain over a mesh is appropriate at 3–6% opacity; above that, the
169
+ combined texture reads as a rendering artifact, not a design decision.
170
+
171
+ **svg-geometric-patterns.md behind the same element** — a pattern and a mesh applied
172
+ to the same background produce competing spatial readings (structured geometry and
173
+ ambient light) that cancel each other's signal.
174
+
175
+ **A mesh where text contrast cannot be verified at all mesh positions** — mesh gradients
176
+ are not uniform; the lightest point and the darkest point may differ enough that text
177
+ legible over one is illegible over the other. Always verify contrast at the gradient's
178
+ lightest zone for dark text and its darkest zone for light text.
@@ -0,0 +1,150 @@
1
+ # Noise / grain texture
2
+
3
+ A grain texture overlays a fine noise pattern — generated by an SVG `feTurbulence` filter
4
+ embedded as a data URI — on a background at low opacity. The effect adds tactile quality
5
+ that flat fills and smooth gradients lack: the surface reads as material rather than
6
+ rendered, as though light is interacting with a slightly imperfect field. At the correct
7
+ opacity (3–8%), the grain reads as depth. Above 15% it reads as a compression artifact
8
+ or a low-resolution encoding error.
9
+
10
+ The technique was documented by Lea Verou and CSS-Tricks: "Grainy Gradients" —
11
+ `feTurbulence` as a composited texture layer, reducing gradient banding on smooth surfaces.
12
+ `expressive.md` cites this directly: "The CSS-Tricks SVG filter technique (an feTurbulence
13
+ filter composited at low opacity over a background) adds texture without weight."
14
+
15
+ ## When it earns its place / When it does not
16
+
17
+ Condition: flat backgrounds or smooth mesh gradients that read as too digital. Grain most
18
+ visibly earns its place over mesh gradients, where it reduces the banding that appears on
19
+ smooth colour transitions on LCD displays. Single-colour sections that feel like developer
20
+ backgrounds — white, off-white, near-black — also benefit: the grain makes the surface a
21
+ material decision rather than a default. Showpiece and confident registers. Hero sections.
22
+ Feature backgrounds. Sections where the surface itself should read as considered.
23
+
24
+ Condition against: over photography or video — grain is redundant over natural image
25
+ texture and reduces legibility. On text at any size — grain under type at small sizes
26
+ degrades the contrast relationship and makes text harder to read. At high opacity on any
27
+ element — above 15%, grain reads as an encoding problem. Any product context where
28
+ surface cleanliness is a trust signal (healthcare, finance, enterprise tooling) — grain
29
+ introduces noise that reads as imprecision.
30
+
31
+ ## Parameters
32
+
33
+ ```css
34
+ :root {
35
+ /* Grain opacity.
36
+ 3–5%: subtle, reads as material quality on close inspection.
37
+ 6–8%: visible grain that reads as deliberate texture.
38
+ 9–15%: strong texture, suitable for decorative elements only.
39
+ Above 15%: renders as compression artifact. Do not use. */
40
+ --grain-opacity: 0.05;
41
+
42
+ /* Turbulence frequency.
43
+ Low values (0.4–0.6): coarse grain, reads as film grain.
44
+ High values (0.7–0.9): fine grain, reads as paper texture.
45
+ Values above 1.0: noise too fine to read as texture — invisible. */
46
+ --grain-frequency: 0.65;
47
+
48
+ /* Number of octaves: detail layers in the noise function.
49
+ 2: smooth, gentle grain.
50
+ 4: more complex, film-like texture.
51
+ Values above 6: significant render cost; diminishing visual returns. */
52
+ --grain-octaves: 4;
53
+ }
54
+ ```
55
+
56
+ ## Implementation
57
+
58
+ The grain is an SVG filter rendered as a data URI and applied as a `::after` pseudo-element.
59
+ This approach requires no external file, no JavaScript, and works in all modern browsers.
60
+
61
+ ```css
62
+ /* ── Grain texture as a pseudo-element overlay ────────────────────────────── */
63
+ .grain {
64
+ position: relative;
65
+ isolation: isolate; /* ensures the pseudo-element stacks correctly */
66
+ }
67
+
68
+ .grain::after {
69
+ content: '';
70
+ position: absolute;
71
+ inset: 0;
72
+ z-index: 1; /* above the background, below the content */
73
+ pointer-events: none;
74
+ user-select: none;
75
+ opacity: var(--grain-opacity);
76
+
77
+ /* The SVG feTurbulence filter as a data URI.
78
+ fractalNoise: produces organic grain (vs turbulence which produces cloud-like noise).
79
+ seed: change this value to produce a different grain pattern on different sections. */
80
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23noise)'/%3E%3C/svg%3E");
81
+ background-size: 300px 300px; /* tile size: smaller = finer visual grain */
82
+ background-repeat: repeat;
83
+ }
84
+
85
+ /* ── Content must sit above the grain pseudo-element ─────────────────────── */
86
+ /* Any direct children of .grain that hold readable content need z-index: 2
87
+ to layer above the ::after grain overlay. */
88
+ .grain > * {
89
+ position: relative;
90
+ z-index: 2;
91
+ }
92
+ ```
93
+
94
+ ```html
95
+ <!-- Apply .grain to a section to add texture to its background. -->
96
+ <section class="mesh-bg grain">
97
+ <div class="section-content">
98
+ <h2>The surface is now material.</h2>
99
+ </div>
100
+ </section>
101
+
102
+ <!-- Or to a specific card: -->
103
+ <div class="card grain">
104
+ <h3>Card with grain texture.</h3>
105
+ </div>
106
+ ```
107
+
108
+ **Adjusting the grain:** To change the frequency or octave count without editing the data
109
+ URI, modify the SVG string in the background-image. The key parameters are `baseFrequency`
110
+ (mapped to `--grain-frequency`) and `numOctaves` (mapped to `--grain-octaves`). The data
111
+ URI must be regenerated when these values change — they are baked into the string, not
112
+ CSS variables. Consider a small build step or a Sass function if multiple grain densities
113
+ are needed.
114
+
115
+ **Performance:** The `feTurbulence` filter renders in the browser at paint time. On modern
116
+ hardware this is imperceptible. On older mobile hardware with large grain tiles, the
117
+ initial render may take 10–20ms. Tile the grain at 300×300px rather than applying it as
118
+ a full-viewport element to keep the painted area bounded. The `stitchTiles: stitch`
119
+ parameter ensures the tile seams are invisible at any scale.
120
+
121
+ ## Linter notes
122
+
123
+ - Grain opacity above 15% (`--grain-opacity > 0.15`) is a defect. At this level the
124
+ texture reads as a rendering error rather than a design decision. The `omd check`
125
+ system cannot detect opacity set inside a CSS variable directly, but a visual check
126
+ of any rendered surface should reject opacity this high.
127
+
128
+ - Grain applied directly to text containers degrades legibility. The grain pseudo-element
129
+ must be layered below content (`z-index` of the pseudo-element below content children).
130
+ Verify by running `omd check` — the contrast checker measures effective contrast
131
+ between text and the painted background, including overlaid grain.
132
+
133
+ - A grain-only texture on a white background with no other design decisions is not a
134
+ texture choice — it is an avoidance of a background decision. Grain earns its place
135
+ as a layer over a considered colour, not as a substitute for one.
136
+
137
+ ## Do not combine with
138
+
139
+ **gradient-mesh.md at grain opacity above 8%** — grain at 3–6% over a mesh gradient is
140
+ the canonical combination; it reduces banding and adds material quality. Above 8%, the
141
+ two textures begin to fight: the mesh's smooth colour transition and the grain's random
142
+ noise produce a muddy middle ground that reads as neither.
143
+
144
+ **drop-shadow on the same element** — grain texture and heavy box shadows are competing
145
+ depth signals. Both communicate material weight; using both says the element is both
146
+ thick with grain texture and elevated with shadow, which is contradictory. Choose one.
147
+
148
+ **photography or video backgrounds** — grain is a substitute for natural surface texture,
149
+ not a complement. Real photographic grain and synthetic `feTurbulence` grain at the same
150
+ opacity produce visual interference, not depth.
@@ -0,0 +1,185 @@
1
+ # Placeholder policy
2
+
3
+ A grey box is a design defect: it defers a decision the build must make before shipping,
4
+ and it ships as an admission that nobody decided what belongs here. A placeholder that
5
+ reaches production — as a grey rectangle, a "Coming soon" block, or a `background: #ccc`
6
+ div — is not a placeholder anymore. It is content that says the design is unfinished.
7
+
8
+ This recipe defines what to do instead. For every situation where a grey box would appear,
9
+ there is a deliberate alternative: a typographic block, a pattern fill, or a generated
10
+ gradient. Each carries more information about the intended content than a grey rectangle
11
+ and requires the designer to make a decision — which is the point.
12
+
13
+ ## When it earns its place / When it does not
14
+
15
+ Condition: any section where final imagery is not available at build time. Team photos
16
+ not yet shot. Product screenshots not yet captured. User-generated content zones in an
17
+ empty state. Hero sections awaiting final photography. The placeholder policy applies
18
+ to all of these: each one must ship as a deliberate, designed alternative to the missing
19
+ image, not as a grey box.
20
+
21
+ The placeholder policy is a forcing function. Deciding what to use instead of the grey
22
+ box requires answering: "What does this space communicate when the real content is
23
+ absent?" A typographic block communicates the category and scale of the expected content.
24
+ A pattern fill communicates that the space is structural, not content-bearing. A gradient
25
+ communicates the colour register of the expected content.
26
+
27
+ Condition against: this policy does not apply to actual content loading states (skeleton
28
+ screens during data fetch are correct — see `core/theory/layout.md` on loading states).
29
+ The policy targets design-time decisions, not runtime states.
30
+
31
+ ## Parameters
32
+
33
+ ```css
34
+ :root {
35
+ /* Typography placeholder: the font size should approximate the heading or body
36
+ size of the expected content, so the layout holds its rhythm even without
37
+ the real content. */
38
+ --placeholder-type-size: var(--text-xl, 1.25rem);
39
+ --placeholder-type-color: var(--text-secondary, rgba(0,0,0,0.35));
40
+
41
+ /* Pattern placeholder: same parameters as svg-geometric-patterns.md.
42
+ Use the same pattern token so the placeholder and a deliberate pattern
43
+ application are visually consistent. */
44
+ --placeholder-pattern-opacity: 0.06;
45
+
46
+ /* Gradient placeholder: a single-hue gradient using the brand palette.
47
+ Not a generic grey → white; the brand colour in a light wash is more
48
+ informative and more finished than any grey value. */
49
+ --placeholder-gradient-start: hsl(var(--hue-brand, 220deg) 20% 95%);
50
+ --placeholder-gradient-end: hsl(var(--hue-brand, 220deg) 10% 88%);
51
+ }
52
+ ```
53
+
54
+ ## Implementation
55
+
56
+ **Option 1: Typographic block** — for content zones that will hold a heading, a body
57
+ paragraph, or a feature statement. The typographic placeholder communicates scale and
58
+ category without pretending to be real content.
59
+
60
+ ```html
61
+ <!-- Instead of: <div class="hero-image" style="background:#ccc; height:400px;"></div> -->
62
+
63
+ <!-- Use: a typographic placeholder that holds the layout and signals intent. -->
64
+ <div class="placeholder placeholder--type" role="img" aria-label="Hero image: [brief description of intended content]">
65
+ <span class="placeholder__label">Hero image</span>
66
+ <span class="placeholder__detail">Photography: [subject or concept]</span>
67
+ </div>
68
+ ```
69
+
70
+ ```css
71
+ .placeholder--type {
72
+ display: flex;
73
+ flex-direction: column;
74
+ align-items: center;
75
+ justify-content: center;
76
+ /* Match the dimensions of the expected content exactly.
77
+ If the hero image is 100vw × 60vh, this placeholder is too. */
78
+ min-height: 60vh;
79
+ background-color: var(--placeholder-gradient-start);
80
+ background-image: linear-gradient(
81
+ 135deg,
82
+ var(--placeholder-gradient-start),
83
+ var(--placeholder-gradient-end)
84
+ );
85
+ border-radius: var(--radius-section, 0);
86
+ gap: var(--space-2, 0.5rem);
87
+ }
88
+
89
+ .placeholder__label {
90
+ font-size: var(--placeholder-type-size);
91
+ font-weight: var(--weight-medium, 500);
92
+ color: var(--placeholder-type-color);
93
+ letter-spacing: 0.04em;
94
+ text-transform: uppercase;
95
+ }
96
+
97
+ .placeholder__detail {
98
+ font-size: var(--text-sm, 0.875rem);
99
+ color: var(--placeholder-type-color);
100
+ opacity: 0.7;
101
+ }
102
+ ```
103
+
104
+ **Option 2: Pattern fill** — for content zones that are structural (a card image slot,
105
+ a sidebar graphic zone) where the space needs visual weight but no textual label.
106
+
107
+ ```html
108
+ <div class="placeholder placeholder--pattern" role="img" aria-label="[describe intended content]"></div>
109
+ ```
110
+
111
+ ```css
112
+ .placeholder--pattern {
113
+ /* Dimensions match the intended content zone. */
114
+ min-height: 200px;
115
+ background-color: var(--surface-subtle, #f5f5f3);
116
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='12' cy='12' r='1' fill='rgba(0%2C0%2C0%2C0.12)'/%3E%3C/svg%3E");
117
+ background-size: 24px 24px;
118
+ border-radius: var(--radius-card, 8px);
119
+ }
120
+ ```
121
+
122
+ **Option 3: Generated gradient** — for hero and full-bleed sections where the missing
123
+ image is photographic and the gradient conveys the intended colour register.
124
+
125
+ ```html
126
+ <section class="placeholder placeholder--gradient placeholder--hero" role="img" aria-label="[describe intended imagery]">
127
+ <div class="section-content">
128
+ <!-- Section content sits above the gradient placeholder background. -->
129
+ </div>
130
+ </section>
131
+ ```
132
+
133
+ ```css
134
+ .placeholder--gradient {
135
+ background-image: linear-gradient(
136
+ 160deg,
137
+ var(--placeholder-gradient-start) 0%,
138
+ var(--placeholder-gradient-end) 100%
139
+ );
140
+ }
141
+
142
+ .placeholder--hero {
143
+ min-height: 100svh;
144
+ }
145
+ ```
146
+
147
+ **Decision rule:** choose by content type and visibility:
148
+ - The expected content is a heading or copy block → **typographic block**.
149
+ - The expected content is an image in a card, thumbnail, or secondary zone → **pattern fill**.
150
+ - The expected content is a full-bleed hero or section background photo → **generated gradient**.
151
+ - When in doubt between pattern and gradient: gradient, because it communicates the
152
+ colour register of the eventual photography.
153
+
154
+ ## Linter notes
155
+
156
+ - A `background: #ccc`, `background: #d9d9d9`, `background: grey`, or any near-neutral
157
+ background on a container that has explicit height but no content is the signal the
158
+ `omd check` system identifies as a potential placeholder. The check looks for elements
159
+ with fixed-height, no semantic content, and a near-neutral background. If a legitimate
160
+ design element fires this check, record the reason with `omd decision`.
161
+
162
+ - Generated gradient placeholders that land in the SLOP-GRADIENT hue bands (indigo–violet,
163
+ purple–pink) should use the brand's actual hue variable (`var(--hue-brand)`) rather than
164
+ a hardcoded hue. The placeholder inherits the brand palette, so if the brand changes,
165
+ the placeholders update automatically.
166
+
167
+ - Every placeholder element must have a meaningful `aria-label` that describes the
168
+ intended content. A placeholder without an accessible description is both a design
169
+ defect and an accessibility failure — screen readers announce it as an unlabelled image.
170
+
171
+ ## Do not combine with
172
+
173
+ **Actual content** — when the real image or content is available, use it. The placeholder
174
+ policy does not mean "use a gradient everywhere instead of photography." Photography,
175
+ illustration, or purposeful imagery always supersedes a placeholder when it exists.
176
+
177
+ **Lorem ipsum text** — text placeholders are governed by the same principle: `hand.agent.yaml`
178
+ states "Placeholder copy is a defect. 'Lorem ipsum' and 'Your content here' never ship."
179
+ A typographic block that serves as an image placeholder is acceptable; a paragraph of
180
+ lorem ipsum text is not. If body copy is absent, use structural whitespace or a typographic
181
+ label indicating the content category, never placeholder prose.
182
+
183
+ **More than one placeholder type in the same layout zone** — choosing between typographic,
184
+ pattern, and gradient means choosing one. Stacking a pattern fill and a gradient on the
185
+ same placeholder element produces a background system, not a placeholder decision.