@zvndev/darkmown 0.20.0 → 1.0.0

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 (57) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +115 -5
  4. package/docs/spec-alignment.md +16 -3
  5. package/package.json +3 -3
  6. package/site/_/base.skin +372 -0
  7. package/site/_/base.wd +4 -0
  8. package/site/_/footer.wd +14 -0
  9. package/site/_/metrics.json +13 -0
  10. package/site/_/nav.skin +35 -12
  11. package/site/_/nav.wd +8 -10
  12. package/site/_/products.json +10 -0
  13. package/site/pages/app.wd +1 -1
  14. package/site/pages/carousel/index.js +82 -0
  15. package/site/pages/carousel/index.skin +185 -0
  16. package/site/pages/carousel/index.wd +109 -0
  17. package/site/pages/carousel/slide-1.jpg +0 -0
  18. package/site/pages/carousel/slide-2.jpg +0 -0
  19. package/site/pages/carousel/slide-3.jpg +0 -0
  20. package/site/pages/carousel/slide-4.jpg +0 -0
  21. package/site/pages/carousel/slide-5.jpg +0 -0
  22. package/site/pages/compass/index.skin +247 -0
  23. package/site/pages/compass/index.wd +194 -0
  24. package/site/pages/docs/index.skin +48 -28
  25. package/site/pages/docs/index.wd +71 -5
  26. package/site/pages/folio/-style.skin +375 -0
  27. package/site/pages/folio/-style.wd +6 -0
  28. package/site/pages/folio/cart.skin +309 -0
  29. package/site/pages/folio/cart.wd +201 -0
  30. package/site/pages/folio/index.skin +24 -0
  31. package/site/pages/folio/index.wd +167 -0
  32. package/site/pages/forge/index.skin +676 -0
  33. package/site/pages/forge/index.wd +330 -0
  34. package/site/pages/index.skin +135 -144
  35. package/site/pages/index.wd +17 -5
  36. package/site/pages/ledger/index.skin +257 -0
  37. package/site/pages/ledger/index.wd +174 -0
  38. package/site/pages/media/clip-poster.jpg +0 -0
  39. package/site/pages/media/clip.mp4 +0 -0
  40. package/site/pages/media/index.skin +82 -0
  41. package/site/pages/media/index.wd +84 -0
  42. package/site/pages/media/tone.mp3 +0 -0
  43. package/site/pages/pulse/index.skin +434 -0
  44. package/site/pages/pulse/index.wd +178 -0
  45. package/site/pages/showcase/index.skin +130 -0
  46. package/site/pages/showcase/index.wd +168 -0
  47. package/src/compiler/body.js +26 -1
  48. package/src/compiler/context.js +1 -1
  49. package/src/compiler/directives.js +199 -3
  50. package/src/compiler/format.js +244 -0
  51. package/src/compiler/interpolation.js +35 -1
  52. package/src/compiler/loops.js +95 -24
  53. package/src/compiler/markdown.js +16 -7
  54. package/src/compiler/predicates.js +35 -2
  55. package/src/headers.js +5 -0
  56. package/src/runtime.js +168 -14
  57. package/src/skin.js +9 -1
