@spinabot/brigade 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -0
  4. package/SECURITY.md +208 -0
  5. package/assets/brigade-wordmark-on-black.png +0 -0
  6. package/assets/brigade-wordmark.png +0 -0
  7. package/brigade.mjs +96 -0
  8. package/dist/cli/chat-cmd.js +120 -0
  9. package/dist/cli/config-cmd.js +132 -0
  10. package/dist/cli/connect-cmd.js +447 -0
  11. package/dist/cli/doctor-cmd.js +317 -0
  12. package/dist/cli/gateway-cmd.js +92 -0
  13. package/dist/cli.js +287 -0
  14. package/dist/core/agent.js +1123 -0
  15. package/dist/core/config.js +80 -0
  16. package/dist/core/console-stream.js +188 -0
  17. package/dist/core/error-classifier.js +354 -0
  18. package/dist/core/event-logger.js +122 -0
  19. package/dist/core/model-caps.js +185 -0
  20. package/dist/core/provider-payload-mutators.js +517 -0
  21. package/dist/core/provider-quirks.js +285 -0
  22. package/dist/core/server.js +459 -0
  23. package/dist/core/smart-compaction.js +209 -0
  24. package/dist/core/system-prompt-defaults.js +88 -0
  25. package/dist/core/system-prompt-guidance.js +269 -0
  26. package/dist/core/system-prompt.js +884 -0
  27. package/dist/index.js +30 -0
  28. package/dist/integrations/ollama.js +140 -0
  29. package/dist/protocol.js +49 -0
  30. package/dist/providers/catalog.js +100 -0
  31. package/dist/providers/validate-key.js +197 -0
  32. package/dist/tui/client.js +263 -0
  33. package/dist/ui/brand-frames-cli.js +20 -0
  34. package/dist/ui/brand-frames.js +36 -0
  35. package/dist/ui/brand.js +402 -0
  36. package/dist/ui/chat.js +929 -0
  37. package/dist/ui/onboarding.js +400 -0
  38. package/dist/ui/theme.js +51 -0
  39. package/package.json +92 -0
