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 &amp; 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}