@@ -0,0 +1,247 @@
1
+ /* ===================================================================
2
+ Compass — a guided product-finder quiz.
3
+ Identity: playful & warm. A big friendly rounded card centered on a
4
+ soft gradient backdrop, large question type, satisfying pill answers,
5
+ a celebratory result. Distinct accent ($brand, warm amber) scoped to
6
+ .app-compass so it never leaks into the shared design system. Inherits
7
+ tokens + components from base.skin; dark overrides verified below.
8
+ =================================================================== */
9
+
10
+ .app-compass
11
+ --brand #e0894a
12
+ --brand-deep #c46f33
13
+ --brand-soft #fbeede
14
+ --glow-1 #fcefe0
15
+ --glow-2 #f6e7f1
16
+ --glow-3 #f9f4ea
17
+
18
+ @media (prefers-color-scheme: dark)
19
+ .app-compass
20
+ --brand #f0a868
21
+ --brand-deep #f6c08c
22
+ --brand-soft #2e2014
23
+ --glow-1 #1b1610
24
+ --glow-2 #181219
25
+ --glow-3 #14110d
26
+
27
+ /* ── The stage: a soft gradient backdrop, card floated dead center ──── */
28
+
29
+ .app-compass
30
+ max-width 100%
31
+ padding 0
32
+
33
+ .compass-stage
34
+ min-height calc(100vh - 60px)
35
+ display flex
36
+ align-items center
37
+ justify-content center
38
+ padding 3rem 1.25rem 4rem
39
+ background radial-gradient(120% 90% at 15% 0%, var(--glow-1) 0%, transparent 55%), radial-gradient(110% 90% at 95% 10%, var(--glow-2) 0%, transparent 50%), radial-gradient(140% 120% at 50% 110%, var(--glow-3) 0%, transparent 60%)
40
+
41
+ /* ── The card ───────────────────────────────────────────────────────── */
42
+
43
+ .compass-card
44
+ width 100%
45
+ max-width 560px
46
+ background $panel
47
+ border 1px solid $line
48
+ border-radius 28px
49
+ padding 2.75rem 2.5rem 2.5rem
50
+ box-shadow 0 1px 0 rgba(255,255,255,.6) inset, 0 24px 60px -28px rgba(28,24,20,.45), 0 8px 22px -16px rgba(28,24,20,.3)
51
+ text-align center
52
+
53
+ @media (prefers-color-scheme: dark)
54
+ .compass-card
55
+ box-shadow 0 1px 0 rgba(255,255,255,.04) inset, 0 28px 70px -30px rgba(0,0,0,.7)
56
+
57
+ @media (max-width: 540px)
58
+ .compass-card
59
+ padding 2rem 1.4rem 1.75rem
60
+ border-radius 22px
61
+
62
+ /* ── Top: eyebrow + progress rail ───────────────────────────────────── */
63
+
64
+ .compass-top
65
+ margin-bottom 2rem
66
+
67
+ .compass-eyebrow
68
+ display inline-block
69
+ text-transform uppercase
70
+ letter-spacing .16em
71
+ font-size .72rem
72
+ font-weight 700
73
+ color var(--brand-deep)
74
+ margin 0 0 1rem
75
+
76
+ .compass-eyebrow.is-result
77
+ color var(--brand-deep)
78
+
79
+ .compass-rail
80
+ position relative
81
+ height 8px
82
+ border-radius 999px
83
+ background $sunk
84
+ overflow hidden
85
+
86
+ .compass-rail::after
87
+ content ""
88
+ position absolute
89
+ inset 0
90
+ width 25%
91
+ border-radius 999px
92
+ background linear-gradient(90deg, var(--brand) 0%, var(--brand-deep) 100%)
93
+ transition width .45s cubic-bezier(.34, 1.56, .64, 1)
94
+
95
+ .compass-rail.at-1::after
96
+ width 25%
97
+
98
+ .compass-rail.at-2::after
99
+ width 50%
100
+
101
+ .compass-rail.at-3::after
102
+ width 75%
103
+
104
+ .compass-rail.at-4::after
105
+ width 100%
106
+
107
+ /* ── The question ───────────────────────────────────────────────────── */
108
+
109
+ .compass-card h2
110
+ font-size clamp(1.7rem, 5vw, 2.3rem)
111
+ line-height 1.12
112
+ letter-spacing -.02em
113
+ margin .2rem 0 0
114
+
115
+ .compass-lead
116
+ color $muted
117
+ font-size 1.02rem
118
+ margin .85rem auto 0
119
+ max-width 38ch
120
+
121
+ /* ── Answers: big satisfying pills ──────────────────────────────────── */
122
+
123
+ .compass-answers
124
+ display flex
125
+ flex-direction column
126
+ gap .7rem
127
+ margin 1.9rem 0 0
128
+
129
+ .compass-answers button
130
+ display block
131
+ width 100%
132
+ margin 0
133
+ padding 1.05rem 1.3rem
134
+ font-family system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
135
+ font-size 1.05rem
136
+ font-weight 550
137
+ line-height 1.3
138
+ text-align left
139
+ color $ink
140
+ background $paper-2
141
+ border 1.5px solid $line
142
+ border-radius 16px
143
+ cursor pointer
144
+ transition transform .14s cubic-bezier(.34,1.56,.64,1), box-shadow .14s ease, border-color .14s ease, background .14s ease
145
+
146
+ .compass-answers button:hover
147
+ transform translateY(-2px)
148
+ border-color var(--brand)
149
+ background var(--brand-soft)
150
+ box-shadow 0 12px 26px -16px rgba(196,111,51,.55)
151
+
152
+ .compass-answers button:active
153
+ transform translateY(0) scale(.985)
154
+
155
+ .compass-answers button:focus-visible
156
+ outline 3px solid var(--brand)
157
+ outline-offset 2px
158
+
159
+ /* ── Result: a result that feels earned ─────────────────────────────── */
160
+
161
+ .compass-result
162
+ padding .4rem 0 .2rem
163
+
164
+ .compass-burst
165
+ font-size 3.4rem
166
+ line-height 1
167
+ margin 0 0 .4rem
168
+ animation pop .55s cubic-bezier(.34,1.56,.64,1) both
169
+
170
+ .compass-result h1
171
+ font-size clamp(2rem, 7vw, 2.9rem)
172
+ line-height 1.05
173
+ letter-spacing -.025em
174
+ margin .15rem 0 0
175
+ background linear-gradient(100deg, var(--brand-deep), var(--brand))
176
+ -webkit-background-clip text
177
+ background-clip text
178
+ color transparent
179
+
180
+ .compass-why
181
+ color $ink-soft
182
+ font-size 1.06rem
183
+ line-height 1.6
184
+ margin 1rem auto 0
185
+ max-width 40ch
186
+
187
+ .compass-why strong
188
+ color var(--brand-deep)
189
+
190
+ /* ── CTA: the styled shop link ──────────────────────────────────────── */
191
+
192
+ .compass-cta
193
+ display inline-flex
194
+ align-items center
195
+ gap .45rem
196
+ margin 1.7rem 0 0
197
+ padding .95rem 1.7rem
198
+ font-family system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
199
+ font-size 1.02rem
200
+ font-weight 650
201
+ color #fff
202
+ text-decoration none
203
+ background linear-gradient(100deg, var(--brand) 0%, var(--brand-deep) 100%)
204
+ border-radius 999px
205
+ box-shadow 0 14px 30px -12px rgba(196,111,51,.6)
206
+ transition transform .14s ease, box-shadow .14s ease
207
+
208
+ .compass-cta:hover
209
+ color #fff
210
+ transform translateY(-2px)
211
+ box-shadow 0 20px 38px -14px rgba(196,111,51,.7)
212
+
213
+ .compass-cta:focus-visible
214
+ outline 3px solid var(--brand-deep)
215
+ outline-offset 3px
216
+
217
+ /* ── Start over ─────────────────────────────────────────────────────── */
218
+
219
+ .compass-restart
220
+ margin 1.7rem 0 0
221
+
222
+ .compass-restart button
223
+ margin 0
224
+ padding .55rem 1.1rem
225
+ font-family system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
226
+ font-size .9rem
227
+ font-weight 550
228
+ color $muted
229
+ background transparent
230
+ border 1.5px solid $line
231
+ border-radius 999px
232
+ cursor pointer
233
+ transition color .14s ease, border-color .14s ease
234
+
235
+ .compass-restart button:hover
236
+ color var(--brand-deep)
237
+ border-color var(--brand)
238
+
239
+ .compass-restart button:focus-visible
240
+ outline 3px solid var(--brand)
241
+ outline-offset 2px
242
+
243
+ /* ── Source reveal sits below the card, full-width-ish, calm ────────── */
244
+
245
+ .app-compass .source-reveal
246
+ max-width 560px
247
+ margin 1.75rem auto 0
@@ -0,0 +1,194 @@
1
+ ---
2
+ title: Compass — find your match
3
+ description: A guided product-finder quiz built in one Markdown file. Four questions advance a state machine with :if branching, accumulate scores with :button actions, and a :computed recommendation lands the perfect pick. Zero hand-written JavaScript.
4
+ image: https://darkmown.com/__wd/media/og-compass.png
5
+ transitions: true
6
+ ---
7
+
8
+ @include /nav.wd
9
+ @include /base.wd
10
+
11
+ <style>@keyframes pop { 0% { transform: scale(0) rotate(-18deg); opacity: 0 } 100% { transform: scale(1) rotate(0); opacity: 1 } }</style>
12
+
13
+ <main class="app-compass">
14
+
15
+ :state step = 1
16
+ :state calm = 0
17
+ :state bold = 0
18
+ :state budget = 0
19
+
20
+ ::: section #quiz .compass-stage
21
+
22
+ ::: compass-card .compass-card
23
+
24
+ :if step <= 4
25
+
26
+ ::: compass-top .compass-top
27
+
28
+ ::: eyebrow .compass-eyebrow
29
+ Question { step } of 4
30
+ :::
31
+
32
+ ::: compass-rail .compass-rail .at-1 when step == 1 .at-2 when step == 2 .at-3 when step == 3 .at-4 when step == 4
33
+ :::
34
+
35
+ :::
36
+
37
+ :endif
38
+
39
+ :if step == 1
40
+
41
+ ## Where should it live?
42
+
43
+ ::: lead .compass-lead
44
+ Picture the spot you'd reach for it most. That tells us a lot.
45
+ :::
46
+
47
+ ::: answers .compass-answers
48
+ :button "🛋️ A cozy nook to unwind" -> calm += 2; step++
49
+ :button "🎉 The room where people gather" -> bold += 2; step++
50
+ :button "🧺 Wherever — it should just be useful" -> budget += 2; step++
51
+ :::
52
+
53
+ :else if step == 2
54
+
55
+ ## What's the mood you're after?
56
+
57
+ ::: lead .compass-lead
58
+ No wrong answers. Go with your gut.
59
+ :::
60
+
61
+ ::: answers .compass-answers
62
+ :button "🌙 Soft, calm, a little dreamy" -> calm += 2; step++
63
+ :button "⚡ Lively, bold, turn it up" -> bold += 2; step++
64
+ :button "🧘 Fresh and easy — no fuss" -> calm += 1; budget += 1; step++
65
+ :::
66
+
67
+ :else if step == 3
68
+
69
+ ## How does it earn its keep?
70
+
71
+ ::: lead .compass-lead
72
+ The thing it has to nail before anything else.
73
+ :::
74
+
75
+ ::: answers .compass-answers
76
+ :button "✨ It sets the whole vibe" -> calm += 2; step++
77
+ :button "🔊 It fills the room" -> bold += 2; step++
78
+ :button "💧 It quietly makes the day nicer" -> calm += 1; budget += 1; step++
79
+ :::
80
+
81
+ :else if step == 4
82
+
83
+ ## And the budget?
84
+
85
+ ::: lead .compass-lead
86
+ Last one — then we'll find your match.
87
+ :::
88
+
89
+ ::: answers .compass-answers
90
+ :button "🌱 Keep it gentle" -> budget += 3; step++
91
+ :button "⚖️ A fair middle" -> budget += 1; step++
92
+ :button "💎 Splurge for the right one" -> bold += 2; step++
93
+ :::
94
+
95
+ :else
96
+
97
+ ::: result .compass-result
98
+
99
+ ::: burst .compass-burst
100
+ 🎉
101
+ :::
102
+
103
+ ::: eyebrow .compass-eyebrow .is-result
104
+ Your match
105
+ :::
106
+
107
+ :if bold > calm and bold > budget
108
+
109
+ # The Cove Speaker
110
+
111
+ ::: why .compass-why
112
+ You're here to fill the room. The **Cove Speaker** throws warm, room-filling 360° sound out of a palm-sized shell — the heart of every gathering, turned all the way up.
113
+ :::
114
+
115
+ [Shop this →](/folio/){.compass-cta}
116
+
117
+ :else if calm > bold and calm > budget
118
+
119
+ # The Aurora Lamp
120
+
121
+ ::: why .compass-why
122
+ You want a space that *feels* like something. The **Aurora Lamp** pours a warm, dimmable glow from a hand-turned oak base — the quiet centerpiece of your cozy nook.
123
+ :::
124
+
125
+ [Shop this →](/folio/){.compass-cta}
126
+
127
+ :else if budget > calm and budget > bold
128
+
129
+ # The Haven Diffuser
130
+
131
+ ::: why .compass-why
132
+ You like things that quietly make the day nicer — without the splurge. The **Haven Diffuser** breathes a soft cedar-scented mist into the room, refillable and easy to love.
133
+ :::
134
+
135
+ [Shop this →](/folio/){.compass-cta}
136
+
137
+ :else
138
+
139
+ # The Isle Earbuds
140
+
141
+ ::: why .compass-why
142
+ You're a little of everything — calm, bold, and practical. The **Isle Earbuds** go anywhere you do: active noise cancelling, a 30-hour case, and tap controls for whatever the day brings.
143
+ :::
144
+
145
+ [Shop this →](/folio/){.compass-cta}
146
+
147
+ :endif
148
+
149
+ ::: restart .compass-restart
150
+ :button "↺ Start over" -> step reset; calm reset; bold reset; budget reset
151
+ :::
152
+
153
+ :::
154
+
155
+ :endif
156
+
157
+ :::
158
+
159
+ :::
160
+
161
+ <details class="source-reveal">
162
+ <summary>View source — it's Markdown</summary>
163
+
164
+ ```wd
165
+ :state step = 1
166
+ :state calm = 0
167
+ :state bold = 0
168
+ :state budget = 0
169
+
170
+ :if step <= 4
171
+ Question { step } of 4
172
+ :endif
173
+
174
+ :if step == 1
175
+ ## Where should it live?
176
+ :button "🛋️ A cozy nook to unwind" -> calm += 2; step++
177
+ :button "🎉 The room where people gather" -> bold += 2; step++
178
+ :else if step == 2
179
+ ## What's the mood you're after?
180
+ :button "🌙 Soft, calm, a little dreamy" -> calm += 2; step++
181
+ ... three more questions ...
182
+ :else
183
+ :if bold > calm and bold > budget
184
+ # The Cove Speaker
185
+ :else if calm > bold and calm > budget
186
+ # The Aurora Lamp
187
+ :endif
188
+ :button "↺ Start over" -> step reset; calm reset; bold reset; budget reset
189
+ :endif
190
+ ```
191
+
192
+ </details>
193
+
194
+ </main>
@@ -1,39 +1,59 @@
1
- tokens
2
- ink #171717
3
- paper #fbfaf7
4
- panel #ffffff
5
- line #dedbd3
6
- accent #16645a
7
- muted #666158
8
- radius 8px
9
-
10
- page
11
- margin 0
12
- font ui-serif, Georgia, Cambria, serif
13
- color $ink
14
- bg $paper
15
- line-height 1.7
1
+ /* Docs — layers on /base.wd (tokens, type, code, dark mode). Refined,
2
+ readable single-column reference. Only docs-specific layout here. */
16
3
 
