code wiki / _hdl_build / nx_nishi_editorial.nx
nx_nishi_editorial.nx source
↩ module page · 613 lines · 47329 B
1// nx_nishi_editorial.nx -- LIB: THE estate's editorial design system, emitted as sovereign zero-JS CSS.
2//
3// Every generated surface used to hand-roll its own stylesheet, which is why the estate reads like a dozen
4// different products. This is the ONE canonical system: adopt it, do not copy it (rule 15 -- the pattern is
5// past three call sites, so it belongs in a lib). Tuned against a reference editorial artifact: flat, 2px
6// radii, ZERO shadows, ZERO gradients, serif display over sans body with a mono voice for labels and figures.
7// Restraint is the whole aesthetic -- the typography carries it, so nothing else has to shout.
8//
9// THEME-AWARE BOTH WAYS: `prefers-color-scheme: dark` is the default signal, and :root[data-theme='dark'|
10// 'light'] overrides win in BOTH directions, so an explicit toggle beats the OS rather than fighting it.
11//
12// ALL markup here uses SINGLE-quoted attributes and CSS strings on purpose. It keeps every emitted literal
13// free of backslash escaping, and -- the real reason -- it means this stylesheet can never be routed through
14// nw_render: that substituter treats `{` as a binding opener, so a CSS rule body would be parsed as a lookup
15// key and SILENTLY DROPPED. Design systems are emitted RAW; only untrusted data goes through ed_esc.
16//
17// Rule 12 at the boundary: ed_raw is for trusted template text, ed_esc for anything a person or a fetched
18// page supplied. The two are deliberately different function names so a reviewer can see which is which.
19// license_tier: ORIGINAL No hw writes (Rule 26).
20import "nx_ad_serve.nx"
21import "nx_syscalls.nx"
22
23const ED_TAB: i64 = 9
24
25func ed_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
26
27// A REAL empty string. The two-double-quote literal is NOT safe in this dialect -- it reads as length > 0 and
28// walks whatever follows it in memory, so passing "" where a caller expects an empty value corrupts the
29// record being built (banked as NX-EMPTY-STR-MLEN). Anything that wants "no value" calls this instead.
30static ED_EMPTY: i64
31func ed_empty() -> *u8 {
32 if ED_EMPTY == 0 { ED_EMPTY = sys_mmap(8) as i64 }
33 let b: *u8 = ED_EMPTY as *u8
34 b[0] = 0 as u8
35 return b
36}
37
38// TRUSTED template text -- emitted verbatim.
39func ed_raw(out: *u8, off: i64, s: *u8) -> i64 { return as_append(out, off, s) }
40
41// UNTRUSTED value -- HTML-escaped. Everything a person typed or a crawler fetched goes through here.
42func ed_esc(out: *u8, off: i64, s: *u8) -> i64 { return as_append_escaped(out, off, s, ed_len(s)) }
43
44func ed_num(out: *u8, off: i64, v: i64) -> i64 {
45 var m: i64 = v
46 var o: i64 = off
47 if m < 0 { out[o] = 45 as u8; o = o + 1; m = 0 - m }
48 let t: *u8 = sys_mmap(28)
49 var k: i64 = 0
50 if m == 0 { t[0] = 48 as u8; k = 1 }
51 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
52 var i: i64 = 0
53 while i < k { out[o + i] = t[k - 1 - i]; i = i + 1 }
54 return o + k
55}
56
57// integer cents -> $D.CC (no float anywhere in the estate)
58func ed_money(out: *u8, off: i64, cents: i64) -> i64 {
59 var c: i64 = cents
60 var o: i64 = off
61 if c < 0 { out[o] = 45 as u8; o = o + 1; c = 0 - c }
62 out[o] = 36 as u8; o = o + 1
63 o = ed_num(out, o, c / 100)
64 out[o] = 46 as u8; o = o + 1
65 let r: i64 = c % 100
66 out[o] = (48 + (r / 10)) as u8; o = o + 1
67 out[o] = (48 + (r % 10)) as u8; o = o + 1
68 return o
69}
70
71// ---- the palette ------------------------------------------------------------------------------------
72// Warm paper neutrals rather than clinical greys, with four SEMANTIC accents. On a meal surface those read
73// have / buy / alert directly, which is why state colour never has to be re-invented per page.
74func ed_tokens_light(out: *u8, off: i64) -> i64 {
75 var o: i64 = off
76 o = ed_raw(out, o, "--ground:#EFEFE9;--ground-sunk:#E6E6DE;--card:#F7F7F3;" as *u8)
77 // --ink-faint DARKENED from the reference palette's #767A72. Measured: #767A72 on --card is 4.07:1, and
78 // this system spends that token on 9.5-11px mono eyebrows, table headers and tier labels -- SMALL text,
79 // so WCAG AA wants 4.5. #6E7269 measures ~4.65:1. Inheriting a palette is not the same as inheriting its
80 // defects; the reference fails this pair too.
81 o = ed_raw(out, o, "--ink:#1A1D19;--ink-soft:#4C5049;--ink-faint:#6E7269;" as *u8)
82 o = ed_raw(out, o, "--accent:#2A3D6B;--accent-wash:#E2E5EE;" as *u8)
83 o = ed_raw(out, o, "--rule:#B5A78B;--rule-faint:#D8D0BE;" as *u8)
84 o = ed_raw(out, o, "--have:#3F6046;--have-wash:#E1E9E1;" as *u8)
85 o = ed_raw(out, o, "--buy:#7E641F;--buy-wash:#EFE9D6;" as *u8)
86 o = ed_raw(out, o, "--alert:#8E4034;--alert-wash:#F1E2DE;" as *u8)
87 return o
88}
89func ed_tokens_dark(out: *u8, off: i64) -> i64 {
90 var o: i64 = off
91 o = ed_raw(out, o, "--ground:#14161A;--ground-sunk:#101216;--card:#1C1F24;" as *u8)
92 // same correction on the dark side: #7E8078 on --card #1C1F24 measures ~4.13:1; #8A8C84 reaches ~4.85:1.
93 o = ed_raw(out, o, "--ink:#E5E4DC;--ink-soft:#AFB0A8;--ink-faint:#8A8C84;" as *u8)
94 o = ed_raw(out, o, "--accent:#93A9DC;--accent-wash:#1E2635;" as *u8)
95 o = ed_raw(out, o, "--rule:#8E836C;--rule-faint:#35322B;" as *u8)
96 o = ed_raw(out, o, "--have:#86AE8C;--have-wash:#1B241D;" as *u8)
97 o = ed_raw(out, o, "--buy:#C4A84F;--buy-wash:#26210F;" as *u8)
98 o = ed_raw(out, o, "--alert:#D08678;--alert-wash:#2B1D1A;" as *u8)
99 return o
100}
101
102func ed_css(out: *u8, off: i64) -> i64 {
103 var o: i64 = off
104 o = ed_raw(out, o, "<style>" as *u8)
105
106 o = ed_raw(out, o, ":root{" as *u8)
107 o = ed_tokens_light(out, o)
108 o = ed_raw(out, o, "--serif:'Iowan Old Style','Palatino Linotype',Palatino,'Book Antiqua',Georgia,serif;" as *u8)
109 o = ed_raw(out, o, "--sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',sans-serif;" as *u8)
110 o = ed_raw(out, o, "--mono:ui-monospace,'SF Mono','Cascadia Mono','Roboto Mono',Consolas,monospace;" as *u8)
111 o = ed_raw(out, o, "--measure:68ch;--page:1180px;color-scheme:light dark}" as *u8)
112
113 o = ed_raw(out, o, "@media (prefers-color-scheme:dark){:root{" as *u8)
114 o = ed_tokens_dark(out, o)
115 o = ed_raw(out, o, "}}" as *u8)
116 o = ed_raw(out, o, ":root[data-theme='dark']{" as *u8)
117 o = ed_tokens_dark(out, o)
118 o = ed_raw(out, o, "}" as *u8)
119 o = ed_raw(out, o, ":root[data-theme='light']{" as *u8)
120 o = ed_tokens_light(out, o)
121 o = ed_raw(out, o, "}" as *u8)
122
123 o = ed_raw(out, o, "*{box-sizing:border-box}" as *u8)
124 o = ed_raw(out, o, "body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);font-size:17px;line-height:1.65;-webkit-font-smoothing:antialiased}" as *u8)
125 o = ed_raw(out, o, ".wrap{max-width:var(--page);margin:0 auto;padding:0 28px 120px}" as *u8)
126 o = ed_raw(out, o, "img{max-width:100%;height:auto}" as *u8)
127
128 // masthead
129 o = ed_raw(out, o, ".mast{padding:72px 0 40px;border-bottom:2px solid var(--ink)}" as *u8)
130 o = ed_raw(out, o, ".mast-eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-faint);margin:0 0 22px}" as *u8)
131 o = ed_raw(out, o, "h1{font-family:var(--serif);font-weight:400;font-size:clamp(2.4rem,5.6vw,4.1rem);line-height:1.04;letter-spacing:-.015em;text-wrap:balance;margin:0 0 24px;max-width:17ch}" as *u8)
132 o = ed_raw(out, o, "h1 em{font-style:italic;color:var(--accent)}" as *u8)
133 o = ed_raw(out, o, ".thesis{font-family:var(--serif);font-size:clamp(1.1rem,2vw,1.32rem);line-height:1.5;color:var(--ink-soft);max-width:56ch;margin:0 0 34px}" as *u8)
134 o = ed_raw(out, o, ".mast-meta{display:flex;flex-wrap:wrap;gap:10px 30px;font-family:var(--mono);font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint)}" as *u8)
135 o = ed_raw(out, o, ".mast-meta b{color:var(--ink-soft);font-weight:600}" as *u8)
136
137 // the two credit strips -- present, legible, deliberately subordinate
138 o = ed_raw(out, o, ".credits{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:1px;background:var(--rule-faint);border:1px solid var(--rule-faint);margin:26px 0 0}" as *u8)
139 o = ed_raw(out, o, ".credit{background:var(--card);padding:14px 18px;display:flex;flex-direction:column;gap:5px}" as *u8)
140 o = ed_raw(out, o, ".credit-label{font-family:var(--mono);font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-faint)}" as *u8)
141 o = ed_raw(out, o, ".credit-body{font-size:.86rem;line-height:1.45;color:var(--ink-soft);margin:0}" as *u8)
142 o = ed_raw(out, o, ".credit-body b{color:var(--ink);font-weight:650}" as *u8)
143 o = ed_raw(out, o, ".credit-note{font-family:var(--mono);font-size:10px;letter-spacing:.06em}" as *u8)
144 o = ed_raw(out, o, ".credit-note.kept{color:var(--have)}" as *u8)
145 o = ed_raw(out, o, ".credit-note.risk{color:var(--alert)}" as *u8)
146
147 // sections
148 o = ed_raw(out, o, "section{padding-top:68px}" as *u8)
149 o = ed_raw(out, o, ".sec-head{display:flex;align-items:baseline;gap:16px;margin-bottom:26px;padding-bottom:12px;border-bottom:1px solid var(--rule-faint)}" as *u8)
150 o = ed_raw(out, o, ".sec-num{font-family:var(--mono);font-size:11px;letter-spacing:.14em;color:var(--rule);flex:none;padding-top:4px}" as *u8)
151 o = ed_raw(out, o, "h2{font-family:var(--serif);font-weight:400;font-size:clamp(1.5rem,3vw,2.05rem);line-height:1.15;letter-spacing:-.01em;margin:0;text-wrap:balance}" as *u8)
152 o = ed_raw(out, o, "h3{font-family:var(--sans);font-weight:650;font-size:.95rem;letter-spacing:.02em;margin:40px 0 12px;color:var(--ink)}" as *u8)
153 o = ed_raw(out, o, "p{max-width:var(--measure);margin:0 0 18px}" as *u8)
154 o = ed_raw(out, o, "a{color:var(--accent);text-decoration-thickness:1px;text-underline-offset:2px}" as *u8)
155 o = ed_raw(out, o, "a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:2px}" as *u8)
156 o = ed_raw(out, o, "strong,b{font-weight:650}" as *u8)
157 o = ed_raw(out, o, ".lede{font-size:1.06rem;color:var(--ink-soft)}" as *u8)
158
159 // callouts
160 o = ed_raw(out, o, ".note{border-left:3px solid var(--rule);padding:4px 0 4px 20px;margin:26px 0;max-width:var(--measure);color:var(--ink-soft);font-size:.95rem}" as *u8)
161 o = ed_raw(out, o, ".note.flag{border-left-color:var(--alert)}" as *u8)
162 o = ed_raw(out, o, ".note.open{border-left-color:var(--accent)}" as *u8)
163 o = ed_raw(out, o, ".note-label{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:6px}" as *u8)
164 o = ed_raw(out, o, ".note p{margin:0 0 10px;max-width:none}.note p:last-child{margin-bottom:0}" as *u8)
165
166 // card grid (ingredients, similar dishes)
167 o = ed_raw(out, o, ".grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:3px;margin:30px 0 14px}" as *u8)
168 o = ed_raw(out, o, ".cell{background:var(--card);border-top:4px solid var(--rule);padding:16px 16px 20px;display:flex;flex-direction:column;gap:8px}" as *u8)
169 o = ed_raw(out, o, ".cell[data-state='have']{border-top-color:var(--have)}" as *u8)
170 o = ed_raw(out, o, ".cell[data-state='buy']{border-top-color:var(--buy)}" as *u8)
171 o = ed_raw(out, o, ".cell[data-state='alert']{border-top-color:var(--alert)}" as *u8)
172 o = ed_raw(out, o, ".cell-tier{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint)}" as *u8)
173 o = ed_raw(out, o, ".cell-name{font-family:var(--serif);font-size:1.14rem;line-height:1.2}" as *u8)
174 o = ed_raw(out, o, ".cell-state{font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;align-self:flex-start;padding:3px 8px;border-radius:2px}" as *u8)
175 o = ed_raw(out, o, ".cell[data-state='have'] .cell-state{background:var(--have-wash);color:var(--have)}" as *u8)
176 o = ed_raw(out, o, ".cell[data-state='buy'] .cell-state{background:var(--buy-wash);color:var(--buy)}" as *u8)
177 o = ed_raw(out, o, ".cell[data-state='alert'] .cell-state{background:var(--alert-wash);color:var(--alert)}" as *u8)
178 o = ed_raw(out, o, ".cell-note{font-size:.83rem;line-height:1.45;color:var(--ink-soft);margin:0;max-width:none}" as *u8)
179 o = ed_raw(out, o, ".key{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint)}" as *u8)
180
181 // tables
182 o = ed_raw(out, o, ".scroller{overflow-x:auto;margin:26px 0 10px;border-top:1px solid var(--ink)}" as *u8)
183 o = ed_raw(out, o, "table{width:100%;border-collapse:collapse;font-size:.88rem;min-width:640px}" as *u8)
184 o = ed_raw(out, o, "thead th{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;color:var(--ink-faint);text-align:left;padding:12px 14px 12px 0;vertical-align:bottom;white-space:nowrap}" as *u8)
185 o = ed_raw(out, o, "tbody td{padding:13px 14px 13px 0;border-top:1px solid var(--rule-faint);vertical-align:top;font-variant-numeric:tabular-nums;color:var(--ink-soft)}" as *u8)
186 o = ed_raw(out, o, "tbody td:first-child{color:var(--ink);font-weight:600;white-space:nowrap}" as *u8)
187 o = ed_raw(out, o, "tbody tr[data-hi] td{background:var(--accent-wash)}" as *u8)
188 o = ed_raw(out, o, "tbody tr[data-hi] td:first-child{box-shadow:inset 3px 0 0 var(--accent);padding-left:12px}" as *u8)
189 o = ed_raw(out, o, ".num{font-family:var(--mono);font-size:.84rem}" as *u8)
190 o = ed_raw(out, o, "caption{caption-side:bottom;text-align:left;font-size:.8rem;color:var(--ink-faint);padding-top:12px;line-height:1.5}" as *u8)
191
192 // tags
193 o = ed_raw(out, o, ".tag{display:inline-block;font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;padding:2px 7px;border-radius:2px;white-space:nowrap}" as *u8)
194 o = ed_raw(out, o, ".tag.have{background:var(--have-wash);color:var(--have)}" as *u8)
195 o = ed_raw(out, o, ".tag.buy{background:var(--buy-wash);color:var(--buy)}" as *u8)
196 o = ed_raw(out, o, ".tag.alert{background:var(--alert-wash);color:var(--alert)}" as *u8)
197 o = ed_raw(out, o, ".tag.info{background:var(--accent-wash);color:var(--accent)}" as *u8)
198 o = ed_raw(out, o, ".tag.mute{background:var(--ground-sunk);color:var(--ink-faint)}" as *u8)
199
200 // the cost ledger
201 o = ed_raw(out, o, ".math{background:var(--card);border:1px solid var(--rule-faint);padding:26px 28px;margin:28px 0;max-width:760px}" as *u8)
202 o = ed_raw(out, o, ".math-rows{display:grid;gap:0}" as *u8)
203 o = ed_raw(out, o, ".math-row{display:grid;grid-template-columns:1fr auto;gap:20px;padding:10px 0;border-bottom:1px solid var(--rule-faint);font-size:.92rem}" as *u8)
204 o = ed_raw(out, o, ".math-row:last-child{border-bottom:none}" as *u8)
205 o = ed_raw(out, o, ".math-row.total{border-top:2px solid var(--ink);border-bottom:none;margin-top:6px;padding-top:14px;font-weight:650;color:var(--ink)}" as *u8)
206 o = ed_raw(out, o, ".math-val{font-family:var(--mono);font-variant-numeric:tabular-nums;white-space:nowrap}" as *u8)
207 o = ed_raw(out, o, ".math-row.total .math-val{color:var(--accent)}" as *u8)
208 o = ed_raw(out, o, ".math-label{color:var(--ink-soft)}.math-row.total .math-label{color:var(--ink)}" as *u8)
209 o = ed_raw(out, o, ".math-val.save{color:var(--have)}" as *u8)
210 o = ed_raw(out, o, ".kicker{font-family:var(--serif);font-size:1.22rem;line-height:1.45;color:var(--ink);max-width:54ch;margin:24px 0 0;padding-left:20px;border-left:3px solid var(--accent)}" as *u8)
211
212 // paired cards
213 o = ed_raw(out, o, ".tracks{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:28px;margin:28px 0}" as *u8)
214 o = ed_raw(out, o, ".track{background:var(--card);border:1px solid var(--rule-faint);border-top:4px solid var(--accent);padding:24px 26px 28px}" as *u8)
215 o = ed_raw(out, o, ".track h3{margin-top:0}" as *u8)
216 o = ed_raw(out, o, ".track dl{margin:0;display:grid;gap:14px}" as *u8)
217 o = ed_raw(out, o, ".track dt{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:3px}" as *u8)
218 o = ed_raw(out, o, ".track dd{margin:0;font-size:.9rem;color:var(--ink-soft)}" as *u8)
219 o = ed_raw(out, o, ".track dd b{color:var(--ink);font-weight:650}" as *u8)
220
221 // lists
222 o = ed_raw(out, o, "ul.plain,ol.steps{max-width:var(--measure);padding-left:0;list-style:none;margin:18px 0;display:grid;gap:13px}" as *u8)
223 o = ed_raw(out, o, "ul.plain li{padding-left:22px;position:relative;color:var(--ink-soft)}" as *u8)
224 o = ed_raw(out, o, "ul.plain li::before{content:'';position:absolute;left:0;top:.62em;width:9px;height:1px;background:var(--rule)}" as *u8)
225 o = ed_raw(out, o, "ol.steps{counter-reset:step}" as *u8)
226 o = ed_raw(out, o, "ol.steps li{padding-left:40px;position:relative;counter-increment:step;color:var(--ink-soft)}" as *u8)
227 o = ed_raw(out, o, "ol.steps li::before{content:counter(step,decimal-leading-zero);position:absolute;left:0;top:.1em;font-family:var(--mono);font-size:11px;letter-spacing:.08em;color:var(--accent);font-weight:600}" as *u8)
228 o = ed_raw(out, o, "ul.plain li b,ol.steps li b{color:var(--ink);font-weight:650}" as *u8)
229
230 // no-JS filter pills: real links to pre-rendered variants, never scripted state
231 o = ed_raw(out, o, ".filters{display:flex;flex-wrap:wrap;gap:8px;margin:24px 0 4px;padding:0;list-style:none}" as *u8)
232 o = ed_raw(out, o, ".filters a{display:inline-block;font-family:var(--mono);font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;text-decoration:none;color:var(--ink-soft);background:var(--card);border:1px solid var(--rule-faint);border-radius:2px;padding:7px 12px}" as *u8)
233 o = ed_raw(out, o, ".filters a:hover{border-color:var(--accent);color:var(--accent)}" as *u8)
234 o = ed_raw(out, o, ".filters a[aria-current='page']{background:var(--ink);color:var(--ground);border-color:var(--ink)}" as *u8)
235
236 // forms
237 o = ed_raw(out, o, ".form{display:grid;gap:15px;max-width:540px;margin:22px 0 0}" as *u8)
238 o = ed_raw(out, o, ".form label{display:grid;gap:6px;font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint)}" as *u8)
239 o = ed_raw(out, o, ".form input,.form textarea{font-family:var(--sans);font-size:.95rem;color:var(--ink);background:var(--ground-sunk);border:1px solid var(--rule-faint);border-radius:2px;padding:10px 12px;width:100%}" as *u8)
240 o = ed_raw(out, o, ".form textarea{min-height:130px;resize:vertical;line-height:1.55}" as *u8)
241 o = ed_raw(out, o, ".form input:focus,.form textarea:focus{outline:2px solid var(--accent);outline-offset:1px;border-color:var(--accent)}" as *u8)
242 o = ed_raw(out, o, ".form button{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:600;color:var(--ground);background:var(--ink);border:0;border-radius:2px;padding:12px 22px;cursor:pointer;justify-self:start}" as *u8)
243 o = ed_raw(out, o, ".form button:hover{background:var(--accent)}" as *u8)
244 o = ed_raw(out, o, ".form-hint{font-family:var(--sans);font-size:.82rem;letter-spacing:0;text-transform:none;color:var(--ink-faint);margin:0}" as *u8)
245 // the honeypot: off-canvas, zero-size, untabbable. Invisible to people, irresistible to form bots.
246 o = ed_raw(out, o, ".hp{position:absolute;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none}" as *u8)
247
248 o = ed_raw(out, o, "footer.colophon{margin-top:80px;padding-top:26px;border-top:1px solid var(--rule-faint);font-size:.8rem;line-height:1.55;color:var(--ink-faint)}" as *u8)
249 o = ed_raw(out, o, "footer.colophon p{max-width:none;margin:0 0 8px}" as *u8)
250
251 o = ed_raw(out, o, "@media (max-width:620px){.wrap{padding:0 20px 80px}.mast{padding-top:48px}section{padding-top:52px}}" as *u8)
252 o = ed_raw(out, o, "@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}" as *u8)
253
254 o = ed_raw(out, o, "</style>" as *u8)
255 return o
256}
257
258// full document open: doctype through <body>, with the stylesheet inlined (zero external requests, so the
259// page renders identically offline and cannot be de-styled by a blocked CDN).
260func ed_doc_open(out: *u8, off: i64, title: *u8) -> i64 {
261 var o: i64 = off
262 o = ed_raw(out, o, "<!doctype html><html lang='en'><head><meta charset='utf-8'>" as *u8)
263 o = ed_raw(out, o, "<meta name='viewport' content='width=device-width,initial-scale=1'>" as *u8)
264 o = ed_raw(out, o, "<title>" as *u8)
265 o = ed_esc(out, o, title)
266 o = ed_raw(out, o, "</title>" as *u8)
267 o = ed_css(out, o)
268 // <main>, not <div>: nx_page_verify counts a missing main landmark as a real a11y issue, and a screen
269 // reader has no other way to skip the masthead. Fixing it in the shared lib means every surface built on
270 // this system inherits the landmark instead of each one re-earning the same finding.
271 o = ed_raw(out, o, "</head><body><main class='wrap'>" as *u8)
272 return o
273}
274
275func ed_doc_close(out: *u8, off: i64) -> i64 {
276 return ed_raw(out, off, "</main></body></html>" as *u8)
277}
278
279// a numbered section header (the 01 / 02 / 03 editorial rhythm)
280func ed_sec_open(out: *u8, off: i64, num: *u8, heading: *u8) -> i64 {
281 var o: i64 = off
282 o = ed_raw(out, o, "<section><div class='sec-head'><span class='sec-num'>" as *u8)
283 o = ed_esc(out, o, num)
284 o = ed_raw(out, o, "</span><h2>" as *u8)
285 o = ed_esc(out, o, heading)
286 o = ed_raw(out, o, "</h2></div>" as *u8)
287 return o
288}
289func ed_sec_close(out: *u8, off: i64) -> i64 { return ed_raw(out, off, "</section>" as *u8) }
290
291// a labelled callout. kind: '' plain, 'flag' alert, 'open' accent.
292func ed_note(out: *u8, off: i64, kind: *u8, label: *u8, body: *u8) -> i64 {
293 var o: i64 = off
294 o = ed_raw(out, o, "<div class='note " as *u8)
295 o = ed_esc(out, o, kind)
296 o = ed_raw(out, o, "'><span class='note-label'>" as *u8)
297 o = ed_esc(out, o, label)
298 o = ed_raw(out, o, "</span><p>" as *u8)
299 o = ed_esc(out, o, body)
300 o = ed_raw(out, o, "</p></div>" as *u8)
301 return o
302}
303
304// one ledger row; `save` marks it as money saved rather than spent.
305func ed_math_row(out: *u8, off: i64, label: *u8, cents: i64, total: i64, save: i64) -> i64 {
306 var o: i64 = off
307 o = ed_raw(out, o, "<div class='math-row" as *u8)
308 if total == 1 { o = ed_raw(out, o, " total" as *u8) }
309 o = ed_raw(out, o, "'><span class='math-label'>" as *u8)
310 o = ed_esc(out, o, label)
311 o = ed_raw(out, o, "</span><span class='math-val" as *u8)
312 if save == 1 { o = ed_raw(out, o, " save" as *u8) }
313 o = ed_raw(out, o, "'>" as *u8)
314 o = ed_money(out, o, cents)
315 o = ed_raw(out, o, "</span></div>" as *u8)
316 return o
317}
318
319// a ledger row carrying a plain string instead of money (counts, store names, dates).
320func ed_math_text(out: *u8, off: i64, label: *u8, value: *u8, total: i64) -> i64 {
321 var o: i64 = off
322 o = ed_raw(out, o, "<div class='math-row" as *u8)
323 if total == 1 { o = ed_raw(out, o, " total" as *u8) }
324 o = ed_raw(out, o, "'><span class='math-label'>" as *u8)
325 o = ed_esc(out, o, label)
326 o = ed_raw(out, o, "</span><span class='math-val'>" as *u8)
327 o = ed_esc(out, o, value)
328 o = ed_raw(out, o, "</span></div>" as *u8)
329 return o
330}
331
332func ed_tag(out: *u8, off: i64, cls: *u8, text: *u8) -> i64 {
333 var o: i64 = off
334 o = ed_raw(out, o, "<span class='tag " as *u8)
335 o = ed_esc(out, o, cls)
336 o = ed_raw(out, o, "'>" as *u8)
337 o = ed_esc(out, o, text)
338 o = ed_raw(out, o, "</span>" as *u8)
339 return o
340}
341
342// ============================================================================================================
343// FIRM BANDS -- the professional-services surface (law / advisory) built in THIS system's language rather than
344// the marketing-hero language of nx_sclass_kit. PURE ADDITIONS: ed_css and every function above are untouched,
345// so /kitchen and the meal pages emit BYTE-IDENTICAL output. A firm page composes:
346// ed_doc_open -> ed_firm_css -> ed_firm_nav -> ed_firm_mast -> ed_firm_search -> bands -> ed_consult -> ...
347// WHY a separate css hook rather than extending ed_css: ed_css is the shared SSOT that /kitchen already ships;
348// growing it would rewrite a live page's bytes to serve a surface that page does not use.
349// license_tier: ORIGINAL
350// ============================================================================================================
351func ed_firm_css(out: *u8, off: i64) -> i64 {
352 var o: i64 = off
353 o = ed_raw(out, o, "<style>" as *u8)
354 // site nav: a hairline rule + mono links. Deliberately NOT sticky-blurred chrome -- print mastheads do not float.
355 o = ed_raw(out, o, ".sitenav{display:flex;flex-wrap:wrap;align-items:baseline;gap:14px 26px;padding:20px 0 16px;border-bottom:1px solid var(--rule-faint)}" as *u8)
356 o = ed_raw(out, o, ".sitenav .wordmark{font-family:var(--serif);font-size:1.2rem;letter-spacing:-.01em;color:var(--ink);text-decoration:none;margin-right:auto}" as *u8)
357 o = ed_raw(out, o, ".sitenav a.nl{font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;font-weight:600;color:var(--ink-soft);text-decoration:none}" as *u8)
358 o = ed_raw(out, o, ".sitenav a.nl:hover{color:var(--accent)}.sitenav a.nl[aria-current='page']{color:var(--accent);box-shadow:inset 0 -2px 0 var(--accent)}" as *u8)
359 // masthead + artwork column
360 o = ed_raw(out, o, ".mastrow{display:grid;grid-template-columns:1.08fr .92fr;gap:52px;align-items:center}@media(max-width:860px){.mastrow{grid-template-columns:1fr;gap:34px}}" as *u8)
361 o = ed_raw(out, o, ".artpanel{position:relative;background:var(--card);border:1px solid var(--rule-faint);border-top:4px solid var(--rule);aspect-ratio:4/5;display:grid;place-items:center;padding:26px}" as *u8)
362 o = ed_raw(out, o, ".artpanel svg{width:82%;height:auto;display:block}.artpanel img{width:100%;height:100%;object-fit:cover;display:block}" as *u8)
363 o = ed_raw(out, o, ".phchip{position:absolute;left:0;bottom:0;font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;font-weight:600;color:var(--ink-faint);background:var(--ground-sunk);border-top:1px solid var(--rule-faint);border-right:1px solid var(--rule-faint);padding:6px 10px}" as *u8)
364 // primary action -- a squared editorial button, matching .form button rather than a pill.
365 o = ed_raw(out, o, ".act{display:inline-flex;align-items:center;gap:9px;font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:600;color:var(--ground);background:var(--ink);border:0;border-radius:2px;padding:13px 22px;text-decoration:none}" as *u8)
366 o = ed_raw(out, o, ".act:hover{background:var(--accent)}.act.ghost{background:none;color:var(--ink);box-shadow:inset 0 0 0 1px var(--rule)}.act.ghost:hover{color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);background:none}" as *u8)
367 o = ed_raw(out, o, ".actrow{display:flex;flex-wrap:wrap;gap:12px;margin:30px 0 0}.acthint{font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-faint);margin:16px 0 0}" as *u8)
368 // site search -- the capability that must survive the redesign.
369 o = ed_raw(out, o, ".sitesearch{display:flex;gap:0;margin:26px 0 0;max-width:520px;border:1px solid var(--rule-faint);background:var(--card)}.sitesearch input{flex:1;min-width:0;font-family:var(--sans);font-size:.95rem;color:var(--ink);background:none;border:0;padding:12px 14px}.sitesearch input:focus{outline:2px solid var(--accent);outline-offset:-2px}.sitesearch button{font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;font-weight:600;color:var(--ground);background:var(--ink);border:0;padding:12px 18px;cursor:pointer}.sitesearch button:hover{background:var(--accent)}" as *u8)
370 // practice cells as real links (the base .cell is a div; a firm's areas must be navigable).
371 // The cell is a DIV whose title and footer are links -- NOT an <a> wrapping the whole card. Sub-item chips
372 // are anchors, and an <a> may not contain another <a>: the browser silently closes the outer one and the
373 // card falls apart. Linking the title and the footer keeps every destination reachable and the HTML valid.
374 o = ed_raw(out, o, ".cell-name a{color:var(--ink);text-decoration:none}.cell:hover{border-top-color:var(--accent)}.cell:hover .cell-name a{color:var(--accent)}a.cell-go{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--accent);text-decoration:none;margin-top:auto}a.cell-go:hover{text-decoration:underline}" as *u8)
375 o = ed_raw(out, o, ".subs{display:flex;flex-wrap:wrap;gap:6px;margin:4px 0 0;padding:0;list-style:none}.subs a{font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-soft);border:1px solid var(--rule-faint);border-radius:2px;padding:3px 7px;text-decoration:none}.subs a:hover{border-color:var(--accent);color:var(--accent)}" as *u8)
376 // attorney roster
377 o = ed_raw(out, o, ".roster{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:28px;margin:28px 0 0}.pcard{background:var(--card);border:1px solid var(--rule-faint);border-top:4px solid var(--rule)}" as *u8)
378 o = ed_raw(out, o, ".pshot{aspect-ratio:1/1;background:var(--ground-sunk);display:grid;place-items:center;border-bottom:1px solid var(--rule-faint);position:relative}.pshot img{width:100%;height:100%;object-fit:cover;display:block}.pmono{font-family:var(--serif);font-size:2.9rem;color:var(--ink-faint);letter-spacing:.04em}" as *u8)
379 o = ed_raw(out, o, ".pbody{padding:20px 22px 24px}.pbody h3{font-family:var(--serif);font-weight:400;font-size:1.32rem;letter-spacing:-.01em;margin:0 0 5px;text-transform:none}.prole{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:12px}.pbody p{font-size:.9rem;color:var(--ink-soft);margin:0;max-width:none}" as *u8)
380 o = ed_raw(out, o, ".pending{display:inline-block;margin-top:12px;font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;color:var(--buy);background:var(--buy-wash);border-radius:2px;padding:3px 8px}" as *u8)
381 // consult band: contact column beside the hardened form
382 o = ed_raw(out, o, ".crow{display:grid;grid-template-columns:.85fr 1.15fr;gap:44px;align-items:start;margin:28px 0 0}@media(max-width:860px){.crow{grid-template-columns:1fr;gap:30px}}.nap{list-style:none;padding:0;margin:20px 0 0;display:grid;gap:12px}.nap li{display:grid;grid-template-columns:auto 1fr;gap:12px;align-items:baseline}.nap .nk{font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint)}.nap a{font-weight:600}" as *u8)
383 // persistent CTA rail
384 o = ed_raw(out, o, ".ctabar{position:sticky;bottom:0;z-index:6;background:var(--ink);color:var(--ground);margin:60px -28px 0;padding:12px 28px;display:flex;flex-wrap:wrap;gap:12px 22px;align-items:center;justify-content:space-between}.ctabar b{font-family:var(--serif);font-weight:400;font-size:1.02rem}.ctabar .cbl{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;opacity:.72}.ctabar a{color:var(--ground)}.ctabar a.cbtn{font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;font-weight:600;background:var(--ground);color:var(--ink);border-radius:2px;padding:10px 18px;text-decoration:none}@media(max-width:620px){.ctabar{margin-left:-20px;margin-right:-20px;padding:10px 20px}.ctabar .cbl{display:none}}" as *u8)
385 o = ed_raw(out, o, "</style>" as *u8)
386 return o
387}
388
389// ============================================================================================================
390// PAPER TIER -- a reading surface tuned to the measured properties of PAPER rather than of a screen.
391//
392// THE COUNTERINTUITIVE PART, AND THE WHOLE POINT: the PLOS 2013 e-reader study measured the paper book at
393// Michelson contrast 0.90 and the LCD at 0.99 -- paper has LOWER contrast -- and paper still produced less
394// visual fatigue. Peak luminance, not contrast, is what the evidence implicates. So this palette deliberately
395// does NOT maximise contrast: it raises the ink off black and drops the ground off white until the pair
396// measures like paper. Every value below was chosen by running nx_eink_tone contrast, not by eye:
397//
398// ink #3E3830 on ground #EDE8DC -> michelson 904 (paper reference 900, LCD 990)
399// ink-soft #5C554A on card -> WCAG 6.40:1
400// ink-faint #6E6658 on card -> WCAG 4.93:1
401// accent #6B5836 on card -> WCAG 5.94:1
402//
403// ink-faint was FIRST set to #7A7264 and MEASURED at 4.13:1 -- below the 4.5 AA floor for the 9.5-11px mono
404// eyebrows and table headers this token is spent on. Lowering contrast toward paper must not be allowed to
405// walk small text under the accessibility floor; those are two different requirements and both bind.
406//
407// STILLNESS is the other half. E-ink cannot animate, and that absence is part of why it reads as restful, so
408// this tier removes every transition and animation rather than merely shortening them.
409// Emit AFTER ed_doc_open, like ed_firm_css. license_tier: ORIGINAL
410// ============================================================================================================
411func ed_paper_css(out: *u8, off: i64) -> i64 {
412 var o: i64 = off
413 o = ed_raw(out, o, "<style>:root{--ground:#EDE8DC;--ground-sunk:#E4DED0;--card:#F3EFE5;--ink:#3E3830;--ink-soft:#5C554A;--ink-faint:#6E6658;--accent:#6B5836;--accent-wash:#E6DCC6;--rule:#C3B99F;--rule-faint:#DCD3C0;--have:#4A5F42;--have-wash:#E2E7DB;--buy:#7A6224;--buy-wash:#EDE4CC;--alert:#8A4436;--alert-wash:#F0DFD9}" as *u8)
414 // Night reading: same paper discipline, inverted. Still no pure black -- an OLED true-black ground with
415 // bright text is the highest-fatigue combination there is.
416 o = ed_raw(out, o, "@media(prefers-color-scheme:dark){:root{--ground:#191714;--ground-sunk:#131110;--card:#221F1B;--ink:#D8D2C6;--ink-soft:#A9A294;--ink-faint:#8A8375;--accent:#C0A468;--accent-wash:#2A2419;--rule:#6A6252;--rule-faint:#332E27;--have:#87A47C;--have-wash:#1D241A;--buy:#C3A552;--buy-wash:#251F10;--alert:#C88274;--alert-wash:#2A1C18}}" as *u8)
417 o = ed_raw(out, o, ":root[data-theme='paper-dark']{--ground:#191714;--ground-sunk:#131110;--card:#221F1B;--ink:#D8D2C6;--ink-soft:#A9A294;--ink-faint:#8A8375;--accent:#C0A468;--accent-wash:#2A2419;--rule:#6A6252;--rule-faint:#332E27}" as *u8)
418 // stillness: no motion at all, for every user, not only those who asked for reduced motion.
419 o = ed_raw(out, o, "*,*::before,*::after{transition:none!important;animation:none!important;scroll-behavior:auto!important}" as *u8)
420 // reading measure and rhythm: a book column, not a screen column.
421 o = ed_raw(out, o, "body{font-size:18px;line-height:1.72}p{max-width:66ch}.doc,.wrap{--measure:66ch}h1,h2,h3{text-wrap:balance}" as *u8)
422 // justified with hyphenation -- the browser's first-fit is not Knuth-Plass, but hyphens make its rivers
423 // far shallower, and it is the best available until nx_kp_break renders the column itself.
424 o = ed_raw(out, o, "p,li{hyphens:auto;-webkit-hyphens:auto}article p,.doc p{text-align:justify;text-justify:inter-word}" as *u8)
425 o = ed_raw(out, o, "</style>" as *u8)
426 return o
427}
428
429// site nav. Caller emits ed_firm_nav_open -> ed_firm_nav_item* -> ed_firm_nav_close.
430func ed_firm_nav_open(out: *u8, off: i64, wordmark: *u8, href: *u8) -> i64 {
431 var o: i64 = ed_raw(out, off, "<nav class='sitenav' aria-label='Primary'><a class='wordmark' href='" as *u8)
432 o = ed_esc(out, o, href)
433 o = ed_raw(out, o, "'>" as *u8)
434 o = ed_esc(out, o, wordmark)
435 return ed_raw(out, o, "</a>" as *u8)
436}
437// current=1 marks the active page for both sighted users and assistive tech.
438func ed_firm_nav_item(out: *u8, off: i64, label: *u8, href: *u8, current: i64) -> i64 {
439 var o: i64 = ed_raw(out, off, "<a class='nl' href='" as *u8)
440 o = ed_esc(out, o, href)
441 if current == 1 { o = ed_raw(out, o, "' aria-current='page" as *u8) }
442 o = ed_raw(out, o, "'>" as *u8)
443 o = ed_esc(out, o, label)
444 return ed_raw(out, o, "</a>" as *u8)
445}
446func ed_firm_nav_close(out: *u8, off: i64) -> i64 { return ed_raw(out, off, "</nav>" as *u8) }
447
448// MASTHEAD with an artwork column. `h1_em` sets the italic accent phrase the editorial voice uses; pass an
449// empty string for none (no stray empty element is emitted -- the sclass hero emitted <span> </span> whenever
450// its caller had no accent text, which is a visible artefact in the rendered heading).
451// Split open/close so the caller composes the artwork column between them: this compiler resolves calls in
452// source order, and the artwork helpers are defined below. Composition also lets a page put a real photograph
453// in that column without touching the masthead.
454func ed_firm_mast_open(out: *u8, off: i64, eyebrow: *u8, h1: *u8, h1_em: *u8, thesis: *u8, cta1: *u8, cta1href: *u8, cta2: *u8, cta2href: *u8, hint: *u8) -> i64 {
455 var o: i64 = ed_raw(out, off, "<header class='mast'><div class='mastrow'><div><p class='mast-eyebrow'>" as *u8)
456 o = ed_esc(out, o, eyebrow)
457 o = ed_raw(out, o, "</p><h1>" as *u8)
458 o = ed_esc(out, o, h1)
459 if ed_len(h1_em) > 0 {
460 o = ed_raw(out, o, " <em>" as *u8)
461 o = ed_esc(out, o, h1_em)
462 o = ed_raw(out, o, "</em>" as *u8)
463 }
464 o = ed_raw(out, o, "</h1><p class='thesis'>" as *u8)
465 o = ed_esc(out, o, thesis)
466 o = ed_raw(out, o, "</p><div class='actrow'><a class='act' href='" as *u8)
467 o = ed_esc(out, o, cta1href)
468 o = ed_raw(out, o, "'>" as *u8)
469 o = ed_esc(out, o, cta1)
470 o = ed_raw(out, o, "</a><a class='act ghost' href='" as *u8)
471 o = ed_esc(out, o, cta2href)
472 o = ed_raw(out, o, "'>" as *u8)
473 o = ed_esc(out, o, cta2)
474 o = ed_raw(out, o, "</a></div><p class='acthint'>" as *u8)
475 o = ed_esc(out, o, hint)
476 return ed_raw(out, o, "</p></div>" as *u8)
477}
478// closes the artwork column and the masthead grid.
479func ed_firm_mast_close(out: *u8, off: i64) -> i64 { return ed_raw(out, off, "</div></header>" as *u8) }
480
481// ---- placeholder ARTWORK -----------------------------------------------------------------------------------
482// Authored line art, not stock: the page ships zero third-party requests, so remote imagery would simply fail to
483// render. Each panel carries a VISIBLE mono chip plus data-nx-placeholder, so an asset still awaiting real
484// photography can never be mistaken for a finished one.
485// kind: 1 will+seal (default) | 2 scales | 3 colonnade | 4 legacy tree
486func ed_art(out: *u8, off: i64, kind: i64) -> i64 {
487 var o: i64 = off
488 if kind == 2 {
489 return ed_raw(out, o, "<svg viewBox='0 0 300 340' role='img' aria-label='Line illustration: scales of justice' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' style='color:var(--rule)'><path d='M150 46v238'/><path d='M104 292h92'/><path d='M92 300c0-12 26-16 58-16s58 4 58 16z'/><circle cx='150' cy='38' r='6' fill='currentColor' stroke='none'/><path d='M58 94h184'/><path d='M58 94 30 168h56z'/><path d='M242 94l28 74h-56z'/><path d='M22 168a36 36 0 0 0 72 0'/><path d='M206 168a36 36 0 0 0 72 0'/><path d='M58 94V80M242 94V80'/></svg>" as *u8)
490 }
491 if kind == 3 {
492 return ed_raw(out, o, "<svg viewBox='0 0 300 340' role='img' aria-label='Line illustration: a classical colonnade' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' style='color:var(--rule)'><path d='M26 104 150 40l124 64z' stroke-linejoin='round'/><path d='M34 118h232M20 300h260M40 286h220'/><path d='M70 132v154M118 132v154M182 132v154M230 132v154'/><path d='M60 132h20M108 132h20M172 132h20M220 132h20'/><path d='M60 286h20M108 286h20M172 286h20M220 286h20'/></svg>" as *u8)
493 }
494 if kind == 4 {
495 return ed_raw(out, o, "<svg viewBox='0 0 300 340' role='img' aria-label='Line illustration: a family legacy tree' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' style='color:var(--rule)'><path d='M150 300V150'/><path d='M150 214 88 160M150 214l62-54M150 170 106 122M150 170l44-48'/><circle cx='150' cy='96' r='21'/><circle cx='88' cy='152' r='16'/><circle cx='212' cy='152' r='16'/><circle cx='104' cy='114' r='12'/><circle cx='196' cy='114' r='12'/><path d='M112 300h76'/></svg>" as *u8)
496 }
497 return ed_raw(out, o, "<svg viewBox='0 0 300 340' role='img' aria-label='Line illustration: a will and testament bearing a wax seal' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' style='color:var(--rule)'><path d='M62 30h150l32 32v250H62z' stroke-linejoin='round'/><path d='M212 30v32h32' stroke-linejoin='round'/><path d='M92 98h116M92 126h116M92 154h116M92 182h74M92 218h60'/><circle cx='196' cy='242' r='26' fill='currentColor' fill-opacity='.14'/><circle cx='196' cy='242' r='17'/><path d='M188 242l6 6 11-12'/><path d='M196 268v32l-11-8-11 8v-26' stroke-linejoin='round'/></svg>" as *u8)
498}
499func ed_artpanel(out: *u8, off: i64, kind: i64, label: *u8) -> i64 {
500 var o: i64 = ed_raw(out, off, "<div class='artpanel' data-nx-placeholder='artwork'>" as *u8)
501 o = ed_art(out, o, kind)
502 o = ed_raw(out, o, "<span class='phchip'>" as *u8)
503 o = ed_esc(out, o, label)
504 return ed_raw(out, o, "</span></div>" as *u8)
505}
506
507// site search -- kept as a first-class band so a redesign cannot quietly drop the capability.
508func ed_firm_search(out: *u8, off: i64, action: *u8, placeholder: *u8, label: *u8) -> i64 {
509 var o: i64 = ed_raw(out, off, "<form class='sitesearch' role='search' method='get' action='" as *u8)
510 o = ed_esc(out, o, action)
511 o = ed_raw(out, o, "'><input type='search' name='q' aria-label='" as *u8)
512 o = ed_esc(out, o, label)
513 o = ed_raw(out, o, "' placeholder='" as *u8)
514 o = ed_esc(out, o, placeholder)
515 o = ed_raw(out, o, "'><button type='submit'>Search</button></form>" as *u8)
516 return o
517}
518
519// practice-area cell as a real link. Caller may follow with ed_sub()* then ed_cell_close().
520func ed_cell_open(out: *u8, off: i64, tier: *u8, name: *u8, note: *u8, href: *u8) -> i64 {
521 var o: i64 = ed_raw(out, off, "<div class='cell'><span class='cell-tier'>" as *u8)
522 o = ed_esc(out, o, tier)
523 o = ed_raw(out, o, "</span><span class='cell-name'><a href='" as *u8)
524 o = ed_esc(out, o, href)
525 o = ed_raw(out, o, "'>" as *u8)
526 o = ed_esc(out, o, name)
527 o = ed_raw(out, o, "</a>" as *u8)
528 o = ed_raw(out, o, "</span><p class='cell-note'>" as *u8)
529 o = ed_esc(out, o, note)
530 return ed_raw(out, o, "</p>" as *u8)
531}
532// sub-item chips are OPTIONAL and opened separately: a cell with no sub-items must not emit an empty <ul>,
533// which is the same stray-empty-element artefact the sclass hero shipped for headings with no accent phrase.
534func ed_subs_open(out: *u8, off: i64) -> i64 { return ed_raw(out, off, "<ul class='subs'>" as *u8) }
535func ed_subs_close(out: *u8, off: i64) -> i64 { return ed_raw(out, off, "</ul>" as *u8) }
536func ed_sub(out: *u8, off: i64, label: *u8, href: *u8) -> i64 {
537 var o: i64 = ed_raw(out, off, "<li><a href='" as *u8)
538 o = ed_esc(out, o, href)
539 o = ed_raw(out, o, "'>" as *u8)
540 o = ed_esc(out, o, label)
541 return ed_raw(out, o, "</a></li>" as *u8)
542}
543func ed_cell_close(out: *u8, off: i64, go: *u8, href: *u8) -> i64 {
544 var o: i64 = ed_raw(out, off, "<a class='cell-go' href='" as *u8)
545 o = ed_esc(out, o, href)
546 o = ed_raw(out, o, "'>" as *u8)
547 o = ed_esc(out, o, go)
548 return ed_raw(out, o, "</a></div>" as *u8)
549}
550
551// attorney roster. pending=1 stamps the card as awaiting real copy.
552func ed_person(out: *u8, off: i64, name: *u8, role: *u8, bio: *u8, mono: *u8, pending: i64) -> i64 {
553 var o: i64 = ed_raw(out, off, "<article class='pcard'><div class='pshot' data-nx-placeholder='portrait'><span class='pmono'>" as *u8)
554 o = ed_esc(out, o, mono)
555 o = ed_raw(out, o, "</span></div><div class='pbody'><h3>" as *u8)
556 o = ed_esc(out, o, name)
557 o = ed_raw(out, o, "</h3><span class='prole'>" as *u8)
558 o = ed_esc(out, o, role)
559 o = ed_raw(out, o, "</span><p>" as *u8)
560 o = ed_esc(out, o, bio)
561 o = ed_raw(out, o, "</p>" as *u8)
562 if pending == 1 { o = ed_raw(out, o, "<span class='pending'>Bio & headshot pending</span>" as *u8) }
563 return ed_raw(out, o, "</div></article>" as *u8)
564}
565
566// CONSULT band -- emits id='consult', the anchor the mastheadCTA points at, and a hardened intake form
567// (honeypot, maxlength caps, no published address to harvest) modelled on the proven /kitchen intake.
568func ed_consult(out: *u8, off: i64, phone: *u8, email: *u8, addr: *u8, hours: *u8, action: *u8, back: *u8, submit: *u8, note: *u8) -> i64 {
569 var o: i64 = ed_raw(out, off, "<div class='crow' id='consult'><div><ul class='nap'>" as *u8)
570 o = ed_raw(out, o, "<li><span class='nk'>Phone</span><a href='tel:" as *u8)
571 o = ed_esc(out, o, phone)
572 o = ed_raw(out, o, "'>" as *u8)
573 o = ed_esc(out, o, phone)
574 o = ed_raw(out, o, "</a></li><li><span class='nk'>Email</span><a href='mailto:" as *u8)
575 o = ed_esc(out, o, email)
576 o = ed_raw(out, o, "'>" as *u8)
577 o = ed_esc(out, o, email)
578 o = ed_raw(out, o, "</a></li><li><span class='nk'>Office</span><span>" as *u8)
579 o = ed_esc(out, o, addr)
580 o = ed_raw(out, o, "</span></li><li><span class='nk'>Hours</span><span>" as *u8)
581 o = ed_esc(out, o, hours)
582 o = ed_raw(out, o, "</span></li></ul></div><div><form class='form' method='post' action='" as *u8)
583 o = ed_esc(out, o, action)
584 o = ed_raw(out, o, "'><label>Your name<input name='name' maxlength='120' autocomplete='name'></label>" as *u8)
585 o = ed_raw(out, o, "<label>Email<input name='email' type='email' maxlength='160' autocomplete='email'></label>" as *u8)
586 o = ed_raw(out, o, "<label>Phone<input name='phone' type='tel' maxlength='40' autocomplete='tel'></label>" as *u8)
587 o = ed_raw(out, o, "<label>How can we help?<textarea name='message' maxlength='1200' required></textarea></label>" as *u8)
588 o = ed_raw(out, o, "<input class='hp' type='text' name='website' tabindex='-1' aria-hidden='true' autocomplete='off'>" as *u8)
589 o = ed_raw(out, o, "<input type='hidden' name='back' value='" as *u8)
590 o = ed_esc(out, o, back)
591 o = ed_raw(out, o, "'><button type='submit'>" as *u8)
592 o = ed_esc(out, o, submit)
593 o = ed_raw(out, o, "</button><p class='form-hint'>" as *u8)
594 o = ed_esc(out, o, note)
595 return ed_raw(out, o, "</p></form></div></div>" as *u8)
596}
597
598// persistent CTA rail -- the primary action travels with the reader instead of waiting at the page foot.
599func ed_ctabar(out: *u8, off: i64, title: *u8, line: *u8, cta: *u8, ctahref: *u8, phone: *u8) -> i64 {
600 var o: i64 = ed_raw(out, off, "<div class='ctabar'><div><b>" as *u8)
601 o = ed_esc(out, o, title)
602 o = ed_raw(out, o, "</b> <span class='cbl'>" as *u8)
603 o = ed_esc(out, o, line)
604 o = ed_raw(out, o, "</span></div><div style='display:flex;gap:20px;align-items:center'><a href='tel:" as *u8)
605 o = ed_esc(out, o, phone)
606 o = ed_raw(out, o, "'>" as *u8)
607 o = ed_esc(out, o, phone)
608 o = ed_raw(out, o, "</a><a class='cbtn' href='" as *u8)
609 o = ed_esc(out, o, ctahref)
610 o = ed_raw(out, o, "'>" as *u8)
611 o = ed_esc(out, o, cta)
612 return ed_raw(out, o, "</a></div></div>" as *u8)
613}