@@ -0,0 +1,36 @@
1
+ // Auto-generated by scripts/build-brand-frames.mjs from the source MP4.
2
+ // Each frame is 16×16 source pixels (RGB24, base64-encoded).
3
+ // Rendered as 16 cols × 8 terminal rows via half-block (▀) cells.
4
+ // Re-run the build script if the source video changes.
5
+ export const VIDEO_COLS = 16;
6
+ export const VIDEO_SRC_ROWS = 16;
7
+ export const VIDEO_TERM_ROWS = 8;
8
+ export const VIDEO_FRAME_COUNT = 24;
9
+ export const VIDEO_FPS = 4;
10
+ export const VIDEO_FRAMES_BASE64 = [
11
+ "AQACAAABAQACAAAAAAAAAAAAAAAAdXBZhH5iBQAAAAAAAAAAAAAAAQACAAABAAABAAABAQACAAAACwkKcXSPODtWPTAA/O+y//evVEMAKSxGdHaRFBITAAAAAQAAAAAAAAAAAgABAAAAPDhB//7/oqDRFBcaWVtfYmVkFBcWi4q8////U1BdAAAAAwECAAAAAAAAAwECAAABBAQJYnfAhJnhg6LrkbD5j6/0hKXpgZngcInQDw0XAAABAwECAAAABQIAAAAAAAAtWmuZssj63/X/7/T55enu6Onv7/D24PX/u9D8ZXelAAY0AAAABAEAAAAAHx8jVmmbxNj/zMzReHh9UlBRTUtMT0xNUlBRdXV3xcXIy9//W2+hICEnAAAAPFB0yt7/0eD2nq3DX1kvw72TdnFmPTgtPDcsaGRYxsGXbGY8l6S7zNnwyN3/SV6Gi6vtr8//xM/aU15pi4hdenZLjIhzVlE8TUk0j4p1d3FIj4lfTFZewMnRrMv/i6v0ZIPPq8r/yNHpfYafTUgemJJocF4vn4xepY9hblgqlI9lUkwiZm+DydLmqMn/bY3bBBRBbn6rkaTP5ff/iouRQ0RKNigTcGJNfmhUPigUQ0JJfHuC4fP/m63Ud4u7DyJSAAAAAAAAAwYTb3N/8vf/5ev0ycrSs7S8sLW8xMnQ3+Tw8/f/hImZCQ0eAQAAAAAAAQACAQACAAAAAAAAUWuop8H+xNr/zeT/0OT/yNv/qcT/XXe3AAAAAAAAAQAAAQAAAAABAAABAAAEBAQJHCpMHCpMLkV7NUyCNk2DLkV7GShKGilLBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
12
+ "AQACAAABAAABAAABAQACAAAAAAAAAAAAAAAAUk8/SkU6AAAAAwAIAwAIAwACAgABAAABAAABAAAAAgABAAAALiwvvsDhUVJ0PTQA//m6//etLyEAAAAAAAABAAAAAAAAAAABAAABAAAABAQEAAAAQz5E4eX/gYTEIB4IhYNtfndeLygPhIamjZCvAgABAAAAAAADAQEFAQAAAAAAAhI+QVF9bpHnf6L5g6jycJbfV3O5NlKYvr3yy8r+EhAWAAAAAwAAAAAAAAISKC09VHfNncD/3+z/5PD/7fL+8PX/5/3/0uj/mK7rL0WCAAAAAwECAAAIMztKp8PztND/tsThw9HujYuCa2hfWVdYbWtsioiLtbO22ez/p7rlERguAAALAA04ip3IwN3/nrv1zc/ITE1Gta5lycJ4TUlKOzY3ODMiWVNDdnx82N7egY2pAAklAAs9fpLEq8b/qcX/oqCIWFY94d6oXVkjUE1dOzhIc20m7eegsq6SbWlN0+X/pbbbAAANLDNIhqHrnbf/3d3fPDw+hH1XYls1dGE8n4xniYE72NCKRkQ3PTsus8Puucn0AQAAAAAABhAlOkNY5ez/zNPoVlZaLCwwd1supYlbOi0WbWBJODQodG9kz+b/lq3mAAABAQEDAAAAAAAAMztK2ODv9Pn/w8jRnZqbhoSFdXN4eHZ8tLnJ2+Dwg5bDMURxAAABAAABAgEAAAAAGDd6X37Bm7f7wNv/4O7/5PL/5/X/4e//qLDAHyc2AAAAAAAAAAABAAABAQACAAAAEBQhEhcjHjFhOk19Ql6fVHCyS2adPFeNBQoTAAAAAwECAgABAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQACAgADAAABAAABAAABAAABAAABAAABAQACAQACAQEFAQEFAgADAQACAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
13
+ "AQACAAABAAABAAABAAABAAABAQACAAAAAAAAAAAAAAAACwYIAQAEAAABAQAEAAADAAABAAABAAABAAABAAAAAQEBAAAAIB4jt7nTUVJtLScA3titraR6BQAAAQAJAAAIAAABAAABAAACAQEDAwIAAAAAAAAAQj9F5+n/honMRTYA5NSU2MmMHg8AAAAAAAAGAAABAAACAAAAAAAAAAAADA0YOlirWHbJcZTvbZDrWXGiVG2eIipIV198ubbJJyU3AQAAAAAAAAUXUFlrf5rPhJ/TfJritdP/3uj96fT/3/b/xNv/iaHsfJTf4t//UU11AgADAAAAP1qYr8r/yuf/pMHw3+f0r7fEi4mKfXt8mpmYysnI6fn/0uH/Ym+WAAAjAAAFAAACTW2vrs3/k6zdxt//m5mBVFI6z8aBlo1INTAxQj0+WFlkt7nD0N7/FCJEAQAAAAAAGC9opbz1jaXe2fD/VE0ZubJ9vbuLb209RUNSOzhIYFk3dW5MtLzJd3+MAgAAAwAAAAAPLDNIdYm75/v/i4h1U1A9mZJwRD0bfHBXXVE33tZ/4tqEcHF3xsjNAAABAAABAQIAAAAAEhksytHk8vb/bnODMSMWXU9BxqhpfV8g0c2YZGAqMzZGur3NAAABAAABAAAAAwIBAAAXS1Rv0dr18Pr/ubOuc21oeGlbOyweRkEwPzopd4Srr7ziAAADAAADAwAAAAAACyNWT2eZZ4vfmLz/3un/6fX/0tbpvcHUr7PBxMjWk6XMM0RsAAABAAABAAABAAAABAMCBAMCDBs9OEhpSWWmZoLDipvBlqfMipGkO0JUAAAAAAAAAAABAAABAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQACAgADAAABAAABAAABAAABAAABAAABAAACAQEDAQACAQACAgABAgABAwEEAgADAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
14
+ "AQACAAABAAABAAABAAABAAABAQACAAABAAAAAAAAAAAAAAAAAAABAAAAAQACAAABAAABAAABAAABAAABAAAAAAAAAwEEAAAAfHuSkpGoDwwAhoRu0MiWIxsAAAAEAgEKAAABAAABAAABAQEDAgEAAAAAAAAABAEAtrb21tb/PTMSr6WE//+mX0oAAAAAAAAEAAABAAABAgAAAAAAAAAAAgIGL0qUV3G8aYzsc5b1WnCgU2mZRkpYNThGiIqeLS9DAgAAAgAAAAASRE1hiKDRnbXmdpbhnr3/1uT/3+3/3vX/xNv/gp3nZH7J7un/jYmzBAEAAAAAIDdwnrXuyef/pML30dvvy9bpmJaZiYaKnpudzMrL6/r/1uX/gYy6CxdEAgABAAAAJT99pL78nrnvqMP60tHQQ0JBr6hdtK1hQz04Pzo1X2Bot7nB1+X/IjBQAwAAAQAAAAgzi57JiaLnyeL/koxsaGJC7uiof3o6WlhnOjdGRUEsV1I9q7PCjZalAAAAAQAAAAADEBQeVGia1un/wsC2Pz00pZ91UUshZl1SUkk+rKRN+vKbf4F8tbayAAABAAABAgEAAAAAAAAQp6y8+v//kZirMSojPzgx07Jumng1ycN/nphUNzhDqKm0AAABAAABAgAABAEAAAAaN0Fbvcnn7/v/xsTFZWJkcWBSSTcpT0UtTEMqSVFusLnWAAADAAADAwAAAAAABRtLT2WUYYXbkLT/2ub/7vr/zdLiq6/Al5iiqau1prnmT2GPAAABAAABAAABAAABBgMAAwAACBY1OEZmSWWoZ4PGlKDBpbDRoqm8aXCDCwsNAAAAAAABAAABAAABAAABAQACAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgADAAABAAABAAABAAABAAABAAABAQACAgADAQACAQACAgADAgADAwEEAwEEAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
15
+ "AQACAAABAAABAAABAAABAAABAQACAAAAAAAAAAAAAQAAIBwZAAABAAABAQAEAAADAAABAAABAAABAAABAAAAAwMDAAAAYF1lvL3YICE8dGwp///Dd3BPAAAAAQAJAAAIAAABAAABAAAEAgIGBAMAAAAAAAAAioWW5ej/SUyLcFsL79qKjIRlCwMAAgMNAAAEAAABAAACAAAAAAAAAAAAGR4sQWC3YH/Xd5rubZDkX3quSWSYGB5GkpjB0M3eFhMjAQAAAAAAAwsYUlpndI/Kd5LNiaXow9//4ez/7ff/4PX/y+D/lKz2i6LtubXYHBk8AAAEAAADWXSywNr/xOH/q8jv4OXun6Stg4GCd3V2mJaXxsTF5vX/0eD/SFNxAAATAAAJBgsZcJDUr8//lKnT1On/dG9KeHRP0suLfHU1NC81QTxCT09dtrbEytj3BRMzAAACAAAAQV6dqMX/n7Dhzd//QToA5d6elpJxaGVEPjtFPzxGhX1NiH9QsrrJcXqJAwAAAAAAAAQgRVJukKLQ5vn/Z2JJa2ZMiH1dVEophnpRcGQ79O6evbdnaGx61NjmAAABAAACAAAAAAAALjdL4On94OX1WV5uNCMQbV1J1rZxfl8axMKSQj8QQkVbxMjeAAABAAABAQAAAgEAAAATYGiB3eX97fX/s6+ucG1sZFlWPDEuQzw0UElBoLDforPhAAADAAADAwAAAAAADyleUGufbZHhm8D/3un/6fX/2eDzydDixsrezNDkdYGhHilKAAABAAABAAABAAAABAMCCAYFEB5COEZrRmKmZH/DgpTCiZvJbnWIExotAAAAAAAAAAABAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwEEAgADAAABAAABAAABAAABAAABAAABAAACAQEDAQACAQACAgADAgADAgADAgADAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
16
+ "AAABAAABAAABAQACAAAAAAAAAAAAAgAAfnpXZF88AAAAAAAAAAAAAQAEAAABAAABAAABAAABAgABAAAALiw8qae3PDw+QUFD//+R/+l4IyMjFBQUIyMoAAAAAQAAAAAAAAABAAABAwIBAAAAWFJ4+fP/dnyiERc9dnFYXlk/GBk7sLLT7+n/HxkzAAAAAQEBAAABAgIEBAEAAAAAFyprd4vLfqDrja/6hqjvepviYXjEhJvniourAwQjAAAAAQAABAAAAAAAAAATNkNnkKzvyeX/6/D65+325+zz7/T75fv/yuD/c4a2BBhIAAAABQIAAAACPUJJgZnKmLDh2N3pl5uobGZhUkxIVFJYYF5kb290qamu3/P/e4/BAAAKAAAAJTtrwNb/wNf/wNf/cGxJhH9dxMCdWlYzPzw7Ozc2hn5Ropptd3+LzdbhoK7NN0VlSWKisMr/orXforXfUEofq6V6a2dLdnNXQj4tXlpJrad9lI9lV1hRmJmSxuH/oLv2DSVdmrLpnrPsxdr/WlY8aGRKjH5McGIw5cWElnY1jYVTg3tJPzw2npqUtc//kavtAAACIicwSFqFqLvm6+77a257QTg1OzMvinFWWkImOjQxT0lGf4aZ2eDzn73/VnS8AwAAAAAAAAAADwwQpq678/v/2t/mt7zDm6Cpn6StsrO719jg5u//W2V/KS4+AAISAAABAgADAgIEAAAAKESFhKDhsMr/xN7/1+f/2Oj/y+H/ornsLC0zAAAAAAAAAQAAAAABAAABAAABAAACGSM4FyE2JjxuNkx+OFKNPFaRKT5rITZiAwQKAAAFAwECAQAAAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAQEDAQEDAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
17
+ "AAABAAABAQACAAAAAAAAAAAAAAAAbWhTioJlCgIAAAAAAAAAAAAAAQACAAABAAABAAABAQACAAAACAUGb3GMP0JdNSgA8OSo//utW0kAICI9bW+KFxQWAAAAAQAAAAAAAAAAAgABAAAAMC01+fb/qafZFhccVFZbZ2dnFBQUfn2y////XVhoAAAAAwIBAAAAAAAAAwECAAABAgIGWXC7f5fhfZ/mjK71i6vzgaDoe5LddIvWEhMbAAAAAwIBAAAABAEAAAAAAAAqVmaUrMT22fL/7fL54ufu5enw7PD32/P/t8/7YnWiAAg1AAAABQIAAAAAHyElUWeavNL/zMzRenp+VlFSUUxNUE1PVFJTdXV3w8PFyd//WW+iHB4jAAAAN01zxt3/zd33oK/KX1YsxLuRe3dmPjspPzsvZ2JXycCWcGc9k6C3ydbtw9j/RluFhaXprMv/wcvZWGJwhX9UfHZLkYx1V1I7TEY1jIZ1d3FGlI9kTVNawMXMqcn/iqn1X33Gqcj/wsvohI2rS0Ubl5FnemEnuaBm0LJ1fF4hlpBlWVMoYWh7ydDipsb/b5DeARI6aHqhip3K4fT/kZCXRkVMOC8oTEM8WU1EPDAnQz9Id3R83u7/nq7Zd4u9EydZAAAAAAAAAQQRZml27PD95Oj1yMzTsLW8rrO8wcXP29/t8vX/iY2eCg8fAQAAAAAAAQACAQACAAAAAAAAS2akor37wdf/y+H/zeH/xdn/qcT/YHu5AAAAAAAAAQAAAgABAAAAAAAAAAAEBAQJGylJGylJLUR6NEuBNEuDLUR8GShKGilLBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
18
+ "AAABAAABAQACAAAAAAAAAAAAAAAAd3Nbf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKb3GMNDZRPzEA/e+u//CoTTwALC5Jc3WQExESAAAAAQAAAAAAAAAAAwECAAAAQT1F//3/m5nLExYZWl1gX2FiEhQWi4q+/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYXjDfZTffp7mja31jKz0gaDofZbdbYXMCwwSAAAAAwECAAAABQIAAAAAAAAtWGiXrsT23fP/7fL54ufu5enw6+/22vL/ts36YnWiAAY0AAAABAEAAAAAGxsgUGaZwdf/ysrMd3d6VlFSUUxNUE1PVFJTdnZ4xMTGxt3/WW+iIiMpAAAAOExwxdn9zd31m6vDYlkvxLuRc29gPjssPjoubWhdycCWbmU7l6S7ydbtw9r/RV2Giqnurcz/wsrXVF1pi4Vae3VKkYx1Uk02TUk0jYl0d3FGk41iTVdfvsjQqMj/iaj0ZoXRp8b/xM3me4SdUEofmZNoeF8iwqhsy61weFoel5FnU00ja3SIyNHlpsb/aYrYBRZCb3+skaTP4fT/iIaNRURLOiweZFZIbl1PPy4gQ0JLf36I4PD/lqbRcYSyDSBNAAAAAAAAAwgWb3SC7vP/4ebzxcrRrrO6rbK7w8jR3uLv7/T/foORBQoYAAAAAAAAAQACAQACAAAAAAAAUGuopcD9wtj/y+H/zeH/w9f/psH/WnW1AAAAAAAAAQACAQACAAAAAQAAAAAEBAQJGylJHCpKLUR6NUyCNEuBLEN4FyZFGShIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
19
+ "AAABAAABAQACAAAAAAAAAAAAAAAAdnFafnhdAwAAAAAAAAAAAAAAAwACAAABAAABAAAAAQAAAAAACwkKbnCLMzVQQTMA/e+u//KpTz0AKi1Ic3WQExESAAAAAQAAAAAAAAAAAwECAAAAPzxE/vz/m5nLExYZWl1gYGJhExYUjIu9/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBgUMYXjDfZTffZ3mjKz1iqnyf5/nfZbdbYXMDQwTAAAAAwECAAAABQIAAAAAAAAtV2eWrMT12vP/7vP65enw5uvy7PD32vD/tcv7YXOkAAQ1AAAABAEAAAAAGxsgUGaYwNb/zMzReHh9VlNUUlBRUlBRWFZXd3d6xcXIxdv/WG6hIiMpAAAAOExwxdn9zNv0nazEZFoww7qQcW5fPjssPjoubWhdyL6UbWQ6maa9ydbtw9r/RV2Giqnsrcz/wsrWVl5pi4VafHZLkYx1Uk02TUg2jYh2eHNIk41iT1hgvsjQp8b/iKfzZoXRp8b/xM3meIKaUUsgmZNodV8lt6FnvqRtdVojmJJnVE8jaHGFxtDkpsb/a4vZBBdCboGskqXQ4PP/hIOKRENKOCkaaVpLdWFPQS0aQkNJe3yC3+//mKjTc4WzDyFPAAAAAAAAAwgWcXaE7/X+3+XuwsbNq6+2q6+5wcXP2+Dt7/T/goaUBgsZAQAAAAAAAQACAQACAAAAAAAAUWynp8L9w9n/zOL/z+L/xdn/qMP/XXe1AAAAAAAAAQACAQACAAAAAQAAAAAEBAQJGylJHCpKLkV7Nk2DNk2DLUR6GCdGGShIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
20
+ "AAABAAABAQACAAAAAAAAAAAABgIAc25ZRkEwAAAAAAAAAAAAAgABAwACAAABAAABAAABAQACAAAADwwNRkpgBgoga1oJ///A3c+YKBoANztRWV1zAgABAAAAAAAAAAAAAAAAAgICAAAAd3R+////b26fJiEMiYRvXl1bISAfubbn7+3/JyMsAAAAAgABAAAAAAAAAwECAAAAJyYviJvocITRX4bZaJDibpLmZYndgpffbYLKBAQGAAAAAwECAAAABQIAAAAAAAIwS1uKk67szOf/7fb/7fb/7Pb/5/L/vd3/i6vtVGeUAAAtAAAABAEAAAAABAQJQ1iRvtP/5uv0qK22iIiKe3t9fnx9j4yNtLnG6O37nrf1U22rLCwwAAAAJTRTtMPiytfur7zTTUhFNzEvOjQxPzo3PDg3Ozc2OjQxaWRhxdr3t8zpyuD/RVuLhKHgu9j/xcrXS1BdMy0oRkE8S0ZIRUFCQ0FCQ0FCRkE8LykleIaaucbapsT/g6HwbozWrcv/xsvURktUNzQzRkNCXUstnYtteGxXSTwnSkRCJyEffYqkusbgp8j/XX3NECNTfZHBxNDusLzaPjw9NTM0Ri4Gp49nd2FGPigNODg7b29x2Ov/iZvGaXegBBI7AAAAAwECHyc2vsbW4ebtoaathYKBhoOCiYaKmJaZwMTR7/T/gYSRAAMQAAAAAAAAAQAAAAAAAAAACQkNiqDSzOL/4O7/3+3/3e7/2On/udP/aYS8AAABAAAAAgADAQACAAAAAQAAAQEFAQEFGi5UJzthQViQRl6WQ1mMNUt+GihIFyVEBAQJAAADAAAAAAAAAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAAACAQEDAgADAgADAgADAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
21
+ "AQACAAABAgAFAwEGAAAABgQFVlA+GxYEAAAAAAAAAAAAAQACAAABAAABAAABAAABAAAAAQAAAAAAAAAAAAAAfXdZ//+6sp9BBgoYYmZ0RkRKAAAAAQAAAAAAAAABAAABAgEAAAAALSxBtLPIUE9NSklIxLV1cWIiQkR17vD/tq/DAAAGAQIAAAEAAAABAAABAwIBAAAAW1Z67ef/dovTPFGZWILWW4XZaYjkfp35e4a0ERxKAAAABQIAAQEFAAADAwAAAAAAAAxBbX6zosD+yeb/2ej/2ej/xOD/nrrpcZv+aJL1LTVQAAAJAAAAAwAAAAAKAAAAZ3ur3fD/5ebsw8TKsrO5rK2zrq+32tvk0uj/iZ/PmLPnhaDUMTY/AAAEAAAITVZl1t7tiZGgSUVEPDg3PTgvQTwzkoxhfHZLlp6r5Oz5kLLnxOb/ma7nEidgSFl+3e7/gn9dqKaDw7yIVE0ZNDFBb218yb5l9OmQRUM4xcO5oL36n7z5mrP1Izx+e5DJssb/SkgvdXNa9OubgXcob2ZeTEM7dW0p3dSRNjEowbyzn77/mbn/d4y5AAs3NEuEtMv/ZmRZNzUqnY9dZVclxad4bU8gTEg+PjowdnqK+fz/c4a5O0+BDQ0QAAAAAAAQMzpMub3LhYqYRj47RDw4gm5bbVlGaGlxs7S89Pn/i5CgAAAAAAAAAAAAAQAABAEAAAAAGR4qsLXC2+X/0933z9jt3+j94vP/0eH/i6foLUmKAAADAQEFAQACAAABAAAABAIDAAAAAAAESl6NZHendIvBZ360UmmhMUmBGCU+GCU+AQEDAAABAAABAAABAAABAAABAgADAQACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAgADAgADAQACAgADAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
22
+ "AgABAgABAgAGAwAIAQAANzUsFBELAAAAAAAAAAAAAQACAAABAAABAAABAAABAAABAAAAAQAAAAAAAAAAXUwA//++pJl4IhgAcXCFISA1AAAAAgABAAABAAABAAABAAABAQAAAAAAJSg+Oj1TYU8P1sODdm1ic2lf////dG6gAAAAAgEAAQEDAAABAAABAAABAQAAAAAAxcHr3dj/LUOETGKkWHzMZIjYgprycIngPEVeAAIaAAAABAEAAAACAAABBAEAAAAARVOQlqTgrsn/xN//zeL/wdb+kbP9fqDrdpn2UnXSFxYcAAAAAAAAAQAAAAAKAAAGhJO16Pf/3uDkyMrNubrEurvF3+b55ez+j6vfpsL2wdn/YXqrCxAZAAAFAAAAVlpk1NjoYWV1TEhLPTg8U08pmZRvZ2VatrSp3fP/mK7Rs9L/m7v/SFR7AAAcCAwWlpqkkYtgiYNYQT5EODY8jYpQ2NSaz8iKRT4B0d//rbvkk7X/oML/PktvAAAcTFh4c36fkIpF8uynem5SZFg8RD8cj4pnxr6IPTUAzN3/qbrmg6LucJDbCAkPAAACOkRpdoGmKiYAtK+IjHM1vKJlZFQ8XU01NjQ1kpCR7/3/mafQLDVQAAYhAAAAAwAAAAAMbHCBfn6DPT1CPSgQgm1UYFFLYlNNrrbF8Pn/vsXbJy5EAAAAAAAAAQACAAABAAAAAAAAgYWWwsbXt7vPubzQ09rt7vX/0+v/oLfwZYTNJUSNAwECAQAAAAABAAABAQAAAAAAAAAAOD1Gf5C7i5vGf5TLaH20S2WgLUaCGiM3ExwwAAAAAQACAAABAAABAAABAQACAgADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAgADAgADAgADAgADAQACAQACAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
23
+ "AgABAgABAgAFAQAEBQMANTMpCQUCAAAAAAAAAAABAQACAAABAAABAAABAAABAAABAAAAAQAAAAAAAAAAfW0e//+8enNbLSYPcXGDDAweAAAAAQAAAAABAAABAAABAAABAQAAAAAAHCA0KCw/fGYe4MqCYFhZmZGS//z/QzxpAAAAAgEAAQEDAAABAAABAAABAAAAAAAAy8rzzcz1Jjx4UmilWHjKbIzeg53waILWOEJXAAAUAAAAAwAAAAACAAABAwAAAAAAV2Whmafkrsn/wt3/y+D/us/5iq79fKDvd5ryUXTLFhYaAAAAAAAAAQAAAAAKAAAFhZS26Pf/3d/iycvPurvFwcLM5u//2+X5i6jZtNH/vNP/V26kBggSAAAGAAAAXWFr0tbkYmZ0TUlKOzY3ZF02mZJsZGJfzMvI0ej/k6vTttb/kK/7O0NlAAAWBAULk5SakIlidW5IPDhBPzxEop1Z6eSgp59tV08c3/L/m67Zn8H/lLb/KjdTAAAUPkZfcXqSqKRZ7OedZVtIZFpGPzoQsqyCoZlsUEga3fD/lqndjKjuWXW7AAABAAABNj5gbHSWNjMGvLmMj3U3uqBiWUs2XU86NjY4r6+y6/v/hpfCISg9AAATAAAAAwAAAAALbHB+dHFzPDo7Qy0SiHFXVEtEZ15XvsXY7/b/sLnRGSE6AAAAAQAAAQACAAABAAAAAwMFjJGfu8DNr7TEtrvL197w8Pf/y+X/mLLtYoLLJkWPAgABAAAAAAABAAABAgABAAAAAAAARktUipjBkqDJf5TLZnuySWSfKkWBGiM1ExwuAAAAAQACAAABAAABAAABAQACAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAgADAgADAgADAgADAQACAQACAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
24
+ "AgABAgABAgAFAQAEBgQAMzAnBgMAAAAAAAAAAAAAAQACAAABAAABAAABAAABAAABAAAAAQAAAAAAAAAAhnYl//+5b2hTMSoWb29+CAgXAAAAAQAAAAABAAABAAABAAABAQAAAAAAGyAwIyg4hG8j4cyBW1RXopue//n/ODFdAAAAAgEAAQEDAAABAAABAAABAAAAAQAAz832yMbvIzt0U2ukV3fJbY3fg53wZn/TNj9UAAATAAAAAwAAAAACAAABAQAAAAAAXWunmqjlrsn/wt3/y+D/uc36iq79fKDvepnyUnHKFhYaAAAAAAAAAQAAAAAKAAAGhpa36fn/3uDkyszQubrEwsPN5/D/2eL2i6jZt9T/u9L/VGyhBQYPAAAFAAAAYGVu0tbkZGd1TUpJOzc2Z2A6mJFrZWRi0dDPz+b/k6vTttb/jKz3Nj5eAAATBAYKk5aZjYZib2hEPDhDQT1IpqBb7Oahn5dpXVQn4PP/mazXocP/kbP9JzBLAAASPkVbc3qQrqlf6eWaYFhGYlpJPjgNt7KGmZBmVkwi3/P/k6fajKjsUm6yAAAAAAAANz9faXGROjcIvryMkHY4uqBiWEo1XU86Nzc6tra56fr/g5O+HiU3AAAPAAAAAwAAAAAMbXF/b25tOzo4RC8SiXRXUklCaWBZw8rd7/b/rbXNFx83AAAAAgEAAQACAAABAAAABgQIj5Ohur7MrrPBtrvJ2ODv8vr/yuT/l7DuYILKJUaPAgABAAAAAAABAAABAgABAAAAAAABSk9YjZvCk6HIf5TLZnuySGKeKkWBGiM1ExwuAAAAAAACAAABAAABAAABAQACAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAgADAgADAgADAgADAQACAQACAgADAQACAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
25
+ "AgABAgABAgAGAgAGAwAAMy8pCwgEAAAAAAAAAAAAAQACAAABAAABAAABAAABAAABAAAAAQAAAAAAAAAAcWUX//++hn5kIRkAb2+BFhYnAAAAAQAAAAABAAABAAABAAABAQAAAAAAJik9LC9Dd2Ea4syFaWFehX16////XViEAAAAAgEAAQEDAAABAAABAAABAAAABQME2Nf/ysnyJTt3U2mmWHjKaYrbhp7zbYTZN0FWAAEWAAAABAEAAAACAAABAwAAAAAAV2allKTiq8j/vtv/yd7/us/7iq79e5/udZj1TXDNExMYAAAAAAAAAQAAAAEKAAAJhpS56ff/4eTnzdDTvsDKwcLM5ez+4ej7i6fbrMj8vtT/XXOmCgsWAAAGAAAAXmFu2NvpZ2t4UEtMPTg6WFEvl5BuaGVfw8C62vD/l63StdT/l7b/Q09xAAAYCxAZm6Cpj4hhenNMPTpCOjY+mZNW4NqdvrZ9S0MK2OX/qLXhmLr/nb7/N0VnAAAaV19+dX2doJhS8umkbWRLYFc+RD0ZmpNvwbmEQTgE0uL/pbXkhqXua4nSBAYKAAACRVB3b3qhLywBvLmNjXQ2vKJlYFE4YVI6NjQ1l5SW8P7/lKLLKjRJAAUaAAAAAwAAAAETcXiLdnR1PTs8PikPhnFXWU1GXVFKrrXI8vn/vMPZISg+AAAAAAAAAQACAAABAAAAAwECi5CevcLQsLXDtbrI1Nnp8PX/0+v/oLfvYoTMJUaPAwECAQAAAAABAAABAgABAAAAAAAARElSipi+k6HIgpjLaX+zTGahLUaCGyU4FB4xAAAAAAACAAABAAABAAABAQACAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAgADAgADAgADAgADAQACAQACAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
26
+ "AAABAAABAwAEBgIIAwEGAAAAGBQTCAQDAAAAAAAAAAAAAQACAAABAAABAAABAAABAAAAAQAAAAAAAAAAAAAAPToo//ais6RQAAAADQsMEhIXAAAAAQAAAAAAAAABAAABAQAAAAAAFxcmi4uaPj06UE9L9t2BvKJGKSpMvL3f1M3fDwgZAAAAAAAAAAABAAABAgICAAAAV1Bu//3/jJrXOEaDaYzQYYTIYXfLnbP/s7PbGBhBAAAABAMAAAACAAABBAEAAAAABBBFcX2ze5/1mr7/mrv9mrv9j7P/dJjlZ4/wYYnrHyY7AAAAAAAAAwAAAAAEAAAAPVeXrsj/6fP/6PL/6e7z6e7z6+/28PX8u9T/eJLUdI/GPFePEQ0KAAAAAAETVFtuw9Dp0N32i4mKZmRlWFZZVlNXXltffnx/ytLh3ub1lrTnz+3/lKXRDBxJZ3uf5fn/tbW3Ozs9OjQxQTs4Qj49Qj49RD48OzUzODg9w8PIp8P0ob3up8H/OlOYkKXextv/WVNPNjAsSUNDSkRET0lES0VBQz8+SUVELCcoioWGttD/mbPunrn5Ei1tQViQwtn/cG5xKScqSkI0VEw+tp1shWw7QTs4Pzo3LS0xycnNobz8Z4LCPUZaAAAPAAAQOkFTu77LcHSBPzgwNC0llnhSdlkzOjU2ZF9gwMPQ29/sKC02AAAIAAAAAgAAAQAAAAAAIic1wMTS0dbmub3NtLnCwsbQ0dny6fL/yd//V22gAAAAAAADAgADAAABAAAAAwMDAAAABQYPZHahiJrFkqTLi53EfJDDWW2gJjRYFiNIAgIEAAABAAABAAABAAABAAABAgADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAAAAAAAAAABAQACAgADAgADAgABAgABAgADAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
27
+ "AAABAQACAgADBAAFAwEEAwEEAAAAAAAAAAAAAgAGBQEEBQEEAQAEAAADAAABAAABAAABAAABAQAAAAAAAAAAAAAAIRwApJ96Uk02AAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAQAAAAAAKCYpW192BAgfinQU//+uw7eGIhcAYWJ/fn+dBgQFAAAAAQAAAAAAAAAAAgICAAAAn5mh+fz/en26aHeNobDGfpPAX3Sgzcv/4uD/GhgeAAAAAgABAAAAAQAABAMAAAAUYGaEgqD/dZP0bZ3/YZH1bJj7b5v+b43sdZPyNj1TAAAMAgEAAgEAAAAAAAAAGDaIZoTWhaDetdD/2+Tz5e385e7/2OHzrsr/hKDkaYjTEjB8AAAAAAAADBYpfIWZhqHWqMP39fr/1Nngu7zEra62rq+3vL3F29/s8PT/lLDnmbXsl6S9GCU+gp7Wyub/wdDmydjuaWVmQj0+Ozc0Pjs3PDg3PTo4RkJDhH+B0+b/pbfWwd//dZPbi6nypcP/xMnWUldkMSkmSkI+SkVJQj1BQj9DRkRISEI9LigjfoigucLapcX/bo/gOlOWpL3/ytHkQklbNzEvRkE+Ukk1fHNfZ19PRj4uSEI9JiAbfouvvcrui6LaGTBoAAAIHCEuhYyfw8rdSkhLNTM2SzEDtZttgmlJPycGPTxDg4KJ2uH2QUhdCgoPAAAAAQAAAAAAAAAAj5Od5u3/t77TpJ6et7Kysq2wtbC00Nv64Oz/TU9XAAAAAAAAAgABAAAAAgICAAADAAAATV+GjJ7FpbLNoa7Koa3LmqbEZnGQIy9NAAAAAgIGAQAAAAAAAAABAAABAQACAQACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgADAAABAAABAAABAAAAAAAAAAABAAABAgADAgADAgADAgADAgADAgADAwEEAgADAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
28
+ "AAABAAABAAADAQAEAwEEAgADAAADAAAAAAAAAgAGAgADBAIFAQAEAAADAAABAAABAAABAAABAQAAAAAAAAAAAAAACAUAdnRbS0g1AAAAAAAAAAAAAAAAAQAAAAABAAABAAAAAQAAAAAAEhARREZdAAIYaFQA//+z28ySJxgAODtWbW+KCwkKAAAAAQAAAAAAAAAAAgICAAAAhH+F+///gYW+bXeCq7XAkqG8YnGMvbvz9fP/NTA2AAAAAgICAAAAAQAABAMAAAAMYGF8kaj/e5LwaJn/W4z3ZJH3bZr/dJDvf5v7P0NaAAAMAwIBAgEAAQAAAAAAEi+BZYLTdJLbn73/zNfs1+H22OH6y9TtosH/fJria4nUEjB8AAAAAAAACRIjc3yNg57Wl7Lp8/f/6O3609jhxsvUyMzT0dbd5+z58PX/ka3lka3lj5uzFiI6fprSzen/sMHb1+f/jIqNUE1RQz8+Qz8+QT08REE/UE1RjIqN1+n/qLvXwuD/epjgjKvzosH/wcrbbneJLSUhRDw4SENEQj0+Pz1BRUNGRkE8MSwndX6SvMXZpML/d5bnOFSYoLz/y9TpTFZrMy0oSEI9TUQ6aWBWYVpQRj81SkQ/KiUgYWmJxs/uj6nhIDtzAAAIHiIvfoWaxs3iSEVLNTM4QyoBqZFnmn9YQygBNzc8ZGRo2N/yW2J1DAwRAAAAAQAAAAAAAAAAj5CY5u3/sLfMmJCMsKilsqmopp6dw8zp5u//bW52AAAAAAAAAgEAAAAAAgICAAAEAAAAUmCEk6HFq7TPrbbRq7TPpK3IdH2aLjdUAAAAAQEFAgABAAAAAAABAAABAQACAQACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgADAAABAAABAAABAAAAAAAAAAABAAABAgADAgADAgADAQACAQACAgADAwEEAgADAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAQACAAABAAABAAABAAABAAABAAABAAABAAAB",
29
+ "AAABAAABAwACAwACAAAAAAADAAAAMy8pKigfAAAAAAABAAAAAAABAQACAAABAAABAAABAAABAAAAAAAAGRwqAAAMPDAA//ez692dLR8ABgoaJyo7AAAAAAAAAAABAAABAAAAAwECAAAAUU9U9vb/dnakOC0ItKiDoZl6LycIkZG+9/f/PzxEAAAAAgABAAAAAAAAAwECAAAAMy83rLf/gYzWSnPLXobfXoXbT3bMiZTen6v0HhoiAAAAAwECAAAABQIAAAAAAAApQU16a4/lncH/zOD/z+L/z+H/yNr/l7v/dJjsTVqGAAAqAAAABQIAAAAAAgABM0yRkavv5e7/2+X6xsbLubm9urq+xcXK1+H25O7/kKzyOlabDAoLAAAAKDdXtcTktcjoy97+iIWGRkRFOzc2Pjs6PTo4Pjs6SkZFjImIz+H/ssTlu8/1NEhuiqnptdT/xM/idoGUKSEeRT06RkRIPz1BPz0+RUNESUE9MSkmeIOXvsndrs3/haXudZTgp8b/w8vaUVloNjAwSEJCTEQ0bmZWa2JSSD8vSUNBMy0qU1ptwcjap8j/bo/fFCpegpjLu8nrrbvdNjQ1NjQ1Si8Du6B0sJFnSywCNzU2PDo7tcLms8DkfpLEFilbAAAABQUFIik8yM/h2driiouTa2JfdW1pe3Btc2hlkZaf3+TtwcjaGiE0AwECAAAAAgEAAAAAAAAAEhMZpbfh3vD/5/T/4O3/4Oz/5/P/2Oz/oLTkEhMZAAAAAQAAAgABAAAAAAAAAQEFAAADGi9ZNUp0VG+mYn20YXerT2WYLDpeFyVJAQEFAQEFAQAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAAAAAAAAAABAAABAQEDAQEDAgADAQACAgADAgADAQEDAAACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
30
+ "AAABAAABAwACAAAAAAAAAAAAAAAAdG9YfXdbBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbW+KMzVQQTMA/e+u//KoUD4AKi1IcHONExESAAAAAQAAAAAAAAAAAwECAAAAPzxE/fz/mpnLFBcaWl1gYGJkExYXjYvA//3/TUpXAAAAAgICAAAAAAAAAwECAAABBgUMYnrEfpbgf5/nja31jKz0gaDofpfebYXMDQwWAAACAwECAAAABQIAAAAAAAAtV2eWr8X32vD/6+/24OXs4ufu6O302+//us39ZHakAAY0AAAABAEAAAAAGxsgUmaZw9f/xsbJdnZ4U09QT0pLUEtMVFBRdHR2wcHDxt3/WnCkIiMpAAAAN0tvxdn9zNv0mqnCX1kuw72Sd3RlPToqPjoubmlexsGWbGY7lKG5ytfuxtv/SF2Jiqnurcz/w8vYVF1pi4ZYfHdJkItxVFA2T0o1jYl0dnFDko1fT1NgwMTRqMj/iaj0ZoXRqMj/xMzlfoafTEkel5NoeF8iwadrza1xfFsglpBmUkwibXSKyM/lp8X/bIrbBhdDcIGtkKLN4vX/j42URkVMOjApUEY/WU1EPTEoRENKg4KJ4fL/lqbRdIa0DyFPAAAAAAAABAkXbXF/7fL+5Oj1y8zUtrfAs7fBxcrT4OXz7vP/fYKQBgsZAAAAAAAAAAAAAAAAAAAAAAAATWimpL78wNT/yd7/yd//wNb/pcD/WXS0AAAAAAAAAQAAAQAAAAAAAAAAAAAEBAQJGypMGypMLUR6NEuBNEuBLUR6GihIGylJBgYLAAADAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAAAAAAAAAABAAABAAACAQEDAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
31
+ "AAABAAABAQACAAAAAAAAAAAAAAAAdnFaf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbnCLNDZRPzEA/O6u//KpTz0AKi1Ic3WQExESAAAAAQAAAAAAAAAAAgABAAAAPjtD/Pv/mpnLExYZWVtfX2FgExYUi4q8/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYXjDfZTffp7mja31jK3ygaHmfZbdbYXMDQwTAAAAAwECAAAABQIAAAAAAAAtV2eWr8X32/L/7PD34ufu5Ojv6+/22vL/ts36ZHakAAY0AAAABAEAAAAAGxsgUmaYwtb/ycnLd3d6U1FSUE1PUE1PVlNUdnZ4w8PFxt3/WW+iISIoAAAAOExwxdn9zNv0m6vDYlkvxLuRdXFiPToqPjoubWhdyL6UbWQ6l6S7ydbtxdr/SF2Giqnsrcz/wcvXVF9ri4hbe3dLkYx1U083TUk0j4p1d3FGkoxhTVdfvsjQqMj/iajyZYTQqMj/w8zlfIWeUEofmpRpe2Elwadrza1xfV0hmJJnU00ibHOIyM/kpsb/bIzaBRZCb3+skaTP4fT/ioqPRUVKNy4mUklBXk9GPi8nRENKgX+G4fL/lqbRc4WzDyFPAAAAAAAAAwgWb3SC7vP/4uf0xsvSsra9sLW+w8jR3uLv7/T/f4SSBQoYAAAAAAAAAQAAAQAAAAAAAAAAUWumpsD7wtj/yuD/yuD/wtj/psH/W3a2AAAAAAAAAQACAQACAAAAAQAAAAAEBAQJGilJGypKLUR6NUyCNEuBLUR6GCdGGShIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAAAAAAAAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
32
+ "AAABAAABAQACAAAAAAAAAAAAAAAAdnFaf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbnCLNDZRPzEA/O6u//KpTz0AKi1Ic3WQExESAAAAAQAAAAAAAAAAAgABAAAAPjtD/Pv/mpnLExYZWVtfX2FgExYUi4q8/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYXjDfZTffp7mja31jKz0gaDofZbdbYXMDQwWAAACAwECAAAABQIAAAAAAAAtWGiXrsT22/L/7PD14ufs5Ojv6+/22vL/ts36YnWiAAY0AAAABAEAAAAAGxsgUGaYwNb/ycnLd3d6VlFSUUxNUE1PVlNUdnZ4w8PFxt3/WW+iISIoAAAAOExwxdn9zNv0m6vDYFovwryRdXFiPjssPjoubWhdxcCUa2U6l6S7ytfuxdr/SF2Giqnsrcz/wcvXU15pioZaenZKkYx1U083TUk0j4p1dnBFkoxhTVdfvsjQqMj/iajyZYTQqMj/w8zlfIWeUEofmZNoemIjwalrza1xfV0hmJJnU00iaXOGxtDkpsb/bIzaBRZCb3+skKLN4fT/ioqPRUVKNy4mUklBXk9GPi8nQ0JJf36F4PD/lqbRc4WzDyFPAAAAAAAAAwgWb3SC7vP/4uf0xsvSsLW8r7S9w8jR3uLv7/T/f4SSBQoYAAAAAAAAAQAAAQAAAAAAAAAAT2mlpcD7wtj/yuD/yuD/wdf/psH+W3a0AAAAAAAAAQACAQACAAAAAAAAAAAEBAQJGShIGypKLUR6NUyCNEuBLUR6GCdGGShIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAAAAAAAAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
33
+ "AAABAAABAQACAAAAAAAAAAAAAAAAdnFaf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbnCLNDZRPjAA/O6t//KpTz0AKC5IcHaQExESAAAAAQAAAAAAAAAAAgABAAAAPjtD/Pv/mpnLExYZWVtfX2FiExYXi4q8/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYXjDfZTffZ/mi630jK3ygaHmfZbdbYXMDQwTAAAAAwECAAAABQIAAAAAAAAtWGiXrcP12/L/7PD14ufs5enw6e712vL/ts36YnWiAAY0AAAABAEAAAAAGxsgUGaYwNb/y83Rdnh8V1RWT0xNUk1PW1dYdnZ7xsbLxt3/WW+hISIoAAAAN0tvxdn9y9rwoK/FNjAsOzUwQT08Qj49Qj49Qj49PzgxOjMsm6m9ydfrxdr/SF2Giqnsrcz/wMnaWWJ0NS8qRT87SEI9UUtGUUtGRkE8SkQ/PTczTVZlvsbWqMj/iajyZYTQqMj/w8zggoufKSUmQz4/VD4Rwat90K9+Xz4NQj9DJiMnb3aJxs3gpsb/a4vZBRZBbn6pkKLN4fT/kI2PS0lKPDMoUUg9XU9DQzUpSEhKhYWI4PD/lqbRc4WzDyFPAAAAAAAAAwgWbnOB7fL+4ebzxcrRsLW8r7S9wsbQ3eHv7vP/f4SSBQoYAQAAAAAAAQACAQACAAAAAAAAT2mlpcD7wtj/y+H/yuD/wdf/psH/WnW1AAAAAAAAAQACAQACAAAAAAAAAAAEBAQJGShIGypKLUR6NUyCNEuBLEN4GCZFGihIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
34
+ "AAABAAABAQACAAAAAAAAAAAAAAAAdnFaf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbnCLNDZRPjAA/O6t//KpTz0AKi1Ic3WQExESAAAAAQAAAAAAAAAAAgABAAAAPjtD/Pv/mZjKExYZWVtfX2FiExYXi4q8/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYHfCfZTff5/njKz0jKz0gaDofZbdbYXMDQwTAAAAAwECAAAABQIAAAAAAAAtV2eWrsT22/L/7PD14ufs5Ojv6+/22vL/ts36YnWiAAUzAAAABAEAAAAAGhofT2WXwNb/ycnLd3d6VlFSUUxNUk1PWFNUdnZ4w8PFxt3/WW+iISIoAAAAN0tvxNj8zNvym6vBYVgvwrmQdHBhPjssPjoubWhdxL6UaWQ6l6S7ytfuxdr/SF2Giqnsrcz/wcvXU15pi4lZendIkIt0U083TUk0jYl0dnBFkYtgTVdfvsjQqMj/iajyZYTQp8b/w8zlfIWeUEofmZNoeGEjwalsza1xfFsgl5FmU00iaXOGxtDkpsb/a4vZBRZBbn6pkKLN4fT/ioqPRERJNy4mUklBXk9GPi8nQ0JJf36F4PD/lqbRc4WzDyFPAAAAAAAAAwgWbnOB7vP/4uf0xsvSsLW8r7S9w8jR3uLw7/T/foORBQoYAQAAAAAAAQACAQACAAAAAAAAT2mlpcD7wtj/y+H/yuD/wdf/psH/WnW1AAAAAAAAAQACAQACAAAAAAAAAAAEAwMIGSdGGylJLUR6NEuBNEuBLEN4GCZFGihIBQUKAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
35
+ ];
36
+ //# sourceMappingURL=brand-frames.js.map
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Brigade brand mark — animated coloured ASCII video clip beside a chunky wordmark.
3
+ *
4
+ * Layout (top to bottom):
5
+ *
6
+ * [pad]
7
+ * [ASCII video LEFT (56×30)] GAP [BRIGADE wordmark RIGHT (12 rows)]
8
+ * [tagline below the whole thing — cornsilk fullwidth]
9
+ * [pad]
10
+ *
11
+ * The video is a 56-col × 30-row × 89-frame char-based ASCII clip baked at
12
+ * module-build time (see scripts/gen-brand-frames-cli.mjs and brand-frames-cli.ts).
13
+ * Each cell carries a unicode codepoint and an RGB colour; we paint cells with
14
+ * `chalk.hex(toHex(r,g,b))(char)`, plus a skip-on-space-and-black optimisation
15
+ * that emits a literal space (no chalk wrapper) for empty/black cells so the
16
+ * pre-rendered string size stays sane while remaining visually identical.
17
+ * 89 frames at 15 fps play once, then the last frame holds.
18
+ *
19
+ * Wordmark colours are unchanged from the prior pure-text mark: a 4-stop
20
+ * vertical metallic gradient (cornsilk → gold → amber → bronze) so the
21
+ * lettering still reads as polished metal.
22
+ */
23
+ // cfonts is CommonJS — Node's ESM loader can't statically extract named
24
+ // exports from it, so `import { render } from "cfonts"` fails at runtime
25
+ // even when esModuleInterop lets it typecheck. Use default-import + destructure.
26
+ import cfontsPkg from "cfonts";
27
+ const { render } = cfontsPkg;
28
+ import { Text } from "@mariozechner/pi-tui";
29
+ import chalk from "chalk";
30
+ import { VIDEO_COLS, VIDEO_FPS, VIDEO_FRAME_COUNT, VIDEO_FRAMES, VIDEO_ROWS } from "./brand-frames-cli.js";
31
+ // Wordmark palette — unchanged from the prior pure-text version.
32
+ const BRIGADE_HIGHLIGHT = "#FFF8DC"; // cornsilk — barely-there warm white
33
+ const BRIGADE_GOLD = "#FFD700"; // bright reflective gold
34
+ const BRIGADE_AMBER = "#FFBF00"; // amber midline
35
+ const BRIGADE_BRONZE = "#CD7F32"; // bronze fadeout
36
+ // Nearest-neighbour upscale factor applied to the cfonts `block` output.
37
+ // 2 = each char duplicated horizontally and each row duplicated vertically,
38
+ // so 6 letterform rows become 12. Tune this to scale the wordmark.
39
+ const WORDMARK_SCALE = 2;
40
+ // Cells of left margin applied to ALL rows of the wordmark column (wordmark
41
+ // glyphs, blank padding rows, and tagline). Shifts the entire left column
42
+ // right so it doesn't hug terminal column 0.
43
+ const WORDMARK_INDENT = 4;
44
+ // Cells of left margin applied to the tagline so it sits slightly inside
45
+ // the BRIGADE wordmark's left edge instead of flush with column 0.
46
+ const TAGLINE_INDENT = 10;
47
+ // Cells of vertical gap between the wordmark and the tagline.
48
+ const TAGLINE_TOP_GAP = 2;
49
+ // Per-row colour gradient distributed across the actual row count. With the
50
+ // 2x scale the wordmark is 12 rows; the 4-stop ramp flows smoothly regardless.
51
+ const COLOR_STOPS = [BRIGADE_HIGHLIGHT, BRIGADE_GOLD, BRIGADE_AMBER, BRIGADE_BRONZE];
52
+ function colorForRow(i, total) {
53
+ const t = total <= 1 ? 0 : i / (total - 1);
54
+ const idx = Math.min(COLOR_STOPS.length - 1, Math.floor(t * COLOR_STOPS.length));
55
+ return COLOR_STOPS[idx];
56
+ }
57
+ // Render the wordmark glyph shapes once at module load. cfonts is used only
58
+ // for SHAPES; we strip its colour escapes and re-apply our own per-row colour
59
+ // below so the gradient flows top-to-bottom (cfonts only does horizontal
60
+ // gradients).
61
+ const cfontsResult = render("BRIGADE", {
62
+ font: "block",
63
+ align: "left",
64
+ colors: ["white"],
65
+ background: "transparent",
66
+ letterSpacing: 1,
67
+ lineHeight: 1,
68
+ space: false,
69
+ maxLength: "0",
70
+ env: "node",
71
+ });
72
+ const RAW_ART = cfontsResult === false ? "BRIGADE" : cfontsResult.string;
73
+ // Strip every ANSI colour escape cfonts injected and split into rows. cfonts
74
+ // inserts blank padding rows even with `space: false`, which would shift the
75
+ // per-row colour mapping if we kept them. Then pixel-double (nearest-neighbour)
76
+ // each row by WORDMARK_SCALE in both axes — codepoint-aware via Array.from so
77
+ // multi-byte box-drawing glyphs ('╗','║','═','╔','╚','╝') aren't split between
78
+ // their UTF-16 surrogate halves.
79
+ const STRIPPED_ROWS = RAW_ART.replace(/\x1b\[[0-9;]*m/g, "")
80
+ .split("\n")
81
+ .filter((row) => row.trim().length > 0)
82
+ .flatMap((row) => {
83
+ const widened = Array.from(row)
84
+ .flatMap((ch) => Array.from({ length: WORDMARK_SCALE }, () => ch))
85
+ .join("");
86
+ return Array.from({ length: WORDMARK_SCALE }, () => widened);
87
+ });
88
+ const COLORED_WORDMARK_ROWS = STRIPPED_ROWS.map((row, idx) => chalk.hex(colorForRow(idx, STRIPPED_ROWS.length))(row));
89
+ // Convert ASCII letters/digits/printable punctuation to their Unicode
90
+ // FULLWIDTH twins (U+FFxx range, codepoint = ascii + 0xFEE0). Fullwidth
91
+ // glyphs render at 2 cells wide in most monospace fonts, which makes the
92
+ // tagline look visibly larger than plain ASCII without needing cfonts.
93
+ // SPACE becomes the ideographic space U+3000 so inter-word gaps stay 2 cells.
94
+ function toFullwidth(s) {
95
+ let out = "";
96
+ for (const ch of s) {
97
+ const cp = ch.codePointAt(0);
98
+ if (cp === undefined) {
99
+ out += ch;
100
+ continue;
101
+ }
102
+ if (cp === 0x20) {
103
+ out += " ";
104
+ continue;
105
+ }
106
+ if (cp >= 0x21 && cp <= 0x7e) {
107
+ out += String.fromCodePoint(cp + 0xfee0);
108
+ continue;
109
+ }
110
+ out += ch;
111
+ }
112
+ return out;
113
+ }
114
+ const TAGLINE = chalk.hex(BRIGADE_HIGHLIGHT)(toFullwidth("your personal AI crew · by spinabot"));
115
+ // Three spaces between the video block and the wordmark. The video is now
116
+ // 56 cells wide (the char-based render) and the wordmark is ~146 cells once
117
+ // pixel-doubled, so the whole composite is ~205 cols wide and requires a
118
+ // wide terminal — that's expected for the brand splash.
119
+ const GAP = " ".repeat(25);
120
+ // Minimum terminal width (in cells) needed for the side-by-side layout to
121
+ // fit comfortably. The math:
122
+ // EFFECTIVE_WORDMARK_WIDTH (4 + 118 = 122)
123
+ // + GAP.length (25)
124
+ // + VIDEO_COLS (56)
125
+ // = 203 cells exactly. We add ~7 cells of breathing room so the user isn't
126
+ // staring at a layout pinned to the right edge of their terminal, giving
127
+ // THRESHOLD = 210. Below this width we stack the wordmark on top of the
128
+ // video instead of placing them side by side.
129
+ export const SIDE_BY_SIDE_THRESHOLD = 210;
130
+ // Wordmark column visible width is ~122 cells (WORDMARK_INDENT + WORDMARK_WIDTH);
131
+ // pad a few cells for safety so the wordmark never wraps into the gutter.
132
+ export const WORDMARK_MIN_WIDTH = 125;
133
+ // VIDEO_COLS exactly. Below this width the ASCII video can't fit either, so we
134
+ // render nothing at all rather than something visually broken.
135
+ export const ASCII_MIN_WIDTH = 56;
136
+ // Pick layout mode based on current terminal width. Pi-TUI's TUI exposes
137
+ // `tui.terminal.columns` (see node_modules/@mariozechner/pi-tui/dist/terminal.d.ts);
138
+ // we accept that as an optional override. Otherwise we fall back to
139
+ // process.stdout.columns. If neither is available (non-TTY pipe etc.) we
140
+ // assume the terminal is wide enough and keep the side-by-side layout —
141
+ // 999 is just a sentinel that comfortably exceeds THRESHOLD.
142
+ function pickLayout(termCols) {
143
+ const cols = termCols ?? process.stdout.columns ?? 999;
144
+ if (cols < ASCII_MIN_WIDTH)
145
+ return "empty";
146
+ if (cols < WORDMARK_MIN_WIDTH)
147
+ return "ascii-only";
148
+ if (cols < SIDE_BY_SIDE_THRESHOLD)
149
+ return "stack";
150
+ return "side";
151
+ }
152
+ // Blank rows prepended directly to the composed brand block so the wordmark
153
+ // doesn't hug the top of the terminal. Baked into the block content because
154
+ // pi-tui Text components measure their own height from non-empty content,
155
+ // so a leading "\n\n" Text padder doesn't reliably reserve visible rows.
156
+ const TOP_PAD_LINES = 8;
157
+ // Pre-render every video frame as an array of `VIDEO_ROWS` chalk-coloured
158
+ // strings. Each cell is a single Unicode glyph painted with `chalk.hex(rgb)`
159
+ // — one truecolor escape per cell. We skip the chalk wrapper for cells that
160
+ // are both a literal space AND fully black (invisible anyway), keeping the
161
+ // pre-rendered string size sane while remaining visually identical.
162
+ //
163
+ // Doing this once at module load means animation ticks are just an array
164
+ // lookup + setText — no per-tick chalk allocation.
165
+ //
166
+ // Iteration is codepoint-aware via Array.from(chars) so multi-byte glyphs
167
+ // (Braille, box-drawing, etc.) aren't split between their UTF-16 surrogate
168
+ // halves. Source frames carry exactly VIDEO_COLS * VIDEO_ROWS codepoints and
169
+ // VIDEO_COLS * VIDEO_ROWS * 3 RGB bytes (validated at generation time).
170
+ const PRE_RENDERED_FRAMES = VIDEO_FRAMES.map(({ chars, rgb }) => {
171
+ const buf = Buffer.from(rgb, "base64");
172
+ const cps = Array.from(chars);
173
+ const rows = [];
174
+ for (let y = 0; y < VIDEO_ROWS; y++) {
175
+ let line = "";
176
+ for (let x = 0; x < VIDEO_COLS; x++) {
177
+ const i = y * VIDEO_COLS + x;
178
+ const ch = cps[i] ?? " ";
179
+ const r = buf[i * 3] ?? 0;
180
+ const g = buf[i * 3 + 1] ?? 0;
181
+ const b = buf[i * 3 + 2] ?? 0;
182
+ if (ch === " " && r === 0 && g === 0 && b === 0) {
183
+ line += " ";
184
+ }
185
+ else {
186
+ const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
187
+ line += chalk.hex(hex)(ch);
188
+ }
189
+ }
190
+ rows.push(line);
191
+ }
192
+ return rows;
193
+ });
194
+ // Visible width of an arbitrary string after stripping ANSI colour escapes.
195
+ // Used to right-pad the tagline row so its leftColumn entry matches the
196
+ // wordmark rows in width, keeping the ASCII video aligned on every row.
197
+ //
198
+ // Cell-aware: fullwidth / wide characters (e.g. those produced by toFullwidth,
199
+ // CJK ideographs, the ideographic space U+3000) render as TWO terminal cells
200
+ // each but count as ONE codepoint in JavaScript. Counting visual cells — not
201
+ // codepoints — keeps the tagline row's width matched to the wordmark rows so
202
+ // the ASCII video doesn't shift right on the tagline row.
203
+ function visibleWidth(s) {
204
+ // Strip ANSI escapes first.
205
+ const stripped = s.replace(/\[[0-9;]*m/g, "");
206
+ // Count visual cells: fullwidth / wide characters take 2 cells, the rest 1.
207
+ // We approximate "wide" via the common East Asian Width / Halfwidth-Fullwidth
208
+ // ranges that toFullwidth produces, plus CJK ideographs and the
209
+ // ideographic space U+3000 used for fullwidth spaces.
210
+ let cells = 0;
211
+ for (const ch of stripped) {
212
+ const cp = ch.codePointAt(0);
213
+ if (cp === undefined)
214
+ continue;
215
+ const wide = cp === 0x3000 || // ideographic space
216
+ (cp >= 0xff01 && cp <= 0xff60) || // fullwidth ASCII variants
217
+ (cp >= 0xffe0 && cp <= 0xffe6) || // fullwidth signs
218
+ (cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
219
+ (cp >= 0x2e80 && cp <= 0x303e) || // CJK Radicals/Symbols
220
+ (cp >= 0x3041 && cp <= 0x33ff) || // Kana, Bopomofo, CJK Compat
221
+ (cp >= 0x3400 && cp <= 0x4dbf) || // CJK Ext A
222
+ (cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified
223
+ (cp >= 0xa000 && cp <= 0xa4cf) || // Yi
224
+ (cp >= 0xac00 && cp <= 0xd7a3); // Hangul Syllables
225
+ cells += wide ? 2 : 1;
226
+ }
227
+ return cells;
228
+ }
229
+ // Cell-aware visible width of one wordmark row. Used to pad the left column
230
+ // with blank space once the wordmark runs out so the ASCII video on the right
231
+ // doesn't shift left in those rows. The current cfonts `block` output is all
232
+ // ASCII + box-drawing (1-cell glyphs), so the value here matches raw .length —
233
+ // using visibleWidth() just adds robustness if the wordmark ever changes.
234
+ const WORDMARK_WIDTH = COLORED_WORDMARK_ROWS[0] ? visibleWidth(COLORED_WORDMARK_ROWS[0]) : 0;
235
+ // Build the LEFT COLUMN once at module load: wordmark rows, then a vertical
236
+ // gap, then the indented tagline. This mirrors the OpenTUI layout in
237
+ // asciify-engine/tui-demo/brand.tsx where the Wordmark component is a column
238
+ // of [wordmark rows][marginTop gap][indented tagline] sitting beside the
239
+ // ASCII video. Total = 12 wordmark rows + 2 blank rows + 1 tagline row = 15.
240
+ // Every row in leftColumn must be padded to EFFECTIVE_WORDMARK_WIDTH visible
241
+ // cells so composeFrame's `leftPart + GAP + videoRow` keeps the video at the
242
+ // same screen column on every row (otherwise the tagline row, whose visible
243
+ // width is less than the wordmark width, would let the video shift left
244
+ // there). Each row gets WORDMARK_INDENT spaces prepended to shift the whole
245
+ // left column right by that many cells; the ASCII video on the right shifts
246
+ // in lockstep because every row carries the same prefix.
247
+ // Math.max(0, …) means we never truncate if the tagline ever happens to
248
+ // exceed EFFECTIVE_WORDMARK_WIDTH — we just skip the padding in that case.
249
+ const EFFECTIVE_WORDMARK_WIDTH = WORDMARK_INDENT + WORDMARK_WIDTH;
250
+ const taglineRow = `${" ".repeat(WORDMARK_INDENT)}${" ".repeat(TAGLINE_INDENT)}${TAGLINE}`;
251
+ const taglineVisibleLen = visibleWidth(taglineRow);
252
+ const taglinePadding = Math.max(0, EFFECTIVE_WORDMARK_WIDTH - taglineVisibleLen);
253
+ const leftColumn = [
254
+ ...Array.from({ length: TOP_PAD_LINES }, () => " ".repeat(EFFECTIVE_WORDMARK_WIDTH)),
255
+ ...COLORED_WORDMARK_ROWS.map((row) => `${" ".repeat(WORDMARK_INDENT)}${row}`),
256
+ ...Array(TAGLINE_TOP_GAP).fill(" ".repeat(EFFECTIVE_WORDMARK_WIDTH)),
257
+ `${taglineRow}${" ".repeat(taglinePadding)}`,
258
+ ];
259
+ // Compose one complete brand block (wordmark || video) for a given frame
260
+ // index. Returns the multiline string ready to drop into a single Text
261
+ // component — keeping it a single component lets Pi-TUI redraw it atomically
262
+ // with no row-tearing during animation.
263
+ //
264
+ // Layout adapts to terminal width via pickLayout():
265
+ // - "side": wordmark column LEFT + GAP + video rows RIGHT, paired row-by-row.
266
+ // - "stack": wordmark column on top, blank separator row, then video rows
267
+ // indented with WORDMARK_INDENT so the video lines up flush-left
268
+ // with the wordmark column instead of hugging column 0.
269
+ function composeFrame(frameIdx, termCols) {
270
+ const videoRows = PRE_RENDERED_FRAMES[frameIdx] ?? PRE_RENDERED_FRAMES[0] ?? [];
271
+ const mode = pickLayout(termCols);
272
+ if (mode === "empty") {
273
+ return "";
274
+ }
275
+ if (mode === "ascii-only") {
276
+ const videoIndent = " ".repeat(WORDMARK_INDENT);
277
+ const lines = [];
278
+ for (let i = 0; i < TOP_PAD_LINES; i++)
279
+ lines.push("");
280
+ for (const row of videoRows)
281
+ lines.push(`${videoIndent}${row}`);
282
+ return lines.join("\n");
283
+ }
284
+ if (mode === "stack") {
285
+ const videoIndent = " ".repeat(WORDMARK_INDENT);
286
+ const lines = [];
287
+ for (const row of leftColumn)
288
+ lines.push(row);
289
+ lines.push("");
290
+ for (const row of videoRows)
291
+ lines.push(`${videoIndent}${row}`);
292
+ return lines.join("\n");
293
+ }
294
+ const totalRows = Math.max(leftColumn.length, VIDEO_ROWS);
295
+ const wordmarkBlankPad = " ".repeat(EFFECTIVE_WORDMARK_WIDTH);
296
+ const videoBlankPad = " ".repeat(VIDEO_COLS);
297
+ const lines = [];
298
+ for (let r = 0; r < totalRows; r++) {
299
+ const leftPart = leftColumn[r] ?? wordmarkBlankPad;
300
+ const rightPart = videoRows[r] ?? videoBlankPad;
301
+ lines.push(`${leftPart}${GAP}${rightPart}`);
302
+ }
303
+ return lines.join("\n");
304
+ }
305
+ /**
306
+ * Render the wordmark + animated video clip + tagline. Spawns a setInterval
307
+ * that drives the animation; the frame index wraps via modulo so the clip
308
+ * loops continuously for the rest of the session.
309
+ *
310
+ * Returns the components added to the TUI (caller can use these to remove or
311
+ * inspect them).
312
+ */
313
+ export function renderBrandHeader(tui) {
314
+ const components = [];
315
+ // Pi-TUI's TUI exposes the underlying Terminal which has a `columns` getter
316
+ // (see node_modules/@mariozechner/pi-tui/dist/terminal.d.ts). Prefer that
317
+ // over process.stdout.columns since it's the value pi-tui itself uses for
318
+ // rendering, but pickLayout falls back if either is unavailable.
319
+ const getCols = () => tui.terminal?.columns ?? process.stdout.columns;
320
+ const padTop = new Text("\n\n", 0, 0);
321
+ tui.addChild(padTop);
322
+ components.push(padTop);
323
+ const block = new Text(composeFrame(0, getCols()), 0, 0);
324
+ tui.addChild(block);
325
+ components.push(block);
326
+ const padBottom = new Text("", 0, 0);
327
+ tui.addChild(padBottom);
328
+ components.push(padBottom);
329
+ tui.requestRender();
330
+ // --- Scrollback-friendly render scheduling ---------------------------------
331
+ // pi-tui's differential renderer (see node_modules/@mariozechner/pi-tui/dist/tui.js
332
+ // `doRender`, lines ~674-971) repaints animation frames by moving the
333
+ // hardware cursor up with `\x1b[{n}A`, clearing each row with `\x1b[2K`, and
334
+ // writing the new content. Windows Terminal (and other Conhost-derived
335
+ // emulators) treat any write that targets the bottom region of the buffer
336
+ // as "active output" and snap the viewport to the cursor — fighting the
337
+ // user's scrollback. Two complementary mitigations:
338
+ //
339
+ // 1. Skip the call entirely when the composed frame is byte-identical to
340
+ // what we already pushed (e.g. when terminal width hasn't changed and
341
+ // the layout produced the same string for an empty/clamped layout).
342
+ // This avoids a no-op render that still emits cursor-move bytes.
343
+ //
344
+ // 2. Wrap each tui.requestRender() in DEC mode 2026 (synchronized output
345
+ // begin/end) AND VT100 save/restore-cursor (`\x1b[s` / `\x1b[u`). pi-tui
346
+ // already opens 2026 inside doRender, but it does so AROUND its cursor
347
+ // moves; wrapping at the outer scope means Windows Terminal sees the
348
+ // full repaint as one atomic frame and won't auto-scroll the viewport
349
+ // to follow intermediate cursor moves. The save/restore pair is a
350
+ // belt-and-braces signal that the terminal can keep its viewport pinned
351
+ // where the user put it. Both sequences are cheap (≤8 bytes each) and
352
+ // are no-ops on terminals that don't honour them.
353
+ //
354
+ // References:
355
+ // - pi-tui differential renderer: node_modules/@mariozechner/pi-tui/dist/tui.js:674-971
356
+ // - Synchronized Output (DEC 2026): https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
357
+ // - Cursor save/restore (DECSC / DECRC, ANSI \x1b[s/\x1b[u): VT100 spec
358
+ const SYNC_BEGIN = "\x1b[?2026h\x1b[s"; // begin sync + save cursor
359
+ const SYNC_END = "\x1b[u\x1b[?2026l"; // restore cursor + end sync
360
+ let lastFrameStr;
361
+ const safeRender = (next) => {
362
+ if (next === lastFrameStr) {
363
+ // No visible change — don't re-emit cursor-move bytes that would
364
+ // make the terminal re-anchor the viewport.
365
+ return;
366
+ }
367
+ lastFrameStr = next;
368
+ block.setText(next);
369
+ try {
370
+ process.stdout.write(SYNC_BEGIN);
371
+ tui.requestRender();
372
+ }
373
+ finally {
374
+ process.stdout.write(SYNC_END);
375
+ }
376
+ };
377
+ // Drive the animation. The interval fires every 1000/VIDEO_FPS ms and the
378
+ // frame index wraps via modulo so the clip loops continuously for the rest
379
+ // of the session — no early-exit clearInterval. We refetch the terminal
380
+ // width on every tick so resizes are reflected within ~1/VIDEO_FPS s
381
+ // even if the dedicated resize listener below misses for some reason.
382
+ let frameIdx = 0;
383
+ const tickMs = Math.max(1, Math.round(1000 / VIDEO_FPS));
384
+ const timer = setInterval(() => {
385
+ frameIdx = (frameIdx + 1) % VIDEO_FRAME_COUNT;
386
+ safeRender(composeFrame(frameIdx, getCols()));
387
+ }, tickMs);
388
+ // Don't keep the event loop alive just for this animation — once the user
389
+ // quits, the process should be free to exit even if the timer hasn't yet
390
+ // reached the final frame.
391
+ timer.unref?.();
392
+ // Re-compose the brand block on terminal resize so the responsive layout
393
+ // (side / stack / ascii-only / empty) keeps working as the animation
394
+ // loops forever. During the animation the safeRender identical-frame
395
+ // cache makes this a no-op when the layout didn't actually change.
396
+ const onResize = () => {
397
+ safeRender(composeFrame(frameIdx, getCols()));
398
+ };
399
+ process.stdout.on("resize", onResize);
400
+ return components;
401
+ }
402
+ //# sourceMappingURL=brand.js.map