17
- main
18
- max-width 820px
19
- margin 0 auto
20
- padding 3rem 2rem 5rem
4
+ main.docs
5
+ max-width 760px
21
6
 
22
- h1
23
- font-size 3rem
24
- line-height 1
25
- margin 1rem 0
7
+ .docs-hero
8
+ padding 2rem 0 1.5rem
9
+ margin-bottom 1rem
10
+ border-bottom 1px solid $line
11
+
12
+ .docs-hero h1
13
+ margin .2rem 0 .5rem
14
+
15
+ .docs-hero p
16
+ font-size 1.12rem
17
+ color $ink-soft
18
+ max-width 60ch
26
19
 
27
- h2
28
- margin-top 2.4rem
29
- padding-top 1.2rem
20
+ /* Section headings get a quiet rule above them so the long page scans. */
21
+ main.docs h2
22
+ margin-top 3rem
23
+ padding-top 1.4rem
30
24
  border-top 1px solid $line
25
+ font-size 1.55rem
31
26
 
32
- a
27
+ main.docs h3
28
+ margin-top 2rem
29
+ color $accent-deep
30
+
31
+ main.docs p, main.docs li
32
+ color $ink-soft
33
+
34
+ main.docs strong
35
+ color $ink
36
+
37
+ main.docs a
33
38
  color $accent
