emily-css 1.0.15 → 1.0.18

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to `emily-css` are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v1.0.18 — May 2026
8
+
9
+ ****
10
+
11
+ ### Changed
12
+ - added more utitlies as a code block
13
+
14
+ ---
15
+ ## v1.0.17 — May 2026
16
+
17
+ **added new utilties, and added component patterns**
18
+
19
+ ### Added
20
+ - added new utilties, and added component patterns
21
+
22
+ ---
23
+ ## v1.0.16 — May 2026
24
+
25
+ **feat: add Round 2 utility set — 156/156 tests passing**
26
+
27
+ ### Added
28
+ - feat: add Round 2 utility set — 156/156 tests passing
29
+
30
+ ---
7
31
  ## v1.0.15 — May 2026
8
32
 
9
33
  **updated readme**
package/README.md CHANGED
@@ -20,7 +20,7 @@ emilyCSS is built for real-world systems like **Drupal, Power Pages, WordPress,
20
20
  npx emily-css init
21
21
  ```
22
22
 
23
- This creates your `emily.config.json`, walks you through your brand settings, and runs your first build.
23
+ This creates your `emily.config.json`, walks you through your brand settings (colours, fonts, spacing, etc.), and runs your first build.
24
24
 
25
25
  ### 2. Link the CSS
26
26
 
@@ -28,17 +28,13 @@ This creates your `emily.config.json`, walks you through your brand settings, an
28
28
  <link rel="stylesheet" href="./dist/emily.min.css">
29
29
  ```
30
30
 
31
- ### 3. Development
31
+ ### 3. Start Building
32
32
 
33
- ```bash
34
- npx emily-css watch # Rebuilds automatically on config/template changes
35
- npx emily-css build # Manual rebuild
36
- ```
37
-
38
- Open the showcase:
33
+ Use the generated utilities and browse the showcase for ready-to-copy components.
39
34
 
40
35
  ```bash
41
- npm run emily:showcase # Serves at http://localhost:3456
36
+ npx emily-css build # Rebuild after config changes
37
+ npx emily-css watch # Watch mode for development
42
38
  ```
43
39
 
44
40
  ## Core Features
@@ -48,30 +44,24 @@ npm run emily:showcase # Serves at http://localhost:3456
48
44
  - **Accessibility First** — Focus-visible rings, motion utilities, WCAG 2.2 AA colours
49
45
  - **No Build Pipeline Required** — Just a static CSS file
50
46
  - **Smart Purge** — Remove unused utilities for tiny production files
51
- - **UI Starter Kit** — Copy-paste accessible components from the showcase
47
+ - **UI Starter Kit** — Copy-paste accessible components from showcase.html
52
48
 
53
49
  ## Commands
54
50
 
55
51
  ```bash
56
- npx emily-css init # Setup + first build
57
- npx emily-css build # Regenerate CSS
58
- npx emily-css watch # Development watch mode
59
- npx emily-css purge # Remove unused styles for production
52
+ npx emily-css init # Setup config + first build
53
+ npx emily-css build # Regenerate CSS
54
+ npx emily-css watch # Development watch mode
55
+ npx emily-css purge # Remove unused styles for production
60
56
  ```
61
57
 
62
58
  ## How Purge Works
63
59
 
64
60
  emilyCSS scans your templates for used class names and removes everything else.
65
61
 
66
- Configure it in `emily.config.json`:
67
-
68
- ```json
69
- "purge": {
70
- "extensions": [".html", ".php", ".twig", ".liquid", ".jsx", ".vue", ".astro"]
71
- }
72
- ```
62
+ Supported files: `.html`, `.php`, `.twig`, `.liquid`, `.jsx`, `.vue`, `.astro`, etc. (configurable).
73
63
 
74
- **Important:** Dynamically constructed classes (e.g. `bg-${colour}`) are not detected. Use static strings or add them to your safelist.
64
+ **Important:** Dynamically constructed classes like `bg-${colour}` are not detected. Use static strings or add them to the safelist.
75
65
 
76
66
  ## File Size (Typical)
77
67
 
@@ -80,7 +70,9 @@ Configure it in `emily.config.json`:
80
70
  | Full build | ~1.1 MB |
81
71
  | After purge | 10–50 KB |
82
72
 
83
- ## Configuration Example
73
+ ## Configuration
74
+
75
+ Edit `emily.config.json`:
84
76
 
