@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
@@ -6,18 +6,28 @@ transitions: true
6
6
  ---
7
7
 
8
8
  @include /nav.wd
9
+ @include /base.wd
9
10
 
10
11
  <main>
11
12
 
12
13
  <section class="hero">
13
- <p class="eyebrow">Darkmown vs. Markdoc</p>
14
+ <span class="eyebrow">Darkmown vs. Markdoc</span>
14
15
 
15
- # Markdown that runs. Not Markdown that needs React.
16
+ # Markdown that *runs.* Not Markdown that needs React.
16
17
 
17
18
  The left file is Markdown. It is also a running app — a live search filter, add-to-cart, and a running total. The right file is the closest Markdoc can get on its own: a static list. To make it do what the left one already does, you add a React component, hydration, and a build step.
18
19
 
19
- <a class="btn primary" href="/app/">See a whole app in one file</a>
20
+ <div class="hero-cta">
21
+ <a class="btn primary" href="/showcase/">See the showcase</a>
20
22
  <a class="btn" href="/docs/">Read the docs</a>
23
+ </div>
24
+
25
+ <ul class="hero-stats">
26
+ <li><strong>0 KB</strong><span>framework JS on static pages</span></li>
27
+ <li><strong>~7.4 KB</strong><span>one runtime, CI-enforced &lt; 8 KB</span></li>
28
+ <li><strong>1</strong><span>loop and one binding to learn</span></li>
29
+ <li><strong>MIT</strong><span>zero runtime dependencies</span></li>
30
+ </ul>
21
31
  </section>
22
32
 
23
33
  <section class="versus">