39
+ font-weight 550
40
+
41
+ /* Tight, legible lists for the rule-heavy reference sections. */
42
+ main.docs ul
43
+ padding-left 1.2rem
44
+
45
+ main.docs li
46
+ margin .35rem 0
47
+
48
+ main.docs li code
49
+ white-space nowrap
34
50
 
35
51
  .feature-card
36
52
  border 1px solid $line
37
53
  bg $panel
38
- padding 1rem
54
+ padding 1.1rem 1.25rem
39
55
  radius $radius
56
+ margin 1rem 0
57
+
58
+ .feature-card strong
59
+ color $warm
@@ -5,12 +5,17 @@ transitions: true
5
5
  ---
6
6
 
7
7
  @include /nav.wd
8
+ @include /base.wd
8
9
 
9
- <main>
10
+ <main class="docs">
10
11
 
11
- # Docs
12
+ <header class="docs-hero">
13
+ <span class="eyebrow">Documentation</span>
14
+
15
+ # The Darkmown docs
12
16
 
13
17
  Darkmown is a Markdown-native framework for mostly-static sites with tiny reactive islands. Pages are plain files: `.md` stays pure CommonMark, and `.wd` ("whateverdown") is the same Markdown plus first-party directives.
18
+ </header>
14
19
 
15
20
  @include ./-relative-note.wd