85
77
  ```json
86
78
  {
@@ -95,7 +87,7 @@ Configure it in `emily.config.json`:
95
87
  "neutral": "#57534E"
96
88
  },
97
89
  "purge": {
98
- "extensions": [".html", ".php", ".jsx", ".vue"]
90
+ "content": ["./**/*.{html,php,jsx,tsx,vue}"]
99
91
  }
100
92
  }
101
93
  ```
@@ -104,12 +96,12 @@ After changes: `npx emily-css build`
104
96
 
105
97
  ## Component Showcase
106
98
 
107
- After building, run `npm run emily:showcase` and visit `http://localhost:3456`. It contains production-ready, accessible components built with your exact brand.
99
+ After your first build, open `showcase.html` in your browser. It contains production-ready, accessible components (buttons, forms, alerts, cards, etc.) built with your brand.
108
100
 
109
101
  ## EmilyUI vs emilyCSS
110
102
 
111
103
  - **EmilyUI** — The broader brand / ecosystem
112
- - **emilyCSS** — The current product (`emily-css` npm package + CLI)
104
+ - **emilyCSS** — The current product (the emily-css npm package + CLI)
113
105
 
114
106
  ## Example Components
115
107
 
@@ -124,7 +116,7 @@ After building, run `npm run emily:showcase` and visit `http://localhost:3456`.
124
116
  ### Responsive Card
125
117
 
126
118
  ```html
127
- <div class="w-full md:w-96 p-6 rounded-lg bg-white border border-neutral-30 shadow-sm">
119
+ <div class="w-full md:w-96 p-6 rounded-xl bg-white border border-neutral-20 shadow-sm">
128
120
  <h2 class="text-2xl font-semibold text-neutral-90">Card Title</h2>
129
121
  <p class="mt-3 text-neutral-70">Content goes here.</p>
130
122
  </div>
@@ -132,7 +124,7 @@ After building, run `npm run emily:showcase` and visit `http://localhost:3456`.
132
124
 
133
125
  ## Fonts
134
126
 
135
- emilyCSS applies font stacks but does not include font files. Use `@fontsource` (recommended):
127
+ emilyCSS applies font stacks but does not include font files. Recommended approach:
136
128
 
137
129
  ```bash
138
130
  npm install @fontsource/inter @fontsource/lexend
@@ -147,4 +139,4 @@ Then import the weights you need.
147
139
 
148
140
  ## License
149
141
 
150
- MIT
142
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emily-css",
3
- "version": "1.0.15",
3
+ "version": "1.0.18",
4
4
  "description": "A config-driven utility CSS framework. Define your brand once, generate the CSS.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/generators.js CHANGED
@@ -65,6 +65,7 @@ function sizingUtilities(spacing) {
65
65
  css += `.max-w-5xl { max-width: 64rem; }\n`;
66
66
  css += `.max-w-6xl { max-width: 72rem; }\n`;
67
67
  css += `.max-w-7xl { max-width: 80rem; }\n`;
68
+ css += `.max-w-prose { max-width: 65ch; }\n`;
68
69
 
69
70
  // Aspect ratio
70
71
  css += `.aspect-auto { aspect-ratio: auto; }\n`;
@@ -137,6 +138,7 @@ function overflowUtilities() {
137
138
  .overflow-y-auto { overflow-y: auto; }
138
139
  .overflow-y-hidden { overflow-y: hidden; }
139
140
  .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
141
+ .line-clamp-1 { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
140
142
  .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
141
143
  .line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
142
144
  .line-clamp-4 { display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
@@ -148,7 +150,7 @@ function overflowUtilities() {
148
150
 
149
151
  // Opacity
150
152
  function opacityUtilities() {
151
- const opacities = [0, 5, 10, 25, 50, 75, 90, 95, 100];
153
+ const opacities = [0, 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95, 100];
152
154
  let css = `/* Opacity */\n`;
153
155
 
154
156
  opacities.forEach(op => {
@@ -277,6 +279,11 @@ function ringUtilities(colours) {
277
279
  css += `.outline-0 { outline-width: 0; }\n`;
278
280
  css += `.outline-1 { outline-width: 1px; }\n`;
279
281
  css += `.outline-2 { outline-width: 2px; }\n`;
282
+ css += `.outline-offset-0 { outline-offset: 0px; }\n`;
283
+ css += `.outline-offset-1 { outline-offset: 1px; }\n`;
284
+ css += `.outline-offset-2 { outline-offset: 2px; }\n`;
285
+ css += `.outline-offset-4 { outline-offset: 4px; }\n`;
286
+ css += `.outline-offset-8 { outline-offset: 8px; }\n`;
280
287
 
281
288
  css += `\n`;
282
289
  return css;
@@ -353,6 +360,8 @@ function svgUtilities(colours) {
353
360
  function formUtilities() {
354
361
  return `/* Forms */