@@ -98,14 +108,14 @@ Markdoc.renderers.react(content, React, {
98
108
 
99
109
  </section>
100
110
 
101
- :note "Static pages ship zero JavaScript. The moment a page asks for reactivity — like this very homepage — one ~5.7 KB gzipped runtime loads (CI-enforced under 6 KB), never before. JS optional, CSS optional, always."
111
+ :note "Static pages ship zero JavaScript. The moment a page asks for reactivity — like this very homepage — one ~7.4 KB gzipped runtime loads (CI-enforced under 8 KB), never before. JS optional, CSS optional, always."
102
112
 
103
113
  <section class="props">
104
114
 
105
115
  ## Three rules, no escape hatch
106
116
 
107
117
  - **Zero-JS static.** A page with no state ships no framework JavaScript. `/markdown/` and `/docs/` prove it — open them and check the network tab.
108
- - **One runtime.** Every reactive page shares the same ~5.7 KB gzipped runtime, CI-enforced under 6 KB. No per-component bundles, no hydration tax that scales with your UI.
118
+ - **One runtime.** Every reactive page shares the same ~7.4 KB gzipped runtime, CI-enforced under 8 KB. No per-component bundles, no hydration tax that scales with your UI.
109
119
  - **One loop, one binding.** `@loop … into …` is the only loop; `{ name }` is the only interpolation. Nothing else to learn, nothing else to misuse.
110
120
 
111
121
  </section>
@@ -160,3 +170,5 @@ This button is powered by `site/pages/index.js`, discovered by matching the page
160
170
  <a class="btn" href="https://github.com/ZVN-DEV/darkmown">Star on GitHub</a>
161
171
 
162
172
  </main>
173
+
174
+ @include /footer.wd
@@ -0,0 +1,257 @@
1
+ /* ── Ledger — a sortable expense table. Reuses base tokens ($ink/$paper/…), so
2
+ the :theme toggle re-themes it through base.skin's `tokens dark` (now wired to
3
+ [data-theme] as well as the OS media query). ───────────────────────────── */
4
+
5
+ .app-ledger
6
+ max-width 60rem
7
+ margin 0 auto
8
+ padding 2.5rem 1.25rem 4rem
9
+
10
+ /* Header: title + theme switch */
11
+ .app-ledger .ledger-head
12
+ display flex
13
+ align-items flex-end
14
+ justify-content space-between
15
+ flex-wrap wrap
16
+ gap 1.25rem
17
+ margin-bottom 1.75rem
18
+
19
+ .app-ledger .ledger-eyebrow
20
+ font-size .72rem
21
+ font-weight 700
22
+ letter-spacing .14em
23
+ text-transform uppercase
24
+ color $accent
25
+
26
+ .app-ledger h1
27
+ margin .35rem 0 0
28
+ font-size 2.4rem
29
+ letter-spacing -.02em
30
+ color $ink
31
+
32
+ .app-ledger .ledger-tagline
33
+ margin .6rem 0 0
34
+ max-width 34rem
35
+ color $muted
36
+ font-size .95rem
37
+ line-height 1.5
38
+
39
+ .app-ledger .ledger-tagline code
40
+ bg $code-bg
41
+ color $code-ink
42
+ padding .05rem .3rem
43
+ radius 5px
44
+ font-size .85em
45
+
46
+ /* Theme switch */
47
+ .app-ledger .theme-switch
48
+ display inline-flex
49
+ align-items center
50
+ gap .4rem
51
+ font-size .75rem
52
+ font-weight 600
53
+ letter-spacing .04em
54
+ text-transform uppercase
55
+ color $muted
56
+
57
+ .app-ledger .theme-switch button[data-wd-action]
58
+ border 1px solid $line
59
+ bg $panel
60
+ color $ink-soft
61
+ font-size .8rem
62
+ font-weight 600
63
+ padding .4rem .8rem
64
+ radius 999px
65
+ cursor pointer
66
+ transition color .14s ease, border-color .14s ease, background .14s ease
67
+
68
+ .app-ledger .theme-switch button[data-wd-action]:hover
69
+ color $ink
70
+ border-color $accent
71
+
72
+ /* Stat cards */
73
+ .app-ledger .stat-grid
74
+ display grid
75
+ grid-template-columns repeat(4, 1fr)
76
+ gap .85rem
77
+ margin-bottom 1.5rem
78
+
79
+ .app-ledger .stat
80
+ padding 1rem 1.15rem
81
+ border 1px solid $line-soft
82
+ radius 14px
83
+ bg $panel
84
+
85
+ .app-ledger .stat-label
86
+ font-size .72rem
87
+ font-weight 600
88
+ letter-spacing .06em
89
+ text-transform uppercase
90
+ color $muted
91
+
92
+ .app-ledger .stat-num
93
+ margin-top .3rem
94
+ font-size 1.5rem
95
+ font-weight 700
96
+ letter-spacing -.01em
97
+ color $ink
98
+ font-variant-numeric tabular-nums
99
+
100
+ .app-ledger .stat p, .app-ledger .stat-num p, .app-ledger .stat-label p
101
+ margin 0
102
+
103
+ /* Controls: filter + live ticker */
104
+ .app-ledger .ledger-controls
105
+ display flex
106
+ align-items center
107
+ justify-content space-between
108
+ flex-wrap wrap
109
+ gap 1rem
110
+ margin-bottom 1rem
111
+
112
+ .app-ledger .ledger-controls input[data-wd-bind-input]
113
+ flex 1 1 16rem
114
+ border 1px solid $line
115
+ bg $paper-2
116
+ color $ink
117
+ font-size .95rem
118
+ padding .6rem .9rem
119
+ radius 10px
120
+
121
+ .app-ledger .synced
122
+ font-size .8rem
123
+ color $muted
124
+ white-space nowrap
125
+
126
+ .app-ledger .synced p
127
+ margin 0
128
+
129
+ /* The table (CSS grid rows so it stays one readable Markdown file) */
130
+ .app-ledger .ledger-table
131
+ padding 0
132
+ border 1px solid $line-soft
133
+ radius 16px
134
+ overflow hidden
135
+ bg $panel
136
+
137
+ .app-ledger .thead, .app-ledger .trow
138
+ display grid
139
+ grid-template-columns 9rem 1fr 9rem 7rem 4.5rem
140
+ align-items center
141
+
142
+ .app-ledger .thead
143
+ border-bottom 1px solid $line-soft
144
+ bg $paper-2
145
+
146
+ .app-ledger .th
147
+ padding 0
148
+
149
+ .app-ledger .th button[data-wd-action]
150
+ width 100%
151
+ text-align left
152
+ border 0
153
+ bg transparent
154
+ color $muted
155
+ font-size .72rem
156
+ font-weight 700
157
+ letter-spacing .06em
158
+ text-transform uppercase
159
+ padding .8rem .9rem
160
+ cursor pointer
161
+ transition color .14s ease
162
+
163
+ .app-ledger .th button[data-wd-action]:hover
164
+ color $ink
165
+
166
+ .app-ledger .th.active button[data-wd-action]
167
+ color $accent
168
+
169
+ .app-ledger .th.num button[data-wd-action]
170
+ text-align right
171
+
172
+ .app-ledger .th.dir
173
+ display flex
174
+ gap .15rem
175
+ justify-content center
176
+
177
+ .app-ledger .th.dir button[data-wd-action]
178
+ width auto
179
+ padding .8rem .35rem
180
+ font-size .7rem
181
+
182
+ .app-ledger .trow
183
+ border-top 1px solid $line-soft
184
+ font-size .92rem
185
+
186
+ .app-ledger .trow:first-of-type
187
+ border-top 0
188
+
189
+ .app-ledger .td
190
+ padding .7rem .9rem
191
+ color $ink-soft
192
+
193
+ .app-ledger .td p
194
+ margin 0
195
+
196
+ .app-ledger .td.muted
197
+ color $muted
198
+ font-variant-numeric tabular-nums
199
+
200
+ .app-ledger .td.strong
201
+ color $ink
202
+ font-weight 600
203
+
204
+ .app-ledger .td.num
205
+ text-align right
206
+ font-weight 600
207
+ color $ink
208
+ font-variant-numeric tabular-nums
209
+
210
+ .app-ledger .pill
211
+ display inline-block
212
+ font-size .72rem
213
+ font-weight 600
214
+ padding .15rem .55rem
215
+ radius 999px
216
+ bg $accent-soft
217
+ color $accent-deep
218
+
219
+ .app-ledger .td.dir
220
+ text-align center
221
+
222
+ .app-ledger .td.dir button[data-wd-action]
223
+ border 1px solid transparent
224
+ bg transparent
225
+ color $muted
226
+ font-size .8rem
227
+ padding .25rem .5rem
228
+ radius 8px
229
+ cursor pointer
230
+ transition color .14s ease, border-color .14s ease
231
+
232
+ .app-ledger .td.dir button[data-wd-action]:hover
233
+ color $warm
234
+ border-color $line
235
+
236
+ .app-ledger .trow.blank
237
+ display block
238
+ padding 1.4rem .9rem
239
+ color $muted
240
+ text-align center
241
+
242
+ .app-ledger .ledger-foot
243
+ margin-top 1.1rem
244
+ font-size .85rem
245
+ color $muted
246
+ text-align center
247
+
248
+ .app-ledger .ledger-foot strong
249
+ color $ink-soft
250
+
251
+ @media (max-width: 640px)
252
+ .app-ledger .stat-grid
253
+ grid-template-columns repeat(2, 1fr)
254
+ .app-ledger .thead, .app-ledger .trow
255
+ grid-template-columns 1fr 5.5rem 3.5rem
256
+ .app-ledger .th.cat, .app-ledger .td.cat
257
+ display none
@@ -0,0 +1,174 @@
1
+ ---
2
+ title: Ledger — a sortable expense table in Markdown
3
+ description: A real data table written as one Markdown file — clickable-header sorting, a live filter, running totals, and a light/dark toggle. Format pipes (| money, | date), aggregates (sum, avg, max), reactive sort, and :theme — all declarative, no hand-written JavaScript.
4
+ image: https://darkmown.com/__wd/media/og-showcase.png
5
+ transitions: true
6
+ ---
7
+
8
+ @include /nav.wd
9
+ @include /base.wd
10
+
11
+ <main class="wide app-ledger">
12
+
13
+ ::: section #ledger
14
+
15
+ :theme
16
+ :store expenses = [{"id":1,"date":"2026-06-22","merchant":"Aurora Studio","category":"Software","amount":149},{"id":2,"date":"2026-06-20","merchant":"Briza Coffee","category":"Meals","amount":18.4},{"id":3,"date":"2026-06-19","merchant":"Cobalt Cloud","category":"Hosting","amount":312.5},{"id":4,"date":"2026-06-18","merchant":"Dune Print Co","category":"Office","amount":64},{"id":5,"date":"2026-06-16","merchant":"Aurora Studio","category":"Software","amount":29},{"id":6,"date":"2026-06-15","merchant":"Ember Transit","category":"Travel","amount":212.75},{"id":7,"date":"2026-06-13","merchant":"Briza Coffee","category":"Meals","amount":11.2},{"id":8,"date":"2026-06-11","merchant":"Cobalt Cloud","category":"Hosting","amount":312.5}]
17
+ :state q = ""
18
+ :state sortKey = "amount"
19
+ :state sortDir = "desc"
20
+ :state secs = 0
21
+ :every 1s -> secs++
22
+
23
+ :computed total = sum(expenses, amount)
24
+ :computed average = avg(expenses, amount)
25
+ :computed largest = max(expenses, amount)
26
+
27
+ ::: ledger-head
28
+ ::: ledger-mark
29
+ ::: eyebrow .ledger-eyebrow
30
+ A real data table
31
+ :::
32
+
33
+ # *Ledger*
34
+
35
+ ::: ledger-tagline
36
+ Clickable-header sorting, a live filter, and running totals — a spreadsheet-grade table written as one Markdown file. The numbers below are `{ … | money }`; the dates are `{ … | date }`; the totals are `sum()` / `avg()` / `max()`.
37
+ :::
38
+ :::
39
+
40
+ ::: theme-switch
41
+ Theme
42
+ :button "Auto" -> theme = "auto"
43
+ :button "Light" -> theme = "light"
44
+ :button "Dark" -> theme = "dark"
45
+ :::
46
+ :::
47
+
48
+ ::: stat-grid
49
+ ::: card .stat
50
+ ::: stat-label
51
+ Total spend
52
+ :::
53
+ ::: stat-num
54
+ { total | money }
55
+ :::
56
+ :::
57
+ ::: card .stat
58
+ ::: stat-label
59
+ Average
60
+ :::
61
+ ::: stat-num
62
+ { average | money }
63
+ :::
64
+ :::
65
+ ::: card .stat
66
+ ::: stat-label
67
+ Largest
68
+ :::
69
+ ::: stat-num
70
+ { largest | money }
71
+ :::
72
+ :::
73
+ ::: card .stat
74
+ ::: stat-label
75
+ Entries
76
+ :::
77
+ ::: stat-num
78
+ { expenses | count }
79
+ :::
80
+ :::
81
+ :::
82
+
83
+ ::: ledger-controls
84
+ :bind q placeholder="Filter by merchant…"
85
+ ::: synced
86
+ ↻ live · { secs | pluralize:"second" } on this page
87
+ :::
88
+ :::
89
+
90
+ ::: card .ledger-table
91
+
92
+ ::: thead
93
+ ::: th .active when sortKey == "date"
94
+ :button "Date" -> sortKey = "date" ; sortDir = "asc"
95
+ :::
96
+ ::: th .active when sortKey == "merchant"
97
+ :button "Merchant" -> sortKey = "merchant" ; sortDir = "asc"
98
+ :::
99
+ ::: th .active when sortKey == "category"
100
+ :button "Category" -> sortKey = "category" ; sortDir = "asc"
101
+ :::
102
+ ::: th .num .active when sortKey == "amount"
103
+ :button "Amount" -> sortKey = "amount" ; sortDir = "desc"
104
+ :::
105
+ ::: th .dir
106
+ :button "▲" -> sortDir = "asc"
107
+ :button "▼" -> sortDir = "desc"
108
+ :::
109
+ :::
110
+
111
+ @loop expenses into e where e.merchant contains q sort by { sortKey } { sortDir }
112
+ ::: trow
113
+ ::: td .muted
114
+ { e.date | date:"medium" }
115
+ :::
116
+ ::: td .strong
117
+ { e.merchant }
118
+ :::
119
+ ::: td
120
+ ::: pill
121
+ { e.category }
122
+ :::
123
+ :::
124
+ ::: td .num
125
+ { e.amount | money }
126
+ :::
127
+ ::: td .dir
128
+ :button "✕" -> expenses remove e
129
+ :::
130
+ :::
131
+ @empty
132
+ ::: trow .blank
133
+ No expenses match "**{ q }**".
134
+ :::
135
+ @endloop
136
+
137
+ :::
138
+
139
+ ::: ledger-foot
140
+ { expenses | count } entries · totalling **{ total | money }** · sorted by **{ sortKey }** ({ sortDir })
141
+ :::
142
+
143
+ <details class="source-reveal">
144
+ <summary>View source — it's Markdown</summary>
145
+
146
+ ```wd
147
+ :store expenses = [ … ]
148
+ :state sortKey = "amount"
149
+ :state sortDir = "desc"
150
+ :state q = ""
151
+
152
+ :computed total = sum(expenses, amount)
153
+
154
+ # clickable headers set the sort column + direction
155
+ :button "Amount" -> sortKey = "amount" ; sortDir = "desc"
156
+ :button "▲" -> sortDir = "asc"
157
+
158
+ :bind q placeholder="Filter by merchant…"
159
+
160
+ @loop expenses into e where e.merchant contains q sort by { sortKey } { sortDir }
161
+ | { e.date | date:"medium" } | { e.merchant } | { e.amount | money } |
162
+ :button "✕" -> expenses remove e
163
+ @endloop
164
+
165
+ Total: { total | money }
166
+ ```
167
+
168
+ </details>
169
+
170
+ :::
171
+
172
+ </main>
173
+
174
+ @include /footer.wd
Binary file
Binary file
@@ -0,0 +1,82 @@
1
+ /* ── Media demo — layers on /base.wd. Pure presentation; the page itself ships
2
+ no framework JS, so nothing here is reactive. Reuses base tokens. ───────── */
3
+
4
+ .app-media
5
+ max-width 52rem
6
+ margin 0 auto
7
+ padding 2.5rem 1.25rem 4rem
8
+
9
+ .app-media .media-head
10
+ margin-bottom 2rem
11
+
12
+ .app-media .media-head .eyebrow
13
+ color $accent
14
+
15
+ .app-media h1
16
+ margin .5rem 0 0
17
+ font-size 2.5rem
18
+ letter-spacing -.02em
19
+ color $ink
20
+
21
+ .app-media .media-head p
22
+ margin 1rem 0 0
23
+ max-width 42rem
24
+ color $ink-soft
25
+ font-size 1.02rem
26
+ line-height 1.6
27
+
28
+ .app-media .media-head code, .app-media .media-label code, .app-media .media-note code
29
+ bg $code-bg
30
+ color $code-ink
31
+ padding .08rem .35rem
32
+ radius 6px
33
+ font-size .85em
34
+
35
+ /* Each player sits in its own labelled card */
36
+ .app-media .media-panel
37
+ padding 1.25rem 1.25rem 1.1rem
38
+ margin 1.25rem 0
39
+ border 1px solid $line-soft
40
+ radius 16px
41
+ bg $panel
42
+
43
+ .app-media .media-label
44
+ font-size .8rem
45
+ font-weight 600
46
+ letter-spacing .02em
47
+ color $muted
48
+ margin-bottom .85rem
49
+
50
+ .app-media .media-label p
51
+ margin 0
52
+
53
+ /* The compiled <video>/<audio>/<iframe> wrapper */
54
+ .app-media video
55
+ width 100%
56
+ height auto
57
+ display block
58
+ border 1px solid $line-soft
59
+ radius 12px
60
+ background $sunk
61
+
62
+ .app-media audio
63
+ width 100%
64
+ display block
65
+
66
+ .app-media .wd-embed
67
+ border 1px solid $line-soft
68
+ radius 12px
69
+ overflow hidden
70
+ background $sunk
71
+
72
+ .app-media .media-note
73
+ margin .9rem 0 0
74
+ font-size .88rem
75
+ line-height 1.55
76
+ color $muted
77
+
78
+ .app-media .media-note p
79
+ margin 0
80
+
81
+ .app-media .source-reveal
82
+ margin-top 2rem
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: Media — video, audio, and embeds in Markdown
3
+ description: Three one-line directives — :video, :audio, :embed — compile to hardened, accessible, privacy-friendly players. No framework JavaScript ships: a media page stays zero-JS, just like static Markdown.
4
+ image: https://darkmown.com/__wd/media/og-showcase.png
5
+ transitions: true
6
+ ---
7
+
8
+ @include /nav.wd
9
+ @include /base.wd
10
+
11
+ <main class="wide app-media">
12
+
13
+ <header class="media-head">
14
+ <span class="eyebrow">New in 1.0</span>
15
+
16
+ # *Media*, the Markdown way
17
+
18
+ Video, audio, and third-party embeds used to be the one place a Markdown page fell
19
+ back to raw HTML — `<video>` tags, `<iframe>` soup, privacy footguns, and
20
+ copy-pasted attributes. Darkmown gives you three one-line directives instead.
21
+ Each compiles to a **hardened, accessible, lazy** player, and — because they emit
22
+ no `data-wd-*` — *this entire page ships zero framework JavaScript.*
23
+
24
+ </header>
25
+
26
+ ::: card .media-panel
27
+ ::: media-label
28
+ `:video` — a hardened HTML5 player
29
+ :::
30
+
31
+ :video /media/clip.mp4 poster=/media/clip-poster.jpg controls
32
+
33
+ ::: media-note
34
+ `preload="metadata"` and `controls` are added for you; an `autoplay` clip silently
35
+ implies `muted`, because browsers block sound-on autoplay. The source URL runs
36
+ through the same scheme guard as `:fetch` — only relative paths and `http(s)` get
37
+ through.
38
+ :::
39
+ :::
40
+
41
+ ::: card .media-panel
42
+ ::: media-label
43
+ `:audio` — same guarantees, for sound
44
+ :::
45
+
46
+ :audio /media/tone.mp3 controls
47
+
48
+ ::: media-note
49
+ A 440 Hz reference tone. `:audio` shares the `:video` defaults and validation; flags
50
+ you don't set never appear in the markup, so there are no surprise behaviors.
51
+ :::
52
+ :::
53
+
54
+ ::: card .media-panel
55
+ ::: media-label
56
+ `:embed` — privacy-friendly, responsive iframes
57
+ :::
58
+
59
+ :embed https://youtu.be/aqz-KE-bpKQ title="Big Buck Bunny (open movie)"
60
+
61
+ ::: media-note
62
+ A YouTube or Vimeo URL is rewritten to its **no-cookie / player** form, wrapped in a
63
+ `16/9` box, and marked `loading="lazy"` with a locked-down `referrerpolicy`. Paste a
64
+ share link; get a responsive, tracking-light embed.
65
+ :::
66
+ :::
67
+
68
+ <details class="source-reveal">
69
+ <summary>View source — three lines</summary>
70
+
71
+ ```wd
72
+ :video /media/clip.mp4 poster=/media/clip-poster.jpg controls
73
+ :audio /media/tone.mp3 controls
74
+ :embed https://youtu.be/aqz-KE-bpKQ title="Big Buck Bunny (open movie)"
75
+ ```
76
+
77
+ That's the whole page's interactivity. No `<script>`, no runtime — open DevTools and
78
+ watch the Network tab stay empty of framework JS.
79
+
80
+ </details>
81
+
82
+ </main>
83
+
84
+ @include /footer.wd
Binary file