16
21
 
@@ -152,6 +157,12 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
152
157
  - The page's frontmatter is in scope as `meta` — `{ meta.title }` prints a field.
153
158
  - Otherwise the text is left exactly as written — braces in prose never break a page or pull in the runtime.
154
159
 
160
+ ### Format pipes — `{ value | name:arg }`
161
+
162
+ Shape a value for display with a pipe: `{ price | money }`, `{ joinedAt | date:"medium" }`, `{ ratio | percent }`. Pipes chain (`{ name | trim | capitalize }`) and take literal arguments (`{ total | money:"EUR" }`, `{ bio | truncate:80 }`). The same syntax works on static values (folded at build time, zero-JS) and live bindings (re-applied on render).
163
+
164
+ The whitelist: `money`, `number`, `percent`, `round`, `date` / `time` / `datetime` (Intl-backed; `short` / `medium` / `long`), `upper` / `lower` / `capitalize`, `truncate:n` / `trim`, `pluralize:"item"`, and `default:"—"`. The five aggregates double as list pipes — `{ cart | count }`, `{ cart | sum:"price" | money }`, plus `avg` / `min` / `max`. No custom functions, nothing `eval`'d; an unknown pipe is a compile error. There is deliberately no relative "time ago" formatter, so builds stay reproducible.
165
+
155
166
  ## Frontmatter