355
362
  .appearance-none { appearance: none; }
363
+ .caret-transparent { caret-color: transparent; }
364
+ .caret-current { caret-color: currentColor; }
356
365
  .placeholder-transparent::placeholder { color: transparent; }
357
366
  .placeholder-current::placeholder { color: currentColor; }
358
367
  .autofill\\:bg-transparent:autofill { background-color: transparent !important; }
@@ -398,6 +407,10 @@ function contentScrollUtilities() {
398
407
  .snap-both { scroll-snap-type: both var(--emily-scroll-snap-strictness); }
399
408
  .snap-mandatory { --emily-scroll-snap-strictness: mandatory; }
400
409
  .snap-proximity { --emily-scroll-snap-strictness: proximity; }
410
+ .snap-start { scroll-snap-align: start; }
411
+ .snap-center { scroll-snap-align: center; }
412
+ .snap-end { scroll-snap-align: end; }
413
+ .snap-align-none { scroll-snap-align: none; }
401
414
 
402
415
  `;
403
416
  }
@@ -442,6 +455,16 @@ function cursorUtilities() {
442
455
  .select-text { user-select: text; }
443
456
  .select-all { user-select: all; }
444
457
  .select-auto { user-select: auto; }
458
+ .resize-none { resize: none; }
459
+ .resize { resize: both; }
460
+ .resize-x { resize: horizontal; }
461
+ .resize-y { resize: vertical; }
462
+ .isolate { isolation: isolate; }
463
+ .isolation-auto { isolation: auto; }
464
+ .will-change-auto { will-change: auto; }
465
+ .will-change-scroll { will-change: scroll-position; }
466
+ .will-change-contents { will-change: contents; }
467
+ .will-change-transform { will-change: transform; }
445
468
 
446
469
  `;
447
470
  }
@@ -453,6 +476,15 @@ function accessibilityUtilities() {
453
476
  .not-sr-only { position: static; width: auto; height: auto; padding: 0; margin: 0; overflow: visible; clip: auto; white-space: normal; }
454
477
  .focus-visible:focus { outline: 2px solid currentColor; outline-offset: 2px; }
455
478
  .focus\\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; }
