gitoriaLog in with ident

calendar

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit14ba08c714ba08c7antcolony#40: mission references point to the moved missionsmre14ba08c7/plugins/web/css.hl

20.3 KB

  1. // The framework's STYLESHEET — the Style convention of COMPONENTS §4, as the
  2. // archived hl:web implemented it (archive/hlweb-v1/plugins-web/web_css.hl), re-read
  3. // 2026-09-12 after a version built from the doc alone got most of it wrong.
  4. //
  5. // STRUCTURE FROM THE SYNTAX, VALUES FROM THE INSTANCE. A hybrid's keys come back
  6. // alphabetical, so the ORDER of rules, declarations and media conditions is read
  7. // off the syntax (hlSyntax entries are authored order) while every VALUE is read
  8. // off the constructed instance at the same path — the language has already folded
  9. // the statics (`background = dark`) and merged `base + { … }` by then.
  10. //
  11. // WHAT A RULE NAME MEANS (the archive's `placesFor`, per file):
  12. // `@root` / `':root'` a CONTRIBUTION: collected from every file into ONE
  13. // `:root { }` at the top of the sheet, in order
  14. // `'@media …'` / `@media` a BLOCK of rules: collected at the END of the sheet,
  15. // (`@supports`, the same shape and place for the other two queries
  16. // `@container`) (ticket #66, proposal 5)
  17. // one `@media` per distinct query, first-seen order;
  18. // `@media { minWidth = 960 … }` composes the query from
  19. // conditions (number → px, string verbatim)
  20. // `'@keyframes name'` an ANIMATION: its stops (`'from'`, `to`, `'50%'`) with
  21. // their declarations, emitted where it stands (ticket #66, 4)
  22. // selector-shaped verbatim (`'.icon'`, `'#page-anchor'`, `'a:hover'`)
  23. // referenced `Style.x` the class the View wrote (tag-prefix strip): `.x`, or
  24. // `tag.stripped`
  25. // `#x` id the View writes `id = 'x'` on an element: `#x` (R3)
  26. // `#x` local otherwise matched by NAME to this file's elements `x` (and
  27. // a composed child's root for `#Child`): `tag.<file class>`
  28. // own-name a rule named like an element of this file's View (or
  29. // body/html): the TAG selector, no class
  30. // anything else DEAD: dropped and listed
  31. // In a STYLES FILE (root members, no View) a bare name is a tag, kebab-cased.
  32. // NESTED under a rule, a name of PSEUDOS is that rule's pseudo-class, not a
  33. // descendant: `a { hover { … } }` → `a:hover` (ticket #66, proposal 1).
  34. // A rule with no declarations and nested rules is a CONTAINER: not emitted.
  35. // Two rules emitting one selector with different bodies: refused.
  36. import HlwCssVar from './cssvar.hl'
  37. static NL = "
  38. "
  39. static SELECTOR_CHARS = ":-.#>[]()*+~,/ %@=^|!"
  40. static HEADER = "/* GENERATED by hl:web — ONE global stylesheet.
  41. Components author their Style rules per file; only the rules a View actually
  42. REFERENCES are emitted (dead rules are dropped and listed at boot). Class names
  43. follow the tag-prefix strip rule: rule `headerMain` referenced inside `header`
  44. emits class=\"main\" under the TAG-QUALIFIED selector `header.main`; under any
  45. other tag it keeps `.headerMain`. */
  46. "
  47. Boolean minify = false
  48. view = null // the View walk: kebab, className, localClass live there
  49. gen = 0 // the analysis generation the syntax handles belong to
  50. // per sheet
  51. parts = []
  52. seen = {}
  53. dead = []
  54. mediaOrder = []
  55. mediaParts = []
  56. rootDecls = []
  57. // ---- names ----------------------------------------------------------------------
  58. // ---- css variables (creator's ruling 2026-09-13) --------------------------------
  59. // `bg = var('#FFF')` declares a custom property as a VALUE; the walker names it after
  60. // its member, writes it into the sheet's one `:root`, and rewrites every use.
  61. static counter = { n = 0 }
  62. // `var('#FFF', dark = '#000')` carries its dark-scheme value too (ticket #66,
  63. // proposal 2): the sheet writes it into `:root` under `(prefers-color-scheme: dark)`
  64. static var = (value, dark = null) => {
  65. counter.n = counter.n + 1
  66. return new HlwCssVar(value = value, id = counter.n, dark = dark)
  67. }
  68. static DARK = '@media (prefers-color-scheme: dark)'
  69. // ---- units as functions (ticket #66, proposal 3) ---------------------------------
  70. // a number is emitted unitless as written; `rem(1.5)`, `px(12)`, `pct(50)` answer the
  71. // TEXT with its unit — '1.5rem', '12px', '50%' — so a value says what it measures
  72. static rem = (n) => { return '' + n + 'rem' }
  73. static px = (n) => { return '' + n + 'px' }
  74. static pct = (n) => { return '' + n + '%' }
  75. vars = {} // id → { name value } for the sheet being built
  76. varNames = {} // name → true
  77. isVar(v) { return v != null && hlTypeName(v) == 'Instance' }
  78. // the variables a file's root members declare, named after the member
  79. collectVars(f) {
  80. for (r of fileRules(f)) {
  81. if (!r.isBlock && isVar(r.value)) {
  82. r.value.name = r.key
  83. vars['' + r.value.id] = { name = r.key; value = r.value.value; }
  84. varNames[r.key] = true
  85. }
  86. }
  87. // a token the file INHERITS: its own instance's copy, under the parent's name (ticket #39)
  88. if (f.aliases != null) {
  89. for (a of f.aliases) {
  90. a.value.name = a.key
  91. vars['' + a.value.id] = { name = a.key; value = a.value.value; }
  92. }
  93. }
  94. return null
  95. }
  96. // a value's text with every variable reference resolved: a variable itself →
  97. // var(--name); a composed string carries placeholders var(--hlv<id>) → var(--name)
  98. varText(v) {
  99. if (isVar(v)) {
  100. let d = vars['' + v.id]
  101. if (d == null) { hlError("a css variable is used before any styles file declared it as a root member — `name = var(…)` at the root of the styles file") }
  102. return 'var(--' + view.kebab(d.name) + ')'
  103. }
  104. let s = '' + v
  105. if (s.indexOf('var(--hlv') < 0) { return s }
  106. for (id of vars.keys()) { s = s.replaceAll('var(--hlv' + id + ')', 'var(--' + view.kebab(vars[id].name) + ')') }
  107. return s
  108. }
  109. // the pseudo-classes a nested entry may name, camelCase as written (kebab-cased on emit)
  110. static PSEUDOS = ['hover', 'focus', 'focusVisible', 'focusWithin', 'active', 'visited', 'target',
  111. 'disabled', 'enabled', 'checked', 'required', 'optional', 'valid', 'invalid', 'readOnly',
  112. 'placeholderShown', 'firstChild', 'lastChild', 'onlyChild', 'firstOfType', 'lastOfType', 'empty']
  113. isPseudo(name) { return PSEUDOS.includes(name) }
  114. // the selectors a nested entry composes under its enclosing rule's `sels`: a
  115. // pseudo-class sticks to each (`a:hover`), anything else is a descendant (`a span`)
  116. nestedSels(f, sels, key) {
  117. let out = []
  118. if (isPseudo(key)) {
  119. for (p of sels) { out.push(p + ':' + view.kebab(key)) }
  120. return out
  121. }
  122. let kidSels = selectorsFor(f, key)
  123. for (p of sels) { for (s of kidSels) { out.push(p + ' ' + s) } }
  124. return out
  125. }
  126. isKeyframes(name) { return name == '@keyframes' || name.startsWith('@keyframes ') }
  127. isRootBlock(name) { return name == '@root' || name == ':root' }
  128. // THE THREE QUERIES share one shape: `@media`, `@supports`, `@container` — bare with
  129. // conditions as scalars, or spelled out in the key
  130. static QUERIES = ['@media', '@supports', '@container']
  131. queryKind(name) {
  132. for (k of QUERIES) {
  133. if (name == k || name.startsWith(k + ' ') || name.startsWith(k + '(')) { return k }
  134. }
  135. return ''
  136. }
  137. isMediaBlock(name) { return queryKind(name) != '' }
  138. mediaLiteral(name) { return name.slice(queryKind(name).length, name.length).trim() }
  139. localName(name) { return name.startsWith('#') ? name.slice(1) : '' }
  140. selectorShaped(name) {
  141. if (name.length == 0) { return false }
  142. if ('0123456789'.indexOf(name.charAt(0)) >= 0) { return true }
  143. let i = 0
  144. while (i < name.length) {
  145. if (SELECTOR_CHARS.indexOf(name.charAt(i)) >= 0) { return true }
  146. i = i + 1
  147. }
  148. return false
  149. }
  150. // ---- one block's syntax, with its values --------------------------------------
  151. // the entries of a hybrid node: [{ key node value isBlock }] in authored order,
  152. // values read off `hybrid` (the instance's evaluated member at the same path)
  153. entriesOf(key, node, hybrid) {
  154. let out = []
  155. let n = node == 0 ? null : hlSyntax(gen, key, node)
  156. if (n == null || n.tag != 'object_expression') {
  157. // A MERGED RULE (`extR = baseR + { top = 'auto' }`, COMPONENTS §4: "Style gets it
  158. // for free") has no literal to read: its entries are the merged VALUE's, and a
  159. // nested block is read the same way. It used to read as no entries at all, so the
  160. // View wrote the class and the sheet carried no rule for it (mission 322 row 9).
  161. if (isHybridValue(hybrid)) {
  162. for (k of hybrid.keys()) { out.push({ key = k; node = 0; value = hybrid[k]; isBlock = isHybridValue(hybrid[k]); }) }
  163. }
  164. return out
  165. }
  166. for (h of n.entries) {
  167. let e = hlSyntax(gen, key, h)
  168. if (e != null && e.tag == 'object_property') {
  169. let v = hlSyntax(gen, key, e.value)
  170. let val = hybrid != null ? hybrid[e.key] : null
  171. out.push({ key = e.key; node = e.value; value = val; isBlock = v != null && (v.tag == 'object_expression' || (v.tag == 'binary_expression' && isHybridValue(val))); })
  172. }
  173. }
  174. guardMedia(key, out)
  175. return out
  176. }
  177. // a hybrid VALUE (a rule), as opposed to a scalar declaration or a var() token
  178. isHybridValue(v) { return v != null && hlTypeName(v) == 'Hybrid' }
  179. // TWO BLOCKS UNDER ONE `@media` NAME (creator, 2026-09-13; measured in the social
  180. // app's styles file). This walk reads a block's STRUCTURE off the syntax but its
  181. // VALUES off the CONSTRUCTED INSTANCE, by name — and a name declared twice holds
  182. // one value, the second. Both blocks then read the second one's hybrid, so the
  183. // first comes out with no conditions and no declarations (`@media (prefers-color-scheme: )`)
  184. // and nothing in the sheet says why. Refused here rather than emitted garbled.
  185. guardMedia(key, entries) {
  186. let mediaSeen = {}
  187. for (e of entries) {
  188. if (isMediaBlock(e.key)) {
  189. if (mediaSeen[e.key] != null) {
  190. hlError(key + " declares '" + e.key + "' twice. A block's values are read off the constructed instance BY NAME, so the second binding is what BOTH blocks read and the first one comes out empty. Spell the two queries out as distinct keys — '@media (prefers-color-scheme: dark)' = { … } and '@media (min-width: 640px)' = { … } — or move the second block into a component's own Style")
  191. }
  192. mediaSeen[e.key] = true
  193. }
  194. }
  195. return null
  196. }
  197. // a value as CSS text: a string as written, a number unitless, a static already
  198. // folded, a css variable (or a string composed with one) resolved to var(--name)
  199. text(v) {
  200. return varText(v)
  201. }
  202. declLine(k, v) {
  203. if (minify) { return view.kebab(k) + ':' + text(v) + ';' }
  204. return ' ' + view.kebab(k) + ': ' + text(v) + ';' + NL
  205. }
  206. // a media condition: `minWidth = 960` → `(min-width: 960px)`, a string verbatim
  207. condition(k, v) {
  208. if (hlTypeName(v) == 'Number') { return '(' + view.kebab(k) + ': ' + v + 'px)' }
  209. return '(' + view.kebab(k) + ': ' + v + ')'
  210. }
  211. // the whole prelude, `@media (a) and (b)`: an enclosing query of the SAME kind is
  212. // joined with `and`; one of another kind cannot be, in a sheet written flat
  213. queryText(kind, outer, literal, conds, name) {
  214. let parts = []
  215. if (outer != '') {
  216. if (!outer.startsWith(kind + ' ')) { hlError("'" + name + "' stands inside '" + outer + "' — two kinds of query cannot be joined into one; write the block beside it, not inside") }
  217. parts.push(outer.slice(kind.length + 1, outer.length))
  218. }
  219. if (literal != '') { parts.push(literal) }
  220. for (c of conds) { parts.push(c) }
  221. let out = ''
  222. for (p of parts) { out = out == '' ? p : out + ' and ' + p }
  223. return kind + ' ' + out
  224. }
  225. // ---- where a rule lands (the archive's placesFor) -------------------------------
  226. // `f` is the file block: { key cls refs tags hasView isStyles }; refs are what the
  227. // View wrote ([{ tag rule }]), tags the element names the file's own View renders.
  228. selectorsFor(f, name) {
  229. if (f.isStyles) {
  230. if (selectorShaped(name)) { return [name] }
  231. return [view.domTag(name)]
  232. }
  233. let local = localName(name)
  234. // the View writes `id = 'x'`: `#x` is that id, nothing else (ticket #51, R3)
  235. if (local != '' && f.ids != null && f.ids.includes(local)) { return [name] }
  236. if (local != '' && !selectorShaped(local)) {
  237. let out = []
  238. for (r of f.refs) {
  239. if (r.rule == name) {
  240. let sel = view.domTag(r.tag) + '.' + view.localClass(f.key)
  241. if (!out.includes(sel)) { out.push(sel) }
  242. }
  243. }
  244. return out
  245. }
  246. if (selectorShaped(name)) { return [name] }
  247. let classes = []
  248. for (r of f.refs) {
  249. if (r.rule == name) {
  250. let cls = view.className(name, r.tag)
  251. if (!classes.includes(cls)) { classes.push(cls) }
  252. }
  253. }
  254. if (classes.length == 0) {
  255. if (f.tags.includes(name) || name == 'body' || name == 'html') { return [view.domTag(name)] }
  256. return []
  257. }
  258. let out = []
  259. for (cls of classes) {
  260. if (cls == name) { out.push('.' + view.kebab(cls)) }
  261. else { out.push(view.domTag(name.slice(0, name.length - cls.length)) + '.' + view.kebab(cls)) }
  262. }
  263. return out
  264. }
  265. // ---- emission -------------------------------------------------------------------
  266. putRule(f, name, selector, body, query) {
  267. let first = seen[query + '|' + selector]
  268. if (first != null) {
  269. if (first.body == body) { return null }
  270. hlError("style rule '" + selector + "' is emitted twice with different bodies — '" + first.rule + "' in " + first.file + " and '" + name + "' in " + f.key + " both emit that selector; put the declarations in ONE rule, or rename one of them")
  271. }
  272. seen[query + '|' + selector] = { body = body; rule = name; file = f.key; }
  273. if (query == '') {
  274. parts.push(minify ? selector + '{' + body + '}' : NL + selector + ' {' + NL + body + '}' + NL)
  275. } else {
  276. mediaPut(query, selector, body)
  277. }
  278. return null
  279. }
  280. mediaPut(query, selector, body) {
  281. if (!mediaOrder.includes(query)) { mediaOrder.push(query) }
  282. // inside a media block every line is indented two spaces (the archive's goldens)
  283. let inner = minify ? selector + '{' + body + '}' : ' ' + selector + ' {' + NL + ' ' + body.replace(NL + ' ', NL + ' ') + ' }' + NL
  284. mediaParts.push({ query = query; text = inner; })
  285. return null
  286. }
  287. // ONE RULE: its own declarations under `sels`, its nested rules composed below it
  288. // (descendant), its media blocks under the query; a container emits nothing itself
  289. emitRule(f, name, sels, entries, query) {
  290. let body = ''
  291. let kids = []
  292. let medias = []
  293. for (e of entries) {
  294. if (e.isBlock) {
  295. if (isMediaBlock(e.key)) { medias.push(e) } else { kids.push(e) }
  296. } else if (e.value != null) {
  297. body = body + declLine(e.key, e.value)
  298. }
  299. }
  300. if (body != '') {
  301. for (sel of sels) { putRule(f, name, sel, body, query) }
  302. }
  303. for (k of kids) {
  304. if (isKeyframes(k.key)) { hlError("'" + k.key + "' inside '" + name + "' in " + f.key + " — an animation is not a rule's child: write it at the top of the Style or of the styles file") }
  305. let composed = nestedSels(f, sels, k.key)
  306. if (composed.length == 0) {
  307. dead.push(f.cls + '.' + name + ' ' + k.key)
  308. } else {
  309. emitRule(f, name + ' ' + k.key, composed, entriesOf(f.key, k.node, k.value), query)
  310. }
  311. }
  312. for (m of medias) {
  313. if (query != '') { hlError("'" + m.key + "' inside '" + name + "' is already under a query — state both conditions in ONE block") }
  314. mediaBlock(f, m.key, m.node, m.value, sels, name, '')
  315. }
  316. return null
  317. }
  318. // ONE `@media` BLOCK, at the top of a Style (prefixes empty) or inside a rule
  319. // (prefixes = the enclosing rule's selectors): scalar entries are CONDITIONS
  320. // (only for the bare `@media` spelling), block entries are RULES under the query;
  321. // inside a rule, scalar entries are the ENCLOSING RULE's declarations under it.
  322. mediaBlock(f, name, node, hybrid, prefixes, outer, outerQuery) {
  323. let literal = mediaLiteral(name)
  324. let conds = []
  325. let own = ''
  326. let rules = []
  327. let overrides = ''
  328. for (e of entriesOf(f.key, node, hybrid)) {
  329. if (e.isBlock) { rules.push(e) }
  330. // a css VARIABLE re-assigned under the query: `bg = '#000'` inside
  331. // `@media { prefersColorScheme = 'dark' … }` is `:root { --bg: #000 }` there
  332. else if (prefixes.length == 0 && varNames[e.key] != null) { overrides = overrides + declLine('--' + view.kebab(e.key), e.value) }
  333. else if (literal == '' && prefixes.length == 0) { conds.push(condition(e.key, e.value)) }
  334. else if (prefixes.length > 0) { own = own + declLine(e.key, e.value) }
  335. else { hlError("'" + name + "' already spells its query out, so '" + e.key + "' is a declaration directly inside an at-rule — put it in a rule inside the block") }
  336. }
  337. let q = queryText(queryKind(name), outerQuery, literal, conds, name)
  338. if (overrides != '') { putRule(f, name, ':root', overrides, q) }
  339. if (own != '') { for (sel of prefixes) { putRule(f, outer, sel, own, q) } }
  340. for (r of rules) {
  341. if (isMediaBlock(r.key)) { mediaBlock(f, r.key, r.node, r.value, prefixes, outer, q) }
  342. else if (isKeyframes(r.key)) { hlError("'" + r.key + "' inside '" + name + "' in " + f.key + " — an animation is not scoped by a query: write it at the top of the Style or of the styles file") }
  343. else {
  344. let composed = prefixes.length > 0 ? nestedSels(f, prefixes, r.key) : selectorsFor(f, r.key)
  345. if (composed.length == 0) { dead.push(f.cls + '.' + r.key + ' (in ' + name + ')') }
  346. else { emitRule(f, r.key, composed, entriesOf(f.key, r.node, r.value), q) }
  347. }
  348. }
  349. return null
  350. }
  351. // a file's rules in authored order: [{ key node value isBlock }] — a component's
  352. // Style entries, or a styles file's root members (its inherits' first)
  353. fileRules(f) {
  354. return f.rules
  355. }
  356. // ONE `'@keyframes name' = { 'from' = { … } to { … } '50%' = { … } }`: every entry is
  357. // a stop, its key the stop's selector verbatim, its entries declarations
  358. keyframes(f, r) {
  359. let name = r.key.slice(10, r.key.length).trim()
  360. if (name == '') { hlError("'@keyframes' in " + f.key + " names no animation — write the name into the key: '@keyframes fade' = { 'from' = { opacity = 0 } to { opacity = 1 } }") }
  361. let body = ''
  362. for (st of entriesOf(f.key, r.node, r.value)) {
  363. if (!st.isBlock) { hlError("'" + st.key + "' in '" + r.key + "' (" + f.key + ") is a declaration, and an animation holds stops — 'from', to, '50%' — each a block of declarations") }
  364. let decls = ''
  365. for (d of entriesOf(f.key, st.node, st.value)) {
  366. if (d.isBlock) { hlError("'" + d.key + "' in the stop '" + st.key + "' of '" + r.key + "' (" + f.key + ") is a block — a stop holds declarations only") }
  367. if (d.value != null) { decls = decls + (minify ? declLine(d.key, d.value) : ' ' + declLine(d.key, d.value)) }
  368. }
  369. body = body + (minify ? st.key + '{' + decls + '}' : ' ' + st.key + ' {' + NL + decls + ' }' + NL)
  370. }
  371. putRule(f, r.key, '@keyframes ' + name, body, '')
  372. return null
  373. }
  374. // ---- the sheet ------------------------------------------------------------------
  375. // `files` are the blocks in cascade order: the styles file (with its inherits
  376. // walked first by the caller), then every served component.
  377. sheet(files) {
  378. parts = []
  379. seen = {}
  380. dead = []
  381. mediaOrder = []
  382. mediaParts = []
  383. rootDecls = []
  384. vars = {}
  385. varNames = {}
  386. for (f of files) { collectVars(f) }
  387. // the declared variables come first in :root, in declaration order; their dark
  388. // values open the dark-scheme query with a :root of their own (an explicit
  389. // `@media { prefersColorScheme = 'dark' … }` re-assignment lands after it and wins)
  390. let darkBody = ''
  391. for (f of files) {
  392. for (r of fileRules(f)) {
  393. if (!r.isBlock && isVar(r.value)) {
  394. rootDecls.push(declLine('--' + view.kebab(r.key), r.value.value))
  395. if (r.value.dark != null) { darkBody = darkBody + declLine('--' + view.kebab(r.key), r.value.dark) }
  396. }
  397. }
  398. }
  399. if (darkBody != '') { mediaPut(DARK, ':root', darkBody) }
  400. for (f of files) {
  401. for (r of fileRules(f)) {
  402. if (!r.isBlock) {
  403. // a scalar root member of a styles file is not a rule (a variable was taken above)
  404. } else if (isMediaBlock(r.key)) {
  405. mediaBlock(f, r.key, r.node, r.value, [], '', '')
  406. } else if (isKeyframes(r.key)) {
  407. keyframes(f, r)
  408. } else if (isRootBlock(r.key)) {
  409. for (d of entriesOf(f.key, r.node, r.value)) {
  410. if (d.isBlock) { hlError("'" + r.key + "' holds custom properties only — '" + d.key + "' is a rule; put it beside the block, not inside it") }
  411. rootDecls.push(declLine(d.key, d.value))
  412. }
  413. } else {
  414. let sels = selectorsFor(f, r.key)
  415. if (sels.length == 0) { dead.push(f.cls + '.' + r.key + ' bound to nothing (no reference, no matching element)') }
  416. else { emitRule(f, r.key, sels, entriesOf(f.key, r.node, r.value), '') }
  417. }
  418. }
  419. }
  420. let out = minify ? '' : HEADER
  421. if (rootDecls.length > 0) {
  422. let body = ''
  423. for (d of rootDecls) { body = body + d }
  424. out = out + (minify ? ':root{' + body + '}' : NL + ':root {' + NL + body + '}' + NL)
  425. }
  426. for (p of parts) { out = out + p }
  427. for (q of mediaOrder) {
  428. let inner = ''
  429. for (p of mediaParts) { if (p.query == q) { inner = inner + p.text } }
  430. out = out + (minify ? q + '{' + inner + '}' : NL + q + ' {' + NL + inner + '}' + NL)
  431. }
  432. if (dead.length > 0) {
  433. for (d of dead) { console.log('style DROPPED: ' + d) }
  434. }
  435. return out
  436. }

Branches

Latest commits

  • 14ba08c7antcolony#40: mission references point to the moved missionsmre
  • 99c73346antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 76edaa62calendar: Hybriel master ff51cf46 (re-vendor round, static workaround removed)mre
  • 90a3fc2cdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 6722b72ddeploy.sh: never send .git or .gitignore to Byrodinmre
  • be099807State of 2026-09-27, before the move to gitoriamre