156
167
 
157
168
  Frontmatter sits between `---` fences. Values are strings, plus inline arrays:
@@ -179,7 +190,7 @@ You don't size images by hand. Every `<img>` the compiler emits gets its intrins
179
190
 
180
191
  ## Reactive directives
181
192
 
182
- Reactive pages opt into `/__wd/runtime.js` (~5.7 KB gzipped, under a CI-enforced 6 KB budget). Static pages do not.
193
+ Reactive pages opt into `/__wd/runtime.js` (~7.4 KB gzipped, under a CI-enforced 8 KB budget). Static pages do not.
183
194
 
184
195
  ```wd
185
196
  :state count = 0
@@ -205,6 +216,32 @@ Count is still zero.
205
216
 
206
217
  Directive actions are intentionally narrow and compile-time checked. Arbitrary JavaScript belongs in a colocated `.js` file.
207
218
 
219
+ ### Computed values and aggregates
220
+
221
+ `:computed name = expr` derives state from state — arithmetic, comparisons, and the five list aggregates `sum` / `avg` / `min` / `max` / `count`. It recomputes when an input changes and reads like any binding, so it pairs with format pipes:
222
+
223
+ ```wd
224
+ :store cart = [{"price": 89}, {"price": 12}]
225
+ :computed subtotal = sum(cart, price)
226
+ :computed total = subtotal * 1.08
227
+
228
+ Total: { total | money }
229
+ ```
230
+
231
+ The aggregate's field is a bare row key (`price`, not `item.price`).
232
+
233
+ ### Timers — `:every`
234
+
235
+ `:every <duration> -> <actions>` runs a `:button`-style action on an interval (`ms` / `s` / `m`). Intervals pause while the tab is hidden and resume on return.
236
+
237
+ ```wd
238
+ :fetch board from "/status.json"
239
+ :every 10s -> board refetch
240
+
241
+ :state secs = 0
242
+ :every 1s -> secs++
243
+ ```
244
+
208
245
  ### Multi-branch conditionals
209
246
 
210
247
  A condition reads the same predicate grammar as `.class when` — a bare path (truthy), or the comparisons `==` `!=` `<` `<=` `>` `>=` `contains`, joined with `and`, `or`, and `not`. (`@loop … where` is the comparison-only subset.) Chain with `:else if` (any number; a bare `:else` must come last):
@@ -320,10 +357,35 @@ You have { cart } items.
320
357
  - `:input field placeholder="…" required` and `:submit "Label"` build the form body.
321
358
  - `:state cart = [] persist` survives reloads via localStorage on a single page; use `:store` for state shared across pages and tabs.
322
359
  - `:if item.path` works inside reactive loops for per-row branches, and conditionals nest — an inner `:if` resolves after the outer branch, staying reactive.
323
- - Reactive pages expose `window.wd` (`get`, `set`, `state`, `render`) so colocated `.js` can do anything directives can't. Section state is addressed as `sectionId:name`.
360
+ - Reactive pages expose `window.wd` (`get`, `set`, `subscribe`, `state`, `render`) so colocated `.js` can do anything directives can't. Section state is addressed as `sectionId:name`.
324
361
 
325
362
  See it all live on the [Data & Forms page](/data/).
326
363
 
364
+ ## Theme toggle — `:theme`
365
+
366
+ For an explicit light/dark switch alongside the OS preference, declare `:theme` once and drive it with buttons. It registers a durable `theme` store and reflects onto `<html data-theme>`:
367
+
368
+ ```wd
369
+ :theme
370
+ :button "Auto" -> theme = "auto"
371
+ :button "Light" -> theme = "light"
372
+ :button "Dark" -> theme = "dark"
373
+ ```
374
+
375
+ A single `tokens dark` block in your `.skin` already powers both the OS `prefers-color-scheme` query and this toggle — no extra block needed. `"auto"` clears the attribute and follows the OS again. See it on the [Ledger demo](/ledger/).
376
+
377
+ ## Media — `:video`, `:audio`, `:embed`
378
+
379
+ Three compile-time-only directives (they emit no `data-wd-*`, so a media page stays zero-JS):
380
+
381
+ ```wd
382
+ :video /clip.mp4 poster=/clip.jpg controls
383
+ :audio /track.mp3 controls
384
+ :embed https://youtu.be/aqz-KE-bpKQ title="Big Buck Bunny"
385
+ ```
386
+
387
+ `:video`/`:audio` emit a hardened HTML5 player — `preload="metadata"` and `controls` by default, whitelisted flags (`autoplay` implies `muted`), and the `:fetch` URL scheme guard. `:embed` rewrites a YouTube/Vimeo link to its no-cookie/player form in a lazy, responsive `16/9` iframe. See the zero-JS [Media demo](/media/).
388
+
327
389
  ## Trust boundary
328
390
 
329
391
  Darkmown is a trusted-author site generator: you compile content you wrote, the way you trust your own source. Three things define that boundary.
@@ -337,7 +399,7 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
337
399
  ## Colocation
338
400
 
339
401
  - A matching `.skin` file attaches CSS to the page (indentation-based, compiles to real CSS).
340
- - A matching `.js` file attaches page behavior.
402
+ - A matching `.js` file attaches page behavior. It loads **after** the runtime, so `window.wd` is ready: `wd.get(key)`, `wd.set(key, value)`, and `wd.subscribe(key, cb)` (runs now and on every settled change) bridge declarative state to imperative widgets — the gestures the framework leaves out (keyboard, drag/touch, canvas). The [Swiper demo](/carousel/) is a draggable carousel built this way.
341
403
  - Both work for included fragments too, by basename.
342
404
  - Any other file under `site/pages/` (an image, font, PDF — anything that isn't a `.md`/`.wd` route or a basename-matched `.skin`/`.js`) copies to `dist/` at its own path with the right content-type. So `site/pages/logo.svg` is served at `/logo.svg`. Assets below hidden path segments (`.`, `-`, or `_`) and symlinked page assets are skipped, matching the routing privacy rule. Use the `site/_/` shelf for assets shared across pages; colocate the ones a single page owns.
343
405
 
@@ -345,4 +407,8 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
345
407
 
346
408
  The implementation is faithful to the original core thesis: Markdown-first authoring, no component ceremony, zero runtime on static pages, and tiny direct-DOM reactivity only when declared. `:store`, the full `:fetch` lifecycle (loading/error/empty states, dynamic URLs, `refetch`, dotted loop sources, authenticated `headers=` + `refresh=` token renewal), `:form` (including server round-trips), loop ergonomics (`sort by`/`limit`/`offset`/`reverse`/`@empty` and `$index`/`$number`/`$first`/`$last`/`$count`), `:if … :else if … :else`, reactive `.class when …`, `:effect`, `:computed`, and `persist` are all shipped and live — try them on the [Data & Forms page](/data/) and the [Reactive page](/reactive/). Still on the roadmap: a first-party server runtime (`site/api/`) and HTML-fragment swaps. See `docs/spec-alignment.md` in the package for the full audit.
347
409
 
410
+ See these directives composed into real apps on the [showcase](/showcase/).
411
+
348
412
  </main>
413
+
414
+ @include /footer.wd