479
+
480
+ /* Touch target — WCAG 2.2 SC 2.5.8 minimum 24x24px hit area */
481
+ .touch-target { position: relative; }
482
+ .touch-target::before { content: ''; position: absolute; top: 50%; left: 50%; width: max(100%, 24px); height: max(100%, 24px); transform: translate(-50%, -50%); }
483
+
484
+ /* Skip link — reveals on focus for keyboard/AT users */
485
+ .skip-link { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
486
+ .skip-link:focus { position: fixed; top: 1rem; left: 1rem; z-index: 1070; width: auto; height: auto; padding: 0.75rem 1.25rem; background-color: #ffffff; color: #000000; font-weight: 700; text-decoration: underline; border: 2px solid currentColor; border-radius: 4px; clip: auto; white-space: normal; }
487
+
456
488
  @media (prefers-reduced-motion: reduce) {
457
489
  .motion-reduce\\:transition-none { transition-property: none; }
458
490
  .motion-reduce\\:animate-none { animation: none; }
@@ -515,6 +547,175 @@ function codeUtilities() {
515
547
  `;
516
548
  }
517
549
 
550
+ // Animations
551
+ function animationUtilities() {
552
+ return `/* Animations — keyframes */
553
+ @keyframes spin {
554
+ to { transform: rotate(360deg); }
555
+ }
556
+ @keyframes ping {
557
+ 75%, 100% { transform: scale(2); opacity: 0; }
558
+ }
559
+ @keyframes pulse {
560
+ 50% { opacity: 0.5; }
561
+ }
562
+ @keyframes bounce {
563
+ 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); }
564
+ 50% { transform: translateY(0); animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }
565
+ }
566
+
567
+ /* Animations — utilities */
568
+ .animate-none { animation: none; }
569
+ .animate-spin { animation: spin 1s linear infinite; }
570
+ .animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; }
571
+ .animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
572
+ .animate-bounce { animation: bounce 1s infinite; }
573
+
574
+ `;
575
+ }
576
+
577
+
578
+ // Backdrop Filters
579
+ function backdropUtilities() {
580
+ return `/* Backdrop Filters */
581
+ .backdrop-blur-none { backdrop-filter: blur(0); }
582
+ .backdrop-blur-sm { backdrop-filter: blur(4px); }
583
+ .backdrop-blur { backdrop-filter: blur(8px); }
584
+ .backdrop-blur-md { backdrop-filter: blur(12px); }
585
+ .backdrop-blur-lg { backdrop-filter: blur(16px); }
586
+ .backdrop-blur-xl { backdrop-filter: blur(24px); }
587
+ .backdrop-blur-2xl { backdrop-filter: blur(40px); }
588
+
589
+ `;
590
+ }
591
+
592
+ // Space Between
593
+ function spaceUtilities(spacing) {
594
+ let css = `/* Space Between */\n`;
595
+ Object.entries(spacing).forEach(([key, value]) => {
596
+ const escaped = key.replace(/\./g, '\\.');
597
+ css += `.space-x-${escaped} > * + * { margin-left: ${value}; }\n`;
598
+ css += `.space-y-${escaped} > * + * { margin-top: ${value}; }\n`;
599
+ css += `.-space-x-${escaped} > * + * { margin-left: -${value}; }\n`;
600
+ css += `.-space-y-${escaped} > * + * { margin-top: -${value}; }\n`;
601
+ });
602
+ css += `.space-x-auto > * + * { margin-left: auto; }\n`;
603
+ css += `.space-y-auto > * + * { margin-top: auto; }\n`;
604
+ css += `\n`;
605
+ return css;
606
+ }
607
+
608
+ // Divide
609
+ function divideUtilities(spacing, colours) {
610
+ let css = `/* Divide */\n`;
611
+ // Widths
612
+ css += `.divide-x > * + * { border-left-width: 1px; border-left-style: solid; }\n`;
613
+ css += `.divide-y > * + * { border-top-width: 1px; border-top-style: solid; }\n`;
614
+ css += `.divide-x-0 > * + * { border-left-width: 0px; }\n`;
615
+ css += `.divide-y-0 > * + * { border-top-width: 0px; }\n`;
616
+ css += `.divide-x-2 > * + * { border-left-width: 2px; border-left-style: solid; }\n`;
617
+ css += `.divide-y-2 > * + * { border-top-width: 2px; border-top-style: solid; }\n`;
618
+ css += `.divide-x-4 > * + * { border-left-width: 4px; border-left-style: solid; }\n`;
619
+ css += `.divide-y-4 > * + * { border-top-width: 4px; border-top-style: solid; }\n`;
620
+ // Styles
621
+ css += `.divide-solid > * + * { border-style: solid; }\n`;
622
+ css += `.divide-dashed > * + * { border-style: dashed; }\n`;
623
+ css += `.divide-dotted > * + * { border-style: dotted; }\n`;
624
+ css += `.divide-none > * + * { border-style: none; }\n`;
625
+ // Colours
626
+ Object.entries(colours).forEach(([colourName, shades]) => {
627
+ Object.entries(shades).forEach(([shade]) => {
628
+ css += `.divide-${colourName}-${shade} > * + * { border-color: var(--color-${colourName}-${shade}); }\n`;
629
+ });
630
+ });
631
+ css += `.divide-white > * + * { border-color: #ffffff; }\n`;
632
+ css += `.divide-black > * + * { border-color: #000000; }\n`;
633
+ css += `.divide-transparent > * + * { border-color: transparent; }\n`;
634
+ css += `\n`;
635
+ return css;
636
+ }
637
+
638
+ // Background Utilities
639
+ function backgroundUtilities() {
640
+ return `/* Background */
641
+ .bg-fixed { background-attachment: fixed; }
642
+ .bg-local { background-attachment: local; }
643
+ .bg-scroll { background-attachment: scroll; }
644
+ .bg-clip-border { background-clip: border-box; }
645
+ .bg-clip-padding { background-clip: padding-box; }
646
+ .bg-clip-content { background-clip: content-box; }
647
+ .bg-clip-text { -webkit-background-clip: text; background-clip: text; }
648
+ .bg-repeat { background-repeat: repeat; }
649
+ .bg-no-repeat { background-repeat: no-repeat; }
650
+ .bg-repeat-x { background-repeat: repeat-x; }
651
+ .bg-repeat-y { background-repeat: repeat-y; }
652
+ .bg-repeat-round { background-repeat: round; }
653
+ .bg-repeat-space { background-repeat: space; }
654
+ .bg-auto { background-size: auto; }
655
+ .bg-cover { background-size: cover; }
656
+ .bg-contain { background-size: contain; }
657
+ .bg-center { background-position: center; }
658
+ .bg-top { background-position: top; }
659
+ .bg-bottom { background-position: bottom; }
660
+ .bg-left { background-position: left; }
661
+ .bg-right { background-position: right; }
662
+ .bg-left-top { background-position: left top; }
663
+ .bg-left-bottom { background-position: left bottom; }
664
+ .bg-right-top { background-position: right top; }
665
+ .bg-right-bottom { background-position: right bottom; }
666
+
667
+ `;
668
+ }
669
+
670
+ // CSS Filters
671
+ function filterUtilities() {
672
+ return `/* Filters */
673
+ .filter-none { filter: none; }
674
+ .blur-none { filter: blur(0); }
675
+ .blur-sm { filter: blur(4px); }
676
+ .blur { filter: blur(8px); }
677
+ .blur-md { filter: blur(12px); }
678
+ .blur-lg { filter: blur(16px); }
679
+ .blur-xl { filter: blur(24px); }
680
+ .brightness-0 { filter: brightness(0); }
681
+ .brightness-50 { filter: brightness(.5); }
682
+ .brightness-75 { filter: brightness(.75); }
683
+ .brightness-90 { filter: brightness(.9); }
684
+ .brightness-100 { filter: brightness(1); }
685
+ .brightness-110 { filter: brightness(1.1); }
686
+ .brightness-125 { filter: brightness(1.25); }
687
+ .brightness-150 { filter: brightness(1.5); }
688
+ .brightness-200 { filter: brightness(2); }
689
+ .contrast-0 { filter: contrast(0); }
690
+ .contrast-50 { filter: contrast(.5); }
691
+ .contrast-75 { filter: contrast(.75); }
692
+ .contrast-100 { filter: contrast(1); }
693
+ .contrast-125 { filter: contrast(1.25); }
694
+ .contrast-150 { filter: contrast(1.5); }
695
+ .contrast-200 { filter: contrast(2); }
696
+ .grayscale-0 { filter: grayscale(0); }
697
+ .grayscale { filter: grayscale(100%); }
698
+ .invert-0 { filter: invert(0); }
699
+ .invert { filter: invert(100%); }
700
+ .sepia-0 { filter: sepia(0); }
701
+ .sepia { filter: sepia(100%); }
702
+ .saturate-0 { filter: saturate(0); }
703
+ .saturate-50 { filter: saturate(.5); }
704
+ .saturate-100 { filter: saturate(1); }
705
+ .saturate-150 { filter: saturate(1.5); }
706
+ .saturate-200 { filter: saturate(2); }
707
+ .hue-rotate-0 { filter: hue-rotate(0deg); }
708
+ .hue-rotate-15 { filter: hue-rotate(15deg); }
709
+ .hue-rotate-30 { filter: hue-rotate(30deg); }
710
+ .hue-rotate-60 { filter: hue-rotate(60deg); }
711
+ .hue-rotate-90 { filter: hue-rotate(90deg); }
712
+ .hue-rotate-180 { filter: hue-rotate(180deg); }
713
+ .-hue-rotate-30 { filter: hue-rotate(-30deg); }
714
+ .-hue-rotate-60 { filter: hue-rotate(-60deg); }
715
+ .-hue-rotate-90 { filter: hue-rotate(-90deg); }
716
+
717
+ `;
718
+ }
518
719
  module.exports = {
519
720
  displayUtilities,
520
721
  sizingUtilities,
@@ -535,5 +736,11 @@ module.exports = {
535
736
  cursorUtilities,
536
737
  accessibilityUtilities,
537
738
  containerUtilities,
538
- codeUtilities
739
+ codeUtilities,
740
+ animationUtilities,
741
+ backdropUtilities,
742
+ spaceUtilities,
743
+ divideUtilities,
744
+ backgroundUtilities,
745
+ filterUtilities,
539
746
  };
package/src/index.js CHANGED
@@ -288,7 +288,13 @@ const {
288
288
  cursorUtilities,
289
289
  accessibilityUtilities,
290
290
  containerUtilities,
291
- codeUtilities
291
+ codeUtilities,
292
+ animationUtilities,
293
+ backdropUtilities,
294
+ spaceUtilities,
295
+ divideUtilities,
296
+ backgroundUtilities,
297
+ filterUtilities,
292
298
  } = require('./generators');
293
299
 
294
300
  // ============================================================================
@@ -498,6 +504,27 @@ function generateTypographyUtilities(config) {
498
504
  css += `.underline { text-decoration: underline; }\n`;
499
505
  css += `.no-underline { text-decoration: none; }\n`;
500
506
  css += `.line-through { text-decoration: line-through; }\n`;
507
+ css += `.underline-offset-auto { text-underline-offset: auto; }\n`;
508
+ css += `.underline-offset-1 { text-underline-offset: 1px; }\n`;
509
+ css += `.underline-offset-2 { text-underline-offset: 2px; }\n`;
510
+ css += `.underline-offset-4 { text-underline-offset: 4px; }\n`;
511
+ css += `.underline-offset-8 { text-underline-offset: 8px; }\n`;
512
+ css += `.decoration-auto { text-decoration-thickness: auto; }\n`;
513
+ css += `.decoration-from-font { text-decoration-thickness: from-font; }\n`;
514
+ css += `.decoration-1 { text-decoration-thickness: 1px; }\n`;
515
+ css += `.decoration-2 { text-decoration-thickness: 2px; }\n`;
516
+ css += `.decoration-4 { text-decoration-thickness: 4px; }\n`;
517
+
518
+ // Font variant numeric
519
+ css += `.normal-nums { font-variant-numeric: normal; }\n`;
520
+ css += `.ordinal { font-variant-numeric: ordinal; }\n`;
521
+ css += `.slashed-zero { font-variant-numeric: slashed-zero; }\n`;
522
+ css += `.lining-nums { font-variant-numeric: lining-nums; }\n`;
523
+ css += `.oldstyle-nums { font-variant-numeric: oldstyle-nums; }\n`;
524
+ css += `.proportional-nums { font-variant-numeric: proportional-nums; }\n`;
525
+ css += `.tabular-nums { font-variant-numeric: tabular-nums; }\n`;
526
+ css += `.diagonal-fractions { font-variant-numeric: diagonal-fractions; }\n`;
527
+ css += `.stacked-fractions { font-variant-numeric: stacked-fractions; }\n`;
501
528
 
502
529
  // Text transform
503
530
  css += `.uppercase { text-transform: uppercase; }\n`;
@@ -739,6 +766,7 @@ function addStateVariants(css) {
739
766
  const states = [
740
767
  { name: 'hover', selector: ':hover' },
741
768
  { name: 'focus', selector: ':focus' },
769
+ { name: 'focus-within', selector: ':focus-within' },
742
770
  { name: 'focus-visible', selector: ':focus-visible' },
743
771
  { name: 'active', selector: ':active' },
744
772
  { name: 'disabled', selector: ':disabled' }
@@ -774,6 +802,63 @@ function addStateVariants(css) {
774
802
  return variantCss;
775
803
  }
776
804
 
805
+
806
+ // ============================================================================
807
+ // PATTERN COMPONENTS
808
+ // ============================================================================
809
+ // Composite classes that combine multiple utilities into named patterns.
810
+ // These live in @layer components so utilities always take precedence in the cascade.
811
+ // Gap values reference spacing variables generated from emily.config.json,
812
+ // with pixel fallbacks so they work even without the variables in scope.
813
+
814
+ function generatePatternComponents() {
815
+ return `
816
+ /* ---- Centering ---- */
817
+
818
+ /* Full-viewport overlay centering — use for modals, lightboxes, toasts */
819
+ .center-screen {
820
+ position: fixed;
821
+ inset: 0;
822
+ display: flex;
823
+ align-items: center;
824
+ justify-content: center;
825
+ }
826
+
827
+ /* Transform-based centering within a relative/absolute parent */
828
+ .center-absolute {
829
+ position: absolute;
830
+ top: 50%;
831
+ left: 50%;
832
+ transform: translate(-50%, -50%);
833
+ }
834
+
835
+ /* ---- Reading / Prose ---- */
836
+
837
+ /* Comfortable reading column — limits line length, centers the block */
838
+ .prose {
839
+ max-width: 65ch;
840
+ margin-inline: auto;
841
+ }
842
+
843
+ /* ---- Composition ---- */
844
+
845
+ /* Vertical stack with consistent gap — replaces manual margin chains */
846
+ .stack {
847
+ display: flex;
848
+ flex-direction: column;
849
+ gap: var(--space-4, 1rem);
850
+ }
851
+
852
+ /* Horizontal grouping with wrapping — for tags, button rows, icon lists */
853
+ .cluster {
854
+ display: flex;
855
+ flex-wrap: wrap;
856
+ gap: var(--space-4, 1rem);
857
+ align-items: center;
858
+ }
859
+ `;
860
+ }
861
+
777
862
  // ============================================================================
778
863
  // BUILD FUNCTION
779
864
  // ============================================================================
@@ -831,6 +916,12 @@ function buildFullFramework() {
831
916
  utilityCss += accessibilityUtilities();
832
917
  utilityCss += containerUtilities();
833
918
  utilityCss += codeUtilities();
919
+ utilityCss += animationUtilities();
920
+ utilityCss += backdropUtilities();
921
+ utilityCss += spaceUtilities(spacing);
922
+ utilityCss += divideUtilities(spacing, colours);
923
+ utilityCss += backgroundUtilities();
924
+ utilityCss += filterUtilities();
834
925
 
835
926
  // Add state, dark mode, and responsive variants to utilities
836
927
  utilityCss = addStateVariants(utilityCss);
@@ -892,19 +983,40 @@ function buildFullFramework() {
892
983
  overflow-wrap: break-word;
893
984
  }
894
985
 
895
- /* Code blocks VSCode Dark+ style by default */
986
+ /* Code — terminal style by default */
987
+ code {
988
+ font-family: "Menlo", "Monaco", "Courier New", monospace;
989
+ font-size: 0.875em;
990
+ background-color: #0d0c0b;
991
+ color: #a3c986;
992
+ padding: 0.125rem 0.4rem;
993
+ border-radius: 4px;
994
+ display: inline;
995
+ }
996
+
997
+ /* Block code — terminal command style, no extra classes needed */
998
+ code.block {
999
+ display: block;
1000
+ padding: 0.625rem 1rem;
1001
+ border-radius: 6px;
1002
+ font-size: 0.8125rem;
1003
+ line-height: 1.6;
1004
+ }
1005
+
1006
+ /* Pre — wraps multi-line code, consistent terminal look */
896
1007
  pre {
897
- background-color: #1e1e1e;
898
- color: #d4d4d4;
1008
+ background-color: #0d0c0b;
1009
+ color: #e4e0db;
899
1010
  padding: 1.25rem;
900
- border-radius: 0 0 6px;
1011
+ border-radius: 6px;
901
1012
  overflow-x: auto;
902
1013
  font-family: "Menlo", "Monaco", "Courier New", monospace;
903
1014
  font-size: 0.875rem;
904
1015
  line-height: 1.7;
905
- border: 1px solid #333;
1016
+ border: 1px solid #2a2520;
906
1017
  }
907
1018
 
1019
+ /* Reset code inside pre — inherits pre's colours */
908
1020
  pre code {
909
1021
  background: none;
910
1022
  padding: 0;
@@ -912,16 +1024,7 @@ function buildFullFramework() {
912
1024
  color: inherit;
913
1025
  font-size: inherit;
914
1026
  font-family: inherit;
915
- }
916
-
917
- /* Inline code */
918
- code {
919
- font-family: "Menlo", "Monaco", "Courier New", monospace;
920
- font-size: 0.875em;
921
- background-color: #2d2d2d;
922
- color: #d4d4d4;
923
- padding: 0.125rem 0.375rem;
924
- border-radius: 4px;
1027
+ display: inline;
925
1028
  }
926
1029
  ${bodyFont}`;
927
1030
 
@@ -931,7 +1034,7 @@ ${bodyFont}`;
931
1034
  css += `@layer theme {\n${variablesCss}}\n\n`;
932
1035
  const baseStylesCss = generateBaseStyles(config);
933
1036
  css += `@layer base {${baseCss}${baseStylesCss}}\n\n`;
934
- css += `@layer components {\n /* Reserved for component styles in a future release. */\n}\n\n`;
1037
+ css += `@layer components {\n${generatePatternComponents()}}\n\n`;
935
1038
  css += `@layer utilities {\n${utilityCss}}\n`;
936
1039
 
937
1040
  // Write output